From 08544858defa61f674ccb3e695135f85cc712fbb Mon Sep 17 00:00:00 2001 From: BankaiNoJutsu Date: Mon, 17 Dec 2018 22:49:01 +0100 Subject: [PATCH 01/11] Added Theming functionality and theme folder --- Gruntfile.js | 104 +- package-lock.json | 28 +- themes/main/client/src/css/.directory | 4 + .../main/client/src/css/00-bootstrap.min.css | 6 + themes/main/client/src/css/01-main.css | 67 + themes/main/client/src/css/02-login.css | 132 + themes/main/client/src/css/03-errors.css | 12 + .../client/src/css/03-password-reset-form.css | 4 + .../src/css/03-password-reset-request.css | 4 + .../main/client/src/css/03-totp-register.css | 22 + .../main/client/src/css/03-u2f-register.css | 5 + themes/main/client/src/img/background.svg | 5 + themes/main/client/src/img/icon.png | Bin 0 -> 1461 bytes themes/main/client/src/img/mail.png | Bin 0 -> 3545 bytes .../client/src/img/notifications/.directory | 4 + .../client/src/img/notifications/error.png | Bin 0 -> 863 bytes .../client/src/img/notifications/info.png | Bin 0 -> 732 bytes .../client/src/img/notifications/success.png | Bin 0 -> 931 bytes .../client/src/img/notifications/warning.png | Bin 0 -> 580 bytes themes/main/client/src/img/padlock.png | Bin 0 -> 3265 bytes themes/main/client/src/img/password.png | Bin 0 -> 2178 bytes themes/main/client/src/img/pendrive.png | Bin 0 -> 6721 bytes themes/main/client/src/img/stores/.directory | 4 + .../src/img/stores/applestore-badge.svg | 129 + .../src/img/stores/googleplay-badge.svg | 429 ++ themes/main/client/src/img/success.png | Bin 0 -> 3147 bytes themes/main/client/src/img/user.png | Bin 0 -> 2933 bytes themes/main/client/src/img/warning.png | Bin 0 -> 4038 bytes themes/main/client/src/index.ts | 34 + themes/main/client/src/lib/GetPromised.ts | 14 + themes/main/client/src/lib/INotifier.ts | 14 + themes/main/client/src/lib/Notifier.ts | 83 + .../src/lib/QueryParametersRetriever.ts | 12 + themes/main/client/src/lib/SafeRedirect.ts | 10 + .../lib/firstfactor/FirstFactorValidator.ts | 46 + .../client/src/lib/firstfactor/UISelectors.ts | 5 + .../main/client/src/lib/firstfactor/index.ts | 49 + .../src/lib/reset-password/constants.ts | 2 + .../lib/reset-password/reset-password-form.ts | 57 + .../reset-password/reset-password-request.ts | 56 + .../src/lib/secondfactor/TOTPValidator.ts | 28 + .../src/lib/secondfactor/U2FValidator.ts | 42 + .../client/src/lib/secondfactor/constants.ts | 3 + .../main/client/src/lib/secondfactor/index.ts | 59 + .../src/lib/totp-register/totp-register.ts | 11 + .../src/lib/totp-register/ui-selector.ts | 2 + .../src/lib/u2f-register/u2f-register.ts | 56 + .../client/src/thirdparties/qrcode.min.js | 1 + themes/main/client/test/Notifier.test.ts | 71 + .../firstfactor/FirstFactorValidator.test.ts | 46 + themes/main/client/test/mocks/NotifierStub.ts | 33 + themes/main/client/test/mocks/jquery.ts | 59 + themes/main/client/test/mocks/u2f-api.ts | 14 + .../test/secondfactor/TOTPValidator.test.ts | 37 + .../test/totp-register/totp-register.test.ts | 31 + themes/main/client/tsconfig.json | 24 + themes/main/client/tslint.json | 60 + themes/main/server/.directory | 4 + themes/main/server/src/index.ts | 28 + themes/main/server/src/lib/.directory | 4 + .../src/lib/AuthenticationSessionHandler.ts | 45 + themes/main/server/src/lib/ErrorReplies.ts | 49 + themes/main/server/src/lib/Exceptions.ts | 88 + .../server/src/lib/FirstFactorValidator.ts | 20 + .../src/lib/IdentityCheckMiddleware.spec.ts | 176 + .../server/src/lib/IdentityCheckMiddleware.ts | 138 + .../lib/IdentityCheckPreValidationTemplate.ts | 3 + .../main/server/src/lib/IdentityValidable.ts | 19 + .../src/lib/IdentityValidableStub.spec.ts | 52 + themes/main/server/src/lib/Server.spec.ts | 81 + themes/main/server/src/lib/Server.ts | 93 + themes/main/server/src/lib/ServerVariables.ts | 21 + .../src/lib/ServerVariablesInitializer.ts | 116 + .../lib/ServerVariablesMockBuilder.spec.ts | 87 + .../server/src/lib/authentication/Level.ts | 5 + .../backends/GroupsAndEmails.ts | 5 + .../authentication/backends/IUsersDatabase.ts | 10 + .../backends/IUsersDatabaseStub.spec.ts | 35 + .../backends/file/FileUsersDatabase.spec.ts | 224 + .../backends/file/FileUsersDatabase.ts | 182 + .../backends/file/ReadWriteQueue.ts | 60 + .../authentication/backends/ldap/ISession.ts | 12 + .../backends/ldap/ISessionFactory.ts | 6 + .../backends/ldap/LdapUsersDatabase.spec.ts | 386 ++ .../backends/ldap/LdapUsersDatabase.ts | 107 + .../backends/ldap/SafeSession.spec.ts | 76 + .../backends/ldap/SafeSession.ts | 62 + .../backends/ldap/Sanitizer.spec.ts | 25 + .../authentication/backends/ldap/Sanitizer.ts | 25 + .../backends/ldap/Session.spec.ts | 127 + .../authentication/backends/ldap/Session.ts | 156 + .../backends/ldap/SessionFactory.ts | 37 + .../backends/ldap/SessionFactoryStub.spec.ts | 16 + .../backends/ldap/SessionStub.spec.ts | 46 + .../backends/ldap/connector/Connector.ts | 69 + .../ldap/connector/ConnectorFactory.ts | 18 + .../connector/ConnectorFactoryStub.spec.ts | 17 + .../ldap/connector/ConnectorStub.spec.ts | 34 + .../backends/ldap/connector/IConnector.ts | 9 + .../ldap/connector/IConnectorFactory.ts | 5 + .../lib/authentication/totp/ITotpHandler.ts | 6 + .../authentication/totp/TotpHandler.spec.ts | 39 + .../lib/authentication/totp/TotpHandler.ts | 36 + .../totp/TotpHandlerStub.spec.ts | 22 + .../src/lib/authentication/u2f/IU2fHandler.ts | 9 + .../src/lib/authentication/u2f/U2fHandler.ts | 24 + .../authentication/u2f/U2fHandlerStub.spec.ts | 31 + .../src/lib/authorization/Authorizer.spec.ts | 372 ++ .../src/lib/authorization/Authorizer.ts | 85 + .../lib/authorization/AuthorizerStub.spec.ts | 17 + .../src/lib/authorization/IAuthorizer.ts | 7 + .../server/src/lib/authorization/Level.ts | 6 + .../authorization/MultipleDomainMatcher.ts | 12 + .../server/src/lib/authorization/Object.ts | 5 + .../server/src/lib/authorization/Subject.ts | 5 + .../configuration/ConfigurationParser.spec.ts | 171 + .../lib/configuration/ConfigurationParser.ts | 39 + .../SessionConfigurationBuilder.spec.ts | 149 + .../SessionConfigurationBuilder.ts | 52 + .../schema/AclConfiguration.spec.ts | 34 + .../configuration/schema/AclConfiguration.ts | 41 + ...AuthenticationBackendConfiguration.spec.ts | 11 + .../AuthenticationBackendConfiguration.ts | 25 + .../lib/configuration/schema/Configuration.ts | 68 + .../schema/FileUsersDatabaseConfiguration.ts | 4 + .../schema/LdapConfiguration.spec.ts | 25 + .../configuration/schema/LdapConfiguration.ts | 40 + .../schema/NotifierConfiguration.spec.ts | 40 + .../schema/NotifierConfiguration.ts | 45 + .../schema/RegulationConfiguration.spec.ts | 13 + .../schema/RegulationConfiguration.ts | 23 + .../schema/SessionConfiguration.spec.ts | 16 + .../schema/SessionConfiguration.ts | 32 + .../schema/StorageConfiguration.spec.ts | 15 + .../schema/StorageConfiguration.ts | 30 + .../configuration/schema/TotpConfiguration.ts | 13 + .../schema/UserDatabaseConfiguration.ts | 9 + .../lib/connectors/mongo/IMongoClient.d.ts | 6 + .../lib/connectors/mongo/MongoClient.spec.ts | 119 + .../src/lib/connectors/mongo/MongoClient.ts | 76 + .../connectors/mongo/MongoClientStub.spec.ts | 16 + .../server/src/lib/logging/GlobalLogger.ts | 34 + .../src/lib/logging/GlobalLoggerStub.spec.ts | 38 + .../server/src/lib/logging/IGlobalLogger.ts | 5 + .../server/src/lib/logging/IRequestLogger.ts | 7 + .../server/src/lib/logging/RequestLogger.ts | 45 + .../src/lib/logging/RequestLoggerStub.spec.ts | 38 + .../lib/notifiers/AbstractEmailNotifier.ts | 23 + .../src/lib/notifiers/EmailNotifier.spec.ts | 54 + .../server/src/lib/notifiers/EmailNotifier.ts | 27 + .../src/lib/notifiers/FileSystemNotifier.ts | 22 + .../server/src/lib/notifiers/IMailSender.ts | 6 + .../src/lib/notifiers/IMailSenderBuilder.ts | 7 + .../server/src/lib/notifiers/INotifier.ts | 5 + .../server/src/lib/notifiers/MailSender.ts | 42 + .../lib/notifiers/MailSenderBuilder.spec.ts | 67 + .../src/lib/notifiers/MailSenderBuilder.ts | 42 + .../notifiers/MailSenderBuilderStub.spec.ts | 25 + .../src/lib/notifiers/MailSenderStub.spec.ts | 16 + .../src/lib/notifiers/NotifierFactory.spec.ts | 42 + .../src/lib/notifiers/NotifierFactory.ts | 33 + .../src/lib/notifiers/NotifierStub.spec.ts | 16 + .../server/src/lib/notifiers/SmtpNotifier.ts | 30 + .../server/src/lib/regulation/IRegulator.ts | 6 + .../src/lib/regulation/Regulator.spec.ts | 186 + .../server/src/lib/regulation/Regulator.ts | 55 + .../src/lib/regulation/RegulatorStub.spec.ts | 22 + .../src/lib/routes/error/401/get.spec.ts | 61 + .../server/src/lib/routes/error/401/get.ts | 15 + .../src/lib/routes/error/403/get.spec.ts | 61 + .../server/src/lib/routes/error/403/get.ts | 15 + .../src/lib/routes/error/404/get.spec.ts | 19 + .../server/src/lib/routes/error/404/get.ts | 8 + .../server/src/lib/routes/error/redirector.ts | 13 + .../server/src/lib/routes/firstfactor/get.ts | 72 + .../src/lib/routes/firstfactor/post.spec.ts | 136 + .../server/src/lib/routes/firstfactor/post.ts | 101 + .../server/src/lib/routes/loggedin/get.ts | 23 + .../main/server/src/lib/routes/logout/get.ts | 20 + .../lib/routes/password-reset/constants.ts | 2 + .../routes/password-reset/form/post.spec.ts | 122 + .../lib/routes/password-reset/form/post.ts | 50 + .../identity/PasswordResetHandler.spec.ts | 92 + .../identity/PasswordResetHandler.ts | 69 + .../lib/routes/password-reset/request/get.ts | 13 + .../src/lib/routes/secondfactor/get.spec.ts | 44 + .../server/src/lib/routes/secondfactor/get.ts | 28 + .../lib/routes/secondfactor/redirect.spec.ts | 41 + .../src/lib/routes/secondfactor/redirect.ts | 30 + .../lib/routes/secondfactor/totp/constants.ts | 4 + .../totp/identity/RegistrationHandler.spec.ts | 116 + .../totp/identity/RegistrationHandler.ts | 112 + .../secondfactor/totp/sign/post.spec.ts | 76 + .../lib/routes/secondfactor/totp/sign/post.ts | 42 + .../lib/routes/secondfactor/u2f/U2FCommon.ts | 11 + .../u2f/identity/RegistrationHandler.spec.ts | 96 + .../u2f/identity/RegistrationHandler.ts | 73 + .../secondfactor/u2f/register/post.spec.ts | 146 + .../routes/secondfactor/u2f/register/post.ts | 64 + .../u2f/register_request/get.spec.ts | 86 + .../secondfactor/u2f/register_request/get.ts | 43 + .../routes/secondfactor/u2f/sign/post.spec.ts | 101 + .../lib/routes/secondfactor/u2f/sign/post.ts | 57 + .../secondfactor/u2f/sign_request/get.spec.ts | 68 + .../secondfactor/u2f/sign_request/get.ts | 42 + .../src/lib/routes/verify/access_control.ts | 51 + .../server/src/lib/routes/verify/get.spec.ts | 320 + .../main/server/src/lib/routes/verify/get.ts | 91 + .../src/lib/routes/verify/get_basic_auth.ts | 55 + .../lib/routes/verify/get_session_cookie.ts | 78 + .../storage/AuthenticationTraceDocument.d.ts | 6 + .../lib/storage/CollectionFactoryFactory.ts | 15 + .../lib/storage/CollectionFactoryStub.spec.ts | 16 + .../src/lib/storage/CollectionStub.spec.ts | 39 + .../server/src/lib/storage/ICollection.d.ts | 11 + .../src/lib/storage/ICollectionFactory.d.ts | 6 + .../src/lib/storage/IUserDataStore.d.ts | 21 + .../storage/IdentityValidationDocument.d.ts | 7 + .../src/lib/storage/TOTPSecretDocument.d.ts | 6 + .../lib/storage/U2FRegistrationDocument.d.ts | 8 + .../src/lib/storage/UserDataStore.spec.ts | 264 + .../server/src/lib/storage/UserDataStore.ts | 143 + .../src/lib/storage/UserDataStoreStub.spec.ts | 64 + .../lib/storage/mongo/MongoCollection.spec.ts | 110 + .../src/lib/storage/mongo/MongoCollection.ts | 50 + .../mongo/MongoCollectionFactory.spec.ts | 21 + .../storage/mongo/MongoCollectionFactory.ts | 19 + .../lib/storage/nedb/NedbCollection.spec.ts | 136 + .../src/lib/storage/nedb/NedbCollection.ts | 47 + .../nedb/NedbCollectionFactory.spec.ts | 16 + .../lib/storage/nedb/NedbCollectionFactory.ts | 28 + .../main/server/src/lib/stubs/express.spec.ts | 103 + .../main/server/src/lib/stubs/ldapjs.spec.ts | 50 + .../server/src/lib/stubs/speakeasy.spec.ts | 7 + themes/main/server/src/lib/stubs/u2f.spec.ts | 16 + .../src/lib/utils/HashGenerator.spec.ts | 18 + .../server/src/lib/utils/HashGenerator.ts | 23 + .../main/server/src/lib/utils/ObjectCloner.ts | 6 + .../src/lib/utils/SafeRedirection.spec.ts | 33 + .../server/src/lib/utils/SafeRedirection.ts | 22 + .../src/lib/utils/URLDecomposer.spec.ts | 46 + .../server/src/lib/utils/URLDecomposer.ts | 15 + .../server/src/lib/web_server/Configurator.ts | 47 + .../main/server/src/lib/web_server/RestApi.ts | 125 + .../RequireValidatedFirstFactor.ts | 27 + .../middlewares/WithHeadersLogged.ts | 12 + .../server/src/resources/email-template.ejs | 254 + .../server/src/views/already-logged-in.pug | 14 + .../main/server/src/views/errors/.directory | 4 + themes/main/server/src/views/errors/401.pug | 16 + themes/main/server/src/views/errors/403.pug | 16 + themes/main/server/src/views/errors/404.pug | 11 + themes/main/server/src/views/firstfactor.pug | 23 + .../main/server/src/views/layout/layout.pug | 30 + .../src/views/need-identity-validation.pug | 12 + .../server/src/views/password-reset-form.pug | 18 + .../src/views/password-reset-request.pug | 18 + themes/main/server/src/views/secondfactor.pug | 30 + .../main/server/src/views/totp-register.pug | 25 + themes/main/server/src/views/u2f-register.pug | 11 + themes/main/server/test/requests.ts | 94 + themes/main/server/tsconfig.json | 21 + themes/main/server/tslint.json | 60 + themes/main/server/types/.directory | 4 + .../server/types/AuthenticationSession.ts | 18 + themes/main/server/types/Dependencies.ts | 29 + themes/main/server/types/Identity.ts | 6 + themes/main/server/types/TOTPSecret.ts | 11 + themes/main/server/types/U2FRegistration.ts | 5 + themes/main/server/types/dovehash.d.ts | 4 + themes/main/server/types/speakeasy.d.ts | 96 + themes/matrix/client/src/css/.directory | 4 + .../client/src/css/00-bootstrap.min.css | 5770 +++++++++++++++++ themes/matrix/client/src/css/01-main.css | 77 + themes/matrix/client/src/css/02-login.css | 136 + themes/matrix/client/src/css/03-errors.css | 12 + .../client/src/css/03-password-reset-form.css | 4 + .../src/css/03-password-reset-request.css | 4 + .../client/src/css/03-totp-register.css | 22 + .../matrix/client/src/css/03-u2f-register.css | 5 + themes/matrix/client/src/img/background.jpg | Bin 0 -> 587 bytes themes/matrix/client/src/img/icon.png | Bin 0 -> 1461 bytes themes/matrix/client/src/img/mail.png | Bin 0 -> 3545 bytes .../client/src/img/matrix_circle_128x128.png | Bin 0 -> 35750 bytes .../client/src/img/notifications/.directory | 4 + .../client/src/img/notifications/error.png | Bin 0 -> 863 bytes .../client/src/img/notifications/info.png | Bin 0 -> 732 bytes .../client/src/img/notifications/success.png | Bin 0 -> 931 bytes .../client/src/img/notifications/warning.png | Bin 0 -> 580 bytes themes/matrix/client/src/img/padlock.png | Bin 0 -> 3265 bytes .../matrix/client/src/img/password_white.png | Bin 0 -> 3858 bytes themes/matrix/client/src/img/pendrive.png | Bin 0 -> 6721 bytes .../matrix/client/src/img/stores/.directory | 4 + .../src/img/stores/applestore-badge.svg | 129 + .../src/img/stores/googleplay-badge.svg | 429 ++ themes/matrix/client/src/img/success.png | Bin 0 -> 3147 bytes themes/matrix/client/src/img/user.png | Bin 0 -> 2933 bytes themes/matrix/client/src/img/warning.png | Bin 0 -> 4038 bytes themes/matrix/client/src/index.ts | 34 + themes/matrix/client/src/lib/GetPromised.ts | 14 + themes/matrix/client/src/lib/INotifier.ts | 14 + themes/matrix/client/src/lib/Notifier.ts | 83 + .../src/lib/QueryParametersRetriever.ts | 12 + themes/matrix/client/src/lib/SafeRedirect.ts | 10 + .../lib/firstfactor/FirstFactorValidator.ts | 46 + .../client/src/lib/firstfactor/UISelectors.ts | 5 + .../client/src/lib/firstfactor/index.ts | 49 + .../src/lib/reset-password/constants.ts | 2 + .../lib/reset-password/reset-password-form.ts | 57 + .../reset-password/reset-password-request.ts | 56 + .../src/lib/secondfactor/TOTPValidator.ts | 28 + .../src/lib/secondfactor/U2FValidator.ts | 42 + .../client/src/lib/secondfactor/constants.ts | 3 + .../client/src/lib/secondfactor/index.ts | 59 + .../src/lib/totp-register/totp-register.ts | 11 + .../src/lib/totp-register/ui-selector.ts | 2 + .../src/lib/u2f-register/u2f-register.ts | 56 + .../matrix/client/src/thirdparties/matrix.js | 58 + .../client/src/thirdparties/qrcode.min.js | 1 + .../matrix/client/src/thirdparties/u2f-api.js | 749 +++ themes/matrix/client/test/Notifier.test.ts | 71 + .../firstfactor/FirstFactorValidator.test.ts | 44 + .../matrix/client/test/mocks/NotifierStub.ts | 33 + themes/matrix/client/test/mocks/jquery.ts | 59 + themes/matrix/client/test/mocks/u2f-api.ts | 14 + .../test/secondfactor/TOTPValidator.test.ts | 37 + .../test/totp-register/totp-register.test.ts | 31 + themes/matrix/client/tsconfig.json | 24 + themes/matrix/client/tslint.json | 60 + themes/matrix/server/.directory | 4 + themes/matrix/server/src/index.ts | 28 + themes/matrix/server/src/lib/.directory | 4 + .../src/lib/AuthenticationSessionHandler.ts | 45 + themes/matrix/server/src/lib/ErrorReplies.ts | 49 + themes/matrix/server/src/lib/Exceptions.ts | 88 + .../server/src/lib/FirstFactorValidator.ts | 20 + .../src/lib/IdentityCheckMiddleware.spec.ts | 176 + .../server/src/lib/IdentityCheckMiddleware.ts | 138 + .../lib/IdentityCheckPreValidationTemplate.ts | 3 + .../server/src/lib/IdentityValidable.ts | 19 + .../src/lib/IdentityValidableStub.spec.ts | 52 + themes/matrix/server/src/lib/Server.spec.ts | 81 + themes/matrix/server/src/lib/Server.ts | 93 + .../matrix/server/src/lib/ServerVariables.ts | 21 + .../src/lib/ServerVariablesInitializer.ts | 116 + .../lib/ServerVariablesMockBuilder.spec.ts | 87 + .../server/src/lib/authentication/Level.ts | 5 + .../backends/GroupsAndEmails.ts | 5 + .../authentication/backends/IUsersDatabase.ts | 10 + .../backends/IUsersDatabaseStub.spec.ts | 35 + .../backends/file/FileUsersDatabase.spec.ts | 224 + .../backends/file/FileUsersDatabase.ts | 182 + .../backends/file/ReadWriteQueue.ts | 60 + .../authentication/backends/ldap/ISession.ts | 12 + .../backends/ldap/ISessionFactory.ts | 6 + .../backends/ldap/LdapUsersDatabase.spec.ts | 386 ++ .../backends/ldap/LdapUsersDatabase.ts | 107 + .../backends/ldap/SafeSession.spec.ts | 76 + .../backends/ldap/SafeSession.ts | 62 + .../backends/ldap/Sanitizer.spec.ts | 25 + .../authentication/backends/ldap/Sanitizer.ts | 25 + .../backends/ldap/Session.spec.ts | 127 + .../authentication/backends/ldap/Session.ts | 156 + .../backends/ldap/SessionFactory.ts | 37 + .../backends/ldap/SessionFactoryStub.spec.ts | 16 + .../backends/ldap/SessionStub.spec.ts | 46 + .../backends/ldap/connector/Connector.ts | 69 + .../ldap/connector/ConnectorFactory.ts | 18 + .../connector/ConnectorFactoryStub.spec.ts | 17 + .../ldap/connector/ConnectorStub.spec.ts | 34 + .../backends/ldap/connector/IConnector.ts | 9 + .../ldap/connector/IConnectorFactory.ts | 5 + .../lib/authentication/totp/ITotpHandler.ts | 6 + .../authentication/totp/TotpHandler.spec.ts | 39 + .../lib/authentication/totp/TotpHandler.ts | 36 + .../totp/TotpHandlerStub.spec.ts | 22 + .../src/lib/authentication/u2f/IU2fHandler.ts | 9 + .../src/lib/authentication/u2f/U2fHandler.ts | 24 + .../authentication/u2f/U2fHandlerStub.spec.ts | 31 + .../src/lib/authorization/Authorizer.spec.ts | 372 ++ .../src/lib/authorization/Authorizer.ts | 85 + .../lib/authorization/AuthorizerStub.spec.ts | 17 + .../src/lib/authorization/IAuthorizer.ts | 7 + .../server/src/lib/authorization/Level.ts | 6 + .../authorization/MultipleDomainMatcher.ts | 12 + .../server/src/lib/authorization/Object.ts | 5 + .../server/src/lib/authorization/Subject.ts | 5 + .../configuration/ConfigurationParser.spec.ts | 171 + .../lib/configuration/ConfigurationParser.ts | 39 + .../SessionConfigurationBuilder.spec.ts | 149 + .../SessionConfigurationBuilder.ts | 52 + .../schema/AclConfiguration.spec.ts | 34 + .../configuration/schema/AclConfiguration.ts | 41 + ...AuthenticationBackendConfiguration.spec.ts | 11 + .../AuthenticationBackendConfiguration.ts | 25 + .../lib/configuration/schema/Configuration.ts | 68 + .../schema/FileUsersDatabaseConfiguration.ts | 4 + .../schema/LdapConfiguration.spec.ts | 25 + .../configuration/schema/LdapConfiguration.ts | 40 + .../schema/NotifierConfiguration.spec.ts | 40 + .../schema/NotifierConfiguration.ts | 45 + .../schema/RegulationConfiguration.spec.ts | 13 + .../schema/RegulationConfiguration.ts | 23 + .../schema/SessionConfiguration.spec.ts | 16 + .../schema/SessionConfiguration.ts | 32 + .../schema/StorageConfiguration.spec.ts | 15 + .../schema/StorageConfiguration.ts | 30 + .../configuration/schema/TotpConfiguration.ts | 13 + .../schema/UserDatabaseConfiguration.ts | 9 + .../lib/connectors/mongo/IMongoClient.d.ts | 6 + .../lib/connectors/mongo/MongoClient.spec.ts | 119 + .../src/lib/connectors/mongo/MongoClient.ts | 76 + .../connectors/mongo/MongoClientStub.spec.ts | 16 + .../server/src/lib/logging/GlobalLogger.ts | 34 + .../src/lib/logging/GlobalLoggerStub.spec.ts | 38 + .../server/src/lib/logging/IGlobalLogger.ts | 5 + .../server/src/lib/logging/IRequestLogger.ts | 7 + .../server/src/lib/logging/RequestLogger.ts | 45 + .../src/lib/logging/RequestLoggerStub.spec.ts | 38 + .../lib/notifiers/AbstractEmailNotifier.ts | 23 + .../src/lib/notifiers/EmailNotifier.spec.ts | 54 + .../server/src/lib/notifiers/EmailNotifier.ts | 27 + .../src/lib/notifiers/FileSystemNotifier.ts | 22 + .../server/src/lib/notifiers/IMailSender.ts | 6 + .../src/lib/notifiers/IMailSenderBuilder.ts | 7 + .../server/src/lib/notifiers/INotifier.ts | 5 + .../server/src/lib/notifiers/MailSender.ts | 42 + .../lib/notifiers/MailSenderBuilder.spec.ts | 67 + .../src/lib/notifiers/MailSenderBuilder.ts | 42 + .../notifiers/MailSenderBuilderStub.spec.ts | 25 + .../src/lib/notifiers/MailSenderStub.spec.ts | 16 + .../src/lib/notifiers/NotifierFactory.spec.ts | 42 + .../src/lib/notifiers/NotifierFactory.ts | 33 + .../src/lib/notifiers/NotifierStub.spec.ts | 16 + .../server/src/lib/notifiers/SmtpNotifier.ts | 30 + .../server/src/lib/regulation/IRegulator.ts | 6 + .../src/lib/regulation/Regulator.spec.ts | 186 + .../server/src/lib/regulation/Regulator.ts | 55 + .../src/lib/regulation/RegulatorStub.spec.ts | 22 + .../src/lib/routes/error/401/get.spec.ts | 61 + .../server/src/lib/routes/error/401/get.ts | 15 + .../src/lib/routes/error/403/get.spec.ts | 61 + .../server/src/lib/routes/error/403/get.ts | 15 + .../src/lib/routes/error/404/get.spec.ts | 19 + .../server/src/lib/routes/error/404/get.ts | 8 + .../server/src/lib/routes/error/redirector.ts | 13 + .../server/src/lib/routes/firstfactor/get.ts | 72 + .../src/lib/routes/firstfactor/post.spec.ts | 136 + .../server/src/lib/routes/firstfactor/post.ts | 101 + .../server/src/lib/routes/loggedin/get.ts | 23 + .../server/src/lib/routes/logout/get.ts | 20 + .../lib/routes/password-reset/constants.ts | 2 + .../routes/password-reset/form/post.spec.ts | 122 + .../lib/routes/password-reset/form/post.ts | 50 + .../identity/PasswordResetHandler.spec.ts | 92 + .../identity/PasswordResetHandler.ts | 69 + .../lib/routes/password-reset/request/get.ts | 13 + .../src/lib/routes/secondfactor/get.spec.ts | 44 + .../server/src/lib/routes/secondfactor/get.ts | 28 + .../lib/routes/secondfactor/redirect.spec.ts | 41 + .../src/lib/routes/secondfactor/redirect.ts | 30 + .../lib/routes/secondfactor/totp/constants.ts | 4 + .../totp/identity/RegistrationHandler.spec.ts | 116 + .../totp/identity/RegistrationHandler.ts | 112 + .../secondfactor/totp/sign/post.spec.ts | 76 + .../lib/routes/secondfactor/totp/sign/post.ts | 42 + .../lib/routes/secondfactor/u2f/U2FCommon.ts | 11 + .../u2f/identity/RegistrationHandler.spec.ts | 96 + .../u2f/identity/RegistrationHandler.ts | 73 + .../secondfactor/u2f/register/post.spec.ts | 146 + .../routes/secondfactor/u2f/register/post.ts | 64 + .../u2f/register_request/get.spec.ts | 86 + .../secondfactor/u2f/register_request/get.ts | 43 + .../routes/secondfactor/u2f/sign/post.spec.ts | 101 + .../lib/routes/secondfactor/u2f/sign/post.ts | 57 + .../secondfactor/u2f/sign_request/get.spec.ts | 68 + .../secondfactor/u2f/sign_request/get.ts | 42 + .../src/lib/routes/verify/access_control.ts | 51 + .../server/src/lib/routes/verify/get.spec.ts | 320 + .../server/src/lib/routes/verify/get.ts | 91 + .../src/lib/routes/verify/get_basic_auth.ts | 55 + .../lib/routes/verify/get_session_cookie.ts | 78 + .../storage/AuthenticationTraceDocument.d.ts | 6 + .../lib/storage/CollectionFactoryFactory.ts | 15 + .../lib/storage/CollectionFactoryStub.spec.ts | 16 + .../src/lib/storage/CollectionStub.spec.ts | 39 + .../server/src/lib/storage/ICollection.d.ts | 11 + .../src/lib/storage/ICollectionFactory.d.ts | 6 + .../src/lib/storage/IUserDataStore.d.ts | 21 + .../storage/IdentityValidationDocument.d.ts | 7 + .../src/lib/storage/TOTPSecretDocument.d.ts | 6 + .../lib/storage/U2FRegistrationDocument.d.ts | 8 + .../src/lib/storage/UserDataStore.spec.ts | 264 + .../server/src/lib/storage/UserDataStore.ts | 143 + .../src/lib/storage/UserDataStoreStub.spec.ts | 64 + .../lib/storage/mongo/MongoCollection.spec.ts | 110 + .../src/lib/storage/mongo/MongoCollection.ts | 50 + .../mongo/MongoCollectionFactory.spec.ts | 21 + .../storage/mongo/MongoCollectionFactory.ts | 19 + .../lib/storage/nedb/NedbCollection.spec.ts | 136 + .../src/lib/storage/nedb/NedbCollection.ts | 47 + .../nedb/NedbCollectionFactory.spec.ts | 16 + .../lib/storage/nedb/NedbCollectionFactory.ts | 28 + .../server/src/lib/stubs/express.spec.ts | 103 + .../server/src/lib/stubs/ldapjs.spec.ts | 50 + .../server/src/lib/stubs/speakeasy.spec.ts | 7 + .../matrix/server/src/lib/stubs/u2f.spec.ts | 16 + .../src/lib/utils/HashGenerator.spec.ts | 18 + .../server/src/lib/utils/HashGenerator.ts | 23 + .../server/src/lib/utils/ObjectCloner.ts | 6 + .../src/lib/utils/SafeRedirection.spec.ts | 33 + .../server/src/lib/utils/SafeRedirection.ts | 22 + .../src/lib/utils/URLDecomposer.spec.ts | 46 + .../server/src/lib/utils/URLDecomposer.ts | 15 + .../server/src/lib/web_server/Configurator.ts | 47 + .../server/src/lib/web_server/RestApi.ts | 125 + .../RequireValidatedFirstFactor.ts | 27 + .../middlewares/WithHeadersLogged.ts | 12 + .../server/src/resources/email-template.ejs | 254 + .../server/src/views/already-logged-in.pug | 14 + .../matrix/server/src/views/errors/.directory | 4 + themes/matrix/server/src/views/errors/401.pug | 16 + themes/matrix/server/src/views/errors/403.pug | 16 + themes/matrix/server/src/views/errors/404.pug | 11 + .../matrix/server/src/views/firstfactor.pug | 23 + .../matrix/server/src/views/layout/layout.pug | 30 + .../src/views/need-identity-validation.pug | 12 + .../server/src/views/password-reset-form.pug | 18 + .../src/views/password-reset-request.pug | 18 + .../matrix/server/src/views/secondfactor.pug | 31 + .../matrix/server/src/views/totp-register.pug | 25 + .../matrix/server/src/views/u2f-register.pug | 12 + themes/matrix/server/test/requests.ts | 94 + themes/matrix/server/tsconfig.json | 21 + themes/matrix/server/tslint.json | 60 + themes/matrix/server/types/.directory | 4 + .../server/types/AuthenticationSession.ts | 18 + themes/matrix/server/types/Dependencies.ts | 29 + themes/matrix/server/types/Identity.ts | 6 + themes/matrix/server/types/TOTPSecret.ts | 11 + themes/matrix/server/types/U2FRegistration.ts | 5 + themes/matrix/server/types/dovehash.d.ts | 4 + themes/matrix/server/types/speakeasy.d.ts | 96 + 543 files changed, 31521 insertions(+), 41 deletions(-) create mode 100644 themes/main/client/src/css/.directory create mode 100644 themes/main/client/src/css/00-bootstrap.min.css create mode 100644 themes/main/client/src/css/01-main.css create mode 100644 themes/main/client/src/css/02-login.css create mode 100644 themes/main/client/src/css/03-errors.css create mode 100644 themes/main/client/src/css/03-password-reset-form.css create mode 100644 themes/main/client/src/css/03-password-reset-request.css create mode 100644 themes/main/client/src/css/03-totp-register.css create mode 100644 themes/main/client/src/css/03-u2f-register.css create mode 100644 themes/main/client/src/img/background.svg create mode 100644 themes/main/client/src/img/icon.png create mode 100644 themes/main/client/src/img/mail.png create mode 100644 themes/main/client/src/img/notifications/.directory create mode 100644 themes/main/client/src/img/notifications/error.png create mode 100644 themes/main/client/src/img/notifications/info.png create mode 100644 themes/main/client/src/img/notifications/success.png create mode 100644 themes/main/client/src/img/notifications/warning.png create mode 100644 themes/main/client/src/img/padlock.png create mode 100644 themes/main/client/src/img/password.png create mode 100644 themes/main/client/src/img/pendrive.png create mode 100644 themes/main/client/src/img/stores/.directory create mode 100644 themes/main/client/src/img/stores/applestore-badge.svg create mode 100644 themes/main/client/src/img/stores/googleplay-badge.svg create mode 100644 themes/main/client/src/img/success.png create mode 100644 themes/main/client/src/img/user.png create mode 100644 themes/main/client/src/img/warning.png create mode 100644 themes/main/client/src/index.ts create mode 100644 themes/main/client/src/lib/GetPromised.ts create mode 100644 themes/main/client/src/lib/INotifier.ts create mode 100644 themes/main/client/src/lib/Notifier.ts create mode 100644 themes/main/client/src/lib/QueryParametersRetriever.ts create mode 100644 themes/main/client/src/lib/SafeRedirect.ts create mode 100644 themes/main/client/src/lib/firstfactor/FirstFactorValidator.ts create mode 100644 themes/main/client/src/lib/firstfactor/UISelectors.ts create mode 100644 themes/main/client/src/lib/firstfactor/index.ts create mode 100644 themes/main/client/src/lib/reset-password/constants.ts create mode 100644 themes/main/client/src/lib/reset-password/reset-password-form.ts create mode 100644 themes/main/client/src/lib/reset-password/reset-password-request.ts create mode 100644 themes/main/client/src/lib/secondfactor/TOTPValidator.ts create mode 100644 themes/main/client/src/lib/secondfactor/U2FValidator.ts create mode 100644 themes/main/client/src/lib/secondfactor/constants.ts create mode 100644 themes/main/client/src/lib/secondfactor/index.ts create mode 100644 themes/main/client/src/lib/totp-register/totp-register.ts create mode 100644 themes/main/client/src/lib/totp-register/ui-selector.ts create mode 100644 themes/main/client/src/lib/u2f-register/u2f-register.ts create mode 100644 themes/main/client/src/thirdparties/qrcode.min.js create mode 100644 themes/main/client/test/Notifier.test.ts create mode 100644 themes/main/client/test/firstfactor/FirstFactorValidator.test.ts create mode 100644 themes/main/client/test/mocks/NotifierStub.ts create mode 100644 themes/main/client/test/mocks/jquery.ts create mode 100644 themes/main/client/test/mocks/u2f-api.ts create mode 100644 themes/main/client/test/secondfactor/TOTPValidator.test.ts create mode 100644 themes/main/client/test/totp-register/totp-register.test.ts create mode 100644 themes/main/client/tsconfig.json create mode 100644 themes/main/client/tslint.json create mode 100644 themes/main/server/.directory create mode 100755 themes/main/server/src/index.ts create mode 100644 themes/main/server/src/lib/.directory create mode 100644 themes/main/server/src/lib/AuthenticationSessionHandler.ts create mode 100644 themes/main/server/src/lib/ErrorReplies.ts create mode 100644 themes/main/server/src/lib/Exceptions.ts create mode 100644 themes/main/server/src/lib/FirstFactorValidator.ts create mode 100644 themes/main/server/src/lib/IdentityCheckMiddleware.spec.ts create mode 100644 themes/main/server/src/lib/IdentityCheckMiddleware.ts create mode 100644 themes/main/server/src/lib/IdentityCheckPreValidationTemplate.ts create mode 100644 themes/main/server/src/lib/IdentityValidable.ts create mode 100644 themes/main/server/src/lib/IdentityValidableStub.spec.ts create mode 100644 themes/main/server/src/lib/Server.spec.ts create mode 100644 themes/main/server/src/lib/Server.ts create mode 100644 themes/main/server/src/lib/ServerVariables.ts create mode 100644 themes/main/server/src/lib/ServerVariablesInitializer.ts create mode 100644 themes/main/server/src/lib/ServerVariablesMockBuilder.spec.ts create mode 100644 themes/main/server/src/lib/authentication/Level.ts create mode 100644 themes/main/server/src/lib/authentication/backends/GroupsAndEmails.ts create mode 100644 themes/main/server/src/lib/authentication/backends/IUsersDatabase.ts create mode 100644 themes/main/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts create mode 100644 themes/main/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts create mode 100644 themes/main/server/src/lib/authentication/backends/file/FileUsersDatabase.ts create mode 100644 themes/main/server/src/lib/authentication/backends/file/ReadWriteQueue.ts create mode 100644 themes/main/server/src/lib/authentication/backends/ldap/ISession.ts create mode 100644 themes/main/server/src/lib/authentication/backends/ldap/ISessionFactory.ts create mode 100644 themes/main/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts create mode 100644 themes/main/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts create mode 100644 themes/main/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts create mode 100644 themes/main/server/src/lib/authentication/backends/ldap/SafeSession.ts create mode 100644 themes/main/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts create mode 100644 themes/main/server/src/lib/authentication/backends/ldap/Sanitizer.ts create mode 100644 themes/main/server/src/lib/authentication/backends/ldap/Session.spec.ts create mode 100644 themes/main/server/src/lib/authentication/backends/ldap/Session.ts create mode 100644 themes/main/server/src/lib/authentication/backends/ldap/SessionFactory.ts create mode 100644 themes/main/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts create mode 100644 themes/main/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts create mode 100644 themes/main/server/src/lib/authentication/backends/ldap/connector/Connector.ts create mode 100644 themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts create mode 100644 themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts create mode 100644 themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts create mode 100644 themes/main/server/src/lib/authentication/backends/ldap/connector/IConnector.ts create mode 100644 themes/main/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts create mode 100644 themes/main/server/src/lib/authentication/totp/ITotpHandler.ts create mode 100644 themes/main/server/src/lib/authentication/totp/TotpHandler.spec.ts create mode 100644 themes/main/server/src/lib/authentication/totp/TotpHandler.ts create mode 100644 themes/main/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts create mode 100644 themes/main/server/src/lib/authentication/u2f/IU2fHandler.ts create mode 100644 themes/main/server/src/lib/authentication/u2f/U2fHandler.ts create mode 100644 themes/main/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts create mode 100644 themes/main/server/src/lib/authorization/Authorizer.spec.ts create mode 100644 themes/main/server/src/lib/authorization/Authorizer.ts create mode 100644 themes/main/server/src/lib/authorization/AuthorizerStub.spec.ts create mode 100644 themes/main/server/src/lib/authorization/IAuthorizer.ts create mode 100644 themes/main/server/src/lib/authorization/Level.ts create mode 100644 themes/main/server/src/lib/authorization/MultipleDomainMatcher.ts create mode 100644 themes/main/server/src/lib/authorization/Object.ts create mode 100644 themes/main/server/src/lib/authorization/Subject.ts create mode 100644 themes/main/server/src/lib/configuration/ConfigurationParser.spec.ts create mode 100644 themes/main/server/src/lib/configuration/ConfigurationParser.ts create mode 100644 themes/main/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts create mode 100644 themes/main/server/src/lib/configuration/SessionConfigurationBuilder.ts create mode 100644 themes/main/server/src/lib/configuration/schema/AclConfiguration.spec.ts create mode 100644 themes/main/server/src/lib/configuration/schema/AclConfiguration.ts create mode 100644 themes/main/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts create mode 100644 themes/main/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts create mode 100644 themes/main/server/src/lib/configuration/schema/Configuration.ts create mode 100644 themes/main/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts create mode 100644 themes/main/server/src/lib/configuration/schema/LdapConfiguration.spec.ts create mode 100644 themes/main/server/src/lib/configuration/schema/LdapConfiguration.ts create mode 100644 themes/main/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts create mode 100644 themes/main/server/src/lib/configuration/schema/NotifierConfiguration.ts create mode 100644 themes/main/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts create mode 100644 themes/main/server/src/lib/configuration/schema/RegulationConfiguration.ts create mode 100644 themes/main/server/src/lib/configuration/schema/SessionConfiguration.spec.ts create mode 100644 themes/main/server/src/lib/configuration/schema/SessionConfiguration.ts create mode 100644 themes/main/server/src/lib/configuration/schema/StorageConfiguration.spec.ts create mode 100644 themes/main/server/src/lib/configuration/schema/StorageConfiguration.ts create mode 100644 themes/main/server/src/lib/configuration/schema/TotpConfiguration.ts create mode 100644 themes/main/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts create mode 100644 themes/main/server/src/lib/connectors/mongo/IMongoClient.d.ts create mode 100644 themes/main/server/src/lib/connectors/mongo/MongoClient.spec.ts create mode 100644 themes/main/server/src/lib/connectors/mongo/MongoClient.ts create mode 100644 themes/main/server/src/lib/connectors/mongo/MongoClientStub.spec.ts create mode 100644 themes/main/server/src/lib/logging/GlobalLogger.ts create mode 100644 themes/main/server/src/lib/logging/GlobalLoggerStub.spec.ts create mode 100644 themes/main/server/src/lib/logging/IGlobalLogger.ts create mode 100644 themes/main/server/src/lib/logging/IRequestLogger.ts create mode 100644 themes/main/server/src/lib/logging/RequestLogger.ts create mode 100644 themes/main/server/src/lib/logging/RequestLoggerStub.spec.ts create mode 100644 themes/main/server/src/lib/notifiers/AbstractEmailNotifier.ts create mode 100644 themes/main/server/src/lib/notifiers/EmailNotifier.spec.ts create mode 100644 themes/main/server/src/lib/notifiers/EmailNotifier.ts create mode 100644 themes/main/server/src/lib/notifiers/FileSystemNotifier.ts create mode 100644 themes/main/server/src/lib/notifiers/IMailSender.ts create mode 100644 themes/main/server/src/lib/notifiers/IMailSenderBuilder.ts create mode 100644 themes/main/server/src/lib/notifiers/INotifier.ts create mode 100644 themes/main/server/src/lib/notifiers/MailSender.ts create mode 100644 themes/main/server/src/lib/notifiers/MailSenderBuilder.spec.ts create mode 100644 themes/main/server/src/lib/notifiers/MailSenderBuilder.ts create mode 100644 themes/main/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts create mode 100644 themes/main/server/src/lib/notifiers/MailSenderStub.spec.ts create mode 100644 themes/main/server/src/lib/notifiers/NotifierFactory.spec.ts create mode 100644 themes/main/server/src/lib/notifiers/NotifierFactory.ts create mode 100644 themes/main/server/src/lib/notifiers/NotifierStub.spec.ts create mode 100644 themes/main/server/src/lib/notifiers/SmtpNotifier.ts create mode 100644 themes/main/server/src/lib/regulation/IRegulator.ts create mode 100644 themes/main/server/src/lib/regulation/Regulator.spec.ts create mode 100644 themes/main/server/src/lib/regulation/Regulator.ts create mode 100644 themes/main/server/src/lib/regulation/RegulatorStub.spec.ts create mode 100644 themes/main/server/src/lib/routes/error/401/get.spec.ts create mode 100644 themes/main/server/src/lib/routes/error/401/get.ts create mode 100644 themes/main/server/src/lib/routes/error/403/get.spec.ts create mode 100644 themes/main/server/src/lib/routes/error/403/get.ts create mode 100644 themes/main/server/src/lib/routes/error/404/get.spec.ts create mode 100644 themes/main/server/src/lib/routes/error/404/get.ts create mode 100644 themes/main/server/src/lib/routes/error/redirector.ts create mode 100644 themes/main/server/src/lib/routes/firstfactor/get.ts create mode 100644 themes/main/server/src/lib/routes/firstfactor/post.spec.ts create mode 100644 themes/main/server/src/lib/routes/firstfactor/post.ts create mode 100644 themes/main/server/src/lib/routes/loggedin/get.ts create mode 100644 themes/main/server/src/lib/routes/logout/get.ts create mode 100644 themes/main/server/src/lib/routes/password-reset/constants.ts create mode 100644 themes/main/server/src/lib/routes/password-reset/form/post.spec.ts create mode 100644 themes/main/server/src/lib/routes/password-reset/form/post.ts create mode 100644 themes/main/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts create mode 100644 themes/main/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts create mode 100644 themes/main/server/src/lib/routes/password-reset/request/get.ts create mode 100644 themes/main/server/src/lib/routes/secondfactor/get.spec.ts create mode 100644 themes/main/server/src/lib/routes/secondfactor/get.ts create mode 100644 themes/main/server/src/lib/routes/secondfactor/redirect.spec.ts create mode 100644 themes/main/server/src/lib/routes/secondfactor/redirect.ts create mode 100644 themes/main/server/src/lib/routes/secondfactor/totp/constants.ts create mode 100644 themes/main/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts create mode 100644 themes/main/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts create mode 100644 themes/main/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts create mode 100644 themes/main/server/src/lib/routes/secondfactor/totp/sign/post.ts create mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts create mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts create mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts create mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts create mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/register/post.ts create mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts create mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/register_request/get.ts create mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts create mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/sign/post.ts create mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts create mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts create mode 100644 themes/main/server/src/lib/routes/verify/access_control.ts create mode 100644 themes/main/server/src/lib/routes/verify/get.spec.ts create mode 100644 themes/main/server/src/lib/routes/verify/get.ts create mode 100644 themes/main/server/src/lib/routes/verify/get_basic_auth.ts create mode 100644 themes/main/server/src/lib/routes/verify/get_session_cookie.ts create mode 100644 themes/main/server/src/lib/storage/AuthenticationTraceDocument.d.ts create mode 100644 themes/main/server/src/lib/storage/CollectionFactoryFactory.ts create mode 100644 themes/main/server/src/lib/storage/CollectionFactoryStub.spec.ts create mode 100644 themes/main/server/src/lib/storage/CollectionStub.spec.ts create mode 100644 themes/main/server/src/lib/storage/ICollection.d.ts create mode 100644 themes/main/server/src/lib/storage/ICollectionFactory.d.ts create mode 100644 themes/main/server/src/lib/storage/IUserDataStore.d.ts create mode 100644 themes/main/server/src/lib/storage/IdentityValidationDocument.d.ts create mode 100644 themes/main/server/src/lib/storage/TOTPSecretDocument.d.ts create mode 100644 themes/main/server/src/lib/storage/U2FRegistrationDocument.d.ts create mode 100644 themes/main/server/src/lib/storage/UserDataStore.spec.ts create mode 100644 themes/main/server/src/lib/storage/UserDataStore.ts create mode 100644 themes/main/server/src/lib/storage/UserDataStoreStub.spec.ts create mode 100644 themes/main/server/src/lib/storage/mongo/MongoCollection.spec.ts create mode 100644 themes/main/server/src/lib/storage/mongo/MongoCollection.ts create mode 100644 themes/main/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts create mode 100644 themes/main/server/src/lib/storage/mongo/MongoCollectionFactory.ts create mode 100644 themes/main/server/src/lib/storage/nedb/NedbCollection.spec.ts create mode 100644 themes/main/server/src/lib/storage/nedb/NedbCollection.ts create mode 100644 themes/main/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts create mode 100644 themes/main/server/src/lib/storage/nedb/NedbCollectionFactory.ts create mode 100644 themes/main/server/src/lib/stubs/express.spec.ts create mode 100644 themes/main/server/src/lib/stubs/ldapjs.spec.ts create mode 100644 themes/main/server/src/lib/stubs/speakeasy.spec.ts create mode 100644 themes/main/server/src/lib/stubs/u2f.spec.ts create mode 100644 themes/main/server/src/lib/utils/HashGenerator.spec.ts create mode 100644 themes/main/server/src/lib/utils/HashGenerator.ts create mode 100644 themes/main/server/src/lib/utils/ObjectCloner.ts create mode 100644 themes/main/server/src/lib/utils/SafeRedirection.spec.ts create mode 100644 themes/main/server/src/lib/utils/SafeRedirection.ts create mode 100644 themes/main/server/src/lib/utils/URLDecomposer.spec.ts create mode 100644 themes/main/server/src/lib/utils/URLDecomposer.ts create mode 100644 themes/main/server/src/lib/web_server/Configurator.ts create mode 100644 themes/main/server/src/lib/web_server/RestApi.ts create mode 100644 themes/main/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts create mode 100644 themes/main/server/src/lib/web_server/middlewares/WithHeadersLogged.ts create mode 100644 themes/main/server/src/resources/email-template.ejs create mode 100644 themes/main/server/src/views/already-logged-in.pug create mode 100644 themes/main/server/src/views/errors/.directory create mode 100644 themes/main/server/src/views/errors/401.pug create mode 100644 themes/main/server/src/views/errors/403.pug create mode 100644 themes/main/server/src/views/errors/404.pug create mode 100644 themes/main/server/src/views/firstfactor.pug create mode 100644 themes/main/server/src/views/layout/layout.pug create mode 100644 themes/main/server/src/views/need-identity-validation.pug create mode 100644 themes/main/server/src/views/password-reset-form.pug create mode 100644 themes/main/server/src/views/password-reset-request.pug create mode 100644 themes/main/server/src/views/secondfactor.pug create mode 100644 themes/main/server/src/views/totp-register.pug create mode 100644 themes/main/server/src/views/u2f-register.pug create mode 100644 themes/main/server/test/requests.ts create mode 100644 themes/main/server/tsconfig.json create mode 100644 themes/main/server/tslint.json create mode 100644 themes/main/server/types/.directory create mode 100644 themes/main/server/types/AuthenticationSession.ts create mode 100644 themes/main/server/types/Dependencies.ts create mode 100644 themes/main/server/types/Identity.ts create mode 100644 themes/main/server/types/TOTPSecret.ts create mode 100644 themes/main/server/types/U2FRegistration.ts create mode 100644 themes/main/server/types/dovehash.d.ts create mode 100644 themes/main/server/types/speakeasy.d.ts create mode 100644 themes/matrix/client/src/css/.directory create mode 100644 themes/matrix/client/src/css/00-bootstrap.min.css create mode 100644 themes/matrix/client/src/css/01-main.css create mode 100644 themes/matrix/client/src/css/02-login.css create mode 100644 themes/matrix/client/src/css/03-errors.css create mode 100644 themes/matrix/client/src/css/03-password-reset-form.css create mode 100644 themes/matrix/client/src/css/03-password-reset-request.css create mode 100644 themes/matrix/client/src/css/03-totp-register.css create mode 100644 themes/matrix/client/src/css/03-u2f-register.css create mode 100644 themes/matrix/client/src/img/background.jpg create mode 100644 themes/matrix/client/src/img/icon.png create mode 100644 themes/matrix/client/src/img/mail.png create mode 100644 themes/matrix/client/src/img/matrix_circle_128x128.png create mode 100644 themes/matrix/client/src/img/notifications/.directory create mode 100644 themes/matrix/client/src/img/notifications/error.png create mode 100644 themes/matrix/client/src/img/notifications/info.png create mode 100644 themes/matrix/client/src/img/notifications/success.png create mode 100644 themes/matrix/client/src/img/notifications/warning.png create mode 100644 themes/matrix/client/src/img/padlock.png create mode 100644 themes/matrix/client/src/img/password_white.png create mode 100644 themes/matrix/client/src/img/pendrive.png create mode 100644 themes/matrix/client/src/img/stores/.directory create mode 100644 themes/matrix/client/src/img/stores/applestore-badge.svg create mode 100644 themes/matrix/client/src/img/stores/googleplay-badge.svg create mode 100644 themes/matrix/client/src/img/success.png create mode 100644 themes/matrix/client/src/img/user.png create mode 100644 themes/matrix/client/src/img/warning.png create mode 100644 themes/matrix/client/src/index.ts create mode 100644 themes/matrix/client/src/lib/GetPromised.ts create mode 100644 themes/matrix/client/src/lib/INotifier.ts create mode 100644 themes/matrix/client/src/lib/Notifier.ts create mode 100644 themes/matrix/client/src/lib/QueryParametersRetriever.ts create mode 100644 themes/matrix/client/src/lib/SafeRedirect.ts create mode 100644 themes/matrix/client/src/lib/firstfactor/FirstFactorValidator.ts create mode 100644 themes/matrix/client/src/lib/firstfactor/UISelectors.ts create mode 100644 themes/matrix/client/src/lib/firstfactor/index.ts create mode 100644 themes/matrix/client/src/lib/reset-password/constants.ts create mode 100644 themes/matrix/client/src/lib/reset-password/reset-password-form.ts create mode 100644 themes/matrix/client/src/lib/reset-password/reset-password-request.ts create mode 100644 themes/matrix/client/src/lib/secondfactor/TOTPValidator.ts create mode 100644 themes/matrix/client/src/lib/secondfactor/U2FValidator.ts create mode 100644 themes/matrix/client/src/lib/secondfactor/constants.ts create mode 100644 themes/matrix/client/src/lib/secondfactor/index.ts create mode 100644 themes/matrix/client/src/lib/totp-register/totp-register.ts create mode 100644 themes/matrix/client/src/lib/totp-register/ui-selector.ts create mode 100644 themes/matrix/client/src/lib/u2f-register/u2f-register.ts create mode 100644 themes/matrix/client/src/thirdparties/matrix.js create mode 100644 themes/matrix/client/src/thirdparties/qrcode.min.js create mode 100644 themes/matrix/client/src/thirdparties/u2f-api.js create mode 100644 themes/matrix/client/test/Notifier.test.ts create mode 100644 themes/matrix/client/test/firstfactor/FirstFactorValidator.test.ts create mode 100644 themes/matrix/client/test/mocks/NotifierStub.ts create mode 100644 themes/matrix/client/test/mocks/jquery.ts create mode 100644 themes/matrix/client/test/mocks/u2f-api.ts create mode 100644 themes/matrix/client/test/secondfactor/TOTPValidator.test.ts create mode 100644 themes/matrix/client/test/totp-register/totp-register.test.ts create mode 100644 themes/matrix/client/tsconfig.json create mode 100644 themes/matrix/client/tslint.json create mode 100644 themes/matrix/server/.directory create mode 100755 themes/matrix/server/src/index.ts create mode 100644 themes/matrix/server/src/lib/.directory create mode 100644 themes/matrix/server/src/lib/AuthenticationSessionHandler.ts create mode 100644 themes/matrix/server/src/lib/ErrorReplies.ts create mode 100644 themes/matrix/server/src/lib/Exceptions.ts create mode 100644 themes/matrix/server/src/lib/FirstFactorValidator.ts create mode 100644 themes/matrix/server/src/lib/IdentityCheckMiddleware.spec.ts create mode 100644 themes/matrix/server/src/lib/IdentityCheckMiddleware.ts create mode 100644 themes/matrix/server/src/lib/IdentityCheckPreValidationTemplate.ts create mode 100644 themes/matrix/server/src/lib/IdentityValidable.ts create mode 100644 themes/matrix/server/src/lib/IdentityValidableStub.spec.ts create mode 100644 themes/matrix/server/src/lib/Server.spec.ts create mode 100644 themes/matrix/server/src/lib/Server.ts create mode 100644 themes/matrix/server/src/lib/ServerVariables.ts create mode 100644 themes/matrix/server/src/lib/ServerVariablesInitializer.ts create mode 100644 themes/matrix/server/src/lib/ServerVariablesMockBuilder.spec.ts create mode 100644 themes/matrix/server/src/lib/authentication/Level.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/GroupsAndEmails.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/IUsersDatabase.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/file/FileUsersDatabase.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/file/ReadWriteQueue.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/ISession.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/ISessionFactory.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/SafeSession.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/Sanitizer.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/Session.spec.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/Session.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/SessionFactory.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/connector/Connector.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/connector/IConnector.ts create mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts create mode 100644 themes/matrix/server/src/lib/authentication/totp/ITotpHandler.ts create mode 100644 themes/matrix/server/src/lib/authentication/totp/TotpHandler.spec.ts create mode 100644 themes/matrix/server/src/lib/authentication/totp/TotpHandler.ts create mode 100644 themes/matrix/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts create mode 100644 themes/matrix/server/src/lib/authentication/u2f/IU2fHandler.ts create mode 100644 themes/matrix/server/src/lib/authentication/u2f/U2fHandler.ts create mode 100644 themes/matrix/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts create mode 100644 themes/matrix/server/src/lib/authorization/Authorizer.spec.ts create mode 100644 themes/matrix/server/src/lib/authorization/Authorizer.ts create mode 100644 themes/matrix/server/src/lib/authorization/AuthorizerStub.spec.ts create mode 100644 themes/matrix/server/src/lib/authorization/IAuthorizer.ts create mode 100644 themes/matrix/server/src/lib/authorization/Level.ts create mode 100644 themes/matrix/server/src/lib/authorization/MultipleDomainMatcher.ts create mode 100644 themes/matrix/server/src/lib/authorization/Object.ts create mode 100644 themes/matrix/server/src/lib/authorization/Subject.ts create mode 100644 themes/matrix/server/src/lib/configuration/ConfigurationParser.spec.ts create mode 100644 themes/matrix/server/src/lib/configuration/ConfigurationParser.ts create mode 100644 themes/matrix/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts create mode 100644 themes/matrix/server/src/lib/configuration/SessionConfigurationBuilder.ts create mode 100644 themes/matrix/server/src/lib/configuration/schema/AclConfiguration.spec.ts create mode 100644 themes/matrix/server/src/lib/configuration/schema/AclConfiguration.ts create mode 100644 themes/matrix/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts create mode 100644 themes/matrix/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts create mode 100644 themes/matrix/server/src/lib/configuration/schema/Configuration.ts create mode 100644 themes/matrix/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts create mode 100644 themes/matrix/server/src/lib/configuration/schema/LdapConfiguration.spec.ts create mode 100644 themes/matrix/server/src/lib/configuration/schema/LdapConfiguration.ts create mode 100644 themes/matrix/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts create mode 100644 themes/matrix/server/src/lib/configuration/schema/NotifierConfiguration.ts create mode 100644 themes/matrix/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts create mode 100644 themes/matrix/server/src/lib/configuration/schema/RegulationConfiguration.ts create mode 100644 themes/matrix/server/src/lib/configuration/schema/SessionConfiguration.spec.ts create mode 100644 themes/matrix/server/src/lib/configuration/schema/SessionConfiguration.ts create mode 100644 themes/matrix/server/src/lib/configuration/schema/StorageConfiguration.spec.ts create mode 100644 themes/matrix/server/src/lib/configuration/schema/StorageConfiguration.ts create mode 100644 themes/matrix/server/src/lib/configuration/schema/TotpConfiguration.ts create mode 100644 themes/matrix/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts create mode 100644 themes/matrix/server/src/lib/connectors/mongo/IMongoClient.d.ts create mode 100644 themes/matrix/server/src/lib/connectors/mongo/MongoClient.spec.ts create mode 100644 themes/matrix/server/src/lib/connectors/mongo/MongoClient.ts create mode 100644 themes/matrix/server/src/lib/connectors/mongo/MongoClientStub.spec.ts create mode 100644 themes/matrix/server/src/lib/logging/GlobalLogger.ts create mode 100644 themes/matrix/server/src/lib/logging/GlobalLoggerStub.spec.ts create mode 100644 themes/matrix/server/src/lib/logging/IGlobalLogger.ts create mode 100644 themes/matrix/server/src/lib/logging/IRequestLogger.ts create mode 100644 themes/matrix/server/src/lib/logging/RequestLogger.ts create mode 100644 themes/matrix/server/src/lib/logging/RequestLoggerStub.spec.ts create mode 100644 themes/matrix/server/src/lib/notifiers/AbstractEmailNotifier.ts create mode 100644 themes/matrix/server/src/lib/notifiers/EmailNotifier.spec.ts create mode 100644 themes/matrix/server/src/lib/notifiers/EmailNotifier.ts create mode 100644 themes/matrix/server/src/lib/notifiers/FileSystemNotifier.ts create mode 100644 themes/matrix/server/src/lib/notifiers/IMailSender.ts create mode 100644 themes/matrix/server/src/lib/notifiers/IMailSenderBuilder.ts create mode 100644 themes/matrix/server/src/lib/notifiers/INotifier.ts create mode 100644 themes/matrix/server/src/lib/notifiers/MailSender.ts create mode 100644 themes/matrix/server/src/lib/notifiers/MailSenderBuilder.spec.ts create mode 100644 themes/matrix/server/src/lib/notifiers/MailSenderBuilder.ts create mode 100644 themes/matrix/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts create mode 100644 themes/matrix/server/src/lib/notifiers/MailSenderStub.spec.ts create mode 100644 themes/matrix/server/src/lib/notifiers/NotifierFactory.spec.ts create mode 100644 themes/matrix/server/src/lib/notifiers/NotifierFactory.ts create mode 100644 themes/matrix/server/src/lib/notifiers/NotifierStub.spec.ts create mode 100644 themes/matrix/server/src/lib/notifiers/SmtpNotifier.ts create mode 100644 themes/matrix/server/src/lib/regulation/IRegulator.ts create mode 100644 themes/matrix/server/src/lib/regulation/Regulator.spec.ts create mode 100644 themes/matrix/server/src/lib/regulation/Regulator.ts create mode 100644 themes/matrix/server/src/lib/regulation/RegulatorStub.spec.ts create mode 100644 themes/matrix/server/src/lib/routes/error/401/get.spec.ts create mode 100644 themes/matrix/server/src/lib/routes/error/401/get.ts create mode 100644 themes/matrix/server/src/lib/routes/error/403/get.spec.ts create mode 100644 themes/matrix/server/src/lib/routes/error/403/get.ts create mode 100644 themes/matrix/server/src/lib/routes/error/404/get.spec.ts create mode 100644 themes/matrix/server/src/lib/routes/error/404/get.ts create mode 100644 themes/matrix/server/src/lib/routes/error/redirector.ts create mode 100644 themes/matrix/server/src/lib/routes/firstfactor/get.ts create mode 100644 themes/matrix/server/src/lib/routes/firstfactor/post.spec.ts create mode 100644 themes/matrix/server/src/lib/routes/firstfactor/post.ts create mode 100644 themes/matrix/server/src/lib/routes/loggedin/get.ts create mode 100644 themes/matrix/server/src/lib/routes/logout/get.ts create mode 100644 themes/matrix/server/src/lib/routes/password-reset/constants.ts create mode 100644 themes/matrix/server/src/lib/routes/password-reset/form/post.spec.ts create mode 100644 themes/matrix/server/src/lib/routes/password-reset/form/post.ts create mode 100644 themes/matrix/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts create mode 100644 themes/matrix/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts create mode 100644 themes/matrix/server/src/lib/routes/password-reset/request/get.ts create mode 100644 themes/matrix/server/src/lib/routes/secondfactor/get.spec.ts create mode 100644 themes/matrix/server/src/lib/routes/secondfactor/get.ts create mode 100644 themes/matrix/server/src/lib/routes/secondfactor/redirect.spec.ts create mode 100644 themes/matrix/server/src/lib/routes/secondfactor/redirect.ts create mode 100644 themes/matrix/server/src/lib/routes/secondfactor/totp/constants.ts create mode 100644 themes/matrix/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts create mode 100644 themes/matrix/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts create mode 100644 themes/matrix/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts create mode 100644 themes/matrix/server/src/lib/routes/secondfactor/totp/sign/post.ts create mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts create mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts create mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts create mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts create mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/register/post.ts create mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts create mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/register_request/get.ts create mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts create mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/sign/post.ts create mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts create mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts create mode 100644 themes/matrix/server/src/lib/routes/verify/access_control.ts create mode 100644 themes/matrix/server/src/lib/routes/verify/get.spec.ts create mode 100644 themes/matrix/server/src/lib/routes/verify/get.ts create mode 100644 themes/matrix/server/src/lib/routes/verify/get_basic_auth.ts create mode 100644 themes/matrix/server/src/lib/routes/verify/get_session_cookie.ts create mode 100644 themes/matrix/server/src/lib/storage/AuthenticationTraceDocument.d.ts create mode 100644 themes/matrix/server/src/lib/storage/CollectionFactoryFactory.ts create mode 100644 themes/matrix/server/src/lib/storage/CollectionFactoryStub.spec.ts create mode 100644 themes/matrix/server/src/lib/storage/CollectionStub.spec.ts create mode 100644 themes/matrix/server/src/lib/storage/ICollection.d.ts create mode 100644 themes/matrix/server/src/lib/storage/ICollectionFactory.d.ts create mode 100644 themes/matrix/server/src/lib/storage/IUserDataStore.d.ts create mode 100644 themes/matrix/server/src/lib/storage/IdentityValidationDocument.d.ts create mode 100644 themes/matrix/server/src/lib/storage/TOTPSecretDocument.d.ts create mode 100644 themes/matrix/server/src/lib/storage/U2FRegistrationDocument.d.ts create mode 100644 themes/matrix/server/src/lib/storage/UserDataStore.spec.ts create mode 100644 themes/matrix/server/src/lib/storage/UserDataStore.ts create mode 100644 themes/matrix/server/src/lib/storage/UserDataStoreStub.spec.ts create mode 100644 themes/matrix/server/src/lib/storage/mongo/MongoCollection.spec.ts create mode 100644 themes/matrix/server/src/lib/storage/mongo/MongoCollection.ts create mode 100644 themes/matrix/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts create mode 100644 themes/matrix/server/src/lib/storage/mongo/MongoCollectionFactory.ts create mode 100644 themes/matrix/server/src/lib/storage/nedb/NedbCollection.spec.ts create mode 100644 themes/matrix/server/src/lib/storage/nedb/NedbCollection.ts create mode 100644 themes/matrix/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts create mode 100644 themes/matrix/server/src/lib/storage/nedb/NedbCollectionFactory.ts create mode 100644 themes/matrix/server/src/lib/stubs/express.spec.ts create mode 100644 themes/matrix/server/src/lib/stubs/ldapjs.spec.ts create mode 100644 themes/matrix/server/src/lib/stubs/speakeasy.spec.ts create mode 100644 themes/matrix/server/src/lib/stubs/u2f.spec.ts create mode 100644 themes/matrix/server/src/lib/utils/HashGenerator.spec.ts create mode 100644 themes/matrix/server/src/lib/utils/HashGenerator.ts create mode 100644 themes/matrix/server/src/lib/utils/ObjectCloner.ts create mode 100644 themes/matrix/server/src/lib/utils/SafeRedirection.spec.ts create mode 100644 themes/matrix/server/src/lib/utils/SafeRedirection.ts create mode 100644 themes/matrix/server/src/lib/utils/URLDecomposer.spec.ts create mode 100644 themes/matrix/server/src/lib/utils/URLDecomposer.ts create mode 100644 themes/matrix/server/src/lib/web_server/Configurator.ts create mode 100644 themes/matrix/server/src/lib/web_server/RestApi.ts create mode 100644 themes/matrix/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts create mode 100644 themes/matrix/server/src/lib/web_server/middlewares/WithHeadersLogged.ts create mode 100644 themes/matrix/server/src/resources/email-template.ejs create mode 100644 themes/matrix/server/src/views/already-logged-in.pug create mode 100644 themes/matrix/server/src/views/errors/.directory create mode 100644 themes/matrix/server/src/views/errors/401.pug create mode 100644 themes/matrix/server/src/views/errors/403.pug create mode 100644 themes/matrix/server/src/views/errors/404.pug create mode 100644 themes/matrix/server/src/views/firstfactor.pug create mode 100644 themes/matrix/server/src/views/layout/layout.pug create mode 100644 themes/matrix/server/src/views/need-identity-validation.pug create mode 100644 themes/matrix/server/src/views/password-reset-form.pug create mode 100644 themes/matrix/server/src/views/password-reset-request.pug create mode 100644 themes/matrix/server/src/views/secondfactor.pug create mode 100644 themes/matrix/server/src/views/totp-register.pug create mode 100644 themes/matrix/server/src/views/u2f-register.pug create mode 100644 themes/matrix/server/test/requests.ts create mode 100644 themes/matrix/server/tsconfig.json create mode 100644 themes/matrix/server/tslint.json create mode 100644 themes/matrix/server/types/.directory create mode 100644 themes/matrix/server/types/AuthenticationSession.ts create mode 100644 themes/matrix/server/types/Dependencies.ts create mode 100644 themes/matrix/server/types/Identity.ts create mode 100644 themes/matrix/server/types/TOTPSecret.ts create mode 100644 themes/matrix/server/types/U2FRegistration.ts create mode 100644 themes/matrix/server/types/dovehash.d.ts create mode 100644 themes/matrix/server/types/speakeasy.d.ts diff --git a/Gruntfile.js b/Gruntfile.js index f8b33fd11..5fdbb88d1 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -82,34 +82,58 @@ module.exports = function (grunt) { } }, copy: { - resources: { - expand: true, - cwd: 'server/src/resources/', - src: '**', - dest: `${buildDir}/server/src/resources/` - }, - views: { - expand: true, - cwd: 'server/src/views/', - src: '**', - dest: `${buildDir}/server/src/views/` - }, - images: { - expand: true, - cwd: 'client/src/img', - src: '**', - dest: `${buildDir}/server/src/public_html/img/` - }, - thirdparties: { - expand: true, - cwd: 'client/src/thirdparties', - src: '**', - dest: `${buildDir}/server/src/public_html/js/` - }, - schema: { - src: schemaDir, - dest: `${buildDir}/${schemaDir}` - } + main_resources: { + expand: true, + cwd: 'themes/main/server/src/resources', + src: '**', + dest: `${buildDir}/server/src/resources/` + }, + main_views: { + expand: true, + cwd: 'themes/main/server/src/views', + src: '**', + dest: `${buildDir}/server/src/views/` + }, + main_images: { + expand: true, + cwd: 'themes/main/client/src/img', + src: '**', + dest: `${buildDir}/server/src/public_html/img/` + }, + main_thirdparties: { + expand: true, + 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: '**', + dest: `${buildDir}/server/src/public_html/js/` + }, + schema: { + src: schemaDir, + dest: `${buildDir}/${schemaDir}` + } }, browserify: { dist: { @@ -173,8 +197,14 @@ module.exports = function (grunt) { } }, concat: { - css: { - src: ['client/src/css/*.css'], + main_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` }, }, @@ -187,6 +217,8 @@ module.exports = function (grunt) { } }); + var target = grunt.option('target') || 'main'; + grunt.loadNpmTasks('grunt-browserify'); grunt.loadNpmTasks('grunt-contrib-concat'); 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-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('copy-resources-matrix', ['copy:matrix_resources', 'copy:matrix_views', 'copy:matrix_images', 'copy:matrix_thirdparties', 'concat:matrix_css']); + grunt.registerTask('build-client', ['compile-client', 'browserify']); - grunt.registerTask('build-server', ['compile-server', 'copy-resources', 'generate-config-schema']); - - grunt.registerTask('build', ['build-client', 'build-server']); + 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('schema', ['run:generate-config-schema']) diff --git a/package-lock.json b/package-lock.json index ddcc621e4..358ff9980 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2971,12 +2971,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2991,17 +2993,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3118,7 +3123,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3130,6 +3136,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3144,6 +3151,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3151,12 +3159,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -3175,6 +3185,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3255,7 +3266,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3267,6 +3279,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3388,6 +3401,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", diff --git a/themes/main/client/src/css/.directory b/themes/main/client/src/css/.directory new file mode 100644 index 000000000..eca81829c --- /dev/null +++ b/themes/main/client/src/css/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,56,39 +Version=3 +ViewMode=1 diff --git a/themes/main/client/src/css/00-bootstrap.min.css b/themes/main/client/src/css/00-bootstrap.min.css new file mode 100644 index 000000000..ed3905e0e --- /dev/null +++ b/themes/main/client/src/css/00-bootstrap.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap v3.3.7 (http://getbootstrap.com) + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/themes/main/client/src/css/01-main.css b/themes/main/client/src/css/01-main.css new file mode 100644 index 000000000..ead0852a7 --- /dev/null +++ b/themes/main/client/src/css/01-main.css @@ -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; +} diff --git a/themes/main/client/src/css/02-login.css b/themes/main/client/src/css/02-login.css new file mode 100644 index 000000000..aa59733d3 --- /dev/null +++ b/themes/main/client/src/css/02-login.css @@ -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 */ +} \ No newline at end of file diff --git a/themes/main/client/src/css/03-errors.css b/themes/main/client/src/css/03-errors.css new file mode 100644 index 000000000..e9f97f33b --- /dev/null +++ b/themes/main/client/src/css/03-errors.css @@ -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%; +} \ No newline at end of file diff --git a/themes/main/client/src/css/03-password-reset-form.css b/themes/main/client/src/css/03-password-reset-form.css new file mode 100644 index 000000000..34066bc24 --- /dev/null +++ b/themes/main/client/src/css/03-password-reset-form.css @@ -0,0 +1,4 @@ + +.password-reset-form .header-img { + border-radius: 0%; +} diff --git a/themes/main/client/src/css/03-password-reset-request.css b/themes/main/client/src/css/03-password-reset-request.css new file mode 100644 index 000000000..1a2ad4df8 --- /dev/null +++ b/themes/main/client/src/css/03-password-reset-request.css @@ -0,0 +1,4 @@ + +.password-reset-request .header-img { + border-radius: 0%; +} diff --git a/themes/main/client/src/css/03-totp-register.css b/themes/main/client/src/css/03-totp-register.css new file mode 100644 index 000000000..cb76720ad --- /dev/null +++ b/themes/main/client/src/css/03-totp-register.css @@ -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; +} \ No newline at end of file diff --git a/themes/main/client/src/css/03-u2f-register.css b/themes/main/client/src/css/03-u2f-register.css new file mode 100644 index 000000000..e54cddf89 --- /dev/null +++ b/themes/main/client/src/css/03-u2f-register.css @@ -0,0 +1,5 @@ + +.u2f-register img { + display: block; + margin: 20px auto; +} \ No newline at end of file diff --git a/themes/main/client/src/img/background.svg b/themes/main/client/src/img/background.svg new file mode 100644 index 000000000..93b00339b --- /dev/null +++ b/themes/main/client/src/img/background.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/themes/main/client/src/img/icon.png b/themes/main/client/src/img/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..040d10c1ab5feaf6720fa8e8c0a0aa336402894e GIT binary patch literal 1461 zcmV;m1xosfP)e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00kXML_t(o!_`+?Y*bYgUHhE- zn3=ZI7qzh{%0mzcVu-|44KcBlMg%4JNF)*ye@ry-m;Ooo@C(p{@FNm6K%#tzfV8Dp z(gHC?u?eAyEkY@6v9v&$K4#|5J!kuI=XL2!XD|}uPVUd$_w2p)Ui)z_@E;5PlS6Y? zzD$W&;aqBgCCWIgr3%#B17TU=LfaFniBfPfGHk@o-q0}>pSg3elo_&jXcox?pxuirC( z+64_w<1wcpt~Ct9s1v|4#r%X~rnEyMkPy>M!hxWQY!F}#Xodt_W_^yC&$ChylYpwS z&=(^YTQB4VP`#+(neoZ!X+^?|Fo&2@Mx>OVR#uekyFNbs3W#>4vjG6i{zq#-D?lmO zgwb%&er)to`<;|MBis1xiRpQoq=1lspman6NDy#26twqNR#fgA_~q+c*8$*>20$i( zX43)A%7y?m0U*TPM9rNfXHy+Yt03Y#2NpCw4d56E zr8CNk8ARxwwFZ)MCdHT&Pdu5eCDZpn@^ zg!ri-09%OTsSF5HzMiccym0J>)_N^~%UQkOi}{T!LDU98Rq07el%z1z(&37?5ok7xHr~3h{1!EAv9uU=n1bkaDm3plKtq}^CpAB|@>l6g=Th{?L zO><|;%(Uw^S3mw@#S9R`U6(-E0l-S{0N(|L(*w#PM1vt?cfmoFzLeC?=_s+x9)RJv zx_T-a+gbFrCOycqq7T~N44pf2vuFUnckNe}Et`c$GGYn$Em5~-bAIoXqv}29Cef4~ z2yM@YUF1_#SCxNnnX)rE+6@p6Qr(*6e&3X1gC-+a8c+&BL+%*qK6K@t0rdTNNQVOU z`=%5Qz|c$<11C_oX0Z=|Z|P*x4y9laU$!iJR{F$j%3FW5_5>uoM=4&3>Oj<9vSgh% zpy=+qV9L1*!8#Ez-`$zbhb**wobmo&McKSC1NF&>%U#7=0}m|ebs1q0HD!v9 zy$h(TT*3Ty{Ax#AA^WnJjZ3BTP6{EanRzux5Qt1y^Q$1#gAm97P#~o>v3^Gnb?z_5 zc2NL$a)2xAmbJ20*BIQu3@cgwp7UhUS%QW_rq~p}dg}0f`!}4kbp4`eG`2hHsz#kW zZxFdbQ|=801KV!&9J_Phe@-7=xryR#eBI=9yqTE?O9J-xk&7+8_&>_uokeL63{>@u P00000NkvXXu0mjfXm6c7 literal 0 HcmV?d00001 diff --git a/themes/main/client/src/img/mail.png b/themes/main/client/src/img/mail.png new file mode 100644 index 0000000000000000000000000000000000000000..834bfce9107a94be10da4c011fb78e37a192888f GIT binary patch literal 3545 zcmWkxc{CJ$5dZF4tXtNx*0s3`#S$s!5_0FZ2?r0sk5=BUk&_Rfm`^vTD z%#kBkY*-}m^PBf(=FQA|pU;~&f6Pq0iSbnyMie6efJIMN%k0Dx{#Q8NiEUML>OFC= zAWc0B_=((uyT+Zw3^#Rcf&ici|6d_HOJ!{*N#0;>T(G&ndvJ(Tpc@DY36b{n^A2)x zy6GnEAL#LPLj`rhV5O&}VG%m<>)Dz)r!}gh`sIE>yJEBH0oyW)_DS6=t)VW~UUAj$ zm+hiLcfH+H{fex~j_(uClb=^=A>;#1uWMUy{+4Tb4*!BdqZuD{5fW-KrVz0}Qw(L} za@q)nnRz60-@;pCi~Hhm>IUq&0(@ai{WjCs-tr zACjZUSIC+XRz?w>hDYM}b7}IWaF45pAB@ivor$T$1Y)i(IxTkrrMHkX&kWKPRp1hg zo=mAMrPeyF;iZ{Vt?q%>H0ZzDPZKJ|(U#Jl;f z06i6&8@wn?^&SkpMos(hkQeM4T*f2MgzTaNS1mA$!umiOb5LgSwFu%u(u zxl5l!lfKwD2^vQ@s^A+-wh}8pN2y1)F~TmnSLe`z?}x~jT2_L8%H|axd=D5X`;&{> zP>X5~|M;ND8k#(ZG=gC1d}?gqKl-%2@dGXVHzKB%M(%~U@vF1V8GNgTYJRv*V-%RB zE?7#4CY`tCW6Rh=X>>G{9qOo_vpc4hVxP@Q_CSH)2MD614Seip{M{ETb1p+mL1AN4 zP=*G!s#NrkF=IRj2zEi(Am6vfJ0yS9t@)iN$CIJClYjK3=j^;zF+d;xVL2xn_C8sB zbt&j|^EUgp$dr>75LtlGxGnY7c;~jzrPG&&5M|^-kJE=CYq(H>N7h^*^;cOA<@|L7 zo3*HDm~%9f-e&kdFH^h|Q4$a%B0dOms8)v!9&#R~<$lrR33!A8q6wc?Y!XMKRwfOH z4*XY5U)3mL|0YU{TbM`kq>j85d(Y6R1%<^YK(Rdy|IFlW%_GBRvvkXm9gFbA>2zT4%~wgv zAa{%nEl}FuVcZ8qkO%y;u}g;FXX8CA0(V zHo$2?uxjL&5mzp7SH^_L2C;OiCX>rmMyoy;vTG$k2nW{n0cDEV02>9Bh=VB)tdf$W zrorSuy@_@0rUOW3$ZL-KDckegvIY&h_WoG-|1ck^9o(Uuv9{`?2a#*EvstqrcU;OkT%&O z&?2q|bdqN?hn4Vf7m}CLABr98mRx8-;?q+v5XK08(*u@sx_(3j* zwBweN+hiAHpXTP~5Ms>Mi#JY+7E~4JK=DjQpvuacJBq6F3_{QaKJQs^b}qae=K8>g zmjUqkf+}w1a*BP5n{MVHzz2hy<-_{Dc)dbjoQebl&j$rIfuF2d!KJx{Xan~57;aKY z*cp0oD@2eKqn|i>$qf2D({4472CcKI7Y@N>p{=|McnE1GMUYE&cW91q>S4?h5CYdc zD>jt_+|+-^so@x@KH>st-l0mkN2n*Xf7vo~s4j$KQNxqukdo#tn*4rG<1cq8nW0=t~jx@i5$Yzh}-9R7YT}D7A6DYU5 zB}Sommbv#GGl+$48vKt(LSc7%-B7@|WNpltJ)j^na_{1~R9U)o$SYR#Q*m!7C{I!- zq6B&H1TBRWX!)D}Qy8DT#!Dy$H9;TT!7N8x&)vOj*_r9}hi)PIT%O8qz!uuIyOIMX zmf!B#>W3K9xc`td+{ymLd&R7@QovuOEPBz^2?*eD1G^Svw3mY z--Ts5TPh5EQ{L4ab8gTT(`bvI&U?)fx@UC?;hR>-?)nV@{H=LNX&YboU+{G`mW3mT4zkU=M{34>Ue(w*XT0L=|s)EEY=NJQB zyq{(qvcLA3&T++b|4(_N8|OlznVJ6QlY% zd}XHHJ#=tl^)=sY=cnyIL1U&?XP5cYh7^8gE_ewLFyi9T36^c`=bVb~+_)H4GN63; ze!01GwqHW2(hWpeKI4|we%qg&+E-E<7bRi~BbBbko-X)$BihDeI7>Ei*3(%H9CW6?+!kwP)K}$%-iX&Fx-dO8OayE0>WtE?}Ceu*lDoW3u)c+rZ+>D*wT{+UNT4v z7y!ad%qetWFY*OwQ>w&{#d9_*L_QC`=Oxtk0(TaG=JPJtgT10hh7L^Ni=L+Dk-}j_Zb{DeRbsUtukh*#)(O2maD^ebRpzH&k zc1-{#JRWW@uk?3xYia$^u=P^Uf$z}^$Eya-26HZIyOKl{fJMO_#eP&R3VmL0WNyWL z>|hHtJ009@t0p!BZ))12BVh4c+K$V^w=fsnlpnfpM6JX!51A~t*kfGwF1j9})Q*Uo=fMgwFn^O>KYtsCNIrx$_O6?4zks$w|$aiP)S7HbUnC?4#0( z?9RyvUybw@hfr{p?rAz#dHK3NU30$NVMv4d%yC=7jQgzx)^|a$^*x7mKbVyr%BO3pDW&@U?SP)W=! zDc?Ui-aR+*QAzPrN%B%k(lRg6F)Yt8EY2<}@KQE7hu-4Yr*R{3Rwzk*2yw|}er{!*H?d|RE?(XmJ@9|Pe z@l#6iRZa2L)$&_X^xod}eRuVKcle{8_^zq=uB!R9vH9)o`n|dOzPkFrz5Mt0{o&yK z=jQ(M@&5Su|N8m=|NkefO3nZP04a1*PE!B}2pAb5A}ud5Nl;l_gqN7AtiQp*$ji&x z+SOM_51t${QmxXRol}500C!7L_t(I%XO1yLxV67h6AluQ2_@iwQ8v|?ok!@ z-YYn2>#nW&|35*)Th&V_rjo9T ze#S;A*_l5-mq!xibJ6!wKOoo|s*V1UP3zFvtN+N2dmiZIe60TG@1IwnMAb&U!p;(& z)}V2vi81~RiCPdeuW8l@`p5cH$Duxa;$!@DOgp9kjcwZ?dHnuDpI=KyB(DLJ)l%Oiwu0v z+v3DX>XcwFHCNP3#`xu`i0+p@$j}v zvupdO%QTv77cWeCo)rccc+eBSa%GrMK}}rR1TofuJ(!M(-Rha$>YCo`p5N@L;_j;B?zHCcxaje_>G8bj z^1kZw$L#db@AlE~_S5k8)$#Y&^7q~J_~G^W`|Nj5~ z|Ns9nkp&R|000qmQchC<2M8D$ElGrzn8C=}`_N%@rX)a0Z|ZgN7D!E%EzO4BF|T#Lrx_z7o7 zd?!cSUYQljo-O%8iN{bX7`_+_qS82+VrJ&JnEfw#z64H(U-54aTjihr0J@_p!#+G! zy3)t@`pEEzh@4#i^|Mv;7Key@{s%shl_<~B(S5fjCT5W<>1t;HU|V=x3WPOt0|0}0 z;R+};#==wo_zLLBoFPTVXzf+G2XtgWM5*d=5kONq(p*3b_?p!LkqWp59$A+NrCECi zE?74ypUGPQc2z*22i4BEz^)$kGw`7J#8S(ueFE+LvgEqg05q!2ySiGp5X?S+^Vhc> zR)P_dI{|=~6(ejrC)y2Qa66arabZmMn}^G$K_mOghco%~CLizL=l=r{;7UBdz#9|* O0000yM8sXh&8)@HnE5{yMH&b zi8!)}IJ|&4vWh&ugFU{3KE8xOzlA}+hC{%IMZkzm!;DhJkyFKyRK=22#*$UWlUBZ) zR>qT8#*|sVoM_0SZO)>6)2n{dtbo+6fz_^p&$W-)xRcttr{c%C=hM&c;Lz{k((vNc z@Z;k4?d0|EQchC<2M7rb5f~XBAS*37Npg6EmzbTd zzP`c1#K_v+#*M~}1NYv0PuyGb-`g~4NNM?g=#$?2rOjRL0O0aSsuqeyLz*gifKz@Yg191z zKL>l|7_uoQdqwWkkwdyK%Y$L$qHvJ!cjbnIeZ#AxJkseoWH&ZpK4mk%m=iCbf1Qqz zTu8Qxldn8tjL$}lm15K{;^3>#m}rMo%mX9`Uwbh*KI%1IB%rcG?JhFD&iHE|>={5R z(9G!4E&e^A{EhKx2w`_Z10fQfzcWIaePgxxPWjU$B|M90H0=dj*NGj+-Ny})4&FI4Kv;zI(5W!X~C zUYq=9P~`C<;j-pFNvaznhqborUbw;E+cw@2us2MiAKF!!+j5C68*eOdG_B3iQsUC~ zf}^?Ab#S<&p*YXzQAc}KVcy|29BtJBm-*)7Ur{cM|38x literal 0 HcmV?d00001 diff --git a/themes/main/client/src/img/notifications/warning.png b/themes/main/client/src/img/notifications/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..ab8b54ffc0ea43e214c5b0a9ca13a8b590ded293 GIT binary patch literal 580 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dy%*9TgAsieWw;%dH0CG7CJR*yM z^m`CyyfQK10+1nD;u=vBoS#-wo>-L1P+nfHmzkGcoSayYs+V7sKKq@G6j0H$0G|-o z|NsBro}_YrzQU{d+OKD7zL>B3e6H?`c{%Ew#`y9xfr~3@Z1+ro2RJG>m8qL$S znrCV?&(>?6YtTC1sCB+c>jDrpYh7&Cy4w(iq8y3c@UF36%l#cL8k7YddH`2{nu zb4W<)dbUiTaqz^g$Dclby}H6Mnt_3l+0(@_#Nu>o@X4}620T-m3x$N|_$<_Zxi6UO z#sB(s?ca`0x@1-??YV86;6(=US2wog3cr4k!4t14H$f}^j_b{hPT^vg?4v($bVq(W zKm8B)w9gM~zwM7*D_4BvynO4A|3VhRk^5e2Y)<88Xg}kvq@@4Kp5cJsmq(o|Cbcpi zSpGnCqPI~3>jRC7B9V~nvt(klsYhDhGT*w&)XQlP>IKiN(4Ay_`qsC<)~{1nmVR!PD}COZ z(3kxnC7)%nN;&_XJ{5J>)4TcQI?JNIU2Zq7t-5^t_xGRmjOKwIbEe*~^8khwgQu&X J%Q~loCIDF$0tNs8 literal 0 HcmV?d00001 diff --git a/themes/main/client/src/img/padlock.png b/themes/main/client/src/img/padlock.png new file mode 100644 index 0000000000000000000000000000000000000000..31abbaeefcbc507ac59450a76b222d0f4e2ef298 GIT binary patch literal 3265 zcmX9>2{aV`6aVgk-7GeD>sK}rA(Ry*>^gE3Epmj|b<4VQ&fYA4`JLL~x$GyxygiPF4+|0N z>#b^bO(w)_&M)^CaxMsCWK&r#aHm&lGhQsda2g)u)YZ+nA*$Y=^SD?-yyum$c%Fo+ zu$ZJykg9=Rm)m9Enp(-yxPTR<#xXC42HEHB_4DZg4O#W`_q$H2B2I8p9;H-yavPzH zq>PRmoiMumG_oM7?b?9OKx=_Dkk+mM`wBnshtOJ>eU=7mz?U3Q~$Mj}SCJX1OL zyqf1?ymH|9kdKx=DX?)8JOf!9^UJ%poV#?*v99*RQ2i$@z$~vES|EhDv3k?p4arGu`0~c! zB)j#@7}HKFW*v0jh^7s5seY+tT~QL61vGi3^n&;$V?sqb^m$mcJ2D+&AnT5%LkI5`dy*61((J>5;-?QSiAbncNza z=_&RvR%{Lm(ptkSN4tVOmF{n3?6j5{zR&sjwxo5xQ|}d}l%r)b#65Pmb-wHy%d{L5 zwPQ+6{)w~M9AWrc-NtIJ$ENRvF)A{Jcr?4>y9&B4!~eLW4p`>> zhOsvg0z;G;#Cd?1bqhHtaA;*Rw-MY>E-4btGR_itUOXXU;(CuwOo62{(s@)KAuvtf z94FvGsk{$ylrmS~owI-KK@53|%_`yM&Ga8O>wLP){j(dkJHc<&j@laS3U@w@+G6&w zEevGlg6D?SCiHJ!F%BgKr*L>vX>8T2Yz8_^rTT41_xEvRdH3@!{Y6nDd=hKj!2QFr~YXu#zGc(XeTQN|WFn zQL_-O7SLo_0!Z!Uw1xG0znZF6-!;c4eL1j}u0$=vD0_f6MZe}${`8%K>BVi{5{i64 z8|m|u%LIcFxg8~xj5`zYvL?g-%vn?irwONS$jMqAk3f>u{z3D7%BMV>yDEvMtTO}d zihwHDkS-^BS)X^O)qI+wO7Cc(2D*}y<0#8~eDLL6y<=>IF)Ap{9VFsh4qS?SFhufl z(BAyj_Xz^v|A{MSeU=T;7-%0JP+Fk$q2Jf_V0xC09?A!2-q?Aq)ofD99`8HUCp^82<@{syV+N=MB$R(;352@)bTYSwwhc~HL>YeOhXS2 z7`6_89MT2B;X|FdD&I|?onT7-6g&9z@)L`P1X{VjD@l`xFt`dSWnu-|L(T!BuDGCO zoNZT=%_xMbko*hlYL}Lr*taSNRW1F@1&kj9i@TIdC=fj&0N@eyB`7fWtiKMWbATr7 zg2A&7Cg4T5(+4hN01qd{VyFg4sxm;p3HcbRj)$mUv5*IoG^Zgs!BAG-W%*=6t-UX1 zU4QHH#qWz97EB%r^T_V%r{|qM{wSWG<4a_D^Ew7a$i-0Yk9WjL_=N`g?oYJ(W=ebM z|GFOF|0d|pVM$+nV+MucRs3I=LV87{<7gYRlcx$_ud;|+Mjjfc2%;Bv>51`;2WwAO z{c#Xh5c{q-09~!XFT$9OGxf4;JiF(&p>h>ky0Ibgx8JJ};Iy=w>Y;6~e>xfhJR=#) z?X7~{(N*kOUXV%aks!{hW#9|2?9tcRK#m>F`IY_s5%sSFibr9p&|^RqvfLL<+0hfZ zQ2#zFFl5GcWlkcj8mkvA&?_^zI}S)_xCX$VFb1XnngbM-h5}-I!jWl0to`{s>4a!>rl_SvhYH` z#z%W=pEP5bHRfR-e>gHKrz_RgoAR0_lc8 z1wgm>6)?zy0O_Y4nZOkhU=5=m57R>ePM|>OJ{JSjfjgDPqai|s(%GPC-5q!&@I~B> zSVkm*1!$!dLJMNhmHgZ1#hYDkem|p7SVlnEVB!7mzhr7(ZDwave%rx4k2Hgd2yY zu*oy(^w{+B6AuMNhcISd%EV%^s3qi>3E{XaHr<;vT zLLrQgWh590jnIhuyr>83`CRZp^K;+SWri)#8qXn&eeR2GDV6$hV3 zC;|yg5QWXs$In0}S(;{3+Q)f=6U2}}MMHFMPv@82v>(iUde7*h*H5ztB$Znr8fO>y z!y`qZhxbu(c4#2?(|Dsu>wHrr5L8Mm7v}nG-NOl%x7?KV{2zXuG5c zINn@IL5?joMBh>QMAAEjtiT~pmik?HjwA;M0d{)zA}fX%gcBdHORe)+lk^9Zr_Sa` zqG{W1mf=ek9)Nz+@^^gU;jecyJU>{AcJz_GD5nPjG8kaCTN}IbF=6ffiih+nz7Y=} zg(Wv$gUXyf=OEH{vqj2}e8wO2W(9OkmWPm8m*O-uDNz}#qhmDS>q!LhLM6GcT7m~%ocUNs)9JZz20hA~qKL^0J>S_J*M z)nRkU{h6XFfO=E#6g^4IIX9ymv=`Tp0?dRuu?g;HXq3gz7`?Bzmy1d)VRWn*uU)mN zYWD;+?tYeZ=I>*4nFQYCc=YDtY9sq84;X$x+%XA_Fj)!tqt%uuX0^Y0;;2xn*cJp` zv=$LqqjK7*r)2+UuQcF?iwJcHvD+aWYZ@o41Yr=8*nku=w5P9*7f0p$0HdYq1mjgn zqcU#ytbI(erQh)M!$DN=c zob)aNpOrt>_~Jc-Tol-c0s~2SUY7LD?svNDNk)x9cNst4vPu@PS+VaFOyBnbK1YM?0W`%*k-$2d-mZTsWZ}_GVrOLYb{)xfkykLOnSth0t7LBH#Q4w#lnEDYfbqtK!^A6}Be(TLV z()-%kH^MOnA$DAl#6;0q;(8P`@0<463$}+n65fZVc3~e`j;{ez1xa zR7M72GnlI3{adw^n53_0n$*%)F3AZFNit>LsO49xGM6sRglr-`t*zImB_($s9aRZ^ z&0Sep_H@Bme-XkOn$885sVjIZiO9yULpz z3^?JcX`%@LrO!dfC|1TU>TGb+1OP%L8M=o6aLAy-mjNIE1^}yf06;Aj0RF+J*IB4D z4$NpHeH{RJ8+S>Ov2o%JZuv8k#{M*umNYqtL9!E&Cc5miY@l=Ci{Q8g8iO%M>S$U8 zO>B@oW35N{!avqhB7Aj3Y`R(ow$B)QjLgD2c?-`P$3?P4^Kdo4ZH;nsaE-rVAtZ~I zcGfd-w+uLZpf4_1;Qrd;$timW<;aUI&L#2B?BZ59o;c{98)LRVE2cEYc* zCc%=Utw^M!%AU2lR8;O@&wQRkaMw%Y@@*nplc@cCAm~42^=5^67yow6BTaJ&74KMN z9i|`-UJ*TJEhA~TAe)xxW19tkNLO9xEhN2*mXV#OX5=|cda}V{D)07i+N(CBrWSGr zoHBN+n7NUz0?CA)$*aH1UM3Sm4Fyh8*(H)kFcu$gc{KOXM?+GFLU{r)^ z#B=um5C1~c@7@u9q3^^7fFe3^+4)MjAp2tl=XHg?StDU#8~s6%7Lly(j+GsXtKEbx zQ;=1KZ97MkQ>n~zFAFGt=%wZS@kRT&QQGA&gQsGS{bvK~BVq4w%q7MZp3WykuOd41 zss!Ztk1j6L+k*yLgR%-!JN9kz*!MH#frAdeH2t9TuG1@5lFOC024$T2VrrW_63|dH zR=Nl*rXW>FvD&c-ZDWB=aBRaZtOiRCEGGtTNhe-mZo<=Dh+?;Sg811Vn~uWI;&=q~ zu1Wdg?biIaVcgtBR*CK3Sn>y?i7FvpK1maL`7;|#?r+`OnrBNS)W-`pmMG>to0Z%T zI<;ryU->4r9h(+sxkM;+H=_zV&`F;28>lqSCc+grm5&`Z#l@b@lLkY6D;4^ zf&37x<^Ayv6Q&qCWY&(G%+?(QeI@p|4W4Y5Tus)kOQP@&LDMWl&H~FF@Ata~3ok)- zur`jdE*U?(Db|5-W&PL0TaYrES^WA|P4wLcskG`{2mnkkX*5RtQh`bSvSJ-|^;}Mo z8uq-)4}Pmv`SEl{!u%^IrTgw4b%#2k!+3~|4P%uQ?2g8B z<0mJ_a^d1QAWR_*WNb2aNC-#*99wVu#rE0O^q?;fy&!I;>*XHU~ zn50o6H0jS5!hI!!PunzdHaxVMFU7l2A7v-yHz4E=m4K+bOhGNm;G(5|EaZKliSLCV zNL0P}ZJOwX&$}#{6#vSO{0WKiW){EQGdzM)=heWOI)KyJgQH`CABK2l92qr}|MB<} zo)i)_>%^I#ilg%Qt0VmL_D<7~!8VbTYrWk4#skl@$N^zHR6Uq3NdeH9!AvdsAhVBD zw25102*1+)MgFS6I|z-b-hn$dWf^LNA~8wUG43L4TS#>}n%Kpr$t3n5s;RjgjrFa>r`fm47er1Z`WCzpF_WeS-;6RO$ zc)zP$ofCVO-y@v9tsAeUvm#QSswz9co_5bw7{Qk5pYsFUxLhGgPvKU$T^r`uTSc1- z=h2u8FAZu}G#HKUS?Bm5E0JW`+{&yjb0`zg;IlcE)Z=&RK8M>Bmw=fjmmK6^tODAS6L^*!DUEiPdAf7Xtu=G9ju2#dVk}$3Mjx+6YSNA+6R$~CcX=J zQ8Nx(D%a0jd+(l;Oa4tE&_esgzxPBv;pba~?BqY;XEpta<0I-1Ko_*XTnF1hGjd`V zvwhz{%_7_HMMC;!Gntp9iYsbI;shE_rx4wGpGf4R{!SuCj zx<-9PFVlmK3PL#0*H$jmJM$c)Tn~k3Fw$f2G>h2mIDUIKMOB=x1@B*~@zwE#uVO#f zr6;oSjCU(R(6uADpa>W>XFm)B07V#FMIMHbhr_Mls%nbLYL}JdV2Wxm*mRy2^#2Ur zI2U(p@ZSx^2Ztnv0sJSy5{D%aQGOVJNF+kteLVf0QFsg#=jTe^R2O830Hm&|PO;XV G$o~PE>i-e| literal 0 HcmV?d00001 diff --git a/themes/main/client/src/img/pendrive.png b/themes/main/client/src/img/pendrive.png new file mode 100644 index 0000000000000000000000000000000000000000..fa49178c326631167ec642aaacc117c38b31f6bf GIT binary patch literal 6721 zcmYj0cRbba_va4owYNlZGooSNP}U_QA;hPFjO=I_A@eN_J7wjPk;R>;VR z$X=mHWZd7o&-e4k@4jC5^*qlx=Q+gJU`;0Wjzk14Kr2 zu=aZFj1G8jZ37D;`U@gDMxbk^E62`y1F$Uo{bPtXm}F7lP9NPfKIWI5ef;dboWRe| zPsZ(%ySIb=6(^a?UM^`1YJw^rtlsTWbmwuD!;DB%b?}U>BPr*-HV6K#5krRGRm-9 z$5_1ZA<|iRr?ly@qMTxS>fxa+^Is{+QLIMv;nu^Cs@f0Sh(FC4!V~br!kfBEZX+*{ zHOSF!r@C}H-*t$0jdvz?rRP)_dEd^zHNn17;yph+{<1yNp8ny&YUrP$!f&m$&faQ- zAc7`7BkRo?vc6c z0<-nnd#{u&*2GL*ml!r6UHJCpQ)_R~1i&ocb>33(%f-Sy%Dq-rJL0 z%@@6di!&emRP?wh&j9{X!HRK`*zEHZLr1WJJNeDrs=LCzEX`f>yHz_vejtT^zF*1RtrrIo!F_j4Ef`}>UWp5OND z%&DkR;*N>KSKkW|;8xV4j<=vLmqxSq{0(^y$l#;1`Hm7N;tw=?Z!xr_Q!{J!c!R9kXI2heg$7akcGqZIWikBl#xb5=fi-~bD7xU63(VmHW@61}0@z^5Y`d16 zBInALloRu$7ARjPII^BJ*y$eRb@E^U_|8YaN@Zm=eRtgK{lFYhgeJ;9oU)FHE!|r} zfYO>$5hL^4^$~qiS3OvjaPV20UZB!)$8~;xeF4w{l$#E|&pJv@J}-PZs(Gw)=s2sA zPR^6@i;?ms6N?dZ8$`)vh9g+G+ASBjX`4Z01G@W`zM9BtUsz|_0Ybmz_%Bg z=-&eGg^sBfIJf^!QW$D$Wdic^O&KzW)3u4U$=<+sR1wX=N`n_e<(GQW*S3YYy(%gE z_{8ElEVLe|+wsvM_3M#lp!_JanT4r&~g{&YS{q zoh-`(%lKCcZ#fcz#0ezBU1vxjW>WtG=haEQjXe%SF$6>DFzU1DzlGE~4( zhMMsykhrj~KbP;wKD5voof-m^e?VNDGSlX+hC}AIfF=sZZcvz0po6dwei8?USd?DF z8bcAea+)fbC7bFc3Mv1O3bfLKYzIy`R?J_naT0r;zKiwfp@Gay9Sf*+V7c* zFzh}-vJ3Hgj)g7lU{lAgk~ItYowK!JeRr&%r26<#O}>VFuri-fzQmQvE5wFxH574& z_*>d({ACwCO>Dlswg`ES&B{&75%ILC6eix!AV*^T+b)+ z+%)H219+3$`~Bphr&tR8ZE4>`@2kE$mP}xA zymwFSeM!4NGH)*S;Auj6N*b6Cac;GnYM3m?&DHM|+AIG8CJIB;#h`qTK@PBJ3#@mS zbLe-Z`A#?5`60`T(P%@U>z1*J(_S# zYgCtd=6Vgx`gd}#HC+PIUhMG?Rbm-7k=V4*%db{Np7C;ilZvDb= zM*NJzAU~sQx8`u2efkcd){^(Mz_C6`9Vol-Sd<|4=T8ob-h$it`P;=HRS`1A7^$Al z0q-azOA(NelC1-k7-k6buuY}g$@RIp zxu&Fo-lin?-o{@dm+s%7HOaQ&pq8ur*DhG$u*@mn#9qlAG0Zt0x;RA_TbFF(smm*N z?C2|+n#7<1LJ{?Ql+e69EO?VvKduclW$g2-nCaW2KbdH=jnDL==|%Z*2U!i}0z^$| zKV45=t@+`N-BW-XzK*!k#X#e0nBYLd9JCFLFP;mZpwEt8Rp@q#ZBoQ+93jL%Y_Ij; z414O{YoM$z0yIhNi%NK=?YWL4sXzLrxP3Qs^3b*YML+iscO1T6bK46bllZKYfyC4> z!GdJ-^v3Jo5f(ZadbUBg=QDO!lV(3nHB9JT+hI;vx=pz!11@IPwGd&rPx;LDuyA+0 zmctth9HnC3uVR*}g-=Yl4TvhfO#_2`$1K5LmUGIBJMkoymH62Lj7#esIe#hBYulnQ z!hy+K{>TRdIj@2+a{hx)C3}DT3J%)(&K50AkWspF(ljjlgQ+IaM6loaAl=Ne7Kp?5 z?3a2%TQaPpFYwI+DcYD}K}y;d#B(zhh_t?$H0jDc5!r8%-STGv(ZmlH zv&h{B-W1yotL@6l5B$Si)165L0hnVSWf)*{&qxjw3FM)oE)P%Ju6R#&N{K~Nb8QZN-qYz|vO-mLsyEF+fxTnYxL*hnRK;E<^ZiEGB zV%WRPATK42hRR7%&K!u3>BOROB7tpUhKm2d@daQS_aAtI8Kz7vQR-Zsk#sE03C-I8 z`?$)V!4*Ot+WAn9;}@i$JL?q+J{Cz~LkFPP;4fSu(zswT#`dVQOq8S>!|>hmoZ)-8 zvpX_dYFAjqMOR;aI$Qi22|9mX>H7gmEB5pNnJ=RU8owsWHtH{zCfF=8U?GIFJUjtO z2RZd==uh>J(~$_yvwV|tBEI8+?$f;^doMdS+(?yZY#^xd&ohAF$7c?=UpO4)WWyDC z1v32x{PQ*d!C05C-wKN*?%%=?hs~PRp?>ZqAbMl}#0PPwUH@xlx9aGVckHaLphgmp zU5tzo7!<&xa|1f9D^sM-66_tD_>qy*9k{+BKW#)5JZ@ zSk%{1(0J3zGu-RrzcVrMH_%!Oq#p7};J-07ff-2%&AsPoK6x+ha*gp3-&yU-Rbfj0 z#&aBSJNce?(_0DU56it^q|8TkW+;>s?WQP4s(Vsjg)TprT+giAdcZ2Ax*Lw&t5`RP ztb~=Lo(%-M?=P^OU$))WZSf;#of$7O0=MJWAH1RG08J~SIJuABO^XxCcuJktTMWVr zx!d3UKmc22YEz8y4^6kqfz2AL%Rt)ZLq4`oH2h{j=|ek`N7E@XzzPyb>F za`=~KcBnb=Ux_QOo9aVtbwmm$^WWN3o|=$yW`Gma&FqRXeX0JRM=VArth_PEv^m0Z zZ%YVE=@%N2Ieu{UeAZnaH5+VJU?%1at9ZRIpiZ;x!3no0%*PKZf#GLi(O?4WHFo5Q zO*X2JP}pEd@!}t#$<%Yt6N;3cB7JOvdW>vB`q($4MIs0*55jDIG8U7icszzp3lM|{ zIpG+0ffsJkjQ=f^o}-@QBVxhlP;0CGuZp_R=}yd0V&;e{a)U~*8{&n5Pior#)?a6q zr8qqtjq|^9Jo zkA?3=_dTW*qx%9Al;Y1BOjY1W9;caR==IuQSK?0pQ771S(E+ohhms)NWyqFe?>Fy3 zD7tW3ey6f~$pw^A{)a~@(kF~zptCz`uoOdS>WG|c{GHu9bcua z{wKB<>n3XrgTl%T;F5SQ)LNsT4J_ius^t=gme@3}KWB%dBV=Z}mvz}LJnVPzZ#sxIxN~XFm=e zHO34vL006>;`3t7bf2`v3-p)ICaDBuPX}KWv1A=`0O?Pm-&(upwy%79F}cH5N8>3a+)j#c~M@;4^; zel2p;mu926lT>dB?fg!N{onTUckx@K|Hm5j4}$C;8E|VoSA5#oXshz=Kh1<3DM+ znhU?B4KnR#LM(ToSY&Q`z=q%Yu|sIAo8ViMiwf&|;{7topfa z4aZU83gzvkeJ7Snoh6?$qiUR)^uve+l>t2a(-k^s8=vm}U1I=?I4iVsm^u^KGs*>_ zI^JfHU{Eu6$0g*Rm;a!VKPpXOCvQs><=8c@opO75p~y#M)0`9_*VIyk;5z=_xN# z0`rF>TNg^_vy9Qe@yV@_{%%meZk5Ymw=|#hb73}Zr`4j>J&nQ7TP3JRhS`vNI@`F@ zEA@`RpIDB&0n|c2vjHP%9}^y!9(0<{PCU)nmcsLBW-b>)Ym>NPWc$3!@-t`Z3(?Uc~|8Bkcn?o$f%jiPGZuY^-4TJSAHUDY;9WdQ0P%W;CJ^udUzzN36 z8*YrqSFs)O#c85>vKm#vy<%txeX3rG5(1yk>=xLtM>|D|AEc5+T0ICh@|QYd+H87 zjlmioUFGWVML!nJ_6-iTR_3ngm-6Eh|tbr$7LQ8W2Lkt z`-_$g-k>v$M-(nx#CvAze%M+ws&KJ=zsG8RTudEgJ;;^}c+R;O6PjnHY^f7%fuZFD zic8B6RuXc?g{APQ=AQ?R^CWG*8!&QHm9qzndFPepCoGd&r?ewaQ5%!xE&Ilh0%q5| z+Q%vw45D@BqOYLNmZZW!B<-)6sGImFezP><92Yn2CI-1)K;ZmjLG8=?Q(29>xdhRO zI89X`A@}bcu88ytKQY0|S0nNUm!;@J3!juEA>>jk+2Ec#qw512AjhgDBskIZoPdqU zP+hTpII$>hE}4s^5U{FLMGDv-Seu>X7ni{jkOeEWJjs6DjOc3*_7h9HbqPLknhlngQJ`( zU5QxCx4p}o3U%`ln(tLWj{kZ+xq^Z$wl>BUPap7J)kS@R>X(CT!*`VYVjv zWIH}tg9jL^D!TOP|+c&KR|ifYSVbz>1=5IeJrRsbi_F;8LNFXCfQvvi+lO~bKPXh?#Q(@ zk0x7rUqBv{e4MNJ_I*hnO5(#z_=ik>T2sxqc6)h>FTXtMRsUY}J9I((E-q>({gFu%7jo0_>I8H0YfRO8S#q;8YL$e+j(RPv7q;Gw3T&=i! zpzd(R`zonJfnR@gi~qS$dw6NQxMI&EmZT@2RGg>O=p0T9x)1t)$FZZ`3K-~~(0QS4 Hcm4kWRQYbi literal 0 HcmV?d00001 diff --git a/themes/main/client/src/img/stores/.directory b/themes/main/client/src/img/stores/.directory new file mode 100644 index 000000000..9c9dfe047 --- /dev/null +++ b/themes/main/client/src/img/stores/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,57,26 +Version=3 +ViewMode=1 diff --git a/themes/main/client/src/img/stores/applestore-badge.svg b/themes/main/client/src/img/stores/applestore-badge.svg new file mode 100644 index 000000000..ac111e597 --- /dev/null +++ b/themes/main/client/src/img/stores/applestore-badge.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/themes/main/client/src/img/stores/googleplay-badge.svg b/themes/main/client/src/img/stores/googleplay-badge.svg new file mode 100644 index 000000000..9e33e3aa2 --- /dev/null +++ b/themes/main/client/src/img/stores/googleplay-badge.svg @@ -0,0 +1,429 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/themes/main/client/src/img/success.png b/themes/main/client/src/img/success.png new file mode 100644 index 0000000000000000000000000000000000000000..ee9d6841bbb7208220a831c1c42917eb8facffa5 GIT binary patch literal 3147 zcmV-R47Br!P)@~8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H13&=@CK~#9!?VWj0Ro5NIWpwOJQDRbwNDwr}T5D`fYExS^ZNS!! z({?)3WhP^$({{SdG*eL?n@DR!L!g~W+NQSuv}t7VDXU9R+=7CjxFCufZipa@@1Y=Q z0fk@B56^R+j|cDGckj99-22XPzT=Dw+=`0CLjfNNgnC0Sw=jZrT^)0zv?CqKirDB=}*$K3Qy8 z0m#XXB}uo)ojuDZ2JEA;mJ)zo>nkK+#i=mr-oOa>%SoIPs_I~$EjO(IrB0#^a(-M+lx<9lN`o{IqVIbS~NiSdXS z#6y4#+4;gG!%zX}@U9%;6Klk9xgi43i5)q%FXr4q1XBU%l&+lB6MGhMEK32>WhV=t zbS47Ol|1sgZ;tzc#kvYW*YL?@T;^I{Hcn3gY_d~L7n?2u(7S%QdRK>hyX-yq-N}z3 znAFu7~3~!grS-_H?$ML&Q-^1W1)o)xkf}aUEDS zXKy#WS(dKX@un&fK(6jp0srJ3cJN)TsPU9;tBFSyfR$Ka zd)I0$W;x!Q0MQcg=vhqQ*W9@QJld}-vWEn~JeRu~UMbZiL9{mkHcH5&!#{nO4}5H| z@5p!}0A^CoDwww?#ozPU=uLo133>$hpKXBJyN%r7clAAhXR=m5B>*NQI~L~d_Dh1w zrv#uS*ZU=G@!(^7r^-GIzAF>6VqwmwYy7El9_%i_G6~x|{4VmS0-$Z zh1t83)t=uncL6eabTBT7di&VFV8i+CU~jB~e_YuQf2hfVm_zS#hrdJs`1PG_Fk@S) zPXRE&n^(fD9Z71=G1FauGNU{At)rXZ(!Cb1Cyq;{{(89-7O-72H!A$IoZ(+0@TYHE zs|db(UMx;jdY)zO0<;*}!JEiWI!<)>YUjF|;d_VwY$$wOpI6t}#1;Obs;}|&gAf2c_BYu$vKA@y7)sad{=9{?}TNC zGnpRSw-7%0#NpX45cpABwZnHMY?}>U^|43<1y~_6d{?`1-vQrcBmu%d&l&#tyDhrH zcjfuf=MPq(0BNGbe#g z>Rjl$#TEX!JGYp^cjejK)$o>w3e-M>eC{6(T{j!bOv~xH8b;KQ(l`Gr0`~ zaOf?-%9?CLJN0sRtIw6Q`1keqTDihMBMAH_5|E$(eR>Pt7 zZATNlU{4kXzAH=wz#T1ICQ!)7J6_{G;p1gO!uLLn%;XaA!-^01&7!$UfH#P|;h%Ok zLHHK&;M2##0VX$rU)}ddXnt^q-pY4~L!-c(xknO=02F!J%GekAc5E`5&K^ zF<7?Vfqn_!M<(0jx|j_FVG5d#Lm;+O&PJHJB}r)bbfT!8=}_?353Gao+m`|g{z->9 z;A5ud*{(643_=Mi9DyOh3C9fxGYNdUXr#uF>0qpxbHq_E;R}|Cg7_VVEeKItHOoxx zxC!B7rWGXo#n2^T{#MTtxmDk8fGN3&rh`u}0Y5iMu|$jr`|&+n1%V%$6K@&#bO2~Oh*~5{gss_DhL8E;&g7qh z$YtVC0^VD+%iFG468@x|Rgb)ui37n3MZUptw{Cgs6_{*6_;d|G9hjw5ERT(whg&aO z7Ct6CFaGynree`X?%~6+eJjI%HfL2Yn6X?uO2B$`Z$r1!((vj0e*u{Fg%K#hEk-Mi zdmi>en29HLlnwq26_tDm%>2q2-1*b`zq`7^fAFXuUM@_tG<-Vue;%yx(x~Vm2|9Iw zfB#`$K)}b1ptE-+$ADE{8;3i7+B8al@V{KR-tzDt5B=Jjz-lj!T&h6=gg>X?-GG3P znO>CmHdyi1u}jM`pyffQgg<(Ls5@Zgmq%m4s2e>_Mev=yJ@BOff=_S$;=$^#kAHpt zI{4_~K^PeHz84YqDCS(PhDly(?pryb_PCp%<@NDYfC!Rq(IvoQh>Lb-K+^Fd*wuIz z>bu%ufAa-ccWM{RFMcnez|%#agENW~=V=yzN?f)qcl}Oe#7qFUrvj&xK)k#DsG@}VYI&*_fJ(63EcXL}B0+dw{Ca;_`XvCB zAj3?rfU~j$5e4zP{pF}dfFKgPlz{tCFIIUAGF$++rR4jh1l)=|-Ed#xZMXnbf(axi zqy&b~@jG5AT{}@LKg|MA2|`6%O>AWe$m3r3%ezyi>cLJI0jLC#QUdmQE_d~VIiIeX zr588d1aSK#ke35?+q&-0C{CQICo^3IaJwgvH-`Gx^=+S4kPxOfFWm)jdpeMhi26P1 zogvr!&#_>|L;$yOf($ugsBLEN#p1-RTAuT{3E(koEawi?*YbZjYiH6gSo2{nfZGI; zoJCY#!ljMe>CZHRjUfWKO(mzxMJ1!};33!hJ!+?PO(B5C$|AYEZ20qiPh?@jFOBA5 zxBzZz3~gpvZOl>v@@zjqz<;cC4WA_h@VGa~4@3#@4xc>NpPsiTWjap=Tm|shfm9v{ z65w?{JTJa!ZpoS-h|u>!2;i|HwbZH-gxcd;UfPlL6OsB{C;>d`6BLQ~#GqCJ{HUL@ z&327^+`lDaUkfRKN4c6*(PkNaNr10pCC~ErrWYkQ%Z2PeMs@COMQUL#i2&V5l z$fR#RVfp%-7WzFh>Gupad;ghUfDxn!p*NB%=z9@!=^Iij=)01y)3>Jg(KzUz)zW`c lLjO-1{TleSLaf+V{|9F)WMK{Ekgos$002ovPDHLkV1l3q_6h(1 literal 0 HcmV?d00001 diff --git a/themes/main/client/src/img/user.png b/themes/main/client/src/img/user.png new file mode 100644 index 0000000000000000000000000000000000000000..00941399d34f0f2d85d323f61daa67c2c00c494d GIT binary patch literal 2933 zcmV-*3ySoKP)>+Y_1buC zheF*1yf*GR)m>@otnGcw%sqGZe)vfU+q>t^|F^R<_ujcTYIJlo%4jV9WAaR39I1IcZ1yCpS zIY0`~LOAsCXaEv{=cNF_2S1lxgn0!F-rv{6d0@U2VDjK!2d)?(^zpb(O#~{z<1L%h zz`>Ihfp_q;fNbDV1ck2z&vq|HoB+`1Xdnp~jL_hfe8nynpNI$nprH$ZHW~ak_zd`^ zVFiH3O##fnhzxsNhFJ&KlZFxi8uS9|F_R$&2KI2yF@ylnkj21V8EVk<1HThm0M?5q z!wen#x-@Ay#-LU3Q-KpQK-IVm#0Vh(6gr)CbjaY;1?MwNCoKRJI2)*zVQI!~;5m{4 zKw;knT4hL@Q-{ZzlmKWYD~#$v`_Iw@bH~becUG-S%q-brIc!hQ%CTE>@`@1rH~t^~ z41WiI7ypKEd+>ucn}h)DJO4JN0|WtfTGFky((<~-_O71$J^cftp@4sjuYs?HuSraT zYBjD2!VN!$U7T<~AWPy*p3UYD9S&zzZK#1y2EI1F2fi1fQN^iT1YmRXE_461$D3Uj zt`?WMJ^jO41wR&iFMLmYZvqmW;3a^GcjNIpyUo>oPp{y|g71wr5Rf26R{^k#lJlU> z;E|J`7V-*yELaPHq3y-G2*4(4_H+EMA1JSBYNZms53C7m<0t@*x4fXK060v^Myhx< z^3f-GCAX-B?*nV&Ji7@;T&HL$KpM}}+_t|Sw^I$@2iC~>tH7*@061HPO<3U4u(#Ll zsCIk$ywnB28nIT+BZv!B6#(a}u_;U3%(j=$=Do z#TDQ{E@c6dl>%I)Smctph;09_P|%>BeJ>}(((xfktJ z7s0;&KLKzvB#H$t|I_wH(ZlzFZ6PYb%qX@a7P;cyPd?-@iU8Oaq5{l|V%wsTEAFGy z*NTiH0Jepw04t){-gxASJA0|%rcngIwottjh+=#5NC6^K0BaQ6B~uCznF16>v8__2 z0Ffy`B??e41&CMy8c~2ADL}*u(1`*(ASuA*0;d!}b9zyLhw=;%u>#yj0luVI+|iR) z3XLLwHTR!XJA#H#02jsLHvDC;VR{11e>-6#D!>C2powB}vGJxJqX>XY&=VEFjRI6q zowRWAT5*}^;a|S)yiN7^;*#@GRF-=mZ=;7B#TC_}hF@9V(it0Xb`w6nHnOvU6tjZv zQCu5%GVtsb8)v#heEXR*D8MP|`{X294%5Wjv`g+KzVLjis&La`#O6b=`CF3!uv2g`gE;3*M` z#;x4QFBQM@Sb;#{W9HL+)51GMDB4+3-$eJ~&*NPZXX+P7fL8JkhK%LUCb~0)Po2vz z6DWM-oDh$Ojp1QkbGw`1CJ`gcS6NyF6W{}-0KXTd@WgEg$(;rE9XVelSop{vlmbkn zIxoQA5N1tN8<~EArgql=;phJ*x&WLxc7cc+qBP*Wtyu+B1lX9gzgWOezti6eA)5ti zfVZT)u7UgBK&6$ng5CG~*y2xspNrIpH&+Myu$D9>mOOMdV9Tq;UhoxC%|Js zpGXZc?K;kFCGqsE9J`?5w@)Hw&vy)6A®tx`|Cc!K{R~B2jc1N|ng#YPY z01+^g*}>)+v;a(kg*ZQ7)0sYS5YJw62)S4!2N7^)@}f|(3`qbcL4r0*!k9bJT`jgp}PPI5fXrZ%isracGF5;IdBtzN$?_Y z46k-0FnAp9&IZ2EGXt&yC`6b8>;=AxfZ)FdvVb2@IQAq2P>3)S*uhSVMm*K;1fHjK z%t;BL5Mc(81b7S(JhovA@HE9^O;P}b2rkn{6#(u#g!>KLW&ICB?B8ANXZSm~9Q{dlI$z1o fINvtfu|oa}IMcn@FFyTa00000NkvXXu0mjfV5=n> literal 0 HcmV?d00001 diff --git a/themes/main/client/src/img/warning.png b/themes/main/client/src/img/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..c6acd953b142a8e43e293c353a60e6313a8d39eb GIT binary patch literal 4038 zcmZ8kc{CK>7k@Km%nZhmB)g(al4J@g%*Ybi@{R0UV<~HPGGh#)vXiopt-hj2MImcp zY$;2&EGf;GWSK0JWq#AYzjw}k@4R>Jx$oW2UEdvZQv)=Q7!LpdZFo`Fg0oWo5|W!U zw`;lda28mgj^Py~XG9{237kF3|DsJG09+w|2|^mC_HqtIgY>L}{_%4U3Uvu^1EHaz zO4oh80%A^>WZHKCL(h;uQN z$(4fz2^gkydwnSstYvq?z;jH?4dHpjOIsi?F~5mv-=X~B1+IfT=c@TV0XW+s38jp3 zIm`8ol6U1Y3*FZ4G|uCdEr5XCymsumB+=8f9+kg$8eP;^u9f!rnogW5jie4Qx&UZq%;ioEF43zF#7yy~zM-eI zO?nd_=$d}*1Zdj89sj6I?h<22KC>b!%CwF;CBL2iff7f&(SHL%Y3gOm9%rm>z`BQ< zags$B!oM7@imA=`0F#zKMyK{>K{K;0WBK$OE(4l07$MAiJTzu}?T}$Eu6~)JEgc2l zf=SEgU@ShC_l!E7E{fG`pTiY)xdsO#!EcwQcM9VpAIi>eEYTaUlkwl)(ssK%=%={( zyLCh_G^+Jtx;(pcl>ozzx!j?_lkK#SDH7&caoLxQxBIRw{7*%VTL*&Jr`VX~UcGAs z20}q%Os|>1VApPeKz|^ki=-gYS z9Y@17L~Ki*qd&(F9GL7rg&nPk`xaYE{WG~yqtOG|AGd;=yeICQ3?P8Wl(?P zFZ7V8e_`3g%Z~$IWK8`8B+2Uaw1D@FC+7 z-$J8b@&2r&R`X87_vAo=1n`SqVsgn1)n)~(GkKZ^+y9#v%9lDe!PdCl<}d?4l&Z)= zLn&Pk*;lOwuF)@Xq&>Lrv$yIxxEBk9P*f6q;C&Gxctr z>UH@f5#<~SKVIH7{8TvYp}h5)?t8@rj)0$ z=(|UP)`a4gb}Nfh$}-Aot~qRv-2NG&Pw?!YJ_L{-cI0;gx6C17f#<(JU-shN4s}Xm zsr`bk<+y&nAzmdTfd%Fm{E6d?o6#b_8AlL0OZR6X0~$FN0^=Va&iV>~OZ62FfY!fr z+G@7W*EE5ULNUF6+k=dc=OuP(y~w^QFmG}IU~z1DYm-#g5&k$r%25&3Dt&RF(P6Qy z!hED53NQMhpoEN40!T@U!U(PZ!f|Zebkh%KFy7&Ljg0S%@80Y9{`L*_p0Gq(-Xa_n zWwVhZa3tlb{&BvDJc*;YmC03wmleLmlfhCIJloH!f053eR9|}uD3}(DuE;rORKDF~ z8OF`gF;dSjm&vc;j;H-8dLiCxQey{Hz=`iQ)BzERRTZji7F7reYFTV&VRD5#mM;P( zOnzY7S=<_1pZRu#%rT>*GCwrZtj>RI@Q?16O+_|h&X4@_qT z{7RG|7N$Y~RTfv^0XLX@uwvGl4+Up|W7er#Ssc0MwmZo`pUyz>8<(%}N)*8iLT<*$ zO7nn-T=u0({;o{JGM*TzB-T(t)5aqsCE#OXne$tyGP|&oKl$KN3NP@5DkD^g0|1p@ z4(AS`US%VnTz+8Z^%`KGKzv~a`sOjR&m!IZiOqmEvGch4*@fGRIo{5gpM0we7|#}f z^CTPH)gw)SBkBRwS>h6OBTukHNYUs@{TU=wUvp<|w38svkaGfXF17 zWRj^r4BA@-?+x!da`Cm2p9d}nF!q(p6NOeC5{5={Kr7n#qM8v0M{@o_vGlW5W}(rbH_w)a22X{DsMW>q`VKlT zXk-Y086nM7%Po?soy))lmj}z#H&QEp9}4F_KI_@4j-*^)ygEts*RkiKn62eo$G03- zOi`)M-~=((Ir@vJgpNwW2eeFB3y_uX|F=QcfJs>&llMuD- z;!BoDIu2Ioq}!HTPb2l-x0v3yVVB%8h?y?A85*|%wy3iFW1CtV+P>P+YD!=Qp&6nh zpIe}j|SXo%pJ_yAdzPv0Hd-05QBz!d!eVX!e_ z-js2+CCJYh%8^@Wg9~fkF6Qu>bT&Ro^dIN&*5>tgan0OP%w$V%fA-%-^*%>}*H>=U z!jTaxwGN=$~V1I2jb9T`^h|OQK?o+N?D|4 z%z=m!488|@gJ9jym%RV<;yy5CvE=1Zp4VzOXMit87iM+GU@MG@F&tH|m(TSZJC+>> zVV7QsOjvSjJ`@G(vuKq+ovB&m7i#;On+EK^sDZq)O`%>Aot~S$T6tk$6miZ z37nON_ttN@aBChO!q9G=2`91_S^1OMbP-+Qzb~7M7k%ZmY*tzbI>2DcboU%unRGe_ z^g6>eZQIZhx<$yz=BE-TqN+ya*KDr>e5)V=r(T0o09y}kAUswax2W8N6l5)zy{FXT zBCBMbJKg8c8{RJV4?lJfI=NJj3;36BD84{BOqIIaih|ifAHPMW@E@HB`s-w(SAV(o z8BYmb{tLZdgx&WRQ_Flp++DL9J%{!bU5L_O@OV^m-WN_@GfqN+t#6a>ZJ)fIEc54H z_Ahf?jW>B!W)Hfw>LIikDG8h3(g;8IH78vtMT}9pZk9gqaafF_b|zx9h71z{H7U<{ z+UCstV>w|G^N5qtm{_m!4V!Cc_#$GTw0CkB!PSFBD(8Zm(v9+Jmfk6V{CR2+G|9x>_Jus|!Ap5u#`Lp(Fwz*3I*OOGUT(q-M!Afy{tE@U zJ2F=Cu$IgfDXw+ZF2+tPekHA`bg@1(%I2vkB|8wwf zRU;Ls;@*5M8xL*Febm=t`gFhF?0_;=b~>tWNj@wC1yr}RMl6=> zX6-I}2_osksR_lEmhYexY@erD5yZS_@mPt{mq!Rag+PIruP1DNV3J?1b5O|b#ua+1 zKK*Qlxf{Zjnf!_bXYF=)Kg>LK0X;cl;x&6Us;m43gbnAydy`^s!rJ|y34o}Mlo51w#*DkxKu*ZSO^rjzY4`S%iTMPk?%#D9Hq0u zXO7WS9c7da`CWXmSEK4H`Zo6u$TGFji_X0wF zYHis}o|pf`w?RCV<8AhzFDtAF=BVX(@&7)kpvZHP-ER?mQZc1eo%81ghI*#D&vcxr F{{xrIJYfI; literal 0 HcmV?d00001 diff --git a/themes/main/client/src/index.ts b/themes/main/client/src/index.ts new file mode 100644 index 000000000..6c22d17c7 --- /dev/null +++ b/themes/main/client/src/index.ts @@ -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 () { + (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); +})(); diff --git a/themes/main/client/src/lib/GetPromised.ts b/themes/main/client/src/lib/GetPromised.ts new file mode 100644 index 000000000..779139654 --- /dev/null +++ b/themes/main/client/src/lib/GetPromised.ts @@ -0,0 +1,14 @@ +import BluebirdPromise = require("bluebird"); + +export default function ($: JQueryStatic, url: string, data: Object, fn: any, + dataType: string): BluebirdPromise { + return new BluebirdPromise((resolve, reject) => { + $.get(url, {}, undefined, dataType) + .done((data: any) => { + resolve(data); + }) + .fail((xhr: JQueryXHR, textStatus: string) => { + reject(textStatus); + }); + }); +} \ No newline at end of file diff --git a/themes/main/client/src/lib/INotifier.ts b/themes/main/client/src/lib/INotifier.ts new file mode 100644 index 000000000..df947538f --- /dev/null +++ b/themes/main/client/src/lib/INotifier.ts @@ -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; +} \ No newline at end of file diff --git a/themes/main/client/src/lib/Notifier.ts b/themes/main/client/src/lib/Notifier.ts new file mode 100644 index 000000000..c0252b9b9 --- /dev/null +++ b/themes/main/client/src/lib/Notifier.ts @@ -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('status %s\ + %s', 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); + } +} \ No newline at end of file diff --git a/themes/main/client/src/lib/QueryParametersRetriever.ts b/themes/main/client/src/lib/QueryParametersRetriever.ts new file mode 100644 index 000000000..a529adb6e --- /dev/null +++ b/themes/main/client/src/lib/QueryParametersRetriever.ts @@ -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, " ")); + } +} \ No newline at end of file diff --git a/themes/main/client/src/lib/SafeRedirect.ts b/themes/main/client/src/lib/SafeRedirect.ts new file mode 100644 index 000000000..7e7684b8a --- /dev/null +++ b/themes/main/client/src/lib/SafeRedirect.ts @@ -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(); +} \ No newline at end of file diff --git a/themes/main/client/src/lib/firstfactor/FirstFactorValidator.ts b/themes/main/client/src/lib/firstfactor/FirstFactorValidator.ts new file mode 100644 index 000000000..eaa496fdd --- /dev/null +++ b/themes/main/client/src/lib/firstfactor/FirstFactorValidator.ts @@ -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 { + return new BluebirdPromise(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)); + }); + }); +} diff --git a/themes/main/client/src/lib/firstfactor/UISelectors.ts b/themes/main/client/src/lib/firstfactor/UISelectors.ts new file mode 100644 index 000000000..0e971b3c3 --- /dev/null +++ b/themes/main/client/src/lib/firstfactor/UISelectors.ts @@ -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"; diff --git a/themes/main/client/src/lib/firstfactor/index.ts b/themes/main/client/src/lib/firstfactor/index.ts new file mode 100644 index 000000000..24affee2d --- /dev/null +++ b/themes/main/client/src/lib/firstfactor/index.ts @@ -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); + }); +} + diff --git a/themes/main/client/src/lib/reset-password/constants.ts b/themes/main/client/src/lib/reset-password/constants.ts new file mode 100644 index 000000000..d48d4e67d --- /dev/null +++ b/themes/main/client/src/lib/reset-password/constants.ts @@ -0,0 +1,2 @@ + +export const FORM_SELECTOR = ".form-signin"; \ No newline at end of file diff --git a/themes/main/client/src/lib/reset-password/reset-password-form.ts b/themes/main/client/src/lib/reset-password/reset-password-form.ts new file mode 100644 index 000000000..b94279cde --- /dev/null +++ b/themes/main/client/src/lib/reset-password/reset-password-form.ts @@ -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); + }); +} diff --git a/themes/main/client/src/lib/reset-password/reset-password-request.ts b/themes/main/client/src/lib/reset-password/reset-password-request.ts new file mode 100644 index 000000000..846226d75 --- /dev/null +++ b/themes/main/client/src/lib/reset-password/reset-password-request.ts @@ -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); + }); +} + diff --git a/themes/main/client/src/lib/secondfactor/TOTPValidator.ts b/themes/main/client/src/lib/secondfactor/TOTPValidator.ts new file mode 100644 index 000000000..5394139a6 --- /dev/null +++ b/themes/main/client/src/lib/secondfactor/TOTPValidator.ts @@ -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 { + return new BluebirdPromise(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)); + }); + }); +} \ No newline at end of file diff --git a/themes/main/client/src/lib/secondfactor/U2FValidator.ts b/themes/main/client/src/lib/secondfactor/U2FValidator.ts new file mode 100644 index 000000000..5812922f7 --- /dev/null +++ b/themes/main/client/src/lib/secondfactor/U2FValidator.ts @@ -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 { + return new BluebirdPromise(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 { + 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, $); + }); +} diff --git a/themes/main/client/src/lib/secondfactor/constants.ts b/themes/main/client/src/lib/secondfactor/constants.ts new file mode 100644 index 000000000..50bba7571 --- /dev/null +++ b/themes/main/client/src/lib/secondfactor/constants.ts @@ -0,0 +1,3 @@ + +export const TOTP_FORM_SELECTOR = ".form-signin.totp"; +export const TOTP_TOKEN_SELECTOR = ".form-signin #token"; diff --git a/themes/main/client/src/lib/secondfactor/index.ts b/themes/main/client/src/lib/secondfactor/index.ts new file mode 100644 index 000000000..279723dce --- /dev/null +++ b/themes/main/client/src/lib/secondfactor/index.ts @@ -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); + }); +} \ No newline at end of file diff --git a/themes/main/client/src/lib/totp-register/totp-register.ts b/themes/main/client/src/lib/totp-register/totp-register.ts new file mode 100644 index 000000000..6a9aa7ee0 --- /dev/null +++ b/themes/main/client/src/lib/totp-register/totp-register.ts @@ -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); +} diff --git a/themes/main/client/src/lib/totp-register/ui-selector.ts b/themes/main/client/src/lib/totp-register/ui-selector.ts new file mode 100644 index 000000000..9d43fabea --- /dev/null +++ b/themes/main/client/src/lib/totp-register/ui-selector.ts @@ -0,0 +1,2 @@ + +export const QRCODE_ID_SELECTOR = "#qrcode"; \ No newline at end of file diff --git a/themes/main/client/src/lib/u2f-register/u2f-register.ts b/themes/main/client/src/lib/u2f-register/u2f-register.ts new file mode 100644 index 000000000..abf40ee05 --- /dev/null +++ b/themes/main/client/src/lib/u2f-register/u2f-register.ts @@ -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 { + return new BluebirdPromise(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 { + 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); + }); + }); +} diff --git a/themes/main/client/src/thirdparties/qrcode.min.js b/themes/main/client/src/thirdparties/qrcode.min.js new file mode 100644 index 000000000..993e88f39 --- /dev/null +++ b/themes/main/client/src/thirdparties/qrcode.min.js @@ -0,0 +1 @@ +var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); \ No newline at end of file diff --git a/themes/main/client/test/Notifier.test.ts b/themes/main/client/test/Notifier.test.ts new file mode 100644 index 000000000..70bfea146 --- /dev/null +++ b/themes/main/client/test/Notifier.test.ts @@ -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"); + }); +}); \ No newline at end of file diff --git a/themes/main/client/test/firstfactor/FirstFactorValidator.test.ts b/themes/main/client/test/firstfactor/FirstFactorValidator.test.ts new file mode 100644 index 000000000..027bc71d8 --- /dev/null +++ b/themes/main/client/test/firstfactor/FirstFactorValidator.test.ts @@ -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."); + }); + }); +}); \ No newline at end of file diff --git a/themes/main/client/test/mocks/NotifierStub.ts b/themes/main/client/test/mocks/NotifierStub.ts new file mode 100644 index 000000000..9c268d66d --- /dev/null +++ b/themes/main/client/test/mocks/NotifierStub.ts @@ -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(); + } +} \ No newline at end of file diff --git a/themes/main/client/test/mocks/jquery.ts b/themes/main/client/test/mocks/jquery.ts new file mode 100644 index 000000000..273f90861 --- /dev/null +++ b/themes/main/client/test/mocks/jquery.ts @@ -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() + }; +} diff --git a/themes/main/client/test/mocks/u2f-api.ts b/themes/main/client/test/mocks/u2f-api.ts new file mode 100644 index 000000000..d123f6a95 --- /dev/null +++ b/themes/main/client/test/mocks/u2f-api.ts @@ -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() + }; +} \ No newline at end of file diff --git a/themes/main/client/test/secondfactor/TOTPValidator.test.ts b/themes/main/client/test/secondfactor/TOTPValidator.test.ts new file mode 100644 index 000000000..5dd6f15c3 --- /dev/null +++ b/themes/main/client/test/secondfactor/TOTPValidator.test.ts @@ -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(); + }); + }); +}); \ No newline at end of file diff --git a/themes/main/client/test/totp-register/totp-register.test.ts b/themes/main/client/test/totp-register/totp-register.test.ts new file mode 100644 index 000000000..86fc455a1 --- /dev/null +++ b/themes/main/client/test/totp-register/totp-register.test.ts @@ -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); + }); +}); \ No newline at end of file diff --git a/themes/main/client/tsconfig.json b/themes/main/client/tsconfig.json new file mode 100644 index 000000000..0bb4d62ff --- /dev/null +++ b/themes/main/client/tsconfig.json @@ -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/**/*" + ] +} diff --git a/themes/main/client/tslint.json b/themes/main/client/tslint.json new file mode 100644 index 000000000..c2c1b7501 --- /dev/null +++ b/themes/main/client/tslint.json @@ -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 + } +} diff --git a/themes/main/server/.directory b/themes/main/server/.directory new file mode 100644 index 000000000..a9c754bbb --- /dev/null +++ b/themes/main/server/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,58,21 +Version=3 +ViewMode=1 diff --git a/themes/main/server/src/index.ts b/themes/main/server/src/index.ts new file mode 100755 index 000000000..fcbf4d022 --- /dev/null +++ b/themes/main/server/src/index.ts @@ -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 "); + 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); diff --git a/themes/main/server/src/lib/.directory b/themes/main/server/src/lib/.directory new file mode 100644 index 000000000..006b379ad --- /dev/null +++ b/themes/main/server/src/lib/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,59,13 +Version=3 +ViewMode=1 diff --git a/themes/main/server/src/lib/AuthenticationSessionHandler.ts b/themes/main/server/src/lib/AuthenticationSessionHandler.ts new file mode 100644 index 000000000..57361bf8a --- /dev/null +++ b/themes/main/server/src/lib/AuthenticationSessionHandler.ts @@ -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; + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/ErrorReplies.ts b/themes/main/server/src/lib/ErrorReplies.ts new file mode 100644 index 000000000..f1c5f4fd1 --- /dev/null +++ b/themes/main/server/src/lib/ErrorReplies.ts @@ -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 }); +} \ No newline at end of file diff --git a/themes/main/server/src/lib/Exceptions.ts b/themes/main/server/src/lib/Exceptions.ts new file mode 100644 index 000000000..83fa4eb6b --- /dev/null +++ b/themes/main/server/src/lib/Exceptions.ts @@ -0,0 +1,88 @@ + +export class LdapSearchError extends Error { + constructor(message?: string) { + super(message); + this.name = "LdapSearchError"; + (Object).setPrototypeOf(this, LdapSearchError.prototype); + } +} + +export class LdapBindError extends Error { + constructor(message?: string) { + super(message); + this.name = "LdapBindError"; + (Object).setPrototypeOf(this, LdapBindError.prototype); + } +} + +export class LdapError extends Error { + constructor(message?: string) { + super(message); + this.name = "LdapError"; + (Object).setPrototypeOf(this, LdapError.prototype); + } +} + +export class IdentityError extends Error { + constructor(message?: string) { + super(message); + this.name = "IdentityError"; + (Object).setPrototypeOf(this, IdentityError.prototype); + } +} + +export class AccessDeniedError extends Error { + constructor(message?: string) { + super(message); + this.name = "AccessDeniedError"; + (Object).setPrototypeOf(this, AccessDeniedError.prototype); + } +} + +export class AuthenticationRegulationError extends Error { + constructor(message?: string) { + super(message); + this.name = "AuthenticationRegulationError"; + (Object).setPrototypeOf(this, AuthenticationRegulationError.prototype); + } +} + +export class InvalidTOTPError extends Error { + constructor(message?: string) { + super(message); + this.name = "InvalidTOTPError"; + (Object).setPrototypeOf(this, InvalidTOTPError.prototype); + } +} + +export class NotAuthenticatedError extends Error { + constructor(message?: string) { + super(message); + this.name = "NotAuthenticatedError"; + (Object).setPrototypeOf(this, NotAuthenticatedError.prototype); + } +} + +export class NotAuthorizedError extends Error { + constructor(message?: string) { + super(message); + this.name = "NotAuthanticatedError"; + (Object).setPrototypeOf(this, NotAuthorizedError.prototype); + } +} + +export class FirstFactorValidationError extends Error { + constructor(message?: string) { + super(message); + this.name = "FirstFactorValidationError"; + (Object).setPrototypeOf(this, FirstFactorValidationError.prototype); + } +} + +export class SecondFactorValidationError extends Error { + constructor(message?: string) { + super(message); + this.name = "SecondFactorValidationError"; + (Object).setPrototypeOf(this, FirstFactorValidationError.prototype); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/FirstFactorValidator.ts b/themes/main/server/src/lib/FirstFactorValidator.ts new file mode 100644 index 000000000..231060002 --- /dev/null +++ b/themes/main/server/src/lib/FirstFactorValidator.ts @@ -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 { + 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(); + }); +} \ No newline at end of file diff --git a/themes/main/server/src/lib/IdentityCheckMiddleware.spec.ts b/themes/main/server/src/lib/IdentityCheckMiddleware.spec.ts new file mode 100644 index 000000000..842ed6bcb --- /dev/null +++ b/themes/main/server/src/lib/IdentityCheckMiddleware.spec.ts @@ -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")); + }); + }); + }); +}); diff --git a/themes/main/server/src/lib/IdentityCheckMiddleware.ts b/themes/main/server/src/lib/IdentityCheckMiddleware.ts new file mode 100644 index 000000000..e72ea4db1 --- /dev/null +++ b/themes/main/server/src/lib/IdentityCheckMiddleware.ts @@ -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 { + + 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 { + 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 { + 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 { + + let authSession: AuthenticationSession; + const identityToken = objectPath.get( + 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 { + 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)); + }; +} diff --git a/themes/main/server/src/lib/IdentityCheckPreValidationTemplate.ts b/themes/main/server/src/lib/IdentityCheckPreValidationTemplate.ts new file mode 100644 index 000000000..0161ce406 --- /dev/null +++ b/themes/main/server/src/lib/IdentityCheckPreValidationTemplate.ts @@ -0,0 +1,3 @@ + + +export const PRE_VALIDATION_TEMPLATE = "need-identity-validation"; \ No newline at end of file diff --git a/themes/main/server/src/lib/IdentityValidable.ts b/themes/main/server/src/lib/IdentityValidable.ts new file mode 100644 index 000000000..075580c9e --- /dev/null +++ b/themes/main/server/src/lib/IdentityValidable.ts @@ -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; + postValidationInit(req: Express.Request): Bluebird; + + // 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; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/IdentityValidableStub.spec.ts b/themes/main/server/src/lib/IdentityValidableStub.spec.ts new file mode 100644 index 000000000..20a977140 --- /dev/null +++ b/themes/main/server/src/lib/IdentityValidableStub.spec.ts @@ -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 { + return this.preValidationInitStub(req); + } + + postValidationInit(req: Express.Request): Bluebird { + 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(); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/Server.spec.ts b/themes/main/server/src/lib/Server.spec.ts new file mode 100644 index 000000000..365163254 --- /dev/null +++ b/themes/main/server/src/lib/Server.spec.ts @@ -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(); + }); + }); +}); diff --git a/themes/main/server/src/lib/Server.ts b/themes/main/server/src/lib/Server.ts new file mode 100644 index 000000000..4090f6294 --- /dev/null +++ b/themes/main/server/src/lib/Server.ts @@ -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 { + 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((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 { + 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(); + } +} + diff --git a/themes/main/server/src/lib/ServerVariables.ts b/themes/main/server/src/lib/ServerVariables.ts new file mode 100644 index 000000000..cd3dd6dc8 --- /dev/null +++ b/themes/main/server/src/lib/ServerVariables.ts @@ -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; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/ServerVariablesInitializer.ts b/themes/main/server/src/lib/ServerVariablesInitializer.ts new file mode 100644 index 000000000..df79238cc --- /dev/null +++ b/themes/main/server/src/lib/ServerVariablesInitializer.ts @@ -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 { + 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 { + + 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); + }); + } +} diff --git a/themes/main/server/src/lib/ServerVariablesMockBuilder.spec.ts b/themes/main/server/src/lib/ServerVariablesMockBuilder.spec.ts new file mode 100644 index 000000000..7874702a0 --- /dev/null +++ b/themes/main/server/src/lib/ServerVariablesMockBuilder.spec.ts @@ -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 + }; + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/Level.ts b/themes/main/server/src/lib/authentication/Level.ts new file mode 100644 index 000000000..57b6a2346 --- /dev/null +++ b/themes/main/server/src/lib/authentication/Level.ts @@ -0,0 +1,5 @@ +export enum Level { + NOT_AUTHENTICATED = 0, + ONE_FACTOR = 1, + TWO_FACTOR = 2 +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/GroupsAndEmails.ts b/themes/main/server/src/lib/authentication/backends/GroupsAndEmails.ts new file mode 100644 index 000000000..3434ba66f --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/GroupsAndEmails.ts @@ -0,0 +1,5 @@ + +export interface GroupsAndEmails { + groups: string[]; + emails: string[]; +} diff --git a/themes/main/server/src/lib/authentication/backends/IUsersDatabase.ts b/themes/main/server/src/lib/authentication/backends/IUsersDatabase.ts new file mode 100644 index 000000000..d7fa13b7a --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/IUsersDatabase.ts @@ -0,0 +1,10 @@ +import Bluebird = require("bluebird"); + +import { GroupsAndEmails } from "./GroupsAndEmails"; + +export interface IUsersDatabase { + checkUserPassword(username: string, password: string): Bluebird; + getEmails(username: string): Bluebird; + getGroups(username: string): Bluebird; + updatePassword(username: string, newPassword: string): Bluebird; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts b/themes/main/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts new file mode 100644 index 000000000..19341a5dd --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts @@ -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 { + return this.checkUserPasswordStub(username, password); + } + + getEmails(username: string): Bluebird { + return this.getEmailsStub(username); + } + + getGroups(username: string): Bluebird { + return this.getGroupsStub(username); + } + + updatePassword(username: string, newPassword: string): Bluebird { + return this.updatePasswordStub(username, newPassword); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts b/themes/main/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts new file mode 100644 index 000000000..a258a78f7 --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts @@ -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()); + }); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/file/FileUsersDatabase.ts b/themes/main/server/src/lib/authentication/backends/file/FileUsersDatabase.ts new file mode 100644 index 000000000..d34dde21e --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/file/FileUsersDatabase.ts @@ -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 { + return new Bluebird((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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + return this.readDatabase() + .then((database) => { + return this.checkUserExists(database, username) + .then(() => this.retrieveEmails(database, username)); + }); + } + + getGroups(username: string): Bluebird { + return this.readDatabase() + .then((database) => { + return this.checkUserExists(database, username) + .then(() => this.retrieveGroups(database, username)); + }); + } + + updatePassword(username: string, newPassword: string): Bluebird { + return this.readDatabase() + .then((database) => { + return this.checkUserExists(database, username) + .then(() => this.replacePassword(database, username, newPassword)); + }); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/file/ReadWriteQueue.ts b/themes/main/server/src/lib/authentication/backends/file/ReadWriteQueue.ts new file mode 100644 index 000000000..957ddaec5 --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/file/ReadWriteQueue.ts @@ -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(); + } + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/ISession.ts b/themes/main/server/src/lib/authentication/backends/ldap/ISession.ts new file mode 100644 index 000000000..da2c74433 --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/ldap/ISession.ts @@ -0,0 +1,12 @@ + +import BluebirdPromise = require("bluebird"); + +export interface ISession { + open(): BluebirdPromise; + close(): BluebirdPromise; + + searchUserDn(username: string): BluebirdPromise; + searchEmails(username: string): BluebirdPromise; + searchGroups(username: string): BluebirdPromise; + modifyPassword(username: string, newPassword: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/ISessionFactory.ts b/themes/main/server/src/lib/authentication/backends/ldap/ISessionFactory.ts new file mode 100644 index 000000000..014d1eea5 --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/ldap/ISessionFactory.ts @@ -0,0 +1,6 @@ + +import { ISession } from "./ISession"; + +export interface ISessionFactory { + create(userDN: string, password: string): ISession; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts b/themes/main/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts new file mode 100644 index 000000000..f4a6e6306 --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts @@ -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); + }) + }); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts b/themes/main/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts new file mode 100644 index 000000000..edda62ec6 --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts @@ -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 = (session: ISession) => Bluebird; + +export class LdapUsersDatabase implements IUsersDatabase { + private sessionFactory: ISessionFactory; + private configuration: LdapConfiguration; + + constructor( + sessionFactory: ISessionFactory, + configuration: LdapConfiguration) { + this.sessionFactory = sessionFactory; + this.configuration = configuration; + } + + private withSession( + username: string, + password: string, + cb: SessionCallback): Bluebird { + const session = this.sessionFactory.create(username, password); + return session.open() + .then(() => cb(session)) + .finally(() => session.close()); + } + + checkUserPassword(username: string, password: string): Bluebird { + const that = this; + function verifyUserPassword(userDN: string) { + return that.withSession( + 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 { + 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 { + 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 { + 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)); + }); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts b/themes/main/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts new file mode 100644 index 000000000..9dedfcb7c --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts @@ -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"); + }); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/SafeSession.ts b/themes/main/server/src/lib/authentication/backends/ldap/SafeSession.ts new file mode 100644 index 000000000..572209066 --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/ldap/SafeSession.ts @@ -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 { + return this.sesion.open(); + } + + close(): BluebirdPromise { + return this.sesion.close(); + } + + searchGroups(username: string): BluebirdPromise { + 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 { + 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 { + 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 { + try { + const sanitizedUsername = Sanitizer.sanitize(username); + return this.sesion.modifyPassword(sanitizedUsername, newPassword); + } + catch (e) { + return BluebirdPromise.reject(new Error(SPECIAL_CHAR_USED_MESSAGE)); + } + } +} diff --git a/themes/main/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts b/themes/main/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts new file mode 100644 index 000000000..9dd33fed4 --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts @@ -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 { 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); + }); +}); diff --git a/themes/main/server/src/lib/authentication/backends/ldap/Sanitizer.ts b/themes/main/server/src/lib/authentication/backends/ldap/Sanitizer.ts new file mode 100644 index 000000000..be74132ad --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/ldap/Sanitizer.ts @@ -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; + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/Session.spec.ts b/themes/main/server/src/lib/authentication/backends/ldap/Session.spec.ts new file mode 100644 index 000000000..d55f6a805 --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/ldap/Session.spec.ts @@ -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"]); + }); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/Session.ts b/themes/main/server/src/lib/authentication/backends/ldap/Session.ts new file mode 100644 index 000000000..e0284b3c4 --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/ldap/Session.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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(); + }); + } +} diff --git a/themes/main/server/src/lib/authentication/backends/ldap/SessionFactory.ts b/themes/main/server/src/lib/authentication/backends/ldap/SessionFactory.ts new file mode 100644 index 000000000..0b6c4bff3 --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/ldap/SessionFactory.ts @@ -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 + ) + ); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts b/themes/main/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts new file mode 100644 index 000000000..face3930d --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts @@ -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); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts b/themes/main/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts new file mode 100644 index 000000000..5faf2ba18 --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts @@ -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 { + return this.openStub(); + } + + close(): Bluebird { + return this.closeStub(); + } + + searchUserDn(username: string): Bluebird { + return this.searchUserDnStub(username); + } + + searchEmails(username: string): Bluebird { + return this.searchEmailsStub(username); + } + + searchGroups(username: string): Bluebird { + return this.searchGroupsStub(username); + } + + modifyPassword(username: string, newPassword: string): Bluebird { + return this.modifyPasswordStub(username, newPassword); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/connector/Connector.ts b/themes/main/server/src/lib/authentication/backends/ldap/connector/Connector.ts new file mode 100644 index 000000000..2542ea7f9 --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/ldap/connector/Connector.ts @@ -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; + unbindAsync(): Bluebird; + searchAsync(base: string, query: LdapJs.SearchOptions): Bluebird; + modifyAsync(userdn: string, change: LdapJs.Change): Bluebird; +} + +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 { + return this.client.bindAsync(username, password); + } + + unbindAsync(): Bluebird { + return this.client.unbindAsync(); + } + + searchAsync(base: string, query: any): Bluebird { + const that = this; + return this.client.searchAsync(base, query) + .then(function (res: EventEmitter) { + const doc: SearchEntry[] = []; + return new Bluebird((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 { + return this.client.modifyAsync(dn, changeRequest); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts b/themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts new file mode 100644 index 000000000..61fef07a4 --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts @@ -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); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts b/themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts new file mode 100644 index 000000000..d11fa6386 --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts @@ -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(); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts b/themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts new file mode 100644 index 000000000..0b78225bf --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts @@ -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 { + return this.bindAsyncStub(username, password); + } + + unbindAsync(): BluebirdPromise { + return this.unbindAsyncStub(); + } + + searchAsync(base: string, query: any): BluebirdPromise { + return this.searchAsyncStub(base, query); + } + + modifyAsync(dn: string, changeRequest: any): BluebirdPromise { + return this.modifyAsyncStub(dn, changeRequest); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/connector/IConnector.ts b/themes/main/server/src/lib/authentication/backends/ldap/connector/IConnector.ts new file mode 100644 index 000000000..1e63ab193 --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/ldap/connector/IConnector.ts @@ -0,0 +1,9 @@ +import Bluebird = require("bluebird"); +import EventEmitter = require("events"); + +export interface IConnector { + bindAsync(username: string, password: string): Bluebird; + unbindAsync(): Bluebird; + searchAsync(base: string, query: any): Bluebird; + modifyAsync(dn: string, changeRequest: any): Bluebird; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts b/themes/main/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts new file mode 100644 index 000000000..f9ed65eff --- /dev/null +++ b/themes/main/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts @@ -0,0 +1,5 @@ +import { IConnector } from "./IConnector"; + +export interface IConnectorFactory { + create(): IConnector; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/totp/ITotpHandler.ts b/themes/main/server/src/lib/authentication/totp/ITotpHandler.ts new file mode 100644 index 000000000..d600d31e4 --- /dev/null +++ b/themes/main/server/src/lib/authentication/totp/ITotpHandler.ts @@ -0,0 +1,6 @@ +import { TOTPSecret } from "../../../../types/TOTPSecret"; + +export interface ITotpHandler { + generate(label: string, issuer: string): TOTPSecret; + validate(token: string, secret: string): boolean; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/totp/TotpHandler.spec.ts b/themes/main/server/src/lib/authentication/totp/TotpHandler.spec.ts new file mode 100644 index 000000000..67cffa638 --- /dev/null +++ b/themes/main/server/src/lib/authentication/totp/TotpHandler.spec.ts @@ -0,0 +1,39 @@ + +import { TotpHandler } from "./TotpHandler"; +import Sinon = require("sinon"); +import Speakeasy = require("speakeasy"); +import Assert = require("assert"); + +describe("authentication/totp/TotpHandler", function() { + let totpValidator: TotpHandler; + let validateStub: Sinon.SinonStub; + + beforeEach(() => { + validateStub = Sinon.stub(Speakeasy.totp, "verify"); + totpValidator = new TotpHandler(Speakeasy); + }); + + afterEach(function() { + validateStub.restore(); + }); + + it("should validate the TOTP token", function() { + const totp_secret = "NBD2ZV64R9UV1O7K"; + const token = "token"; + validateStub.withArgs({ + secret: totp_secret, + token: token, + encoding: "base32", + window: 1 + }).returns(true); + Assert(totpValidator.validate(token, totp_secret)); + }); + + it("should not validate a wrong TOTP token", function() { + const totp_secret = "NBD2ZV64R9UV1O7K"; + const token = "wrong token"; + validateStub.returns(false); + Assert(!totpValidator.validate(token, totp_secret)); + }); +}); + diff --git a/themes/main/server/src/lib/authentication/totp/TotpHandler.ts b/themes/main/server/src/lib/authentication/totp/TotpHandler.ts new file mode 100644 index 000000000..dfab502a7 --- /dev/null +++ b/themes/main/server/src/lib/authentication/totp/TotpHandler.ts @@ -0,0 +1,36 @@ +import { ITotpHandler } from "./ITotpHandler"; +import { TOTPSecret } from "../../../../types/TOTPSecret"; +import Speakeasy = require("speakeasy"); + +const TOTP_ENCODING = "base32"; +const WINDOW: number = 1; + +export class TotpHandler implements ITotpHandler { + private speakeasy: typeof Speakeasy; + + constructor(speakeasy: typeof Speakeasy) { + this.speakeasy = speakeasy; + } + + generate(label: string, issuer: string): TOTPSecret { + const secret = this.speakeasy.generateSecret({ + otpauth_url: false + }) as TOTPSecret; + + secret.otpauth_url = this.speakeasy.otpauthURL({ + secret: secret.ascii, + label: label, + issuer: issuer + }); + return secret; + } + + validate(token: string, secret: string): boolean { + return this.speakeasy.totp.verify({ + secret: secret, + encoding: TOTP_ENCODING, + token: token, + window: WINDOW + }); + } +} diff --git a/themes/main/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts b/themes/main/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts new file mode 100644 index 000000000..ea93330d9 --- /dev/null +++ b/themes/main/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts @@ -0,0 +1,22 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import { ITotpHandler } from "./ITotpHandler"; +import { TOTPSecret } from "../../../../types/TOTPSecret"; + +export class TotpHandlerStub implements ITotpHandler { + generateStub: Sinon.SinonStub; + validateStub: Sinon.SinonStub; + + constructor() { + this.generateStub = Sinon.stub(); + this.validateStub = Sinon.stub(); + } + + generate(label: string, issuer: string): TOTPSecret { + return this.generateStub(label, issuer); + } + + validate(token: string, secret: string): boolean { + return this.validateStub(token, secret); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/u2f/IU2fHandler.ts b/themes/main/server/src/lib/authentication/u2f/IU2fHandler.ts new file mode 100644 index 000000000..b9b7d6f26 --- /dev/null +++ b/themes/main/server/src/lib/authentication/u2f/IU2fHandler.ts @@ -0,0 +1,9 @@ +import U2f = require("u2f"); + +export interface IU2fHandler { + request(appId: string, keyHandle?: string): U2f.Request; + checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) + : U2f.RegistrationResult | U2f.Error; + checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) + : U2f.SignatureResult | U2f.Error; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/u2f/U2fHandler.ts b/themes/main/server/src/lib/authentication/u2f/U2fHandler.ts new file mode 100644 index 000000000..bf3891e5b --- /dev/null +++ b/themes/main/server/src/lib/authentication/u2f/U2fHandler.ts @@ -0,0 +1,24 @@ +import { IU2fHandler } from "./IU2fHandler"; +import U2f = require("u2f"); + +export class U2fHandler implements IU2fHandler { + private u2f: typeof U2f; + + constructor(u2f: typeof U2f) { + this.u2f = u2f; + } + + request(appId: string, keyHandle?: string): U2f.Request { + return this.u2f.request(appId, keyHandle); + } + + checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) + : U2f.RegistrationResult | U2f.Error { + return this.u2f.checkRegistration(registrationRequest, registrationResponse); + } + + checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) + : U2f.SignatureResult | U2f.Error { + return this.u2f.checkSignature(signatureRequest, signatureResponse, publicKey); + } +} diff --git a/themes/main/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts b/themes/main/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts new file mode 100644 index 000000000..135d7eb06 --- /dev/null +++ b/themes/main/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts @@ -0,0 +1,31 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import U2f = require("u2f"); +import { IU2fHandler } from "./IU2fHandler"; + + +export class U2fHandlerStub implements IU2fHandler { + requestStub: Sinon.SinonStub; + checkRegistrationStub: Sinon.SinonStub; + checkSignatureStub: Sinon.SinonStub; + + constructor() { + this.requestStub = Sinon.stub(); + this.checkRegistrationStub = Sinon.stub(); + this.checkSignatureStub = Sinon.stub(); + } + + request(appId: string, keyHandle?: string): U2f.Request { + return this.requestStub(appId, keyHandle); + } + + checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) + : U2f.RegistrationResult | U2f.Error { + return this.checkRegistrationStub(registrationRequest, registrationResponse); + } + + checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) + : U2f.SignatureResult | U2f.Error { + return this.checkSignatureStub(signatureRequest, signatureResponse, publicKey); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authorization/Authorizer.spec.ts b/themes/main/server/src/lib/authorization/Authorizer.spec.ts new file mode 100644 index 000000000..586814042 --- /dev/null +++ b/themes/main/server/src/lib/authorization/Authorizer.spec.ts @@ -0,0 +1,372 @@ + +import Assert = require("assert"); +import winston = require("winston"); +import { Authorizer } from "./Authorizer"; +import { ACLConfiguration, ACLRule } from "../configuration/schema/AclConfiguration"; +import { Level } from "./Level"; + +describe("authorization/Authorizer", function () { + let authorizer: Authorizer; + let configuration: ACLConfiguration; + + describe("configuration is null", function() { + it("should allow access to anything, anywhere for anybody", function() { + configuration = undefined; + authorizer = new Authorizer(configuration, winston); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1", "group2"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "admin.example.com", resource: "/"}, {user: "user3", groups: ["group3"]}), Level.BYPASS); + }); + }); + + describe("configuration is not null", function () { + beforeEach(function () { + configuration = { + default_policy: "deny", + rules: [] + }; + authorizer = new Authorizer(configuration, winston); + }); + + describe("check access control with default policy to deny", function () { + beforeEach(function () { + configuration.default_policy = "deny"; + }); + + it("should deny access when no rule is provided", function () { + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should control access when multiple domain matcher is provided", function () { + configuration.rules = [{ + domain: "*.mail.example.com", + policy: "two_factor", + subject: "user:user1", + resources: [".*"] + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should allow access to all resources when resources is not provided", function () { + configuration.rules = [{ + domain: "*.mail.example.com", + policy: "two_factor", + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + describe("check user rules", function () { + it("should allow access when user has a matching allowing rule", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: [".*"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should deny to other users", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: [".*"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user2", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); + }); + + it("should allow user access only to specific resources", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: ["/private/.*", "^/begin", "/end$"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/middle/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/begin"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/not/begin"}, {user: "user1", groups: ["group1"]}), Level.DENY); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end/x"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should allow access to multiple domains", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: [".*"], + subject: "user:user1" + }, { + domain: "home1.example.com", + policy: "one_factor", + resources: [".*"], + subject: "user:user1" + }, { + domain: "home2.example.com", + policy: "deny", + resources: [".*"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home1.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); + Assert.equal(authorizer.authorization({domain: "home2.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home3.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should apply rules in order", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "one_factor", + resources: ["/my/private/resource"], + subject: "user:user1" + }, { + domain: "home.example.com", + policy: "deny", + resources: ["^/my/private/.*"], + subject: "user:user1" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/my/.*"], + subject: "user:user1" + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/poney"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/duck"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/resource"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); + }); + }); + + describe("check group rules", function () { + it("should allow access when user is in group having a matching allowing rule", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: ["^/$"], + subject: "group:group1" + }, { + domain: "home.example.com", + policy: "one_factor", + resources: ["^/test$"], + subject: "group:group2" + }, { + domain: "home.example.com", + policy: "deny", + resources: ["^/private$"], + subject: "group:group2" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.ONE_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); + }); + }); + }); + + describe("check any rules", function () { + it("should control access when any rules are defined", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "bypass", + resources: ["^/public$"] + }, { + domain: "home.example.com", + policy: "deny", + resources: ["^/private$"] + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, + {user: "user4", groups: ["group5"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, + {user: "user4", groups: ["group5"]}), Level.DENY); + }); + }); + + describe("check access control with default policy to allow", function () { + beforeEach(function () { + configuration.default_policy = "bypass"; + }); + + it("should allow access to anything when no rule is provided", function () { + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + }); + + it("should deny access to one resource when defined", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["/test"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + }); + }); + + describe("check access control with complete use case", function () { + beforeEach(function () { + configuration.default_policy = "deny"; + }); + + it("should control access of multiple user (real use case)", function () { + // Let say we have three users: admin, john, harry. + // admin is in groups ["admins"] + // john is in groups ["dev", "admin-private"] + // harry is in groups ["dev"] + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: ["^/public$", "^/$"] + }, { + domain: "home.example.com", + policy: "two_factor", + resources: [".*"], + subject: "group:admins" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/private/?.*"], + subject: "group:admin-private" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/private/john$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/private/harry"], + subject: "user:harry" + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); + }); + + it("should allow when allowed at group level and denied at user level", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"], + subject: "group:dev" + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); + }); + + it("should allow access when allowed at 'any' level and denied at user level", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"] + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); + }); + + it("should allow access when allowed at 'any' level and denied at group level", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "group:dev" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"] + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); + }); + + it("should respect rules precedence", function () { + // the priority from least to most is 'default_policy', 'all', 'group', 'user' + // and the first rules in each category as a lower priority than the latest. + // You can think of it that way: they override themselves inside each category. + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "group:dev" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"] + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + }); + }); + }); +}); diff --git a/themes/main/server/src/lib/authorization/Authorizer.ts b/themes/main/server/src/lib/authorization/Authorizer.ts new file mode 100644 index 000000000..889b7ec20 --- /dev/null +++ b/themes/main/server/src/lib/authorization/Authorizer.ts @@ -0,0 +1,85 @@ + +import { ACLConfiguration, ACLPolicy, ACLRule } from "../configuration/schema/AclConfiguration"; +import { IAuthorizer } from "./IAuthorizer"; +import { Winston } from "../../../types/Dependencies"; +import { MultipleDomainMatcher } from "./MultipleDomainMatcher"; +import { Level } from "./Level"; +import { Object } from "./Object"; +import { Subject } from "./Subject"; + +function MatchDomain(actualDomain: string) { + return function (rule: ACLRule): boolean { + return MultipleDomainMatcher.match(actualDomain, rule.domain); + }; +} + +function MatchResource(actualResource: string) { + return function (rule: ACLRule): boolean { + // If resources key is not provided, the rule applies to all resources. + if (!rule.resources) return true; + + for (let i = 0; i < rule.resources.length; ++i) { + const regexp = new RegExp(rule.resources[i]); + if (regexp.test(actualResource)) return true; + } + return false; + }; +} + +function MatchSubject(subject: Subject) { + return (rule: ACLRule) => { + // If no subject, matches anybody + if (!rule.subject) return true; + + if (rule.subject.startsWith("user:")) { + const ruleUser = rule.subject.split(":")[1]; + if (subject.user == ruleUser) return true; + } + + if (rule.subject.startsWith("group:")) { + const ruleGroup = rule.subject.split(":")[1]; + if (subject.groups.indexOf(ruleGroup) > -1) return true; + } + return false; + }; +} + +export class Authorizer implements IAuthorizer { + private logger: Winston; + private readonly configuration: ACLConfiguration; + + constructor(configuration: ACLConfiguration, logger_: Winston) { + this.logger = logger_; + this.configuration = configuration; + } + + private getMatchingRules(object: Object, subject: Subject): ACLRule[] { + const rules = this.configuration.rules; + if (!rules) return []; + return rules + .filter(MatchDomain(object.domain)) + .filter(MatchResource(object.resource)) + .filter(MatchSubject(subject)); + } + + private ruleToLevel(policy: string): Level { + if (policy == "bypass") { + return Level.BYPASS; + } else if (policy == "one_factor") { + return Level.ONE_FACTOR; + } else if (policy == "two_factor") { + return Level.TWO_FACTOR; + } + return Level.DENY; + } + + authorization(object: Object, subject: Subject): Level { + if (!this.configuration) return Level.BYPASS; + + const rules = this.getMatchingRules(object, subject); + + return (rules.length > 0) + ? this.ruleToLevel(rules[0].policy) // extract the policy of the first matching rule + : this.ruleToLevel(this.configuration.default_policy); // otherwise use the default policy + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authorization/AuthorizerStub.spec.ts b/themes/main/server/src/lib/authorization/AuthorizerStub.spec.ts new file mode 100644 index 000000000..9bd6f4a85 --- /dev/null +++ b/themes/main/server/src/lib/authorization/AuthorizerStub.spec.ts @@ -0,0 +1,17 @@ +import Sinon = require("sinon"); +import { IAuthorizer } from "./IAuthorizer"; +import { Level } from "./Level"; +import { Object } from "./Object"; +import { Subject } from "./Subject"; + +export class AuthorizerStub implements IAuthorizer { + authorizationMock: Sinon.SinonStub; + + constructor() { + this.authorizationMock = Sinon.stub(); + } + + authorization(object: Object, subject: Subject): Level { + return this.authorizationMock(object, subject); + } +} diff --git a/themes/main/server/src/lib/authorization/IAuthorizer.ts b/themes/main/server/src/lib/authorization/IAuthorizer.ts new file mode 100644 index 000000000..fe7ba367a --- /dev/null +++ b/themes/main/server/src/lib/authorization/IAuthorizer.ts @@ -0,0 +1,7 @@ +import { Level } from "./Level"; +import { Subject } from "./Subject"; +import { Object } from "./Object"; + +export interface IAuthorizer { + authorization(object: Object, subject: Subject): Level; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authorization/Level.ts b/themes/main/server/src/lib/authorization/Level.ts new file mode 100644 index 000000000..d12802610 --- /dev/null +++ b/themes/main/server/src/lib/authorization/Level.ts @@ -0,0 +1,6 @@ +export enum Level { + BYPASS = 0, + ONE_FACTOR = 1, + TWO_FACTOR = 2, + DENY = 3 +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authorization/MultipleDomainMatcher.ts b/themes/main/server/src/lib/authorization/MultipleDomainMatcher.ts new file mode 100644 index 000000000..64c647a4a --- /dev/null +++ b/themes/main/server/src/lib/authorization/MultipleDomainMatcher.ts @@ -0,0 +1,12 @@ + +export class MultipleDomainMatcher { + static match(domain: string, pattern: string): boolean { + if (pattern.startsWith("*") && + domain.endsWith(pattern.substr(1))) { + return true; + } + else if (domain == pattern) { + return true; + } + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authorization/Object.ts b/themes/main/server/src/lib/authorization/Object.ts new file mode 100644 index 000000000..5411b0d24 --- /dev/null +++ b/themes/main/server/src/lib/authorization/Object.ts @@ -0,0 +1,5 @@ + +export interface Object { + domain: string; + resource: string; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/authorization/Subject.ts b/themes/main/server/src/lib/authorization/Subject.ts new file mode 100644 index 000000000..310d6b4c0 --- /dev/null +++ b/themes/main/server/src/lib/authorization/Subject.ts @@ -0,0 +1,5 @@ + +export interface Subject { + user: string; + groups: string[]; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/ConfigurationParser.spec.ts b/themes/main/server/src/lib/configuration/ConfigurationParser.spec.ts new file mode 100644 index 000000000..60c0f6182 --- /dev/null +++ b/themes/main/server/src/lib/configuration/ConfigurationParser.spec.ts @@ -0,0 +1,171 @@ +import * as Assert from "assert"; +import { Configuration } from "./schema/Configuration"; +import { ACLConfiguration } from "./schema/AclConfiguration"; +import { ConfigurationParser } from "./ConfigurationParser"; + +describe("configuration/ConfigurationParser", function () { + function buildYamlConfig(): Configuration { + const yaml_config: Configuration = { + port: 8080, + authentication_backend: { + ldap: { + url: "http://ldap", + base_dn: "dc=example,dc=com", + additional_users_dn: "ou=users", + additional_groups_dn: "ou=groups", + user: "user", + password: "pass" + }, + }, + session: { + domain: "example.com", + secret: "secret", + expiration: 40000 + }, + storage: { + local: { + path: "/mydirectory" + } + }, + regulation: { + max_retries: 3, + find_time: 5 * 60, + ban_time: 5 * 60 + }, + logs_level: "debug", + notifier: { + email: { + username: "user", + password: "password", + sender: "admin@example.com", + service: "gmail" + } + } + }; + return yaml_config; + } + + describe("port", function () { + it("should read the port from the yaml file", function () { + const yaml_config = buildYamlConfig(); + yaml_config.port = 7070; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.port, 7070); + }); + + it("should default the port to 8080 if not provided", function () { + const yaml_config = buildYamlConfig(); + delete yaml_config.port; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.port, 8080); + }); + }); + + describe("test session configuration", function() { + it("should get the session attributes", function () { + const yaml_config = buildYamlConfig(); + yaml_config.session = { + domain: "example.com", + secret: "secret", + expiration: 3600, + inactivity: 4000 + }; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.session.domain, "example.com"); + Assert.equal(config.session.secret, "secret"); + Assert.equal(config.session.expiration, 3600); + Assert.equal(config.session.inactivity, 4000); + }); + + it("should be ok not specifying inactivity", function () { + const yaml_config = buildYamlConfig(); + yaml_config.session = { + domain: "example.com", + secret: "secret", + expiration: 3600 + }; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.session.domain, "example.com"); + Assert.equal(config.session.secret, "secret"); + Assert.equal(config.session.expiration, 3600); + Assert.equal(config.session.inactivity, undefined); + }); + }); + + it("should get the log level", function () { + const yaml_config = buildYamlConfig(); + yaml_config.logs_level = "debug"; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.logs_level, "debug"); + }); + + it("should get the notifier config", function () { + const userConfig = buildYamlConfig(); + userConfig.notifier = { + email: { + username: "user", + password: "pass", + sender: "admin@example.com", + service: "gmail" + } + }; + const config = ConfigurationParser.parse(userConfig); + Assert.deepEqual(config.notifier, { + email: { + username: "user", + password: "pass", + sender: "admin@example.com", + service: "gmail" + } + }); + }); + + describe("access_control", function() { + it("should adapt access_control when it is already ok", function () { + const userConfig = buildYamlConfig(); + userConfig.access_control = { + default_policy: "deny", + rules: [{ + domain: "www.example.com", + policy: "two_factor", + subject: "user:user" + }, { + domain: "public.example.com", + policy: "two_factor" + }] + }; + const config = ConfigurationParser.parse(userConfig); + Assert.deepEqual(config.access_control, { + default_policy: "deny", + rules: [{ + domain: "www.example.com", + policy: "two_factor", + subject: "user:user" + }, { + domain: "public.example.com", + policy: "two_factor" + }] + } as ACLConfiguration); + }); + + + it("should adapt access_control when it is empty", function () { + const userConfig = buildYamlConfig(); + userConfig.access_control = {} as any; + const config = ConfigurationParser.parse(userConfig); + Assert.deepEqual(config.access_control, { + default_policy: "bypass", + rules: [] + }); + }); + }); + + describe("default_redirection_url", function() { + it("should parse default_redirection_url", function() { + const userConfig = buildYamlConfig(); + userConfig.default_redirection_url = "dummy_url"; + const config = ConfigurationParser.parse(userConfig); + Assert.deepEqual(config.default_redirection_url, "dummy_url"); + }); + }); +}); diff --git a/themes/main/server/src/lib/configuration/ConfigurationParser.ts b/themes/main/server/src/lib/configuration/ConfigurationParser.ts new file mode 100644 index 000000000..d92d163c4 --- /dev/null +++ b/themes/main/server/src/lib/configuration/ConfigurationParser.ts @@ -0,0 +1,39 @@ + +import * as ObjectPath from "object-path"; +import { Configuration, complete } from "./schema/Configuration"; +import Ajv = require("ajv"); +import Path = require("path"); +import Util = require("util"); + +export class ConfigurationParser { + private static parseTypes(configuration: Configuration): string[] { + const schema = require(Path.resolve(__dirname, "./Configuration.schema.json")); + const ajv = new Ajv({ + allErrors: true, + missingRefs: "fail" + }); + ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json")); + const valid = ajv.validate(schema, configuration); + if (!valid) + return ajv.errors.map( + (e: Ajv.ErrorObject) => { return ajv.errorsText([e]); }); + return []; + } + + static parse(configuration: Configuration): Configuration { + const validationErrors = this.parseTypes(configuration); + if (validationErrors.length > 0) { + validationErrors.forEach((e: string) => { console.log(e); }); + throw new Error("Malformed configuration (schema). Please double-check your configuration file."); + } + + const [newConfiguration, completionErrors] = complete(configuration); + + if (completionErrors.length > 0) { + completionErrors.forEach((e: string) => { console.log(e); }); + throw new Error("Malformed configuration (validator). Please double-check your configuration file."); + } + return newConfiguration; + } +} + diff --git a/themes/main/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts b/themes/main/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts new file mode 100644 index 000000000..d4a3093ee --- /dev/null +++ b/themes/main/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts @@ -0,0 +1,149 @@ +import { SessionConfigurationBuilder } from "./SessionConfigurationBuilder"; +import { Configuration } from "./schema/Configuration"; +import { GlobalDependencies } from "../../../types/Dependencies"; + +import ExpressSession = require("express-session"); +import ConnectRedis = require("connect-redis"); +import Sinon = require("sinon"); +import Assert = require("assert"); + +describe("configuration/SessionConfigurationBuilder", function () { + const configuration: Configuration = { + access_control: { + default_policy: "deny", + rules: [] + }, + totp: { + issuer: "authelia.com" + }, + authentication_backend: { + ldap: { + url: "ldap://ldap", + user: "user", + base_dn: "dc=example,dc=com", + password: "password", + additional_groups_dn: "ou=groups", + additional_users_dn: "ou=users", + group_name_attribute: "", + groups_filter: "", + mail_attribute: "", + users_filter: "" + }, + }, + logs_level: "debug", + notifier: { + filesystem: { + filename: "/test" + } + }, + port: 8080, + session: { + name: "authelia_session", + domain: "example.com", + expiration: 3600, + secret: "secret" + }, + regulation: { + max_retries: 3, + ban_time: 5 * 60, + find_time: 5 * 60 + }, + storage: { + local: { + in_memory: true + } + } + }; + + const deps: GlobalDependencies = { + ConnectRedis: Sinon.spy() as any, + ldapjs: Sinon.spy() as any, + nedb: Sinon.spy() as any, + session: Sinon.spy() as any, + speakeasy: Sinon.spy() as any, + u2f: Sinon.spy() as any, + winston: Sinon.spy() as any, + Redis: Sinon.spy() as any + }; + + it("should return session options without redis options", function () { + const options = SessionConfigurationBuilder.build(configuration, deps); + const expectedOptions = { + name: "authelia_session", + secret: "secret", + resave: false, + saveUninitialized: true, + cookie: { + secure: true, + httpOnly: true, + maxAge: 3600, + domain: "example.com" + } + }; + + Assert.deepEqual(expectedOptions, options); + }); + + it("should return session options with redis options", function () { + configuration.session["redis"] = { + host: "redis.example.com", + port: 6379 + }; + const RedisStoreMock = Sinon.spy(); + const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); + + deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; + deps.Redis = { + createClient: Sinon.mock().returns(redisClient) + } as any; + + const options = SessionConfigurationBuilder.build(configuration, deps); + + const expectedOptions: ExpressSession.SessionOptions = { + secret: "secret", + resave: false, + saveUninitialized: true, + name: "authelia_session", + cookie: { + secure: true, + httpOnly: true, + maxAge: 3600, + domain: "example.com" + }, + store: Sinon.match.object as any + }; + + Assert((deps.ConnectRedis as Sinon.SinonStub).calledWith(deps.session)); + Assert.equal(options.secret, expectedOptions.secret); + Assert.equal(options.resave, expectedOptions.resave); + Assert.equal(options.saveUninitialized, expectedOptions.saveUninitialized); + Assert.deepEqual(options.cookie, expectedOptions.cookie); + Assert(options.store != undefined); + }); + + it("should return session options with redis password", function () { + configuration.session["redis"] = { + host: "redis.example.com", + port: 6379, + password: "authelia_pass" + }; + const RedisStoreMock = Sinon.spy(); + const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); + const createClientStub = Sinon.stub(); + + deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; + deps.Redis = { + createClient: createClientStub + } as any; + + createClientStub.returns(redisClient); + + const options = SessionConfigurationBuilder.build(configuration, deps); + + Assert(createClientStub.calledWith({ + host: "redis.example.com", + port: 6379, + password: "authelia_pass" + })); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/SessionConfigurationBuilder.ts b/themes/main/server/src/lib/configuration/SessionConfigurationBuilder.ts new file mode 100644 index 000000000..6ce643d9d --- /dev/null +++ b/themes/main/server/src/lib/configuration/SessionConfigurationBuilder.ts @@ -0,0 +1,52 @@ +import ExpressSession = require("express-session"); +import Redis = require("redis"); + +import { Configuration } from "./schema/Configuration"; +import { GlobalDependencies } from "../../../types/Dependencies"; +import { RedisStoreOptions } from "connect-redis"; + +export class SessionConfigurationBuilder { + + static build(configuration: Configuration, deps: GlobalDependencies): ExpressSession.SessionOptions { + const sessionOptions: ExpressSession.SessionOptions = { + name: configuration.session.name, + secret: configuration.session.secret, + resave: false, + saveUninitialized: true, + cookie: { + secure: true, + httpOnly: true, + maxAge: configuration.session.expiration, + domain: configuration.session.domain + }, + }; + + if (configuration.session.redis) { + let redisOptions; + const options: Redis.ClientOpts = { + host: configuration.session.redis.host, + port: configuration.session.redis.port + }; + + if (configuration.session.redis.password) { + options["password"] = configuration.session.redis.password; + } + const client = deps.Redis.createClient(options); + + client.on("error", function (err: Error) { + console.error("Redis error:", err); + }); + + redisOptions = { + client: client, + logErrors: true + }; + + if (redisOptions) { + const RedisStore = deps.ConnectRedis(deps.session); + sessionOptions.store = new RedisStore(redisOptions); + } + } + return sessionOptions; + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/AclConfiguration.spec.ts b/themes/main/server/src/lib/configuration/schema/AclConfiguration.spec.ts new file mode 100644 index 000000000..d1e2a03a2 --- /dev/null +++ b/themes/main/server/src/lib/configuration/schema/AclConfiguration.spec.ts @@ -0,0 +1,34 @@ +import { ACLConfiguration, complete } from "./AclConfiguration"; +import Assert = require("assert"); + +describe("configuration/schema/AclConfiguration", function() { + it("should complete ACLConfiguration", function() { + const configuration: ACLConfiguration = {}; + const [newConfiguration, errors] = complete(configuration); + + Assert.deepEqual(newConfiguration.default_policy, "bypass"); + Assert.deepEqual(newConfiguration.rules, []); + }); + + it("should return errors when subject is not good", function() { + const configuration: ACLConfiguration = { + default_policy: "deny", + rules: [{ + domain: "dev.example.com", + subject: "user:abc", + policy: "bypass" + }, { + domain: "dev.example.com", + subject: "user:def", + policy: "bypass" + }, { + domain: "dev.example.com", + subject: "badkey:abc", + policy: "bypass" + }] + }; + const [newConfiguration, errors] = complete(configuration); + + Assert.deepEqual(errors, ["Rule 2 has wrong subject. It should be starting with user: or group:."]); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/AclConfiguration.ts b/themes/main/server/src/lib/configuration/schema/AclConfiguration.ts new file mode 100644 index 000000000..40401dd64 --- /dev/null +++ b/themes/main/server/src/lib/configuration/schema/AclConfiguration.ts @@ -0,0 +1,41 @@ + +export type ACLPolicy = "deny" | "bypass" | "one_factor" | "two_factor"; + +export type ACLRule = { + domain: string; + resources?: string[]; + subject?: string; + policy: ACLPolicy; +}; + +export interface ACLConfiguration { + default_policy?: ACLPolicy; + rules?: ACLRule[]; +} + +export function complete(configuration: ACLConfiguration): [ACLConfiguration, string[]] { + const newConfiguration: ACLConfiguration = (configuration) + ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.default_policy) { + newConfiguration.default_policy = "bypass"; + } + + if (!newConfiguration.rules) { + newConfiguration.rules = []; + } + + if (newConfiguration.rules.length > 0) { + const errors: string[] = []; + newConfiguration.rules.forEach((r, idx) => { + if (r.subject && !r.subject.match(/^(user|group):[a-zA-Z0-9]+$/)) { + errors.push(`Rule ${idx} has wrong subject. It should be starting with user: or group:.`); + } + }); + if (errors.length > 0) { + return [newConfiguration, errors]; + } + } + + return [newConfiguration, []]; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts b/themes/main/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts new file mode 100644 index 000000000..3ca86381c --- /dev/null +++ b/themes/main/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts @@ -0,0 +1,11 @@ +import { AuthenticationBackendConfiguration, complete } from "./AuthenticationBackendConfiguration"; +import Assert = require("assert"); + +describe("configuration/schema/AuthenticationBackendConfiguration", function() { + it("should ensure there is at least one key", function() { + const configuration: AuthenticationBackendConfiguration = {} as any; + const [newConfiguration, error] = complete(configuration); + + Assert.equal(error, "Authentication backend must have one of the following keys:`ldap` or `file`"); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts b/themes/main/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts new file mode 100644 index 000000000..7f77f894f --- /dev/null +++ b/themes/main/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts @@ -0,0 +1,25 @@ +import { LdapConfiguration } from "./LdapConfiguration"; +import { FileUsersDatabaseConfiguration } from "./FileUsersDatabaseConfiguration"; + +export interface AuthenticationBackendConfiguration { + ldap?: LdapConfiguration; + file?: FileUsersDatabaseConfiguration; +} + +export function complete( + configuration: AuthenticationBackendConfiguration) + : [AuthenticationBackendConfiguration, string] { + + const newConfiguration: AuthenticationBackendConfiguration = (configuration) + ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (Object.keys(newConfiguration).length != 1) { + return [ + newConfiguration, + "Authentication backend must have one of the following keys:" + + "`ldap` or `file`" + ]; + } + + return [newConfiguration, undefined]; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/Configuration.ts b/themes/main/server/src/lib/configuration/schema/Configuration.ts new file mode 100644 index 000000000..8d16a5fb2 --- /dev/null +++ b/themes/main/server/src/lib/configuration/schema/Configuration.ts @@ -0,0 +1,68 @@ +import { ACLConfiguration, complete as AclConfigurationComplete } from "./AclConfiguration"; +import { AuthenticationBackendConfiguration, complete as AuthenticationBackendComplete } from "./AuthenticationBackendConfiguration"; +import { NotifierConfiguration, complete as NotifierConfigurationComplete } from "./NotifierConfiguration"; +import { RegulationConfiguration, complete as RegulationConfigurationComplete } from "./RegulationConfiguration"; +import { SessionConfiguration, complete as SessionConfigurationComplete } from "./SessionConfiguration"; +import { StorageConfiguration, complete as StorageConfigurationComplete } from "./StorageConfiguration"; +import { TotpConfiguration, complete as TotpConfigurationComplete } from "./TotpConfiguration"; + +export interface Configuration { + access_control?: ACLConfiguration; + authentication_backend: AuthenticationBackendConfiguration; + default_redirection_url?: string; + logs_level?: string; + notifier?: NotifierConfiguration; + port?: number; + regulation?: RegulationConfiguration; + session?: SessionConfiguration; + storage?: StorageConfiguration; + totp?: TotpConfiguration; +} + +export function complete( + configuration: Configuration): + [Configuration, string[]] { + + const newConfiguration: Configuration = JSON.parse( + JSON.stringify(configuration)); + const errors: string[] = []; + + const [acls, aclsErrors] = AclConfigurationComplete( + newConfiguration.access_control); + + newConfiguration.access_control = acls; + if (aclsErrors.length > 0) { + errors.concat(aclsErrors); + } + + const [backend, error] = + AuthenticationBackendComplete( + newConfiguration.authentication_backend); + + if (error) errors.push(error); + newConfiguration.authentication_backend = backend; + + if (!newConfiguration.logs_level) { + newConfiguration.logs_level = "info"; + } + + const [notifier, notifierError] = NotifierConfigurationComplete( + newConfiguration.notifier); + newConfiguration.notifier = notifier; + if (notifierError) errors.push(notifierError); + + if (!newConfiguration.port) { + newConfiguration.port = 8080; + } + + newConfiguration.regulation = RegulationConfigurationComplete( + newConfiguration.regulation); + newConfiguration.session = SessionConfigurationComplete( + newConfiguration.session); + newConfiguration.storage = StorageConfigurationComplete( + newConfiguration.storage); + newConfiguration.totp = TotpConfigurationComplete( + newConfiguration.totp); + + return [newConfiguration, errors]; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts b/themes/main/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts new file mode 100644 index 000000000..d19002ba9 --- /dev/null +++ b/themes/main/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts @@ -0,0 +1,4 @@ + +export interface FileUsersDatabaseConfiguration { + path: string; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/LdapConfiguration.spec.ts b/themes/main/server/src/lib/configuration/schema/LdapConfiguration.spec.ts new file mode 100644 index 000000000..cc73d1085 --- /dev/null +++ b/themes/main/server/src/lib/configuration/schema/LdapConfiguration.spec.ts @@ -0,0 +1,25 @@ +import Assert = require("assert"); +import { LdapConfiguration, complete } from "./LdapConfiguration"; + +describe("configuration/schema/AuthenticationMethodsConfiguration", function() { + it("should ensure at least one key is provided", function() { + const configuration: LdapConfiguration = { + url: "ldap.example.com", + base_dn: "dc=example,dc=com", + user: "admin", + password: "password" + }; + const newConfiguration = complete(configuration); + + Assert.deepEqual(newConfiguration, { + url: "ldap.example.com", + base_dn: "dc=example,dc=com", + user: "admin", + password: "password", + users_filter: "cn={0}", + group_name_attribute: "cn", + groups_filter: "member={dn}", + mail_attribute: "mail" + }); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/LdapConfiguration.ts b/themes/main/server/src/lib/configuration/schema/LdapConfiguration.ts new file mode 100644 index 000000000..5dacb9390 --- /dev/null +++ b/themes/main/server/src/lib/configuration/schema/LdapConfiguration.ts @@ -0,0 +1,40 @@ +import Util = require("util"); + +export interface LdapConfiguration { + url: string; + base_dn: string; + + additional_users_dn?: string; + users_filter?: string; + + additional_groups_dn?: string; + groups_filter?: string; + + group_name_attribute?: string; + mail_attribute?: string; + + user: string; // admin username + password: string; // admin password +} + +export function complete(configuration: LdapConfiguration): LdapConfiguration { + const newConfiguration: LdapConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.users_filter) { + newConfiguration.users_filter = "cn={0}"; + } + + if (!newConfiguration.groups_filter) { + newConfiguration.groups_filter = "member={dn}"; + } + + if (!newConfiguration.group_name_attribute) { + newConfiguration.group_name_attribute = "cn"; + } + + if (!newConfiguration.mail_attribute) { + newConfiguration.mail_attribute = "mail"; + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts b/themes/main/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts new file mode 100644 index 000000000..6c576e8e0 --- /dev/null +++ b/themes/main/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts @@ -0,0 +1,40 @@ +import Assert = require("assert"); +import { NotifierConfiguration, complete } from "./NotifierConfiguration"; + +describe("configuration/schema/NotifierConfiguration", function() { + it("should use a default notifier when none is provided", function() { + const configuration: NotifierConfiguration = {}; + const [newConfiguration, error] = complete(configuration); + + Assert.deepEqual(newConfiguration.filesystem, {filename: "/tmp/authelia/notification.txt"}); + }); + + it("should ensure correct key is provided", function() { + const configuration = { + abc: "badvalue" + }; + const [newConfiguration, error] = complete(configuration as any); + + Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"); + }); + + it("should ensure there is no more than one key", function() { + const configuration: NotifierConfiguration = { + smtp: { + host: "smtp.example.com", + port: 25, + secure: false, + sender: "test@example.com" + }, + email: { + username: "test", + password: "test", + sender: "test@example.com", + service: "gmail" + } + }; + const [newConfiguration, error] = complete(configuration); + + Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/NotifierConfiguration.ts b/themes/main/server/src/lib/configuration/schema/NotifierConfiguration.ts new file mode 100644 index 000000000..7bcce15c2 --- /dev/null +++ b/themes/main/server/src/lib/configuration/schema/NotifierConfiguration.ts @@ -0,0 +1,45 @@ + +export interface EmailNotifierConfiguration { + username: string; + password: string; + sender: string; + service: string; +} + +export interface SmtpNotifierConfiguration { + username?: string; + password?: string; + host: string; + port: number; + secure: boolean; + sender: string; +} + +export interface FileSystemNotifierConfiguration { + filename: string; +} + +export interface NotifierConfiguration { + email?: EmailNotifierConfiguration; + smtp?: SmtpNotifierConfiguration; + filesystem?: FileSystemNotifierConfiguration; +} + +export function complete(configuration: NotifierConfiguration): [NotifierConfiguration, string] { + const newConfiguration: NotifierConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (Object.keys(newConfiguration).length == 0) + newConfiguration.filesystem = { filename: "/tmp/authelia/notification.txt" }; + + const ERROR = "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"; + + if (Object.keys(newConfiguration).length != 1) + return [newConfiguration, ERROR]; + + const key = Object.keys(newConfiguration)[0]; + + if (key != "filesystem" && key != "smtp" && key != "email") + return [newConfiguration, ERROR]; + + return [newConfiguration, undefined]; +} diff --git a/themes/main/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts b/themes/main/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts new file mode 100644 index 000000000..dce2caf4e --- /dev/null +++ b/themes/main/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts @@ -0,0 +1,13 @@ +import Assert = require("assert"); +import { RegulationConfiguration, complete } from "./RegulationConfiguration"; + +describe("configuration/schema/RegulationConfiguration", function() { + it("should return default regulation configuration", function() { + const configuration: RegulationConfiguration = {}; + const newConfiguration = complete(configuration); + + Assert.equal(newConfiguration.ban_time, 300); + Assert.equal(newConfiguration.find_time, 120); + Assert.equal(newConfiguration.max_retries, 3); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/RegulationConfiguration.ts b/themes/main/server/src/lib/configuration/schema/RegulationConfiguration.ts new file mode 100644 index 000000000..117463f43 --- /dev/null +++ b/themes/main/server/src/lib/configuration/schema/RegulationConfiguration.ts @@ -0,0 +1,23 @@ +export interface RegulationConfiguration { + max_retries?: number; + find_time?: number; + ban_time?: number; +} + +export function complete(configuration: RegulationConfiguration): RegulationConfiguration { + const newConfiguration: RegulationConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.max_retries) { + newConfiguration.max_retries = 3; + } + + if (!newConfiguration.find_time) { + newConfiguration.find_time = 120; // seconds + } + + if (!newConfiguration.ban_time) { + newConfiguration.ban_time = 300; // seconds + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/SessionConfiguration.spec.ts b/themes/main/server/src/lib/configuration/schema/SessionConfiguration.spec.ts new file mode 100644 index 000000000..e54010837 --- /dev/null +++ b/themes/main/server/src/lib/configuration/schema/SessionConfiguration.spec.ts @@ -0,0 +1,16 @@ +import Assert = require("assert"); +import { SessionConfiguration, complete } from "./SessionConfiguration"; + +describe("configuration/schema/SessionConfiguration", function() { + it("should return default regulation configuration", function() { + const configuration: SessionConfiguration = { + domain: "example.com", + secret: "unsecure_secret" + }; + const newConfiguration = complete(configuration); + + Assert.equal(newConfiguration.name, 'authelia_session'); + Assert.equal(newConfiguration.expiration, 3600000); + Assert.equal(newConfiguration.inactivity, undefined); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/SessionConfiguration.ts b/themes/main/server/src/lib/configuration/schema/SessionConfiguration.ts new file mode 100644 index 000000000..2c88bb215 --- /dev/null +++ b/themes/main/server/src/lib/configuration/schema/SessionConfiguration.ts @@ -0,0 +1,32 @@ +export interface SessionRedisOptions { + host: string; + port: number; + password?: string; +} + +export interface SessionConfiguration { + name?: string; + domain: string; + secret: string; + expiration?: number; + inactivity?: number; + redis?: SessionRedisOptions; +} + +export function complete(configuration: SessionConfiguration): SessionConfiguration { + const newConfiguration: SessionConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.name) { + newConfiguration.name = "authelia_session"; + } + + if (!newConfiguration.expiration) { + newConfiguration.expiration = 3600000; // 1 hour + } + + if (!newConfiguration.inactivity) { + newConfiguration.inactivity = undefined; // disabled + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/StorageConfiguration.spec.ts b/themes/main/server/src/lib/configuration/schema/StorageConfiguration.spec.ts new file mode 100644 index 000000000..9d02a11b6 --- /dev/null +++ b/themes/main/server/src/lib/configuration/schema/StorageConfiguration.spec.ts @@ -0,0 +1,15 @@ +import Assert = require("assert"); +import { StorageConfiguration, complete } from "./StorageConfiguration"; + +describe("configuration/schema/StorageConfiguration", function() { + it("should return default regulation configuration", function() { + const configuration: StorageConfiguration = {}; + const newConfiguration = complete(configuration); + + Assert.deepEqual(newConfiguration, { + local: { + in_memory: true + } + }); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/StorageConfiguration.ts b/themes/main/server/src/lib/configuration/schema/StorageConfiguration.ts new file mode 100644 index 000000000..47e356ef4 --- /dev/null +++ b/themes/main/server/src/lib/configuration/schema/StorageConfiguration.ts @@ -0,0 +1,30 @@ +export interface MongoStorageConfiguration { + url: string; + database: string; + auth?: { + username: string; + password: string; + }; +} + +export interface LocalStorageConfiguration { + path?: string; + in_memory?: boolean; +} + +export interface StorageConfiguration { + local?: LocalStorageConfiguration; + mongo?: MongoStorageConfiguration; +} + +export function complete(configuration: StorageConfiguration): StorageConfiguration { + const newConfiguration: StorageConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.local && !newConfiguration.mongo) { + newConfiguration.local = { + in_memory: true + }; + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/TotpConfiguration.ts b/themes/main/server/src/lib/configuration/schema/TotpConfiguration.ts new file mode 100644 index 000000000..683135639 --- /dev/null +++ b/themes/main/server/src/lib/configuration/schema/TotpConfiguration.ts @@ -0,0 +1,13 @@ +export interface TotpConfiguration { + issuer: string; +} + +export function complete(configuration: TotpConfiguration): TotpConfiguration { + const newConfiguration: TotpConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.issuer) { + newConfiguration.issuer = "authelia.com"; + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts b/themes/main/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts new file mode 100644 index 000000000..8008b4833 --- /dev/null +++ b/themes/main/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts @@ -0,0 +1,9 @@ + +export interface UserInfo { + username: string; + password_hash: string; + email: string; + groups?: string[]; +} + +export type UserDatabaseConfiguration = UserInfo[]; \ No newline at end of file diff --git a/themes/main/server/src/lib/connectors/mongo/IMongoClient.d.ts b/themes/main/server/src/lib/connectors/mongo/IMongoClient.d.ts new file mode 100644 index 000000000..36cb4b8bf --- /dev/null +++ b/themes/main/server/src/lib/connectors/mongo/IMongoClient.d.ts @@ -0,0 +1,6 @@ +import MongoDB = require("mongodb"); +import Bluebird = require("bluebird"); + +export interface IMongoClient { + collection(name: string): Bluebird +} \ No newline at end of file diff --git a/themes/main/server/src/lib/connectors/mongo/MongoClient.spec.ts b/themes/main/server/src/lib/connectors/mongo/MongoClient.spec.ts new file mode 100644 index 000000000..ca0c68593 --- /dev/null +++ b/themes/main/server/src/lib/connectors/mongo/MongoClient.spec.ts @@ -0,0 +1,119 @@ +import Assert = require("assert"); +import Bluebird = require("bluebird"); +import MongoDB = require("mongodb"); +import Sinon = require("sinon"); + +import { MongoClient } from "./MongoClient"; +import { GlobalLoggerStub } from "../../logging/GlobalLoggerStub.spec"; +import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration"; + +describe("connectors/mongo/MongoClient", function () { + let MongoClientStub: any; + let mongoClientStub: any; + let mongoDatabaseStub: any; + let logger: GlobalLoggerStub = new GlobalLoggerStub(); + + const configuration: MongoStorageConfiguration = { + url: "mongo://url", + database: "databasename" + }; + + describe("connection", () => { + before(() => { + mongoClientStub = { + db: Sinon.stub() + }; + mongoDatabaseStub = { + on: Sinon.stub(), + collection: Sinon.stub() + } + MongoClientStub = Sinon.stub( + MongoDB.MongoClient, "connect"); + MongoClientStub.yields( + undefined, mongoClientStub); + mongoClientStub.db.returns( + mongoDatabaseStub); + }); + + after(() => { + MongoClientStub.restore(); + }); + + it("should use credentials from configuration", () => { + configuration.auth = { + username: "authelia", + password: "authelia_pass" + }; + + const client = new MongoClient(configuration, logger); + return client.collection("test") + .then(() => { + Assert(MongoClientStub.calledWith("mongo://url", { + auth: { + user: "authelia", + password: "authelia_pass" + } + })) + }); + }); + }); + + describe("collection", () => { + before(function() { + mongoClientStub = { + db: Sinon.stub() + }; + mongoDatabaseStub = { + on: Sinon.stub(), + collection: Sinon.stub() + } + }); + + describe("Connection to mongo is ok", function() { + before(function () { + MongoClientStub = Sinon.stub( + MongoDB.MongoClient, "connect"); + MongoClientStub.yields( + undefined, mongoClientStub); + mongoClientStub.db.returns( + mongoDatabaseStub); + }); + + after(function () { + MongoClientStub.restore(); + }); + + it("should create a collection", function () { + const COLLECTION_NAME = "mycollection"; + const client = new MongoClient(configuration, logger); + + mongoDatabaseStub.collection.returns("COL"); + return client.collection(COLLECTION_NAME) + .then((collection) => mongoDatabaseStub.collection.calledWith(COLLECTION_NAME)); + }); + }); + + describe("Connection to mongo is broken", function() { + before(function () { + MongoClientStub = Sinon.stub( + MongoDB.MongoClient, "connect"); + MongoClientStub.yields( + new Error("Failed connection"), undefined); + }); + + after(function () { + MongoClientStub.restore(); + }); + + it("should fail creating the collection", function() { + const COLLECTION_NAME = "mycollection"; + const client = new MongoClient(configuration, logger); + + mongoDatabaseStub.collection.returns("COL"); + return client.collection(COLLECTION_NAME) + .then((collection) => Bluebird.reject(new Error("should not be here."))) + .catch((err) => Bluebird.resolve()); + }); + }) + }); +}); diff --git a/themes/main/server/src/lib/connectors/mongo/MongoClient.ts b/themes/main/server/src/lib/connectors/mongo/MongoClient.ts new file mode 100644 index 000000000..d15731e97 --- /dev/null +++ b/themes/main/server/src/lib/connectors/mongo/MongoClient.ts @@ -0,0 +1,76 @@ + +import MongoDB = require("mongodb"); +import { IMongoClient } from "./IMongoClient"; +import Bluebird = require("bluebird"); +import { AUTHENTICATION_FAILED } from "../../../../../shared/UserMessages"; +import { IGlobalLogger } from "../../logging/IGlobalLogger"; +import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration"; + +export class MongoClient implements IMongoClient { + private configuration: MongoStorageConfiguration; + + private database: MongoDB.Db; + private client: MongoDB.MongoClient; + private logger: IGlobalLogger; + + constructor( + configuration: MongoStorageConfiguration, + logger: IGlobalLogger) { + + this.configuration = configuration; + this.logger = logger; + } + + connect(): Bluebird { + const that = this; + const options: MongoDB.MongoClientOptions = {}; + if (that.configuration.auth) { + options["auth"] = { + user: that.configuration.auth.username, + password: that.configuration.auth.password + }; + } + + return new Bluebird((resolve, reject) => { + MongoDB.MongoClient.connect( + this.configuration.url, + options, + function(err, client) { + if (err) { + reject(err); + return; + } + resolve(client); + }); + }) + .then(function (client: MongoDB.MongoClient) { + that.database = client.db(that.configuration.database); + that.database.on("close", () => { + that.logger.info("[MongoClient] Lost connection."); + }); + that.database.on("reconnect", () => { + that.logger.info("[MongoClient] Reconnected."); + }); + that.client = client; + }); + } + + close(): Bluebird { + if (this.client) { + this.client.close(); + this.database = undefined; + this.client = undefined; + } + return Bluebird.resolve(); + } + + collection(name: string): Bluebird { + if (!this.client) { + const that = this; + return this.connect() + .then(() => Bluebird.resolve(that.database.collection(name))); + } + + return Bluebird.resolve(this.database.collection(name)); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/connectors/mongo/MongoClientStub.spec.ts b/themes/main/server/src/lib/connectors/mongo/MongoClientStub.spec.ts new file mode 100644 index 000000000..1cfd48e32 --- /dev/null +++ b/themes/main/server/src/lib/connectors/mongo/MongoClientStub.spec.ts @@ -0,0 +1,16 @@ +import Sinon = require("sinon"); +import MongoDB = require("mongodb"); +import Bluebird = require("bluebird"); +import { IMongoClient } from "../../../../src/lib/connectors/mongo/IMongoClient"; + +export class MongoClientStub implements IMongoClient { + public collectionStub: Sinon.SinonStub; + + constructor() { + this.collectionStub = Sinon.stub(); + } + + collection(name: string): Bluebird { + return this.collectionStub(name); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/logging/GlobalLogger.ts b/themes/main/server/src/lib/logging/GlobalLogger.ts new file mode 100644 index 000000000..4da7acf49 --- /dev/null +++ b/themes/main/server/src/lib/logging/GlobalLogger.ts @@ -0,0 +1,34 @@ +import { IGlobalLogger } from "./IGlobalLogger"; +import Util = require("util"); +import Express = require("express"); +import Winston = require("winston"); + +declare module "express" { + interface Request { + id: string; + } +} + +export class GlobalLogger implements IGlobalLogger { + private winston: typeof Winston; + constructor(winston: typeof Winston) { + this.winston = winston; + } + + private buildMessage(message: string, ...args: any[]): string { + return Util.format("date='%s' message='%s'", new Date(), + Util.format(message, ...args)); + } + + info(message: string, ...args: any[]): void { + this.winston.info(this.buildMessage(message, ...args)); + } + + debug(message: string, ...args: any[]): void { + this.winston.debug(this.buildMessage(message, ...args)); + } + + error(message: string, ...args: any[]): void { + this.winston.debug(this.buildMessage(message, ...args)); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/logging/GlobalLoggerStub.spec.ts b/themes/main/server/src/lib/logging/GlobalLoggerStub.spec.ts new file mode 100644 index 000000000..d4bb13710 --- /dev/null +++ b/themes/main/server/src/lib/logging/GlobalLoggerStub.spec.ts @@ -0,0 +1,38 @@ +import Sinon = require("sinon"); +import { GlobalLogger } from "./GlobalLogger"; +import Winston = require("winston"); +import Express = require("express"); +import { IGlobalLogger } from "./IGlobalLogger"; + +export class GlobalLoggerStub implements IGlobalLogger { + infoStub: Sinon.SinonStub; + debugStub: Sinon.SinonStub; + errorStub: Sinon.SinonStub; + private globalLogger: IGlobalLogger; + + constructor(enableLogging?: boolean) { + this.infoStub = Sinon.stub(); + this.debugStub = Sinon.stub(); + this.errorStub = Sinon.stub(); + if (enableLogging) + this.globalLogger = new GlobalLogger(Winston); + } + + info(message: string, ...args: any[]): void { + if (this.globalLogger) + this.globalLogger.info(message, ...args); + this.infoStub(message, ...args); + } + + debug(message: string, ...args: any[]): void { + if (this.globalLogger) + this.globalLogger.info(message, ...args); + this.debugStub(message, ...args); + } + + error(message: string, ...args: any[]): void { + if (this.globalLogger) + this.globalLogger.info(message, ...args); + this.errorStub(message, ...args); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/logging/IGlobalLogger.ts b/themes/main/server/src/lib/logging/IGlobalLogger.ts new file mode 100644 index 000000000..548515ec7 --- /dev/null +++ b/themes/main/server/src/lib/logging/IGlobalLogger.ts @@ -0,0 +1,5 @@ +export interface IGlobalLogger { + info(message: string, ...args: any[]): void; + debug(message: string, ...args: any[]): void; + error(message: string, ...args: any[]): void; +} diff --git a/themes/main/server/src/lib/logging/IRequestLogger.ts b/themes/main/server/src/lib/logging/IRequestLogger.ts new file mode 100644 index 000000000..126a601fe --- /dev/null +++ b/themes/main/server/src/lib/logging/IRequestLogger.ts @@ -0,0 +1,7 @@ +import Express = require("express"); + +export interface IRequestLogger { + info(req: Express.Request, message: string, ...args: any[]): void; + debug(req: Express.Request, message: string, ...args: any[]): void; + error(req: Express.Request, message: string, ...args: any[]): void; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/logging/RequestLogger.ts b/themes/main/server/src/lib/logging/RequestLogger.ts new file mode 100644 index 000000000..c45c66018 --- /dev/null +++ b/themes/main/server/src/lib/logging/RequestLogger.ts @@ -0,0 +1,45 @@ +import { IRequestLogger } from "./IRequestLogger"; +import Util = require("util"); +import Express = require("express"); +import Winston = require("winston"); + +declare module "express" { + interface Request { + id: string; + } +} + +export class RequestLogger implements IRequestLogger { + private winston: typeof Winston; + + constructor(winston: typeof Winston) { + this.winston = winston; + } + + private formatHeader(req: Express.Request) { + const clientIP = req.ip; // The IP of the original client going through the proxy chain. + return Util.format("date='%s' method='%s', path='%s' requestId='%s' sessionId='%s' ip='%s'", + new Date(), req.method, req.path, req.id, req.sessionID, clientIP); + } + + private formatBody(message: string) { + return Util.format("message='%s'", message); + } + + private formatMessage(req: Express.Request, message: string) { + return Util.format("%s %s", this.formatHeader(req), + this.formatBody(message)); + } + + info(req: Express.Request, message: string, ...args: any[]): void { + this.winston.info(this.formatMessage(req, message), ...args); + } + + debug(req: Express.Request, message: string, ...args: any[]): void { + this.winston.debug(this.formatMessage(req, message), ...args); + } + + error(req: Express.Request, message: string, ...args: any[]): void { + this.winston.error(this.formatMessage(req, message), ...args); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/logging/RequestLoggerStub.spec.ts b/themes/main/server/src/lib/logging/RequestLoggerStub.spec.ts new file mode 100644 index 000000000..b0e375210 --- /dev/null +++ b/themes/main/server/src/lib/logging/RequestLoggerStub.spec.ts @@ -0,0 +1,38 @@ +import { IRequestLogger } from "./IRequestLogger"; +import Sinon = require("sinon"); +import { RequestLogger } from "./RequestLogger"; +import Winston = require("winston"); +import Express = require("express"); + +export class RequestLoggerStub implements IRequestLogger { + infoStub: Sinon.SinonStub; + debugStub: Sinon.SinonStub; + errorStub: Sinon.SinonStub; + private requestLogger: RequestLogger; + + constructor(enableLogging?: boolean) { + this.infoStub = Sinon.stub(); + this.debugStub = Sinon.stub(); + this.errorStub = Sinon.stub(); + if (enableLogging) + this.requestLogger = new RequestLogger(Winston); + } + + info(req: Express.Request, message: string, ...args: any[]): void { + if (this.requestLogger) + this.requestLogger.info(req, message, ...args); + this.infoStub(req, message, ...args); + } + + debug(req: Express.Request, message: string, ...args: any[]): void { + if (this.requestLogger) + this.requestLogger.info(req, message, ...args); + this.debugStub(req, message, ...args); + } + + error(req: Express.Request, message: string, ...args: any[]): void { + if (this.requestLogger) + this.requestLogger.info(req, message, ...args); + this.errorStub(req, message, ...args); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/AbstractEmailNotifier.ts b/themes/main/server/src/lib/notifiers/AbstractEmailNotifier.ts new file mode 100644 index 000000000..198e4e5db --- /dev/null +++ b/themes/main/server/src/lib/notifiers/AbstractEmailNotifier.ts @@ -0,0 +1,23 @@ + +import { INotifier } from "../notifiers/INotifier"; +import { Identity } from "../../../types/Identity"; + +import Fs = require("fs"); +import Path = require("path"); +import Ejs = require("ejs"); +import BluebirdPromise = require("bluebird"); + +const email_template = Fs.readFileSync(Path.join(__dirname, "../../resources/email-template.ejs"), "UTF-8"); + +export abstract class AbstractEmailNotifier implements INotifier { + notify(to: string, subject: string, link: string): BluebirdPromise { + const d = { + url: link, + button_title: "Continue", + title: subject + }; + return this.sendEmail(to, subject, Ejs.render(email_template, d)); + } + + abstract sendEmail(to: string, subject: string, content: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/EmailNotifier.spec.ts b/themes/main/server/src/lib/notifiers/EmailNotifier.spec.ts new file mode 100644 index 000000000..8211bbc02 --- /dev/null +++ b/themes/main/server/src/lib/notifiers/EmailNotifier.spec.ts @@ -0,0 +1,54 @@ +import * as sinon from "sinon"; +import * as Assert from "assert"; +import BluebirdPromise = require("bluebird"); + +import { MailSenderStub } from "./MailSenderStub.spec"; +import EmailNotifier = require("./EmailNotifier"); + + +describe("notifiers/EmailNotifier", function () { + it("should send an email to given user", function () { + const mailSender = new MailSenderStub(); + const options = { + username: "user_gmail", + password: "pass_gmail", + sender: "admin@example.com", + service: "gmail" + }; + + mailSender.sendStub.returns(BluebirdPromise.resolve()); + const sender = new EmailNotifier.EmailNotifier(options, mailSender); + const subject = "subject"; + const url = "http://test.com"; + + return sender.notify("user@example.com", subject, url) + .then(function () { + Assert.equal(mailSender.sendStub.getCall(0).args[0].to, "user@example.com"); + Assert.equal(mailSender.sendStub.getCall(0).args[0].subject, "subject"); + return BluebirdPromise.resolve(); + }); + }); + + it("should fail while sending an email", function () { + const mailSender = new MailSenderStub(); + const options = { + username: "user_gmail", + password: "pass_gmail", + sender: "admin@example.com", + service: "gmail" + }; + + mailSender.sendStub.returns(BluebirdPromise.reject(new Error("Failed to send mail"))); + const sender = new EmailNotifier.EmailNotifier(options, mailSender); + const subject = "subject"; + const url = "http://test.com"; + + return sender.notify("user@example.com", subject, url) + .then(function () { + return BluebirdPromise.reject(new Error()); + }, function() { + Assert.equal(mailSender.sendStub.getCall(0).args[0].from, "admin@example.com"); + return BluebirdPromise.resolve(); + }); + }); +}); diff --git a/themes/main/server/src/lib/notifiers/EmailNotifier.ts b/themes/main/server/src/lib/notifiers/EmailNotifier.ts new file mode 100644 index 000000000..4df7c861e --- /dev/null +++ b/themes/main/server/src/lib/notifiers/EmailNotifier.ts @@ -0,0 +1,27 @@ + +import * as BluebirdPromise from "bluebird"; + +import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; +import { EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; +import { IMailSender } from "./IMailSender"; + +export class EmailNotifier extends AbstractEmailNotifier { + private mailSender: IMailSender; + private sender: string; + + constructor(options: EmailNotifierConfiguration, mailSender: IMailSender) { + super(); + this.mailSender = mailSender; + this.sender = options.sender; + } + + sendEmail(to: string, subject: string, content: string) { + const mailOptions = { + from: this.sender, + to: to, + subject: subject, + html: content + }; + return this.mailSender.send(mailOptions); + } +} diff --git a/themes/main/server/src/lib/notifiers/FileSystemNotifier.ts b/themes/main/server/src/lib/notifiers/FileSystemNotifier.ts new file mode 100644 index 000000000..23f6242c4 --- /dev/null +++ b/themes/main/server/src/lib/notifiers/FileSystemNotifier.ts @@ -0,0 +1,22 @@ +import * as BluebirdPromise from "bluebird"; +import * as util from "util"; +import * as Fs from "fs"; +import { INotifier } from "./INotifier"; +import { Identity } from "../../../types/Identity"; + +import { FileSystemNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; + +export class FileSystemNotifier implements INotifier { + private filename: string; + + constructor(options: FileSystemNotifierConfiguration) { + this.filename = options.filename; + } + + notify(to: string, subject: string, link: string): BluebirdPromise { + const content = util.format("Date: %s\nEmail: %s\nSubject: %s\nLink: %s", + new Date().toString(), to, subject, link); + const writeFilePromised: any = BluebirdPromise.promisify(Fs.writeFile); + return writeFilePromised(this.filename, content); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/IMailSender.ts b/themes/main/server/src/lib/notifiers/IMailSender.ts new file mode 100644 index 000000000..34ac464a8 --- /dev/null +++ b/themes/main/server/src/lib/notifiers/IMailSender.ts @@ -0,0 +1,6 @@ +import BluebirdPromise = require("bluebird"); +import Nodemailer = require("nodemailer"); + +export interface IMailSender { + send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/IMailSenderBuilder.ts b/themes/main/server/src/lib/notifiers/IMailSenderBuilder.ts new file mode 100644 index 000000000..36d4dcdf7 --- /dev/null +++ b/themes/main/server/src/lib/notifiers/IMailSenderBuilder.ts @@ -0,0 +1,7 @@ +import { IMailSender } from "./IMailSender"; +import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; + +export interface IMailSenderBuilder { + buildEmail(options: EmailNotifierConfiguration): IMailSender; + buildSmtp(options: SmtpNotifierConfiguration): IMailSender; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/INotifier.ts b/themes/main/server/src/lib/notifiers/INotifier.ts new file mode 100644 index 000000000..b9a6b138c --- /dev/null +++ b/themes/main/server/src/lib/notifiers/INotifier.ts @@ -0,0 +1,5 @@ +import * as BluebirdPromise from "bluebird"; + +export interface INotifier { + notify(to: string, subject: string, link: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/MailSender.ts b/themes/main/server/src/lib/notifiers/MailSender.ts new file mode 100644 index 000000000..536a88e63 --- /dev/null +++ b/themes/main/server/src/lib/notifiers/MailSender.ts @@ -0,0 +1,42 @@ +import { IMailSender } from "./IMailSender"; +import Nodemailer = require("nodemailer"); +import NodemailerDirectTransport = require("nodemailer-direct-transport"); +import NodemailerSmtpTransport = require("nodemailer-smtp-transport"); +import BluebirdPromise = require("bluebird"); + +export class MailSender implements IMailSender { + private transporter: Nodemailer.Transporter; + + constructor(options: NodemailerDirectTransport.DirectOptions | + NodemailerSmtpTransport.SmtpOptions, nodemailer: typeof Nodemailer) { + this.transporter = nodemailer.createTransport(options); + } + + verify(): BluebirdPromise { + const that = this; + return new BluebirdPromise(function (resolve, reject) { + that.transporter.verify(function (error: Error, success: any) { + if (error) { + reject(new Error("Unable to connect to SMTP server. \ + Please check the service is running and your credentials are correct.")); + return; + } + resolve(); + }); + }); + } + + send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise { + const that = this; + return new BluebirdPromise(function (resolve, reject) { + that.transporter.sendMail(mailOptions, (error: Error, + data: Nodemailer.SentMessageInfo) => { + if (error) { + reject(new Error("Error while sending email: " + error.message)); + return; + } + resolve(); + }); + }); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/MailSenderBuilder.spec.ts b/themes/main/server/src/lib/notifiers/MailSenderBuilder.spec.ts new file mode 100644 index 000000000..41e0db426 --- /dev/null +++ b/themes/main/server/src/lib/notifiers/MailSenderBuilder.spec.ts @@ -0,0 +1,67 @@ + +import { MailSenderBuilder } from ".//MailSenderBuilder"; +import Nodemailer = require("nodemailer"); +import Sinon = require("sinon"); +import Assert = require("assert"); + +describe("notifiers/MailSenderBuilder", function() { + let createTransportStub: Sinon.SinonStub; + beforeEach(function() { + createTransportStub = Sinon.stub(Nodemailer, "createTransport"); + }); + + afterEach(function() { + createTransportStub.restore(); + }); + + it("should create a email mail sender", function() { + const mailSenderBuilder = new MailSenderBuilder(Nodemailer); + mailSenderBuilder.buildEmail({ + username: "user_gmail", + password: "pass_gmail", + sender: "admin@example.com", + service: "gmail" + }); + Assert.equal(createTransportStub.getCall(0).args[0].auth.user, "user_gmail"); + Assert.equal(createTransportStub.getCall(0).args[0].auth.pass, "pass_gmail"); + Assert.equal(createTransportStub.getCall(0).args[0].service, "gmail"); + }); + + describe("build smtp mail sender", function() { + it("should create a smtp mail sender with authenticated user", function() { + const mailSenderBuilder = new MailSenderBuilder(Nodemailer); + mailSenderBuilder.buildSmtp({ + host: "mail.example.com", + password: "password", + port: 25, + secure: true, + username: "user", + sender: "admin@example.com" + }); + Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { + host: "mail.example.com", + auth: { + pass: "password", + user: "user" + }, + port: 25, + secure: true, + }); + }); + + it("should create a smtp mail sender with anonymous user", function() { + const mailSenderBuilder = new MailSenderBuilder(Nodemailer); + mailSenderBuilder.buildSmtp({ + host: "mail.example.com", + port: 25, + secure: true, + sender: "admin@example.com" + }); + Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { + host: "mail.example.com", + port: 25, + secure: true, + }); + }); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/MailSenderBuilder.ts b/themes/main/server/src/lib/notifiers/MailSenderBuilder.ts new file mode 100644 index 000000000..1d06be52f --- /dev/null +++ b/themes/main/server/src/lib/notifiers/MailSenderBuilder.ts @@ -0,0 +1,42 @@ +import { IMailSender } from "./IMailSender"; +import { IMailSenderBuilder } from "./IMailSenderBuilder"; +import { MailSender } from "./MailSender"; +import Nodemailer = require("nodemailer"); +import NodemailerSmtpTransport = require("nodemailer-smtp-transport"); +import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; + +export class MailSenderBuilder implements IMailSenderBuilder { + private nodemailer: typeof Nodemailer; + + constructor(nodemailer: typeof Nodemailer) { + this.nodemailer = nodemailer; + } + + buildEmail(options: EmailNotifierConfiguration): IMailSender { + const emailOptions = { + service: options.service, + auth: { + user: options.username, + pass: options.password + } + }; + return new MailSender(emailOptions, this.nodemailer); + } + + buildSmtp(options: SmtpNotifierConfiguration): IMailSender { + const smtpOptions: NodemailerSmtpTransport.SmtpOptions = { + host: options.host, + port: options.port, + secure: options.secure, // upgrade later with STARTTLS + }; + + if (options.username && options.password) { + smtpOptions.auth = { + user: options.username, + pass: options.password + }; + } + + return new MailSender(smtpOptions, this.nodemailer); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts b/themes/main/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts new file mode 100644 index 000000000..5b76f6e56 --- /dev/null +++ b/themes/main/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts @@ -0,0 +1,25 @@ +import { IMailSenderBuilder } from "../../../src/lib/notifiers/IMailSenderBuilder"; +import BluebirdPromise = require("bluebird"); +import Nodemailer = require("nodemailer"); +import Sinon = require("sinon"); +import { IMailSender } from "../../../src/lib/notifiers/IMailSender"; +import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../../../src/lib/configuration/schema/NotifierConfiguration"; + +export class MailSenderBuilderStub implements IMailSenderBuilder { + buildEmailStub: Sinon.SinonStub; + buildSmtpStub: Sinon.SinonStub; + + constructor() { + this.buildEmailStub = Sinon.stub(); + this.buildSmtpStub = Sinon.stub(); + } + + buildEmail(options: EmailNotifierConfiguration): IMailSender { + return this.buildEmailStub(options); + } + + buildSmtp(options: SmtpNotifierConfiguration): IMailSender { + return this.buildSmtpStub(options); + } + +} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/MailSenderStub.spec.ts b/themes/main/server/src/lib/notifiers/MailSenderStub.spec.ts new file mode 100644 index 000000000..d57c458fc --- /dev/null +++ b/themes/main/server/src/lib/notifiers/MailSenderStub.spec.ts @@ -0,0 +1,16 @@ +import { IMailSender } from "../../../src/lib/notifiers/IMailSender"; +import BluebirdPromise = require("bluebird"); +import Nodemailer = require("nodemailer"); +import Sinon = require("sinon"); + +export class MailSenderStub implements IMailSender { + sendStub: Sinon.SinonStub; + + constructor() { + this.sendStub = Sinon.stub(); + } + + send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise { + return this.sendStub(mailOptions); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/NotifierFactory.spec.ts b/themes/main/server/src/lib/notifiers/NotifierFactory.spec.ts new file mode 100644 index 000000000..f15e7667f --- /dev/null +++ b/themes/main/server/src/lib/notifiers/NotifierFactory.spec.ts @@ -0,0 +1,42 @@ + +import * as sinon from "sinon"; +import * as BluebirdPromise from "bluebird"; +import * as assert from "assert"; + +import { NotifierFactory } from "./NotifierFactory"; +import { EmailNotifier } from "./EmailNotifier"; +import { SmtpNotifier } from "./SmtpNotifier"; +import { MailSenderBuilderStub } from "./MailSenderBuilderStub.spec"; + + +describe("notifiers/NotifierFactory", function () { + let mailSenderBuilderStub: MailSenderBuilderStub; + it("should build a Email Notifier", function () { + const options = { + email: { + username: "abc", + password: "password", + sender: "admin@example.com", + service: "gmail" + } + }; + mailSenderBuilderStub = new MailSenderBuilderStub(); + assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof EmailNotifier); + }); + + it("should build a SMTP Notifier", function () { + const options = { + smtp: { + username: "user", + password: "pass", + secure: true, + host: "localhost", + port: 25, + sender: "admin@example.com" + } + }; + + mailSenderBuilderStub = new MailSenderBuilderStub(); + assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof SmtpNotifier); + }); +}); diff --git a/themes/main/server/src/lib/notifiers/NotifierFactory.ts b/themes/main/server/src/lib/notifiers/NotifierFactory.ts new file mode 100644 index 000000000..a89155feb --- /dev/null +++ b/themes/main/server/src/lib/notifiers/NotifierFactory.ts @@ -0,0 +1,33 @@ + +import { NotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; +import Nodemailer = require("nodemailer"); +import { INotifier } from "./INotifier"; + +import { FileSystemNotifier } from "./FileSystemNotifier"; +import { EmailNotifier } from "./EmailNotifier"; +import { SmtpNotifier } from "./SmtpNotifier"; +import { IMailSender } from "./IMailSender"; +import { IMailSenderBuilder } from "./IMailSenderBuilder"; + +export class NotifierFactory { + static build(options: NotifierConfiguration, mailSenderBuilder: IMailSenderBuilder): INotifier { + if ("email" in options) { + const mailSender = mailSenderBuilder.buildEmail(options.email); + return new EmailNotifier(options.email, mailSender); + } + else if ("smtp" in options) { + const mailSender = mailSenderBuilder.buildSmtp(options.smtp); + return new SmtpNotifier(options.smtp, mailSender); + } + else if ("filesystem" in options) { + return new FileSystemNotifier(options.filesystem); + } + else { + throw new Error("No available notifier option detected."); + } + } +} + + + + diff --git a/themes/main/server/src/lib/notifiers/NotifierStub.spec.ts b/themes/main/server/src/lib/notifiers/NotifierStub.spec.ts new file mode 100644 index 000000000..f99231b50 --- /dev/null +++ b/themes/main/server/src/lib/notifiers/NotifierStub.spec.ts @@ -0,0 +1,16 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); + +import { INotifier } from "./INotifier"; + +export class NotifierStub implements INotifier { + notifyStub: Sinon.SinonStub; + + constructor() { + this.notifyStub = Sinon.stub(); + } + + notify(to: string, subject: string, link: string): BluebirdPromise { + return this.notifyStub(to, subject, link); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/SmtpNotifier.ts b/themes/main/server/src/lib/notifiers/SmtpNotifier.ts new file mode 100644 index 000000000..f93a6d4a0 --- /dev/null +++ b/themes/main/server/src/lib/notifiers/SmtpNotifier.ts @@ -0,0 +1,30 @@ + + +import * as BluebirdPromise from "bluebird"; + +import { IMailSender } from "./IMailSender"; +import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; +import { SmtpNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; + +export class SmtpNotifier extends AbstractEmailNotifier { + private mailSender: IMailSender; + private sender: string; + + constructor(options: SmtpNotifierConfiguration, + mailSender: IMailSender) { + super(); + this.mailSender = mailSender; + this.sender = options.sender; + } + + sendEmail(to: string, subject: string, content: string) { + const mailOptions = { + from: this.sender, + to: to, + subject: subject, + html: content + }; + const that = this; + return this.mailSender.send(mailOptions); + } +} diff --git a/themes/main/server/src/lib/regulation/IRegulator.ts b/themes/main/server/src/lib/regulation/IRegulator.ts new file mode 100644 index 000000000..c49425b24 --- /dev/null +++ b/themes/main/server/src/lib/regulation/IRegulator.ts @@ -0,0 +1,6 @@ +import BluebirdPromise = require("bluebird"); + +export interface IRegulator { + mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; + regulate(userId: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/regulation/Regulator.spec.ts b/themes/main/server/src/lib/regulation/Regulator.spec.ts new file mode 100644 index 000000000..f9c6e6086 --- /dev/null +++ b/themes/main/server/src/lib/regulation/Regulator.spec.ts @@ -0,0 +1,186 @@ + +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import Assert = require("assert"); + +import { Regulator } from "./Regulator"; +import MockDate = require("mockdate"); +import exceptions = require("../Exceptions"); +import { UserDataStoreStub } from "../storage/UserDataStoreStub.spec"; + +describe("regulation/Regulator", function () { + const USER1 = "USER1"; + const USER2 = "USER2"; + let userDataStoreStub: UserDataStoreStub; + + beforeEach(function () { + userDataStoreStub = new UserDataStoreStub(); + const dataStore: { [userId: string]: { userId: string, date: Date, isAuthenticationSuccessful: boolean }[] } = { + [USER1]: [], + [USER2]: [] + }; + + userDataStoreStub.saveAuthenticationTraceStub.callsFake(function (userId, isAuthenticationSuccessful) { + dataStore[userId].unshift({ + userId: userId, + date: new Date(), + isAuthenticationSuccessful: isAuthenticationSuccessful, + }); + return BluebirdPromise.resolve(); + }); + + userDataStoreStub.retrieveLatestAuthenticationTracesStub.callsFake(function (userId, count) { + const ret = (dataStore[userId].length <= count) ? dataStore[userId] : dataStore[userId].slice(0, 3); + return BluebirdPromise.resolve(ret); + }); + }); + + afterEach(function () { + MockDate.reset(); + }); + + function markAuthenticationAt(regulator: Regulator, user: string, time: string, success: boolean) { + MockDate.set(time); + return regulator.mark(user, success); + } + + it("should mark 2 authentication and regulate (accept)", function () { + const regulator = new Regulator(userDataStoreStub, 3, 10, 10); + + return regulator.mark(USER1, false) + .then(function () { + return regulator.mark(USER1, true); + }) + .then(function () { + return regulator.regulate(USER1); + }); + }); + + it("should mark 3 authentications and regulate (reject)", function () { + const regulator = new Regulator(userDataStoreStub, 3, 10, 10); + + return regulator.mark(USER1, false) + .then(function () { + return regulator.mark(USER1, false); + }) + .then(function () { + return regulator.mark(USER1, false); + }) + .then(function () { + return regulator.regulate(USER1); + }) + .then(function () { return BluebirdPromise.reject(new Error("should not be here!")); }) + .catch(exceptions.AuthenticationRegulationError, function () { + return BluebirdPromise.resolve(); + }); + }); + + it("should mark 1 failed, 1 successful and 1 failed authentications within minimum time and regulate (accept)", function () { + const regulator = new Regulator(userDataStoreStub, 3, 60, 30); + + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", true); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:20", false); + }) + .then(function () { + return regulator.regulate(USER1); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:30", false); + }) + .then(function () { + return regulator.regulate(USER1); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:39", false); + }) + .then(function () { + return regulator.regulate(USER1); + }) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here!")); + }, + function () { + return BluebirdPromise.resolve(); + }); + }); + + it("should regulate user if number of failures is greater than 3 in allowed time lapse", function () { + function markAuthentications(regulator: Regulator, user: string) { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:45", false); + }) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:01:05", false); + }) + .then(function () { + return regulator.regulate(user); + }); + } + + const regulator1 = new Regulator(userDataStoreStub, 3, 60, 60); + const regulator2 = new Regulator(userDataStoreStub, 3, 2 * 60, 60); + + const p1 = markAuthentications(regulator1, USER1); + const p2 = markAuthentications(regulator2, USER2); + + return BluebirdPromise.join(p1, p2) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here...")); + }, function () { + Assert(p1.isFulfilled()); + Assert(p2.isRejected()); + }); + }); + + it("should user wait after regulation to authenticate again", function () { + function markAuthentications(regulator: Regulator, user: string) { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:10", false); + }) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:15", false); + }) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:25", false); + }) + .then(function () { + MockDate.set("1/2/2000 00:00:54"); + return regulator.regulate(user); + }) + .then(function () { + return BluebirdPromise.reject(new Error("should fail at this time")); + }, function () { + MockDate.set("1/2/2000 00:00:56"); + return regulator.regulate(user); + }); + } + + const regulator = new Regulator(userDataStoreStub, 4, 30, 30); + return markAuthentications(regulator, USER1); + }); + + it("should disable regulation when max_retries is set to 0", function () { + const maxRetries = 0; + const regulator = new Regulator(userDataStoreStub, maxRetries, 60, 30); + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", false); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:15", false); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:25", false); + }) + .then(function () { + MockDate.set("1/2/2000 00:00:26"); + return regulator.regulate(USER1); + }); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/regulation/Regulator.ts b/themes/main/server/src/lib/regulation/Regulator.ts new file mode 100644 index 000000000..1037a6a17 --- /dev/null +++ b/themes/main/server/src/lib/regulation/Regulator.ts @@ -0,0 +1,55 @@ + +import * as BluebirdPromise from "bluebird"; +import exceptions = require("../Exceptions"); +import { IUserDataStore } from "../storage/IUserDataStore"; +import { AuthenticationTraceDocument } from "../storage/AuthenticationTraceDocument"; +import { IRegulator } from "./IRegulator"; + +export class Regulator implements IRegulator { + private userDataStore: IUserDataStore; + private banTime: number; + private findTime: number; + private maxRetries: number; + + constructor(userDataStore: any, maxRetries: number, findTime: number, banTime: number) { + this.userDataStore = userDataStore; + this.banTime = banTime; + this.findTime = findTime; + this.maxRetries = maxRetries; + } + + // Mark authentication + mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { + return this.userDataStore.saveAuthenticationTrace(userId, isAuthenticationSuccessful); + } + + regulate(userId: string): BluebirdPromise { + const that = this; + + if (that.maxRetries <= 0) return BluebirdPromise.resolve(); + + return this.userDataStore.retrieveLatestAuthenticationTraces(userId, that.maxRetries) + .then((docs: AuthenticationTraceDocument[]) => { + // less than the max authorized number of authentication in time range, thus authorizing access + if (docs.length < that.maxRetries) return BluebirdPromise.resolve(); + + const numberOfFailedAuth = docs + .map(function (d: AuthenticationTraceDocument) { return d.isAuthenticationSuccessful == false ? 1 : 0; }) + .reduce(function (acc, v) { return acc + v; }, 0); + + if (numberOfFailedAuth < this.maxRetries) return BluebirdPromise.resolve(); + + const newestDocument = docs[0]; + const oldestDocument = docs[that.maxRetries - 1]; + + const authenticationsTimeRangeInSeconds = (newestDocument.date.getTime() - oldestDocument.date.getTime()) / 1000; + const tooManyAuthInTimelapse = (authenticationsTimeRangeInSeconds < this.findTime); + const stillInBannedTimeRange = (new Date(new Date().getTime() - this.banTime * 1000) < newestDocument.date); + + if (tooManyAuthInTimelapse && stillInBannedTimeRange) + throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes."); + + return BluebirdPromise.resolve(); + }); + } +} diff --git a/themes/main/server/src/lib/regulation/RegulatorStub.spec.ts b/themes/main/server/src/lib/regulation/RegulatorStub.spec.ts new file mode 100644 index 000000000..ca8a00fb1 --- /dev/null +++ b/themes/main/server/src/lib/regulation/RegulatorStub.spec.ts @@ -0,0 +1,22 @@ +import Bluebird = require("bluebird"); +import Sinon = require("sinon"); +import { IRegulator } from "./IRegulator"; + + +export class RegulatorStub implements IRegulator { + markStub: Sinon.SinonStub; + regulateStub: Sinon.SinonStub; + + constructor() { + this.markStub = Sinon.stub(); + this.regulateStub = Sinon.stub(); + } + + mark(userId: string, isAuthenticationSuccessful: boolean): Bluebird { + return this.markStub(userId, isAuthenticationSuccessful); + } + + regulate(userId: string): Bluebird { + return this.regulateStub(userId); + } +} diff --git a/themes/main/server/src/lib/routes/error/401/get.spec.ts b/themes/main/server/src/lib/routes/error/401/get.spec.ts new file mode 100644 index 000000000..9fdac9c3c --- /dev/null +++ b/themes/main/server/src/lib/routes/error/401/get.spec.ts @@ -0,0 +1,61 @@ +import Sinon = require("sinon"); +import Express = require("express"); +import Assert = require("assert"); +import Get401 from "./get"; +import { ServerVariables } from "../../../ServerVariables"; +import { ServerVariablesMockBuilder, ServerVariablesMock } + from "../../../ServerVariablesMockBuilder.spec"; + +describe("routes/error/401/get", function () { + let vars: ServerVariables; + let mocks: ServerVariablesMock; + let req: any; + let res: any; + let renderSpy: Sinon.SinonSpy; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + vars = s.variables; + mocks = s.mocks; + + renderSpy = Sinon.spy(); + req = { + headers: {} + }; + res = { + render: renderSpy + }; + }); + + it("should set redirection url to the default redirection url", function () { + vars.config.default_redirection_url = "http://default-redirection"; + return Get401(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/401", { + redirection_url: "http://default-redirection" + })); + }); + }); + + it("should set redirection url to the referer", function () { + req.headers["referer"] = "http://redirection"; + return Get401(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/401", { + redirection_url: "http://redirection" + })); + }); + }); + + it("should render without redirecting the user", function () { + return Get401(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/401", { + redirection_url: undefined + })); + }); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/error/401/get.ts b/themes/main/server/src/lib/routes/error/401/get.ts new file mode 100644 index 000000000..ca4a3963d --- /dev/null +++ b/themes/main/server/src/lib/routes/error/401/get.ts @@ -0,0 +1,15 @@ + +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import redirector from "../redirector"; +import { ServerVariables } from "../../../ServerVariables"; + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response): BluebirdPromise { + const redirectionUrl = redirector(req, vars); + res.render("errors/401", { + redirection_url: redirectionUrl + }); + return BluebirdPromise.resolve(); + }; +} diff --git a/themes/main/server/src/lib/routes/error/403/get.spec.ts b/themes/main/server/src/lib/routes/error/403/get.spec.ts new file mode 100644 index 000000000..22eb84853 --- /dev/null +++ b/themes/main/server/src/lib/routes/error/403/get.spec.ts @@ -0,0 +1,61 @@ +import Sinon = require("sinon"); +import Express = require("express"); +import Assert = require("assert"); +import Get403 from "./get"; +import { ServerVariables } from "../../../ServerVariables"; +import { ServerVariablesMockBuilder, ServerVariablesMock } + from "../../../ServerVariablesMockBuilder.spec"; + +describe("routes/error/403/get", function () { + let vars: ServerVariables; + let mocks: ServerVariablesMock; + let req: any; + let res: any; + let renderSpy: Sinon.SinonSpy; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + vars = s.variables; + mocks = s.mocks; + + renderSpy = Sinon.spy(); + req = { + headers: {} + }; + res = { + render: renderSpy + }; + }); + + it("should set redirection url to the default redirection url", function () { + vars.config.default_redirection_url = "http://default-redirection"; + return Get403(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/403", { + redirection_url: "http://default-redirection" + })); + }); + }); + + it("should set redirection url to the referer", function () { + req.headers["referer"] = "http://redirection"; + return Get403(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/403", { + redirection_url: "http://redirection" + })); + }); + }); + + it("should render without redirecting the user", function () { + return Get403(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/403", { + redirection_url: undefined + })); + }); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/error/403/get.ts b/themes/main/server/src/lib/routes/error/403/get.ts new file mode 100644 index 000000000..3ab0319e5 --- /dev/null +++ b/themes/main/server/src/lib/routes/error/403/get.ts @@ -0,0 +1,15 @@ + +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import redirector from "../redirector"; +import { ServerVariables } from "../../../ServerVariables"; + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response): BluebirdPromise { + const redirectionUrl = redirector(req, vars); + res.render("errors/403", { + redirection_url: redirectionUrl + }); + return BluebirdPromise.resolve(); + }; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/error/404/get.spec.ts b/themes/main/server/src/lib/routes/error/404/get.spec.ts new file mode 100644 index 000000000..73e4e6cef --- /dev/null +++ b/themes/main/server/src/lib/routes/error/404/get.spec.ts @@ -0,0 +1,19 @@ +import Sinon = require("sinon"); +import Express = require("express"); +import Assert = require("assert"); +import Get404 from "./get"; + +describe("routes/error/404/get", function () { + it("should render the page", function () { + const req = {} as Express.Request; + const res = { + render: Sinon.stub() + }; + + return Get404(req, res as any) + .then(function () { + Assert(res.render.calledOnce); + Assert(res.render.calledWith("errors/404")); + }); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/error/404/get.ts b/themes/main/server/src/lib/routes/error/404/get.ts new file mode 100644 index 000000000..6693b6fc8 --- /dev/null +++ b/themes/main/server/src/lib/routes/error/404/get.ts @@ -0,0 +1,8 @@ + +import BluebirdPromise = require("bluebird"); +import express = require("express"); + +export default function (req: express.Request, res: express.Response): BluebirdPromise { + res.render("errors/404"); + return BluebirdPromise.resolve(); +} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/error/redirector.ts b/themes/main/server/src/lib/routes/error/redirector.ts new file mode 100644 index 000000000..b1a3ccc11 --- /dev/null +++ b/themes/main/server/src/lib/routes/error/redirector.ts @@ -0,0 +1,13 @@ +import Express = require("express"); +import { ServerVariables } from "../../ServerVariables"; + +export default function (req: Express.Request, vars: ServerVariables): string { + let redirectionUrl: string; + + if (req.headers && req.headers["referer"]) + redirectionUrl = "" + req.headers["referer"]; + else if (vars.config.default_redirection_url) + redirectionUrl = vars.config.default_redirection_url; + + return redirectionUrl; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/firstfactor/get.ts b/themes/main/server/src/lib/routes/firstfactor/get.ts new file mode 100644 index 000000000..d94f656c8 --- /dev/null +++ b/themes/main/server/src/lib/routes/firstfactor/get.ts @@ -0,0 +1,72 @@ + +import express = require("express"); +import Endpoints = require("../../../../../shared/api"); +import BluebirdPromise = require("bluebird"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Constants = require("../../../../../shared/constants"); +import Util = require("util"); +import { ServerVariables } from "../../ServerVariables"; +import { SafeRedirector } from "../../utils/SafeRedirection"; +import { Level } from "../../authentication/Level"; + +function getRedirectParam( + req: express.Request) { + return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; +} + +function redirectToSecondFactorPage( + req: express.Request, + res: express.Response) { + + const redirectUrl = getRedirectParam(req); + if (!redirectUrl) + res.redirect(Endpoints.SECOND_FACTOR_GET); + else + res.redirect( + Util.format("%s?%s=%s", + Endpoints.SECOND_FACTOR_GET, + Constants.REDIRECT_QUERY_PARAM, + redirectUrl)); +} + +function redirectToService( + req: express.Request, + res: express.Response, + redirector: SafeRedirector) { + const redirectUrl = getRedirectParam(req); + if (!redirectUrl) { + res.redirect(Endpoints.LOGGED_IN); + } else { + redirector.redirectOrElse(res, redirectUrl, Endpoints.LOGGED_IN); + } +} + +function renderFirstFactor( + res: express.Response) { + + res.render("firstfactor", { + first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST, + reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET + }); +} + +export default function ( + vars: ServerVariables) { + + const redirector = new SafeRedirector(vars.config.session.domain); + return function (req: express.Request, res: express.Response): BluebirdPromise { + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (authSession.authentication_level == Level.ONE_FACTOR) { + redirectToSecondFactorPage(req, res); + } else if (authSession.authentication_level == Level.TWO_FACTOR) { + redirectToService(req, res, redirector); + } else { + renderFirstFactor(res); + } + resolve(); + }); + }; +} diff --git a/themes/main/server/src/lib/routes/firstfactor/post.spec.ts b/themes/main/server/src/lib/routes/firstfactor/post.spec.ts new file mode 100644 index 000000000..e1d078cdd --- /dev/null +++ b/themes/main/server/src/lib/routes/firstfactor/post.spec.ts @@ -0,0 +1,136 @@ + +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import Assert = require("assert"); +import FirstFactorPost = require("./post"); +import exceptions = require("../../Exceptions"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../types/AuthenticationSession"; +import Endpoints = require("../../../../../shared/api"); +import AuthenticationRegulatorMock = require("../../regulation/RegulatorStub.spec"); +import ExpressMock = require("../../stubs/express.spec"); +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../ServerVariables"; + +describe("routes/firstfactor/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let emails: string[]; + let groups: string[]; + let vars: ServerVariables; + let mocks: ServerVariablesMock; + let authSession: AuthenticationSession; + + beforeEach(function () { + emails = ["test_ok@example.com"]; + groups = ["group1", "group2" ]; + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + mocks.authorizer.authorizationMock.returns(true); + mocks.regulator.regulateStub.returns(BluebirdPromise.resolve()); + mocks.regulator.markStub.returns(BluebirdPromise.resolve()); + + req = { + originalUrl: "/api/firstfactor", + body: { + username: "username", + password: "password" + }, + query: { + redirect: "http://redirect.url" + }, + session: { + cookie: {} + }, + headers: { + host: "home.example.com" + } + }; + + res = ExpressMock.ResponseMock(); + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + }); + + it("should reply with 204 if success", function () { + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.resolve({ + emails: emails, + groups: groups + })); + return FirstFactorPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal("username", authSession.userid); + Assert(res.send.calledOnce); + }); + }); + + describe("keep me logged in", () => { + beforeEach(() => { + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.resolve({ + emails: emails, + groups: groups + })); + req.body.keepMeLoggedIn = "true"; + return FirstFactorPost.default(vars)(req as any, res as any); + }); + + it("should set keep_me_logged_in session variable to true", function () { + Assert.equal(authSession.keep_me_logged_in, true); + }); + + it("should set cookie maxAge to one year", function () { + Assert.equal(req.session.cookie.maxAge, 365 * 24 * 60 * 60 * 1000); + }); + }); + + it("should retrieve email from LDAP", function () { + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.resolve([{ mail: ["test@example.com"] }])); + return FirstFactorPost.default(vars)(req as any, res as any); + }); + + it("should set first email address as user session variable", function () { + const emails = ["test_ok@example.com"]; + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.resolve({ + emails: emails, + groups: groups + })); + + return FirstFactorPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal("test_ok@example.com", authSession.email); + }); + }); + + it("should return error message when LDAP authenticator throws", function () { + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.reject(new exceptions.LdapBindError("Bad credentials"))); + + return FirstFactorPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.equal(mocks.regulator.markStub.getCall(0).args[0], "username"); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + }); + }); + + it("should return error message when regulator rejects authentication", function () { + const err = new exceptions.AuthenticationRegulationError("Authentication regulation..."); + mocks.regulator.regulateStub.returns(BluebirdPromise.reject(err)); + return FirstFactorPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + }); + }); +}); + + diff --git a/themes/main/server/src/lib/routes/firstfactor/post.ts b/themes/main/server/src/lib/routes/firstfactor/post.ts new file mode 100644 index 000000000..565681d6a --- /dev/null +++ b/themes/main/server/src/lib/routes/firstfactor/post.ts @@ -0,0 +1,101 @@ + +import Exceptions = require("../../Exceptions"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import Endpoint = require("../../../../../shared/api"); +import ErrorReplies = require("../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Constants = require("../../../../../shared/constants"); +import UserMessages = require("../../../../../shared/UserMessages"); +import { ServerVariables } from "../../ServerVariables"; +import { AuthenticationSession } from "../../../../types/AuthenticationSession"; +import { GroupsAndEmails } from "../../authentication/backends/GroupsAndEmails"; +import { Level as AuthenticationLevel } from "../../authentication/Level"; +import { Level as AuthorizationLevel } from "../../authorization/Level"; +import { URLDecomposer } from "../../utils/URLDecomposer"; + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response) + : BluebirdPromise { + const username: string = req.body.username; + const password: string = req.body.password; + const keepMeLoggedIn: boolean = req.body.keepMeLoggedIn && + req.body.keepMeLoggedIn === "true"; + let authSession: AuthenticationSession; + + if (keepMeLoggedIn) { + // Stay connected for 1 year. + vars.logger.debug(req, "User requested to stay logged in for one year."); + req.session.cookie.maxAge = 365 * 24 * 60 * 60 * 1000; + } + + return BluebirdPromise.resolve() + .then(function () { + if (!username || !password) { + return BluebirdPromise.reject(new Error("No username or password.")); + } + vars.logger.info(req, "Starting authentication of user \"%s\"", username); + authSession = AuthenticationSessionHandler.get(req, vars.logger); + return vars.regulator.regulate(username); + }) + .then(function () { + vars.logger.info(req, "No regulation applied."); + return vars.usersDatabase.checkUserPassword(username, password); + }) + .then(function (groupsAndEmails: GroupsAndEmails) { + vars.logger.info(req, + "LDAP binding successful. Retrieved information about user are %s", + JSON.stringify(groupsAndEmails)); + authSession.userid = username; + authSession.keep_me_logged_in = keepMeLoggedIn; + authSession.authentication_level = AuthenticationLevel.ONE_FACTOR; + const redirectUrl: string = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined" + // Fuck, don't know why it is a string! + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : ""; + + const emails: string[] = groupsAndEmails.emails; + const groups: string[] = groupsAndEmails.groups; + const decomposition = URLDecomposer.fromUrl(redirectUrl); + const authorizationLevel = (decomposition) + ? vars.authorizer.authorization( + {domain: decomposition.domain, resource: decomposition.path}, + {user: username, groups: groups}) + : AuthorizationLevel.TWO_FACTOR; + + if (emails.length > 0) + authSession.email = emails[0]; + authSession.groups = groups; + + vars.logger.debug(req, "Mark successful authentication to regulator."); + vars.regulator.mark(username, true); + + if (authorizationLevel <= AuthorizationLevel.ONE_FACTOR) { + let newRedirectionUrl: string = redirectUrl; + if (!newRedirectionUrl) + newRedirectionUrl = Endpoint.LOGGED_IN; + res.send({ + redirect: newRedirectionUrl + }); + vars.logger.debug(req, "Redirect to '%s'", redirectUrl); + } + else { + let newRedirectUrl = Endpoint.SECOND_FACTOR_GET; + if (redirectUrl) { + newRedirectUrl += "?" + Constants.REDIRECT_QUERY_PARAM + "=" + + redirectUrl; + } + vars.logger.debug(req, "Redirect to '%s'", newRedirectUrl); + res.send({ + redirect: newRedirectUrl + }); + } + return BluebirdPromise.resolve(); + }) + .catch(Exceptions.LdapBindError, function (err: Error) { + vars.regulator.mark(username, false); + return ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)(err); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)); + }; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/loggedin/get.ts b/themes/main/server/src/lib/routes/loggedin/get.ts new file mode 100644 index 000000000..283a041b1 --- /dev/null +++ b/themes/main/server/src/lib/routes/loggedin/get.ts @@ -0,0 +1,23 @@ +import Express = require("express"); +import Endpoints = require("../../../../../shared/api"); +import BluebirdPromise = require("bluebird"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { ServerVariables } from "../../ServerVariables"; +import ErrorReplies = require("../../ErrorReplies"); + +export default function (vars: ServerVariables) { + function handler(req: Express.Request, res: Express.Response): BluebirdPromise { + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + res.render("already-logged-in", { + logout_endpoint: Endpoints.LOGOUT_GET, + username: authSession.userid, + redirection_url: vars.config.default_redirection_url + }); + resolve(); + }) + .catch(ErrorReplies.replyWithError401(req, res, vars.logger)); + } + + return handler; +} diff --git a/themes/main/server/src/lib/routes/logout/get.ts b/themes/main/server/src/lib/routes/logout/get.ts new file mode 100644 index 000000000..4d5112146 --- /dev/null +++ b/themes/main/server/src/lib/routes/logout/get.ts @@ -0,0 +1,20 @@ + +import express = require("express"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Constants = require("../../../../../shared/constants"); +import { ServerVariables } from "../../ServerVariables"; + +function getRedirectParam(req: express.Request) { + return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; +} + +export default function (vars: ServerVariables) { + return function(req: express.Request, res: express.Response) { + const redirect_param = getRedirectParam(req); + const redirect_url = redirect_param || "/"; + AuthenticationSessionHandler.reset(req); + res.redirect(redirect_url); + }; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/password-reset/constants.ts b/themes/main/server/src/lib/routes/password-reset/constants.ts new file mode 100644 index 000000000..5c639e92a --- /dev/null +++ b/themes/main/server/src/lib/routes/password-reset/constants.ts @@ -0,0 +1,2 @@ + +export const CHALLENGE = "reset-password"; \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/password-reset/form/post.spec.ts b/themes/main/server/src/lib/routes/password-reset/form/post.spec.ts new file mode 100644 index 000000000..ed029c906 --- /dev/null +++ b/themes/main/server/src/lib/routes/password-reset/form/post.spec.ts @@ -0,0 +1,122 @@ + +import PasswordResetFormPost = require("./post"); +import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; +import { UserDataStore } from "../../../storage/UserDataStore"; +import Sinon = require("sinon"); +import Assert = require("assert"); +import BluebirdPromise = require("bluebird"); +import ExpressMock = require("../../../stubs/express.spec"); +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../ServerVariables"; +import { Level } from "../../../authentication/Level"; + +describe("routes/password-reset/form/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let vars: ServerVariables; + let mocks: ServerVariablesMock; + let authSession: AuthenticationSession; + + beforeEach(function () { + req = { + originalUrl: "/api/password-reset", + body: { + userid: "user" + }, + session: {}, + headers: { + host: "localhost" + } + }; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + + mocks.config.authentication_backend.ldap = { + url: "ldap://ldapjs", + mail_attribute: "mail", + user: "user", + password: "password", + additional_users_dn: "ou=users", + additional_groups_dn: "ou=groups", + base_dn: "dc=example,dc=com", + users_filter: "user", + group_name_attribute: "cn", + groups_filter: "groups" + }; + + res = ExpressMock.ResponseMock(); + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + authSession.userid = "user"; + authSession.email = "user@example.com"; + authSession.authentication_level = Level.ONE_FACTOR; + }); + + describe("test reset password post", () => { + it("should update the password and reset auth_session for reauthentication", function () { + req.body = {}; + req.body.password = "new-password"; + + mocks.usersDatabase.updatePasswordStub.returns(BluebirdPromise.resolve()); + + authSession.identity_check = { + userid: "user", + challenge: "reset-password" + }; + return PasswordResetFormPost.default(vars)(req as any, res as any) + .then(function () { + return AuthenticationSessionHandler.get(req as any, vars.logger); + }).then(function (_authSession) { + Assert.equal(res.status.getCall(0).args[0], 204); + Assert.equal(_authSession.authentication_level, Level.NOT_AUTHENTICATED); + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if identity_challenge does not exist", function () { + authSession.identity_check = { + userid: "user", + challenge: undefined + }; + return PasswordResetFormPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "An error occurred during password reset. Your password has not been changed." + }); + }); + }); + + it("should fail when ldap fails", function () { + req.body = {}; + req.body.password = "new-password"; + + mocks.usersDatabase.updatePasswordStub + .returns(BluebirdPromise.reject("Internal error with LDAP")); + + authSession.identity_check = { + challenge: "reset-password", + userid: "user" + }; + return PasswordResetFormPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "An error occurred during password reset. Your password has not been changed." + }); + return BluebirdPromise.resolve(); + }); + }); + }); +}); diff --git a/themes/main/server/src/lib/routes/password-reset/form/post.ts b/themes/main/server/src/lib/routes/password-reset/form/post.ts new file mode 100644 index 000000000..fccd7471b --- /dev/null +++ b/themes/main/server/src/lib/routes/password-reset/form/post.ts @@ -0,0 +1,50 @@ + +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); +import exceptions = require("../../../Exceptions"); +import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; +import ErrorReplies = require("../../../ErrorReplies"); +import UserMessages = require("../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../ServerVariables"; + +import Constants = require("./../constants"); + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const newPassword = objectPath.get(req, "body.password"); + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (!authSession.identity_check) { + reject(new Error("No identity check initiated")); + return; + } + + vars.logger.info(req, "User %s wants to reset his/her password.", + authSession.identity_check.userid); + vars.logger.debug(req, "Challenge %s", authSession.identity_check.challenge); + + if (authSession.identity_check.challenge != Constants.CHALLENGE) { + reject(new Error("Bad challenge.")); + return; + } + resolve(); + }) + .then(function () { + return vars.usersDatabase.updatePassword(authSession.identity_check.userid, newPassword); + }) + .then(function () { + vars.logger.info(req, "Password reset for user '%s'", + authSession.identity_check.userid); + AuthenticationSessionHandler.reset(req); + res.status(204); + res.send(); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.RESET_PASSWORD_FAILED)); + }; +} diff --git a/themes/main/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts b/themes/main/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts new file mode 100644 index 000000000..ac6a41754 --- /dev/null +++ b/themes/main/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts @@ -0,0 +1,92 @@ + +import PasswordResetHandler + from "./PasswordResetHandler"; +import { UserDataStore } from "../../../storage/UserDataStore"; +import Sinon = require("sinon"); +import winston = require("winston"); +import assert = require("assert"); +import BluebirdPromise = require("bluebird"); +import ExpressMock = require("../../../stubs/express.spec"); +import { ServerVariablesMock, ServerVariablesMockBuilder } + from "../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../ServerVariables"; + +describe("routes/password-reset/identity/PasswordResetHandler", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + req = { + originalUrl: "/non-api/xxx", + query: { + userid: "user" + }, + session: { + auth: { + userid: "user", + email: "user@example.com", + first_factor: true, + second_factor: false + } + }, + headers: { + host: "localhost" + } + }; + + const options = { + inMemoryOnly: true + }; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + mocks.userDataStore.saveU2FRegistrationStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub + .returns(BluebirdPromise.resolve({})); + res = ExpressMock.ResponseMock(); + }); + + describe("test reset password identity pre check", () => { + it("should fail when no userid is provided", function () { + req.query.userid = undefined; + const handler = new PasswordResetHandler(vars.logger, + vars.usersDatabase); + return handler.preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject("It should fail"); + }) + .catch(function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if ldap fail", function () { + mocks.usersDatabase.getEmailsStub + .returns(BluebirdPromise.reject("Internal error")); + new PasswordResetHandler(vars.logger, vars.usersDatabase) + .preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here")); + }, + function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should returns identity when ldap replies", function () { + mocks.usersDatabase.getEmailsStub + .returns(BluebirdPromise.resolve(["test@example.com"])); + return new PasswordResetHandler(vars.logger, vars.usersDatabase) + .preValidationInit(req as any); + }); + }); +}); diff --git a/themes/main/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts b/themes/main/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts new file mode 100644 index 000000000..42ae92cda --- /dev/null +++ b/themes/main/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts @@ -0,0 +1,69 @@ +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); + +import exceptions = require("../../../Exceptions"); +import { Identity } from "../../../../../types/Identity"; +import { IdentityValidable } from "../../../IdentityValidable"; +import { PRE_VALIDATION_TEMPLATE } from "../../../IdentityCheckPreValidationTemplate"; +import Constants = require("../constants"); +import { IRequestLogger } from "../../../logging/IRequestLogger"; +import { IUsersDatabase } from "../../../authentication/backends/IUsersDatabase"; + +export const TEMPLATE_NAME = "password-reset-form"; + +export default class PasswordResetHandler implements IdentityValidable { + private logger: IRequestLogger; + private usersDatabase: IUsersDatabase; + + constructor(logger: IRequestLogger, usersDatabase: IUsersDatabase) { + this.logger = logger; + this.usersDatabase = usersDatabase; + } + + challenge(): string { + return Constants.CHALLENGE; + } + + preValidationInit(req: express.Request): BluebirdPromise { + const that = this; + const userid: string = + objectPath.get(req, "query.userid"); + return BluebirdPromise.resolve() + .then(function () { + that.logger.debug(req, "User '%s' requested a password reset", userid); + if (!userid) + return BluebirdPromise.reject( + new exceptions.AccessDeniedError("No user id provided")); + + return that.usersDatabase.getEmails(userid); + }) + .then(function (emails: string[]) { + if (!emails && emails.length <= 0) throw new Error("No email found"); + const identity = { + email: emails[0], + userid: userid + }; + return BluebirdPromise.resolve(identity); + }) + .catch(function (err: Error) { + return BluebirdPromise.reject(new exceptions.IdentityError(err.message)); + }); + } + + preValidationResponse(req: express.Request, res: express.Response) { + res.render(PRE_VALIDATION_TEMPLATE); + } + + postValidationInit(req: express.Request) { + return BluebirdPromise.resolve(); + } + + postValidationResponse(req: express.Request, res: express.Response) { + res.render(TEMPLATE_NAME); + } + + mailSubject(): string { + return "Reset your password"; + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/password-reset/request/get.ts b/themes/main/server/src/lib/routes/password-reset/request/get.ts new file mode 100644 index 000000000..8f3ae2b4b --- /dev/null +++ b/themes/main/server/src/lib/routes/password-reset/request/get.ts @@ -0,0 +1,13 @@ + +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); +import exceptions = require("../../../Exceptions"); + +import Constants = require("./../constants"); + +const TEMPLATE_NAME = "password-reset-request"; + +export default function (req: express.Request, res: express.Response) { + res.render(TEMPLATE_NAME); +} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/secondfactor/get.spec.ts b/themes/main/server/src/lib/routes/secondfactor/get.spec.ts new file mode 100644 index 000000000..6c77e1f69 --- /dev/null +++ b/themes/main/server/src/lib/routes/secondfactor/get.spec.ts @@ -0,0 +1,44 @@ +import SecondFactorGet from "./get"; +import { ServerVariablesMockBuilder, ServerVariablesMock } + from "../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../ServerVariables"; +import Sinon = require("sinon"); +import ExpressMock = require("../../stubs/express.spec"); +import Assert = require("assert"); +import Endpoints = require("../../../../../shared/api"); +import BluebirdPromise = require("bluebird"); + +describe("routes/secondfactor/get", function () { + let mocks: ServerVariablesMock; + let vars: ServerVariables; + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false + } + }; + }); + + describe("test rendering", function () { + it("should render second factor page", function () { + req.session.auth.second_factor = false; + return SecondFactorGet(vars)(req as any, res as any) + .then(function () { + Assert(res.render.calledWith("secondfactor")); + return BluebirdPromise.resolve(); + }); + }); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/secondfactor/get.ts b/themes/main/server/src/lib/routes/secondfactor/get.ts new file mode 100644 index 000000000..9f6deb4c6 --- /dev/null +++ b/themes/main/server/src/lib/routes/secondfactor/get.ts @@ -0,0 +1,28 @@ + +import Express = require("express"); +import Endpoints = require("../../../../../shared/api"); +import BluebirdPromise = require("bluebird"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { ServerVariables } from "../../ServerVariables"; + +const TEMPLATE_NAME = "secondfactor"; + +export default function (vars: ServerVariables) { + function handler(req: Express.Request, res: Express.Response) + : BluebirdPromise { + + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + + res.render(TEMPLATE_NAME, { + username: authSession.userid, + totp_identity_start_endpoint: + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, + u2f_identity_start_endpoint: + Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET + }); + resolve(); + }); + } + return handler; +} diff --git a/themes/main/server/src/lib/routes/secondfactor/redirect.spec.ts b/themes/main/server/src/lib/routes/secondfactor/redirect.spec.ts new file mode 100644 index 000000000..ea66e6dca --- /dev/null +++ b/themes/main/server/src/lib/routes/secondfactor/redirect.spec.ts @@ -0,0 +1,41 @@ +import Redirect from "./redirect"; +import ExpressMock = require("../../stubs/express.spec"); +import { ServerVariablesMockBuilder, ServerVariablesMock } +from "../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../ServerVariables"; +import Assert = require("assert"); + +describe("routes/secondfactor/redirect", function() { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + }); + + it("should redirect to default_redirection_url", function() { + vars.config.default_redirection_url = "http://default_redirection_url"; + Redirect(vars)(req as any, res as any) + .then(function() { + Assert(res.json.calledWith({ + redirect: "http://default_redirection_url" + })); + }); + }); + + it("should redirect to /", function() { + Redirect(vars)(req as any, res as any) + .then(function() { + Assert(res.json.calledWith({ + redirect: "/" + })); + }); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/secondfactor/redirect.ts b/themes/main/server/src/lib/routes/secondfactor/redirect.ts new file mode 100644 index 000000000..5d84d9ebf --- /dev/null +++ b/themes/main/server/src/lib/routes/secondfactor/redirect.ts @@ -0,0 +1,30 @@ + +import express = require("express"); +import objectPath = require("object-path"); +import Endpoints = require("../../../../../shared/api"); +import { ServerVariables } from "../../ServerVariables"; +import BluebirdPromise = require("bluebird"); +import ErrorReplies = require("../../ErrorReplies"); +import UserMessages = require("../../../../../shared/UserMessages"); +import { RedirectionMessage } from "../../../../../shared/RedirectionMessage"; +import Constants = require("../../../../../shared/constants"); + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response) + : BluebirdPromise { + + return new BluebirdPromise(function (resolve, reject) { + let redirectUrl: string = "/"; + if (vars.config.default_redirection_url) { + redirectUrl = vars.config.default_redirection_url; + } + vars.logger.debug(req, "Request redirection to \"%s\".", redirectUrl); + res.json({ + redirect: redirectUrl + } as RedirectionMessage); + return resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + }; +} diff --git a/themes/main/server/src/lib/routes/secondfactor/totp/constants.ts b/themes/main/server/src/lib/routes/secondfactor/totp/constants.ts new file mode 100644 index 000000000..7b5a1dcfe --- /dev/null +++ b/themes/main/server/src/lib/routes/secondfactor/totp/constants.ts @@ -0,0 +1,4 @@ + +export const CHALLENGE = "totp-register"; +export const TEMPLATE_NAME = "totp-register"; + diff --git a/themes/main/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts b/themes/main/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts new file mode 100644 index 000000000..78b8ea3ea --- /dev/null +++ b/themes/main/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts @@ -0,0 +1,116 @@ +import Sinon = require("sinon"); +import RegistrationHandler from "./RegistrationHandler"; +import { Identity } from "../../../../../../types/Identity"; +import { UserDataStore } from "../../../../storage/UserDataStore"; +import BluebirdPromise = require("bluebird"); +import ExpressMock = require("../../../../stubs/express.spec"); +import { ServerVariablesMock, ServerVariablesMockBuilder } + from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; +import Assert = require("assert"); + +describe("routes/secondfactor/totp/identity/RegistrationHandler", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + req.session = { + auth: { + userid: "user", + email: "user@example.com", + first_factor: true, + second_factor: false, + identity_check: { + userid: "user", + challenge: "totp-register" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.saveTOTPSecretStub + .returns(BluebirdPromise.resolve({})); + + res = ExpressMock.ResponseMock(); + }); + + describe("test totp registration pre validation", function () { + it("should fail if first_factor has not been passed", function () { + req.session.auth.first_factor = false; + return new RegistrationHandler(vars.logger, vars.userDataStore, + vars.totpHandler, vars.config.totp) + .preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("It should fail")); + }) + .catch(function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if userid is missing", function (done) { + req.session.auth.first_factor = false; + req.session.auth.userid = undefined; + + new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler, + vars.config.totp) + .preValidationInit(req as any) + .catch(function (err: Error) { + done(); + }); + }); + + it("should fail if email is missing", function (done) { + req.session.auth.first_factor = false; + req.session.auth.email = undefined; + + new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler, + vars.config.totp) + .preValidationInit(req as any) + .catch(function (err: Error) { + done(); + }); + }); + + it("should succeed if first factor passed, userid and email are provided", + function () { + return new RegistrationHandler(vars.logger, vars.userDataStore, + vars.totpHandler, vars.config.totp) + .preValidationInit(req as any); + }); + }); + + describe("test totp registration post validation", function () { + it("should generate a secret using userId as label and issuer defined in config", function () { + vars.config.totp = { + issuer: "issuer" + }; + return new RegistrationHandler(vars.logger, vars.userDataStore, + vars.totpHandler, vars.config.totp) + .postValidationResponse(req as any, res as any) + .then(function() { + Assert(mocks.totpHandler.generateStub.calledWithExactly("user", "issuer")); + }); + }); + }); +}); diff --git a/themes/main/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts b/themes/main/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts new file mode 100644 index 000000000..b39b6d045 --- /dev/null +++ b/themes/main/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts @@ -0,0 +1,112 @@ + +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); + +import { Identity } from "../../../../../../types/Identity"; +import { IdentityValidable } from "../../../../IdentityValidable"; +import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; +import Constants = require("../constants"); +import Endpoints = require("../../../../../../../shared/api"); +import ErrorReplies = require("../../../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import FirstFactorValidator = require("../../../../FirstFactorValidator"); +import { IRequestLogger } from "../../../../logging/IRequestLogger"; +import { IUserDataStore } from "../../../../storage/IUserDataStore"; +import { ITotpHandler } from "../../../../authentication/totp/ITotpHandler"; +import { TOTPSecret } from "../../../../../../types/TOTPSecret"; +import { TotpConfiguration } from "../../../../configuration/schema/TotpConfiguration"; + + +export default class RegistrationHandler implements IdentityValidable { + private logger: IRequestLogger; + private userDataStore: IUserDataStore; + private totp: ITotpHandler; + private configuration: TotpConfiguration; + + constructor(logger: IRequestLogger, + userDataStore: IUserDataStore, + totp: ITotpHandler, configuration: TotpConfiguration) { + this.logger = logger; + this.userDataStore = userDataStore; + this.totp = totp; + this.configuration = configuration; + } + + challenge(): string { + return Constants.CHALLENGE; + } + + private retrieveIdentity(req: express.Request): BluebirdPromise { + const that = this; + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, that.logger); + const userid = authSession.userid; + const email = authSession.email; + + if (!(userid && email)) { + return reject(new Error("User ID or email is missing")); + } + + const identity = { + email: email, + userid: userid + }; + return resolve(identity); + }); + } + + preValidationInit(req: express.Request): BluebirdPromise { + const that = this; + return FirstFactorValidator.validate(req, this.logger) + .then(function () { + return that.retrieveIdentity(req); + }); + } + + preValidationResponse(req: express.Request, res: express.Response) { + res.render(PRE_VALIDATION_TEMPLATE); + } + + postValidationInit(req: express.Request) { + return FirstFactorValidator.validate(req, this.logger); + } + + postValidationResponse(req: express.Request, res: express.Response) + : BluebirdPromise { + const that = this; + let secret: TOTPSecret; + let userId: string; + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, that.logger); + userId = authSession.userid; + + if (authSession.identity_check.challenge != Constants.CHALLENGE + || !userId) + return reject(new Error("Bad challenge.")); + + resolve(); + }) + .then(function () { + secret = that.totp.generate(userId, + that.configuration.issuer); + that.logger.debug(req, "Save the TOTP secret in DB"); + return that.userDataStore.saveTOTPSecret(userId, secret); + }) + .then(function () { + AuthenticationSessionHandler.reset(req); + + res.render(Constants.TEMPLATE_NAME, { + base32_secret: secret.base32, + otpauth_url: secret.otpauth_url, + login_endpoint: Endpoints.FIRST_FACTOR_GET + }); + }) + .catch(ErrorReplies.replyWithError200(req, res, that.logger, UserMessages.OPERATION_FAILED)); + } + + mailSubject(): string { + return "Set up Authelia's one-time password"; + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts b/themes/main/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts new file mode 100644 index 000000000..70a20d39d --- /dev/null +++ b/themes/main/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts @@ -0,0 +1,76 @@ + +import BluebirdPromise = require("bluebird"); +import Sinon = require("sinon"); +import Assert = require("assert"); +import Exceptions = require("../../../../Exceptions"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import SignPost = require("./post"); +import { ServerVariables } from "../../../../ServerVariables"; + +import ExpressMock = require("../../../../stubs/express.spec"); +import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; +import { Level } from "../../../../authentication/Level"; + +describe("routes/secondfactor/totp/sign/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let authSession: AuthenticationSession; + let vars: ServerVariables; + let mocks: ServerVariablesMock; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + vars = s.variables; + mocks = s.mocks; + const app_get = Sinon.stub(); + req = { + originalUrl: "/api/totp-register", + app: {}, + body: { + token: "abc" + }, + session: {}, + query: { + redirect: "http://redirect" + } + }; + res = ExpressMock.ResponseMock(); + + const doc = { + userid: "user", + secret: { + base32: "ABCDEF" + } + }; + mocks.userDataStore.retrieveTOTPSecretStub.returns(BluebirdPromise.resolve(doc)); + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + authSession.userid = "user"; + authSession.authentication_level = Level.ONE_FACTOR; + }); + + + it("should send status code 200 when totp is valid", function () { + mocks.totpHandler.validateStub.returns(true); + return SignPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(authSession.authentication_level, Level.TWO_FACTOR); + return BluebirdPromise.resolve(); + }); + }); + + it("should send error message when totp is not valid", function () { + mocks.totpHandler.validateStub.returns(false); + return SignPost.default(vars)(req as any, res as any) + .then(function () { + Assert.notEqual(authSession.authentication_level, Level.TWO_FACTOR); + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); +}); + diff --git a/themes/main/server/src/lib/routes/secondfactor/totp/sign/post.ts b/themes/main/server/src/lib/routes/secondfactor/totp/sign/post.ts new file mode 100644 index 000000000..34a276d12 --- /dev/null +++ b/themes/main/server/src/lib/routes/secondfactor/totp/sign/post.ts @@ -0,0 +1,42 @@ +import Bluebird = require("bluebird"); +import Express = require("express"); + +import { TOTPSecretDocument } from "../../../../storage/TOTPSecretDocument"; +import Endpoints = require("../../../../../../../shared/api"); +import Redirect from "../../redirect"; +import ErrorReplies = require("../../../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../../ServerVariables"; +import { Level } from "../../../../authentication/Level"; + +const UNAUTHORIZED_MESSAGE = "Unauthorized access"; + +export default function (vars: ServerVariables) { + function handler(req: Express.Request, res: Express.Response): Bluebird { + let authSession: AuthenticationSession; + const token = req.body.token; + + return new Bluebird(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + vars.logger.info(req, "Initiate TOTP validation for user \"%s\".", authSession.userid); + resolve(); + }) + .then(function () { + return vars.userDataStore.retrieveTOTPSecret(authSession.userid); + }) + .then(function (doc: TOTPSecretDocument) { + if (!vars.totpHandler.validate(token, doc.secret.base32)) + return Bluebird.reject(new Error("Invalid TOTP token.")); + + vars.logger.debug(req, "TOTP validation succeeded."); + authSession.authentication_level = Level.TWO_FACTOR; + Redirect(vars)(req, res); + return Bluebird.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + return handler; +} diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts new file mode 100644 index 000000000..7f16c0ee8 --- /dev/null +++ b/themes/main/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts @@ -0,0 +1,11 @@ + +import util = require("util"); +import express = require("express"); + +function extract_app_id(req: express.Request): string { + return util.format("https://%s", req.headers.host); +} + +export = { + extract_app_id: extract_app_id +}; \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts new file mode 100644 index 000000000..a54bfbfe6 --- /dev/null +++ b/themes/main/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts @@ -0,0 +1,96 @@ +import Sinon = require("sinon"); +import Assert = require("assert"); +import BluebirdPromise = require("bluebird"); + +import { Identity } from "../../../../../../types/Identity"; +import RegistrationHandler from "./RegistrationHandler"; +import ExpressMock = require("../../../../stubs/express.spec"); +import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; + +describe("routes/secondfactor/u2f/identity/RegistrationHandler", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + req.app = {}; + req.session = { + auth: { + userid: "user", + email: "user@example.com", + first_factor: true, + second_factor: false + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + + res = ExpressMock.ResponseMock(); + res.send = Sinon.spy(); + res.json = Sinon.spy(); + res.status = Sinon.spy(); + }); + + describe("test u2f registration check", test_registration_check); + + function test_registration_check() { + it("should fail if first_factor has not been passed", function () { + req.session.auth.first_factor = false; + return new RegistrationHandler(vars.logger).preValidationInit(req as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if userid is missing", function () { + req.session.auth.first_factor = false; + req.session.auth.userid = undefined; + + return new RegistrationHandler(vars.logger).preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here")); + }, + function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if email is missing", function () { + req.session.auth.first_factor = false; + req.session.auth.email = undefined; + + return new RegistrationHandler(vars.logger).preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here")); + }, + function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should succeed if first factor passed, userid and email are provided", function () { + req.session.auth.first_factor = true; + req.session.auth.email = "admin@example.com"; + req.session.auth.userid = "user"; + return new RegistrationHandler(vars.logger).preValidationInit(req as any); + }); + } +}); diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts new file mode 100644 index 000000000..bc4713c77 --- /dev/null +++ b/themes/main/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts @@ -0,0 +1,73 @@ + +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import objectPath = require("object-path"); + +import { IdentityValidable } from "../../../../IdentityValidable"; +import { Identity } from "../../../../../../types/Identity"; +import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; +import FirstFactorValidator = require("../../../../FirstFactorValidator"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { IRequestLogger } from "../../../../logging/IRequestLogger"; + +const CHALLENGE = "u2f-register"; +const MAIL_SUBJECT = "Register your security key with Authelia"; + +const POST_VALIDATION_TEMPLATE_NAME = "u2f-register"; + + +export default class RegistrationHandler implements IdentityValidable { + private logger: IRequestLogger; + + constructor(logger: IRequestLogger) { + this.logger = logger; + } + + challenge(): string { + return CHALLENGE; + } + + private retrieveIdentity(req: express.Request): BluebirdPromise { + const that = this; + return new BluebirdPromise(function(resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, that.logger); + const userid = authSession.userid; + const email = authSession.email; + + if (!(userid && email)) { + return reject(new Error("User ID or email is missing")); + } + + const identity = { + email: email, + userid: userid + }; + return resolve(identity); + }); + } + + preValidationInit(req: express.Request): BluebirdPromise { + const that = this; + return FirstFactorValidator.validate(req, this.logger) + .then(function () { + return that.retrieveIdentity(req); + }); + } + + preValidationResponse(req: express.Request, res: express.Response) { + res.render(PRE_VALIDATION_TEMPLATE); + } + + postValidationInit(req: express.Request) { + return FirstFactorValidator.validate(req, this.logger); + } + + postValidationResponse(req: express.Request, res: express.Response) { + res.render(POST_VALIDATION_TEMPLATE_NAME); + } + + mailSubject(): string { + return MAIL_SUBJECT; + } +} + diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts new file mode 100644 index 000000000..de3347a21 --- /dev/null +++ b/themes/main/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts @@ -0,0 +1,146 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import assert = require("assert"); +import U2FRegisterPost = require("./post"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import ExpressMock = require("../../../../stubs/express.spec"); +import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; + + +describe("routes/secondfactor/u2f/register/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + let authSession: AuthenticationSession; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.originalUrl = "/api/xxxx"; + req.app = {}; + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false, + identity_check: { + challenge: "u2f-register", + userid: "user" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + }); + + describe("test registration", test_registration); + + + function test_registration() { + it("should save u2f meta and return status code 200", function () { + const expectedStatus = { + keyHandle: "keyHandle", + publicKey: "pbk", + certificate: "cert" + }; + mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve(expectedStatus)); + + authSession.register_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { + assert.equal("user", mocks.userDataStore.saveU2FRegistrationStub.getCall(0).args[0]); + assert.equal(authSession.identity_check, undefined); + }); + }); + + it("should return error message on finishRegistration error", function () { + mocks.u2f.checkRegistrationStub.returns({ errorCode: 500 }); + + authSession.register_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function () { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); + + it("should return error message when register_request is not provided", function () { + mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); + authSession.register_request = undefined; + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function () { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); + + it("should return error message when no auth request has been initiated", function () { + mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); + authSession.register_request = undefined; + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function () { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); + + it("should return error message when identity has not been verified", function () { + authSession.identity_check = undefined; + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function () { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); + } +}); + diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/register/post.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/register/post.ts new file mode 100644 index 000000000..7296ccbe5 --- /dev/null +++ b/themes/main/server/src/lib/routes/secondfactor/u2f/register/post.ts @@ -0,0 +1,64 @@ + +import { UserDataStore } from "../../../../storage/UserDataStore"; +import objectPath = require("object-path"); +import u2f_common = require("../U2FCommon"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import U2f = require("u2f"); +import { U2FRegistration } from "../../../../../../types/U2FRegistration"; +import redirect from "../../redirect"; +import ErrorReplies = require("../../../../ErrorReplies"); +import { ServerVariables } from "../../../../ServerVariables"; +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; + + +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const appid = u2f_common.extract_app_id(req); + const registrationResponse: U2f.RegistrationData = req.body; + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + const registrationRequest = authSession.register_request; + + if (!registrationRequest) { + return reject(new Error("No registration request")); + } + + if (!authSession.identity_check + || authSession.identity_check.challenge != "u2f-register") { + return reject(new Error("Bad challenge for registration request")); + } + + vars.logger.info(req, "Finishing registration"); + vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); + vars.logger.debug(req, "RegistrationResponse = %s", JSON.stringify(registrationResponse)); + + return resolve(vars.u2f.checkRegistration(registrationRequest, registrationResponse)); + }) + .then(function (u2fResult: U2f.RegistrationResult | U2f.Error): BluebirdPromise { + if (objectPath.has(u2fResult, "errorCode")) + return BluebirdPromise.reject(new Error("Error while registering.")); + + const registrationResult: U2f.RegistrationResult = u2fResult as U2f.RegistrationResult; + vars.logger.info(req, "Store registration and reply"); + vars.logger.debug(req, "RegistrationResult = %s", JSON.stringify(registrationResult)); + const registration: U2FRegistration = { + keyHandle: registrationResult.keyHandle, + publicKey: registrationResult.publicKey + }; + return vars.userDataStore.saveU2FRegistration(authSession.userid, appid, registration); + }) + .then(function () { + authSession.identity_check = undefined; + redirect(vars)(req, res); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + return handler; +} diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts new file mode 100644 index 000000000..a207c9109 --- /dev/null +++ b/themes/main/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts @@ -0,0 +1,86 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import Assert = require("assert"); +import U2FRegisterRequestGet = require("./get"); +import ExpressMock = require("../../../../stubs/express.spec"); +import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; + +describe("routes/secondfactor/u2f/register_request/get", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.originalUrl = "/api/xxxx"; + req.app = {}; + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false, + identity_check: { + challenge: "u2f-register", + userid: "user" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + }); + + describe("test registration request", () => { + it("should send back the registration request and save it in the session", function () { + const expectedRequest = { + test: "abc" + }; + mocks.u2f.requestStub.returns(BluebirdPromise.resolve(expectedRequest)); + return U2FRegisterRequestGet.default(vars)(req as any, res as any) + .then(function () { + Assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); + }); + }); + + it("should return internal error on registration request", function () { + res.send = sinon.spy(); + const user_key_container = {}; + mocks.u2f.requestStub.returns(BluebirdPromise.reject("Internal error")); + return U2FRegisterRequestGet.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + }); + }); + + it("should return forbidden if identity has not been verified", function () { + req.session.auth.identity_check = undefined; + return U2FRegisterRequestGet.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(403, res.status.getCall(0).args[0]); + }); + }); + }); +}); + diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/register_request/get.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/register_request/get.ts new file mode 100644 index 000000000..f611af933 --- /dev/null +++ b/themes/main/server/src/lib/routes/secondfactor/u2f/register_request/get.ts @@ -0,0 +1,43 @@ + +import { UserDataStore } from "../../../../storage/UserDataStore"; + +import objectPath = require("object-path"); +import u2f_common = require("../U2FCommon"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import U2f = require("u2f"); +import ErrorReplies = require("../../../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../../ServerVariables"; + +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const appid: string = u2f_common.extract_app_id(req); + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (!authSession.identity_check + || authSession.identity_check.challenge != "u2f-register") { + res.status(403); + res.send(); + return reject(new Error("Bad challenge.")); + } + + vars.logger.info(req, "Starting registration for appId '%s'", appid); + return resolve(vars.u2f.request(appid)); + }) + .then(function (registrationRequest: U2f.Request) { + vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); + authSession.register_request = registrationRequest; + res.json(registrationRequest); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + + return handler; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts new file mode 100644 index 000000000..9b137e66d --- /dev/null +++ b/themes/main/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts @@ -0,0 +1,101 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import Assert = require("assert"); +import U2FSignPost = require("./post"); +import { ServerVariables } from "../../../../ServerVariables"; +import winston = require("winston"); + +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; +import ExpressMock = require("../../../../stubs/express.spec"); +import U2FMock = require("../../../../stubs/u2f.spec"); +import U2f = require("u2f"); +import { Level } from "../../../../authentication/Level"; + +describe("routes/secondfactor/u2f/sign/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.app = {}; + req.originalUrl = "/api/xxxx"; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req.session = { + auth: { + userid: "user", + authentication_level: Level.ONE_FACTOR, + identity_check: { + challenge: "u2f-register", + userid: "user" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const options = { + inMemoryOnly: true + }; + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + }); + + it("should return status code 204", function () { + const expectedStatus = { + keyHandle: "keyHandle", + publicKey: "pbk", + certificate: "cert" + }; + mocks.u2f.checkSignatureStub.returns(expectedStatus); + + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ + registration: { + publicKey: "PUBKEY" + } + })); + + req.session.auth.sign_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + return U2FSignPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(req.session.auth.authentication_level, Level.TWO_FACTOR); + }); + }); + + it("should return unauthorized error on registration request internal error", function () { + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ + registration: { + publicKey: "PUBKEY" + } + })); + mocks.u2f.checkSignatureStub.returns({ errorCode: 500 }); + + req.session.auth.sign_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + return U2FSignPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], + { error: "Operation failed." }); + }); + }); +}); + diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/sign/post.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/sign/post.ts new file mode 100644 index 000000000..7ee711c2c --- /dev/null +++ b/themes/main/server/src/lib/routes/secondfactor/u2f/sign/post.ts @@ -0,0 +1,57 @@ + +import objectPath = require("object-path"); +import u2f_common = require("../U2FCommon"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import { UserDataStore } from "../../../../storage/UserDataStore"; +import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; +import { Winston } from "../../../../../../types/Dependencies"; +import U2f = require("u2f"); +import exceptions = require("../../../../Exceptions"); +import redirect from "../../redirect"; +import ErrorReplies = require("../../../../ErrorReplies"); +import { ServerVariables } from "../../../../ServerVariables"; +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import { Level } from "../../../../authentication/Level"; + +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const appId = u2f_common.extract_app_id(req); + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (!authSession.sign_request) { + const err = new Error("No sign request"); + ErrorReplies.replyWithError401(req, res, vars.logger)(err); + return reject(err); + } + resolve(); + }) + .then(function () { + const userid = authSession.userid; + return vars.userDataStore.retrieveU2FRegistration(userid, appId); + }) + .then(function (doc: U2FRegistrationDocument): BluebirdPromise { + const signRequest = authSession.sign_request; + const signData: U2f.SignatureData = req.body; + vars.logger.info(req, "Finish authentication"); + return BluebirdPromise.resolve(vars.u2f.checkSignature(signRequest, signData, doc.registration.publicKey)); + }) + .then(function (result: U2f.SignatureResult | U2f.Error): BluebirdPromise { + if (objectPath.has(result, "errorCode")) + return BluebirdPromise.reject(new Error("Error while signing")); + vars.logger.info(req, "Successful authentication"); + authSession.authentication_level = Level.TWO_FACTOR; + redirect(vars)(req, res); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + + return handler; +} + diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts new file mode 100644 index 000000000..dd52b27e0 --- /dev/null +++ b/themes/main/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts @@ -0,0 +1,68 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import assert = require("assert"); +import U2FSignRequestGet = require("./get"); +import ExpressMock = require("../../../../stubs/express.spec"); +import { Request } from "u2f"; +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; + +import { SignMessage } from "../../../../../../../shared/SignMessage"; + +describe("routes/secondfactor/u2f/sign_request/get", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.originalUrl = "/api/xxxx"; + req.app = {}; + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false, + identity_check: { + challenge: "u2f-register", + userid: "user" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + }); + + it("should send back the sign request and save it in the session", function () { + const expectedRequest: Request = { + version: "U2F_V2", + appId: 'app', + challenge: 'challenge!' + }; + mocks.u2f.requestStub.returns(expectedRequest); + mocks.userDataStore.retrieveU2FRegistrationStub + .returns(BluebirdPromise.resolve({ + registration: { + keyHandle: "KeyHandle" + } + })); + + return U2FSignRequestGet.default(vars)(req as any, res as any) + .then(() => { + assert.deepEqual(expectedRequest, req.session.auth.sign_request); + assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); + }); + }); +}); + diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts new file mode 100644 index 000000000..9e93dde06 --- /dev/null +++ b/themes/main/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts @@ -0,0 +1,42 @@ + +import u2f_common = require("../../../secondfactor/u2f/U2FCommon"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; +import exceptions = require("../../../../Exceptions"); +import ErrorReplies = require("../../../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../../ServerVariables"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; + +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const appId = u2f_common.extract_app_id(req); + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + resolve(); + }) + .then(function () { + return vars.userDataStore.retrieveU2FRegistration(authSession.userid, appId); + }) + .then(function (doc: U2FRegistrationDocument): BluebirdPromise { + if (!doc) + return BluebirdPromise.reject(new exceptions.AccessDeniedError("No U2F registration document found.")); + + const appId: string = u2f_common.extract_app_id(req); + vars.logger.info(req, "Start authentication of app '%s'", appId); + vars.logger.debug(req, "AppId = %s, keyHandle = %s", appId, JSON.stringify(doc.registration.keyHandle)); + + const request = vars.u2f.request(appId, doc.registration.keyHandle); + authSession.sign_request = request; + res.json(request); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + return handler; +} diff --git a/themes/main/server/src/lib/routes/verify/access_control.ts b/themes/main/server/src/lib/routes/verify/access_control.ts new file mode 100644 index 000000000..136239aeb --- /dev/null +++ b/themes/main/server/src/lib/routes/verify/access_control.ts @@ -0,0 +1,51 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import Util = require("util"); + +import Exceptions = require("../../Exceptions"); + +import { Level as AuthorizationLevel } from "../../authorization/Level"; +import { Level as AuthenticationLevel } from "../../authentication/Level"; +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { ServerVariables } from "../../ServerVariables"; + +function isAuthorized( + authorization: AuthorizationLevel, + authentication: AuthenticationLevel): boolean { + + if (authorization == AuthorizationLevel.BYPASS) { + return true; + } else if (authorization == AuthorizationLevel.ONE_FACTOR && + authentication >= AuthenticationLevel.ONE_FACTOR) { + return true; + } else if (authorization == AuthorizationLevel.TWO_FACTOR && + authentication >= AuthenticationLevel.TWO_FACTOR) { + return true; + } + return false; +} + +export default function ( + req: Express.Request, + vars: ServerVariables, + domain: string, resource: string, + user: string, groups: string[], + authenticationLevel: AuthenticationLevel) { + + return new BluebirdPromise(function (resolve, reject) { + const authorizationLevel = vars.authorizer + .authorization({domain, resource}, {user, groups}); + + if (!isAuthorized(authorizationLevel, authenticationLevel)) { + if (authorizationLevel == AuthorizationLevel.DENY) { + reject(new Exceptions.NotAuthorizedError( + Util.format("User %s is not authorized to access %s%s", user, domain, resource))); + return; + } + reject(new Exceptions.NotAuthenticatedError(Util.format( + "User '%s' is not sufficiently authorized to access %s%s.", user, domain, resource))); + return; + } + resolve(); + }); +} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/verify/get.spec.ts b/themes/main/server/src/lib/routes/verify/get.spec.ts new file mode 100644 index 000000000..67cf19fb6 --- /dev/null +++ b/themes/main/server/src/lib/routes/verify/get.spec.ts @@ -0,0 +1,320 @@ + +import Assert = require("assert"); +import BluebirdPromise = require("bluebird"); +import Express = require("express"); +import Sinon = require("sinon"); +import winston = require("winston"); + +import VerifyGet = require("./get"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../types/AuthenticationSession"; +import ExpressMock = require("../../stubs/express.spec"); +import { ServerVariables } from "../../ServerVariables"; +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../ServerVariablesMockBuilder.spec"; +import { Level } from "../../authentication/Level"; +import { Level as AuthorizationLevel } from "../../authorization/Level"; + +describe("routes/verify/get", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + let authSession: AuthenticationSession; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + req.originalUrl = "/api/xxxx"; + req.query = { + redirect: "undefined" + }; + AuthenticationSessionHandler.reset(req as any); + req.headers["x-original-url"] = "https://secret.example.com/"; + const s = ServerVariablesMockBuilder.build(false); + mocks = s.mocks; + vars = s.variables; + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + }); + + describe("with session cookie", function () { + it("should be already authenticated", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + authSession.authentication_level = Level.TWO_FACTOR; + authSession.userid = "myuser"; + authSession.groups = ["mygroup", "othergroup"]; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser"); + Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); + Assert.equal(204, res.status.getCall(0).args[0]); + }); + }); + + function test_session(_authSession: AuthenticationSession, status_code: number) { + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert.equal(status_code, res.status.getCall(0).args[0]); + }); + } + + function test_non_authenticated_401(authSession: AuthenticationSession) { + return test_session(authSession, 401); + } + + function test_unauthorized_403(authSession: AuthenticationSession) { + return test_session(authSession, 403); + } + + function test_authorized(authSession: AuthenticationSession) { + return test_session(authSession, 204); + } + + describe("given user tries to access a 2-factor endpoint", function () { + before(function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + }); + + describe("given different cases of session", function () { + it("should not be authenticated when second factor is missing", function () { + return test_non_authenticated_401({ + keep_me_logged_in: false, + userid: "user", + authentication_level: Level.ONE_FACTOR, + email: undefined, + groups: [], + last_activity_datetime: new Date().getTime() + }); + }); + + it("should not be authenticated when userid is missing", function () { + return test_non_authenticated_401({ + keep_me_logged_in: false, + userid: undefined, + authentication_level: Level.TWO_FACTOR, + email: undefined, + groups: [], + last_activity_datetime: new Date().getTime() + }); + }); + + it("should not be authenticated when level is insufficient", function () { + return test_non_authenticated_401({ + keep_me_logged_in: false, + userid: "user", + authentication_level: Level.NOT_AUTHENTICATED, + email: undefined, + groups: [], + last_activity_datetime: new Date().getTime() + }); + }); + + it("should not be authenticated when session has not be initiated", function () { + return test_non_authenticated_401(undefined); + }); + + it("should not be authenticated when domain is not allowed for user", function () { + authSession.authentication_level = Level.TWO_FACTOR; + authSession.userid = "myuser"; + req.headers["x-original-url"] = "https://test.example.com/"; + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.DENY); + + return test_unauthorized_403({ + keep_me_logged_in: false, + authentication_level: Level.TWO_FACTOR, + userid: "user", + groups: ["group1", "group2"], + email: undefined, + last_activity_datetime: new Date().getTime() + }); + }); + }); + }); + + describe("given user tries to access a single factor endpoint", function () { + beforeEach(function () { + req.headers["x-original-url"] = "https://redirect.url/"; + }); + + it("should be authenticated when first factor is validated", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); + authSession.authentication_level = Level.ONE_FACTOR; + authSession.userid = "user1"; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWith(204)); + Assert(res.send.calledOnce); + }); + }); + + it("should be rejected with 401 when not authenticated", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); + authSession.authentication_level = Level.NOT_AUTHENTICATED; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWith(401)); + }); + }); + }); + + describe("inactivity period", function () { + it("should update last inactivity period on requests on /api/verify", function () { + mocks.config.session.inactivity = 200000; + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + const currentTime = new Date().getTime() - 1000; + AuthenticationSessionHandler.reset(req as any); + authSession.authentication_level = Level.TWO_FACTOR; + authSession.userid = "myuser"; + authSession.groups = ["mygroup", "othergroup"]; + authSession.last_activity_datetime = currentTime; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + return AuthenticationSessionHandler.get(req as any, vars.logger); + }) + .then(function (authSession) { + Assert(authSession.last_activity_datetime > currentTime); + }); + }); + + it("should reset session when max inactivity period has been reached", function () { + mocks.config.session.inactivity = 1; + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + const currentTime = new Date().getTime() - 1000; + AuthenticationSessionHandler.reset(req as any); + authSession.authentication_level = Level.TWO_FACTOR; + authSession.userid = "myuser"; + authSession.groups = ["mygroup", "othergroup"]; + authSession.last_activity_datetime = currentTime; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + return AuthenticationSessionHandler.get(req as any, vars.logger); + }) + .then(function (authSession) { + Assert.equal(authSession.authentication_level, Level.NOT_AUTHENTICATED); + Assert.equal(authSession.userid, undefined); + }); + }); + }); + }); + + describe("response type 401 | 302", function() { + it("should return error code 401", function() { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( + "Invalid credentials")); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should redirect to provided redirection url", function() { + const REDIRECT_URL = "http://redirection_url.com"; + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( + "Invalid credentials")); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + req.query["rd"] = REDIRECT_URL; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.redirect.calledWithExactly(REDIRECT_URL)); + }); + }); + }); + + describe("with basic auth", function () { + it("should authenticate correctly", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.returns({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "john"); + Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); + Assert.equal(204, res.status.getCall(0).args[0]); + }); + }); + + it("should fail when endpoint is protected by two factors", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.config.access_control.rules = [{ + domain: "secret.example.com", + policy: "two_factor" + }]; + mocks.usersDatabase.checkUserPasswordStub.resolves({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should fail when base64 token is not valid", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.resolves({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic i_m*not_a_base64*token"; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should fail when base64 token has not format user:psswd", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.resolves({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzOmJhZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should fail when bad user password is provided", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( + "Invalid credentials")); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should fail when resource is restricted", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.resolves({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + }); +}); + diff --git a/themes/main/server/src/lib/routes/verify/get.ts b/themes/main/server/src/lib/routes/verify/get.ts new file mode 100644 index 000000000..f73861696 --- /dev/null +++ b/themes/main/server/src/lib/routes/verify/get.ts @@ -0,0 +1,91 @@ +import BluebirdPromise = require("bluebird"); +import Express = require("express"); +import Exceptions = require("../../Exceptions"); +import ErrorReplies = require("../../ErrorReplies"); +import { ServerVariables } from "../../ServerVariables"; +import GetWithSessionCookieMethod from "./get_session_cookie"; +import GetWithBasicAuthMethod from "./get_basic_auth"; +import Constants = require("../../../../../shared/constants"); +import ObjectPath = require("object-path"); + +import { AuthenticationSessionHandler } + from "../../AuthenticationSessionHandler"; +import { AuthenticationSession } + from "../../../../types/AuthenticationSession"; + +const REMOTE_USER = "Remote-User"; +const REMOTE_GROUPS = "Remote-Groups"; + + +function verifyWithSelectedMethod(req: Express.Request, res: Express.Response, + vars: ServerVariables, authSession: AuthenticationSession) + : () => BluebirdPromise<{ username: string, groups: string[] }> { + return function () { + const authorization: string = "" + req.headers["proxy-authorization"]; + if (authorization && authorization.startsWith("Basic ")) + return GetWithBasicAuthMethod(req, res, vars, authorization); + + return GetWithSessionCookieMethod(req, res, vars, authSession); + }; +} + +function setRedirectHeader(req: Express.Request, res: Express.Response) { + return function () { + const originalUrl = ObjectPath.get( + req, "headers.x-original-url"); + res.set("Redirect", originalUrl); + return BluebirdPromise.resolve(); + }; +} + +function setUserAndGroupsHeaders(res: Express.Response) { + return function (u: { username: string, groups: string[] }) { + res.setHeader(REMOTE_USER, u.username); + res.setHeader(REMOTE_GROUPS, u.groups.join(",")); + return BluebirdPromise.resolve(); + }; +} + +function replyWith200(res: Express.Response) { + return function () { + res.status(204); + res.send(); + }; +} + +function getRedirectParam(req: Express.Request) { + return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; +} + +export default function (vars: ServerVariables) { + return function (req: Express.Request, res: Express.Response) + : BluebirdPromise { + let authSession: AuthenticationSession; + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + resolve(); + }) + .then(setRedirectHeader(req, res)) + .then(verifyWithSelectedMethod(req, res, vars, authSession)) + .then(setUserAndGroupsHeaders(res)) + .then(replyWith200(res)) + // The user is authenticated but has restricted access -> 403 + .catch(Exceptions.NotAuthorizedError, + ErrorReplies.replyWithError403(req, res, vars.logger)) + .catch(Exceptions.NotAuthenticatedError, + ErrorReplies.replyWithError401(req, res, vars.logger)) + // The user is not yet authenticated -> 401 + .catch((err) => { + const redirectUrl = getRedirectParam(req); + if (redirectUrl) { + ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err); + } + else { + ErrorReplies.replyWithError401(req, res, vars.logger)(err); + } + }); + }; +} + diff --git a/themes/main/server/src/lib/routes/verify/get_basic_auth.ts b/themes/main/server/src/lib/routes/verify/get_basic_auth.ts new file mode 100644 index 000000000..af23c76c9 --- /dev/null +++ b/themes/main/server/src/lib/routes/verify/get_basic_auth.ts @@ -0,0 +1,55 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import ObjectPath = require("object-path"); +import { ServerVariables } from "../../ServerVariables"; +import { AuthenticationSession } + from "../../../../types/AuthenticationSession"; +import AccessControl from "./access_control"; +import { URLDecomposer } from "../../utils/URLDecomposer"; +import { Level } from "../../authentication/Level"; + +export default function (req: Express.Request, res: Express.Response, + vars: ServerVariables, authorizationHeader: string) + : BluebirdPromise<{ username: string, groups: string[] }> { + let username: string; + const uri = ObjectPath.get(req, "headers.x-original-url"); + const urlDecomposition = URLDecomposer.fromUrl(uri); + + return BluebirdPromise.resolve() + .then(() => { + const base64Re = new RegExp("^Basic ((?:[A-Za-z0-9+/]{4})*" + + "(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)$"); + const isTokenValidBase64 = base64Re.test(authorizationHeader); + + if (!isTokenValidBase64) { + return BluebirdPromise.reject(new Error("No valid base64 token found in the header")); + } + + const tokenMatches = authorizationHeader.match(base64Re); + const base64Token = tokenMatches[1]; + const decodedToken = Buffer.from(base64Token, "base64").toString(); + const splittedToken = decodedToken.split(":"); + + if (splittedToken.length != 2) { + return BluebirdPromise.reject(new Error( + "The authorization token is invalid. Expecting 'userid:password'")); + } + + username = splittedToken[0]; + const password = splittedToken[1]; + return vars.usersDatabase.checkUserPassword(username, password); + }) + .then(function (groupsAndEmails) { + return AccessControl(req, vars, urlDecomposition.domain, urlDecomposition.path, + username, groupsAndEmails.groups, Level.ONE_FACTOR) + .then(() => BluebirdPromise.resolve({ + username: username, + groups: groupsAndEmails.groups + })); + }) + .catch(function (err: Error) { + return BluebirdPromise.reject( + new Error("Unable to authenticate the user with basic auth. Cause: " + + err.message)); + }); +} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/verify/get_session_cookie.ts b/themes/main/server/src/lib/routes/verify/get_session_cookie.ts new file mode 100644 index 000000000..070344812 --- /dev/null +++ b/themes/main/server/src/lib/routes/verify/get_session_cookie.ts @@ -0,0 +1,78 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import Util = require("util"); +import ObjectPath = require("object-path"); + +import Exceptions = require("../../Exceptions"); +import { Configuration } from "../../configuration/schema/Configuration"; +import { ServerVariables } from "../../ServerVariables"; +import { IRequestLogger } from "../../logging/IRequestLogger"; +import { AuthenticationSession } + from "../../../../types/AuthenticationSession"; +import { AuthenticationSessionHandler } + from "../../AuthenticationSessionHandler"; +import AccessControl from "./access_control"; +import { URLDecomposer } from "../../utils/URLDecomposer"; + +function verify_inactivity(req: Express.Request, + authSession: AuthenticationSession, + configuration: Configuration, logger: IRequestLogger) + : BluebirdPromise { + + // If inactivity is not specified, then inactivity timeout does not apply + if (!configuration.session.inactivity || authSession.keep_me_logged_in) { + return BluebirdPromise.resolve(); + } + + const lastActivityTime = authSession.last_activity_datetime; + const currentTime = new Date().getTime(); + authSession.last_activity_datetime = currentTime; + + const inactivityPeriodMs = currentTime - lastActivityTime; + logger.debug(req, "Inactivity period was %s s and max period was %s.", + inactivityPeriodMs / 1000, configuration.session.inactivity / 1000); + if (inactivityPeriodMs < configuration.session.inactivity) { + return BluebirdPromise.resolve(); + } + + logger.debug(req, "Session has been reset after too long inactivity period."); + AuthenticationSessionHandler.reset(req); + return BluebirdPromise.reject(new Error("Inactivity period exceeded.")); +} + +export default function (req: Express.Request, res: Express.Response, + vars: ServerVariables, authSession: AuthenticationSession) + : BluebirdPromise<{ username: string, groups: string[] }> { + + return BluebirdPromise.resolve() + .then(() => { + const username = authSession.userid; + const groups = authSession.groups; + + if (!authSession.userid) { + return BluebirdPromise.reject(new Exceptions.AccessDeniedError( + "userid is missing")); + } + + const originalUrl = ObjectPath.get( + req, "headers.x-original-url"); + const originalUri = + ObjectPath.get(req, "headers.x-original-uri"); + + const d = URLDecomposer.fromUrl(originalUrl); + vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", d.domain, + d.path, username, groups.join(",")); + return AccessControl(req, vars, d.domain, d.path, username, groups, + authSession.authentication_level); + }) + .then(() => { + return verify_inactivity(req, authSession, + vars.config, vars.logger); + }) + .then(() => { + return BluebirdPromise.resolve({ + username: authSession.userid, + groups: authSession.groups + }); + }); +} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/AuthenticationTraceDocument.d.ts b/themes/main/server/src/lib/storage/AuthenticationTraceDocument.d.ts new file mode 100644 index 000000000..69818c055 --- /dev/null +++ b/themes/main/server/src/lib/storage/AuthenticationTraceDocument.d.ts @@ -0,0 +1,6 @@ + +export interface AuthenticationTraceDocument { + userId: string; + date: Date; + isAuthenticationSuccessful: boolean; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/CollectionFactoryFactory.ts b/themes/main/server/src/lib/storage/CollectionFactoryFactory.ts new file mode 100644 index 000000000..92b29abfa --- /dev/null +++ b/themes/main/server/src/lib/storage/CollectionFactoryFactory.ts @@ -0,0 +1,15 @@ +import { ICollectionFactory } from "./ICollectionFactory"; +import { NedbCollectionFactory } from "./nedb/NedbCollectionFactory"; +import { MongoCollectionFactory } from "./mongo/MongoCollectionFactory"; +import { IMongoClient } from "../connectors/mongo/IMongoClient"; + + +export class CollectionFactoryFactory { + static createNedb(options: Nedb.DataStoreOptions): ICollectionFactory { + return new NedbCollectionFactory(options); + } + + static createMongo(client: IMongoClient): ICollectionFactory { + return new MongoCollectionFactory(client); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/CollectionFactoryStub.spec.ts b/themes/main/server/src/lib/storage/CollectionFactoryStub.spec.ts new file mode 100644 index 000000000..17f8bb021 --- /dev/null +++ b/themes/main/server/src/lib/storage/CollectionFactoryStub.spec.ts @@ -0,0 +1,16 @@ +import BluebirdPromise = require("bluebird"); +import Sinon = require("sinon"); +import { ICollection } from "./ICollection"; +import { ICollectionFactory } from "./ICollectionFactory"; + +export class CollectionFactoryStub implements ICollectionFactory { + buildStub: Sinon.SinonStub; + + constructor() { + this.buildStub = Sinon.stub(); + } + + build(collectionName: string): ICollection { + return this.buildStub(collectionName); + } +} diff --git a/themes/main/server/src/lib/storage/CollectionStub.spec.ts b/themes/main/server/src/lib/storage/CollectionStub.spec.ts new file mode 100644 index 000000000..42895d672 --- /dev/null +++ b/themes/main/server/src/lib/storage/CollectionStub.spec.ts @@ -0,0 +1,39 @@ +import BluebirdPromise = require("bluebird"); +import Sinon = require("sinon"); +import { ICollection } from "./ICollection"; + +export class CollectionStub implements ICollection { + findStub: Sinon.SinonStub; + findOneStub: Sinon.SinonStub; + updateStub: Sinon.SinonStub; + removeStub: Sinon.SinonStub; + insertStub: Sinon.SinonStub; + + constructor() { + this.findStub = Sinon.stub(); + this.findOneStub = Sinon.stub(); + this.updateStub = Sinon.stub(); + this.removeStub = Sinon.stub(); + this.insertStub = Sinon.stub(); + } + + find(filter: any, sortKeys: any, count: number): BluebirdPromise { + return this.findStub(filter, sortKeys, count); + } + + findOne(filter: any): BluebirdPromise { + return this.findOneStub(filter); + } + + update(filter: any, document: any, options: any): BluebirdPromise { + return this.updateStub(filter, document, options); + } + + remove(filter: any): BluebirdPromise { + return this.removeStub(filter); + } + + insert(document: any): BluebirdPromise { + return this.insertStub(document); + } +} diff --git a/themes/main/server/src/lib/storage/ICollection.d.ts b/themes/main/server/src/lib/storage/ICollection.d.ts new file mode 100644 index 000000000..caa6c2a87 --- /dev/null +++ b/themes/main/server/src/lib/storage/ICollection.d.ts @@ -0,0 +1,11 @@ +/* istanbul ignore next */ +import BluebirdPromise = require("bluebird"); + +/* istanbul ignore next */ +export interface ICollection { + find(query: any, sortKeys: any, count: number): BluebirdPromise; + findOne(query: any): BluebirdPromise; + update(query: any, updateQuery: any, options?: any): BluebirdPromise; + remove(query: any): BluebirdPromise; + insert(document: any): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/ICollectionFactory.d.ts b/themes/main/server/src/lib/storage/ICollectionFactory.d.ts new file mode 100644 index 000000000..39eb42c77 --- /dev/null +++ b/themes/main/server/src/lib/storage/ICollectionFactory.d.ts @@ -0,0 +1,6 @@ + +import { ICollection } from "./ICollection"; + +export interface ICollectionFactory { + build(collectionName: string): ICollection; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/IUserDataStore.d.ts b/themes/main/server/src/lib/storage/IUserDataStore.d.ts new file mode 100644 index 000000000..81df482aa --- /dev/null +++ b/themes/main/server/src/lib/storage/IUserDataStore.d.ts @@ -0,0 +1,21 @@ +import BluebirdPromise = require("bluebird"); +import { TOTPSecretDocument } from "./TOTPSecretDocument"; +import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; +import { U2FRegistration } from "../../../types/U2FRegistration"; +import { TOTPSecret } from "../../../types/TOTPSecret"; +import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; +import { IdentityValidationDocument } from "./IdentityValidationDocument"; + +export interface IUserDataStore { + saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise; + retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise; + + saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; + retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise; + + produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise; + consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise; + + saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise; + retrieveTOTPSecret(userId: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/IdentityValidationDocument.d.ts b/themes/main/server/src/lib/storage/IdentityValidationDocument.d.ts new file mode 100644 index 000000000..e7fd7b3f4 --- /dev/null +++ b/themes/main/server/src/lib/storage/IdentityValidationDocument.d.ts @@ -0,0 +1,7 @@ + +export interface IdentityValidationDocument { + userId: string; + token: string; + challenge: string; + maxDate: Date; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/TOTPSecretDocument.d.ts b/themes/main/server/src/lib/storage/TOTPSecretDocument.d.ts new file mode 100644 index 000000000..a6c0bf9e6 --- /dev/null +++ b/themes/main/server/src/lib/storage/TOTPSecretDocument.d.ts @@ -0,0 +1,6 @@ +import { TOTPSecret } from "../../../types/TOTPSecret"; + +export interface TOTPSecretDocument { + userid: string; + secret: TOTPSecret; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/U2FRegistrationDocument.d.ts b/themes/main/server/src/lib/storage/U2FRegistrationDocument.d.ts new file mode 100644 index 000000000..efec6cb1d --- /dev/null +++ b/themes/main/server/src/lib/storage/U2FRegistrationDocument.d.ts @@ -0,0 +1,8 @@ + +import { U2FRegistration } from "../../../types/U2FRegistration"; + +export interface U2FRegistrationDocument { + userId: string; + appId: string; + registration: U2FRegistration; +} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/UserDataStore.spec.ts b/themes/main/server/src/lib/storage/UserDataStore.spec.ts new file mode 100644 index 000000000..66fb85461 --- /dev/null +++ b/themes/main/server/src/lib/storage/UserDataStore.spec.ts @@ -0,0 +1,264 @@ + +import * as Assert from "assert"; +import * as Sinon from "sinon"; +import * as MockDate from "mockdate"; +import BluebirdPromise = require("bluebird"); + +import { UserDataStore } from "./UserDataStore"; +import { TOTPSecret } from "../../../types/TOTPSecret"; +import { U2FRegistration } from "../../../types/U2FRegistration"; +import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; +import { CollectionStub } from "./CollectionStub.spec"; +import { CollectionFactoryStub } from "./CollectionFactoryStub.spec"; + +describe("storage/UserDataStore", function () { + let factory: CollectionFactoryStub; + let collection: CollectionStub; + let userId: string; + let appId: string; + let totpSecret: TOTPSecret; + let u2fRegistration: U2FRegistration; + + beforeEach(function () { + factory = new CollectionFactoryStub(); + collection = new CollectionStub(); + + userId = "user"; + appId = "https://myappId"; + + totpSecret = { + ascii: "abc", + base32: "ABCDKZLEFZGREJK", + otpauth_url: "totp://test", + google_auth_qr: "dummy", + hex: "dummy", + qr_code_ascii: "dummy", + qr_code_base32: "dummy", + qr_code_hex: "dummy" + }; + + u2fRegistration = { + keyHandle: "KEY_HANDLE", + publicKey: "publickey" + }; + }); + + it("should correctly creates collections", function () { + new UserDataStore(factory); + + Assert.equal(4, factory.buildStub.callCount); + Assert(factory.buildStub.calledWith("authentication_traces")); + Assert(factory.buildStub.calledWith("identity_validation_tokens")); + Assert(factory.buildStub.calledWith("u2f_registrations")); + Assert(factory.buildStub.calledWith("totp_secrets")); + }); + + describe("TOTP secrets collection", function () { + it("should save a totp secret", function () { + factory.buildStub.returns(collection); + collection.updateStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.saveTOTPSecret(userId, totpSecret) + .then(function (doc) { + Assert(collection.updateStub.calledOnce); + Assert(collection.updateStub.calledWith({ userId: userId }, { + userId: userId, + secret: totpSecret + }, { upsert: true })); + return BluebirdPromise.resolve(); + }); + }); + + it("should retrieve a totp secret", function () { + factory.buildStub.returns(collection); + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.retrieveTOTPSecret(userId) + .then(function (doc) { + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ userId: userId })); + return BluebirdPromise.resolve(); + }); + }); + }); + + describe("U2F secrets collection", function () { + it("should save a U2F secret", function () { + factory.buildStub.returns(collection); + collection.updateStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.saveU2FRegistration(userId, appId, u2fRegistration) + .then(function (doc) { + Assert(collection.updateStub.calledOnce); + Assert(collection.updateStub.calledWith({ + userId: userId, + appId: appId + }, { + userId: userId, + appId: appId, + registration: u2fRegistration + }, { upsert: true })); + return BluebirdPromise.resolve(); + }); + }); + + it("should retrieve a U2F secret", function () { + factory.buildStub.returns(collection); + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.retrieveU2FRegistration(userId, appId) + .then(function (doc) { + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ + userId: userId, + appId: appId + })); + return BluebirdPromise.resolve(); + }); + }); + }); + + + describe("Regulator traces collection", function () { + it("should save a trace", function () { + factory.buildStub.returns(collection); + collection.insertStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.saveAuthenticationTrace(userId, true) + .then(function (doc) { + Assert(collection.insertStub.calledOnce); + Assert(collection.insertStub.calledWith({ + userId: userId, + date: Sinon.match.date, + isAuthenticationSuccessful: true + })); + return BluebirdPromise.resolve(); + }); + }); + + function should_retrieve_latest_authentication_traces(count: number) { + factory.buildStub.returns(collection); + collection.findStub.withArgs().returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.retrieveLatestAuthenticationTraces(userId, count) + .then(function (doc: AuthenticationTraceDocument[]) { + Assert(collection.findStub.calledOnce); + Assert(collection.findStub.calledWith({ + userId: userId, + }, { date: -1 }, count)); + return BluebirdPromise.resolve(); + }); + } + + it("should retrieve 3 latest failed authentication traces", function () { + should_retrieve_latest_authentication_traces(3); + }); + }); + + + describe("Identity validation collection", function () { + it("should save a identity validation token", function () { + factory.buildStub.returns(collection); + collection.insertStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + const maxAge = 400; + const token = "TOKEN"; + const challenge = "CHALLENGE"; + + return dataStore.produceIdentityValidationToken(userId, token, challenge, maxAge) + .then(function (doc) { + Assert(collection.insertStub.calledOnce); + Assert(collection.insertStub.calledWith({ + userId: userId, + token: token, + challenge: challenge, + maxDate: Sinon.match.date + })); + return BluebirdPromise.resolve(); + }); + }); + + it("should consume an identity token successfully", function () { + factory.buildStub.returns(collection); + + MockDate.set(100); + + const token = "TOKEN"; + const challenge = "CHALLENGE"; + + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ + userId: "USER", + token: token, + challenge: challenge, + maxDate: new Date() + })); + collection.removeStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + MockDate.set(80); + + return dataStore.consumeIdentityValidationToken(token, challenge) + .then(function (doc) { + MockDate.reset(); + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ + token: token, + challenge: challenge + })); + + Assert(collection.removeStub.calledOnce); + Assert(collection.removeStub.calledWith({ + token: token, + challenge: challenge + })); + return BluebirdPromise.resolve(); + }); + }); + + it("should consume an expired identity token", function () { + factory.buildStub.returns(collection); + + MockDate.set(0); + + const token = "TOKEN"; + const challenge = "CHALLENGE"; + + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ + userId: "USER", + token: token, + challenge: challenge, + maxDate: new Date() + })); + + const dataStore = new UserDataStore(factory); + + MockDate.set(80000); + + return dataStore.consumeIdentityValidationToken(token, challenge) + .then(function () { return BluebirdPromise.reject(new Error("should not be here")); }) + .catch(function () { + MockDate.reset(); + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ + token: token, + challenge: challenge + })); + return BluebirdPromise.resolve(); + }); + }); + }); +}); diff --git a/themes/main/server/src/lib/storage/UserDataStore.ts b/themes/main/server/src/lib/storage/UserDataStore.ts new file mode 100644 index 000000000..27b0cddbd --- /dev/null +++ b/themes/main/server/src/lib/storage/UserDataStore.ts @@ -0,0 +1,143 @@ +import * as BluebirdPromise from "bluebird"; +import * as path from "path"; +import { IUserDataStore } from "./IUserDataStore"; +import { ICollection } from "./ICollection"; +import { ICollectionFactory } from "./ICollectionFactory"; +import { TOTPSecretDocument } from "./TOTPSecretDocument"; +import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; +import { U2FRegistration } from "../../../types/U2FRegistration"; +import { TOTPSecret } from "../../../types/TOTPSecret"; +import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; +import { IdentityValidationDocument } from "./IdentityValidationDocument"; + +// Constants + +const IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME = "identity_validation_tokens"; +const AUTHENTICATION_TRACES_COLLECTION_NAME = "authentication_traces"; + +const U2F_REGISTRATIONS_COLLECTION_NAME = "u2f_registrations"; +const TOTP_SECRETS_COLLECTION_NAME = "totp_secrets"; + + +export interface U2FRegistrationKey { + userId: string; + appId: string; +} + +// Source + +export class UserDataStore implements IUserDataStore { + private u2fSecretCollection: ICollection; + private identityCheckTokensCollection: ICollection; + private authenticationTracesCollection: ICollection; + private totpSecretCollection: ICollection; + + private collectionFactory: ICollectionFactory; + + constructor(collectionFactory: ICollectionFactory) { + this.collectionFactory = collectionFactory; + + this.u2fSecretCollection = this.collectionFactory.build(U2F_REGISTRATIONS_COLLECTION_NAME); + this.identityCheckTokensCollection = this.collectionFactory.build(IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME); + this.authenticationTracesCollection = this.collectionFactory.build(AUTHENTICATION_TRACES_COLLECTION_NAME); + this.totpSecretCollection = this.collectionFactory.build(TOTP_SECRETS_COLLECTION_NAME); + } + + saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { + const newDocument: U2FRegistrationDocument = { + userId: userId, + appId: appId, + registration: registration + }; + + const filter: U2FRegistrationKey = { + userId: userId, + appId: appId + }; + + return this.u2fSecretCollection.update(filter, newDocument, { upsert: true }); + } + + retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { + const filter: U2FRegistrationKey = { + userId: userId, + appId: appId + }; + return this.u2fSecretCollection.findOne(filter); + } + + saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { + const newDocument: AuthenticationTraceDocument = { + userId: userId, + date: new Date(), + isAuthenticationSuccessful: isAuthenticationSuccessful, + }; + + return this.authenticationTracesCollection.insert(newDocument); + } + + retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { + const q = { + userId: userId + }; + + return this.authenticationTracesCollection.find(q, { date: -1 }, count); + } + + produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { + const newDocument: IdentityValidationDocument = { + userId: userId, + token: token, + challenge: challenge, + maxDate: new Date(new Date().getTime() + maxAge) + }; + + return this.identityCheckTokensCollection.insert(newDocument); + } + + consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { + const that = this; + const filter = { + token: token, + challenge: challenge + }; + + let identityValidationDocument: IdentityValidationDocument; + + return this.identityCheckTokensCollection.findOne(filter) + .then(function (doc: IdentityValidationDocument) { + if (!doc) { + return BluebirdPromise.reject(new Error("Registration token does not exist")); + } + + identityValidationDocument = doc; + const current_date = new Date(); + if (current_date > doc.maxDate) + return BluebirdPromise.reject(new Error("Registration token is not valid anymore")); + + return that.identityCheckTokensCollection.remove(filter); + }) + .then(() => { + return BluebirdPromise.resolve(identityValidationDocument); + }); + } + + saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { + const doc = { + userId: userId, + secret: secret + }; + + const filter = { + userId: userId + }; + return this.totpSecretCollection.update(filter, doc, { upsert: true }); + } + + retrieveTOTPSecret(userId: string): BluebirdPromise { + const filter = { + userId: userId + }; + return this.totpSecretCollection.findOne(filter); + } +} diff --git a/themes/main/server/src/lib/storage/UserDataStoreStub.spec.ts b/themes/main/server/src/lib/storage/UserDataStoreStub.spec.ts new file mode 100644 index 000000000..5ea27a2de --- /dev/null +++ b/themes/main/server/src/lib/storage/UserDataStoreStub.spec.ts @@ -0,0 +1,64 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); + +import { TOTPSecretDocument } from "./TOTPSecretDocument"; +import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; +import { U2FRegistration } from "../../../types/U2FRegistration"; +import { TOTPSecret } from "../../../types/TOTPSecret"; +import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; +import { IdentityValidationDocument } from "./IdentityValidationDocument"; +import { IUserDataStore } from "./IUserDataStore"; + +export class UserDataStoreStub implements IUserDataStore { + saveU2FRegistrationStub: Sinon.SinonStub; + retrieveU2FRegistrationStub: Sinon.SinonStub; + saveAuthenticationTraceStub: Sinon.SinonStub; + retrieveLatestAuthenticationTracesStub: Sinon.SinonStub; + produceIdentityValidationTokenStub: Sinon.SinonStub; + consumeIdentityValidationTokenStub: Sinon.SinonStub; + saveTOTPSecretStub: Sinon.SinonStub; + retrieveTOTPSecretStub: Sinon.SinonStub; + + constructor() { + this.saveU2FRegistrationStub = Sinon.stub(); + this.retrieveU2FRegistrationStub = Sinon.stub(); + this.saveAuthenticationTraceStub = Sinon.stub(); + this.retrieveLatestAuthenticationTracesStub = Sinon.stub(); + this.produceIdentityValidationTokenStub = Sinon.stub(); + this.consumeIdentityValidationTokenStub = Sinon.stub(); + this.saveTOTPSecretStub = Sinon.stub(); + this.retrieveTOTPSecretStub = Sinon.stub(); + } + + saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { + return this.saveU2FRegistrationStub(userId, appId, registration); + } + + retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { + return this.retrieveU2FRegistrationStub(userId, appId); + } + + saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { + return this.saveAuthenticationTraceStub(userId, isAuthenticationSuccessful); + } + + retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { + return this.retrieveLatestAuthenticationTracesStub(userId, count); + } + + produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { + return this.produceIdentityValidationTokenStub(userId, token, challenge, maxAge); + } + + consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { + return this.consumeIdentityValidationTokenStub(token, challenge); + } + + saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { + return this.saveTOTPSecretStub(userId, secret); + } + + retrieveTOTPSecret(userId: string): BluebirdPromise { + return this.retrieveTOTPSecretStub(userId); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/mongo/MongoCollection.spec.ts b/themes/main/server/src/lib/storage/mongo/MongoCollection.spec.ts new file mode 100644 index 000000000..74a773a1e --- /dev/null +++ b/themes/main/server/src/lib/storage/mongo/MongoCollection.spec.ts @@ -0,0 +1,110 @@ +import Assert = require("assert"); +import Sinon = require("sinon"); +import MongoDB = require("mongodb"); +import BluebirdPromise = require("bluebird"); +import { MongoClientStub } from "../../connectors/mongo/MongoClientStub.spec"; +import { MongoCollection } from "./MongoCollection"; + +describe("storage/mongo/MongoCollection", function () { + let mongoCollectionStub: any; + let mongoClientStub: MongoClientStub; + let findStub: Sinon.SinonStub; + let findOneStub: Sinon.SinonStub; + let insertOneStub: Sinon.SinonStub; + let updateStub: Sinon.SinonStub; + let removeStub: Sinon.SinonStub; + let countStub: Sinon.SinonStub; + const COLLECTION_NAME = "collection"; + + before(function () { + mongoClientStub = new MongoClientStub(); + mongoCollectionStub = Sinon.createStubInstance(require("mongodb").Collection as any); + findStub = mongoCollectionStub.find as Sinon.SinonStub; + findOneStub = mongoCollectionStub.findOne as Sinon.SinonStub; + insertOneStub = mongoCollectionStub.insertOne as Sinon.SinonStub; + updateStub = mongoCollectionStub.update as Sinon.SinonStub; + removeStub = mongoCollectionStub.remove as Sinon.SinonStub; + countStub = mongoCollectionStub.count as Sinon.SinonStub; + mongoClientStub.collectionStub.returns( + BluebirdPromise.resolve(mongoCollectionStub) + ); + }); + + describe("find", function () { + it("should find a document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + findStub.returns({ + sort: Sinon.stub().returns({ + limit: Sinon.stub().returns({ + toArray: Sinon.stub().returns(BluebirdPromise.resolve([])) + }) + }) + }); + + return collection.find({ key: "KEY" }) + .then(function () { + Assert(findStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("findOne", function () { + it("should find one document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + findOneStub.returns(BluebirdPromise.resolve({})); + + return collection.findOne({ key: "KEY" }) + .then(function () { + Assert(findOneStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("insert", function () { + it("should insert a document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + insertOneStub.returns(BluebirdPromise.resolve({})); + + return collection.insert({ key: "KEY" }) + .then(function () { + Assert(insertOneStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("update", function () { + it("should update a document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + updateStub.returns(BluebirdPromise.resolve({})); + + return collection.update({ key: "KEY" }, { key: "KEY", value: 1 }) + .then(function () { + Assert(updateStub.calledWith({ key: "KEY" }, { key: "KEY", value: 1 })); + }); + }); + }); + + describe("remove", function () { + it("should remove a document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + removeStub.returns(BluebirdPromise.resolve({})); + + return collection.remove({ key: "KEY" }) + .then(function () { + Assert(removeStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("count", function () { + it("should count documents in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + countStub.returns(BluebirdPromise.resolve({})); + + return collection.count({ key: "KEY" }) + .then(function () { + Assert(countStub.calledWith({ key: "KEY" })); + }); + }); + }); +}); diff --git a/themes/main/server/src/lib/storage/mongo/MongoCollection.ts b/themes/main/server/src/lib/storage/mongo/MongoCollection.ts new file mode 100644 index 000000000..9771389f1 --- /dev/null +++ b/themes/main/server/src/lib/storage/mongo/MongoCollection.ts @@ -0,0 +1,50 @@ +import Bluebird = require("bluebird"); +import { ICollection } from "../ICollection"; +import MongoDB = require("mongodb"); +import { IMongoClient } from "../../connectors/mongo/IMongoClient"; + + +export class MongoCollection implements ICollection { + private mongoClient: IMongoClient; + private collectionName: string; + + constructor(collectionName: string, mongoClient: IMongoClient) { + this.collectionName = collectionName; + this.mongoClient = mongoClient; + } + + private collection(): Bluebird { + return this.mongoClient.collection(this.collectionName); + } + + find(query: any, sortKeys?: any, count?: number): Bluebird { + return this.collection() + .then((collection) => collection.find(query).sort(sortKeys).limit(count)) + .then((query) => query.toArray()); + } + + findOne(query: any): Bluebird { + return this.collection() + .then((collection) => collection.findOne(query)); + } + + update(query: any, updateQuery: any, options?: any): Bluebird { + return this.collection() + .then((collection) => collection.update(query, updateQuery, options)); + } + + remove(query: any): Bluebird { + return this.collection() + .then((collection) => collection.remove(query)); + } + + insert(document: any): Bluebird { + return this.collection() + .then((collection) => collection.insertOne(document)); + } + + count(query: any): Bluebird { + return this.collection() + .then((collection) => collection.count(query)); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts b/themes/main/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts new file mode 100644 index 000000000..bd959cacb --- /dev/null +++ b/themes/main/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts @@ -0,0 +1,21 @@ +import Assert = require("assert"); +import Sinon = require("sinon"); +import { MongoClientStub } from "../../connectors/mongo/MongoClientStub.spec"; +import { MongoCollectionFactory } from "./MongoCollectionFactory"; + +describe("storage/mongo/MongoCollectionFactory", function () { + let mongoClient: MongoClientStub; + + before(function() { + mongoClient = new MongoClientStub(); + }); + + describe("create", function () { + it("should create a collection", function () { + const COLLECTION_NAME = "COLLECTION_NAME"; + + const factory = new MongoCollectionFactory(mongoClient); + Assert(factory.build(COLLECTION_NAME)); + }); + }); +}); diff --git a/themes/main/server/src/lib/storage/mongo/MongoCollectionFactory.ts b/themes/main/server/src/lib/storage/mongo/MongoCollectionFactory.ts new file mode 100644 index 000000000..14a8262c9 --- /dev/null +++ b/themes/main/server/src/lib/storage/mongo/MongoCollectionFactory.ts @@ -0,0 +1,19 @@ +import BluebirdPromise = require("bluebird"); +import { ICollection } from "../ICollection"; +import { ICollectionFactory } from "../ICollectionFactory"; +import { MongoCollection } from "./MongoCollection"; +import path = require("path"); +import MongoDB = require("mongodb"); +import { IMongoClient } from "../../connectors/mongo/IMongoClient"; + +export class MongoCollectionFactory implements ICollectionFactory { + private mongoClient: IMongoClient; + + constructor(mongoClient: IMongoClient) { + this.mongoClient = mongoClient; + } + + build(collectionName: string): ICollection { + return new MongoCollection(collectionName, this.mongoClient); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/nedb/NedbCollection.spec.ts b/themes/main/server/src/lib/storage/nedb/NedbCollection.spec.ts new file mode 100644 index 000000000..a69962b67 --- /dev/null +++ b/themes/main/server/src/lib/storage/nedb/NedbCollection.spec.ts @@ -0,0 +1,136 @@ +import Sinon = require("sinon"); +import Assert = require("assert"); + +import { NedbCollection } from "./NedbCollection"; + +describe("storage/nedb/NedbCollection", function () { + describe("insert", function () { + it("should insert one entry", function () { + const nedbOptions = { + inMemoryOnly: true + }; + const collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou" }); + + return collection.count({}).then(function (count: number) { + Assert.equal(1, count); + }); + }); + + it("should insert three entries", function () { + const nedbOptions = { + inMemoryOnly: true + }; + const collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou" }); + collection.insert({ key: "hello" }); + collection.insert({ key: "hey" }); + + return collection.count({}).then(function (count: number) { + Assert.equal(3, count); + }); + }); + }); + + describe("find", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "hello" }); + collection.insert({ key: "hey" }); + collection.insert({ key: "coucou", value: 2 }); + }); + + it("should find one hello", function () { + return collection.find({ key: "hello" }, { key: 1 }) + .then(function (docs: { key: string }[]) { + Assert.equal(1, docs.length); + Assert(docs[0].key == "hello"); + }); + }); + + it("should find two coucou", function () { + return collection.find({ key: "coucou" }, { value: 1 }) + .then(function (docs: { value: number }[]) { + Assert.equal(2, docs.length); + }); + }); + }); + + describe("findOne", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "coucou", value: 1 }); + }); + + it("should find two coucou", function () { + const doc = { key: "coucou", value: 1 }; + return collection.count(doc) + .then(function (count: number) { + Assert.equal(4, count); + return collection.findOne(doc); + }); + }); + }); + + describe("update", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou", value: 1 }); + }); + + it("should update the value", function () { + return collection.update({ key: "coucou" }, { key: "coucou", value: 2 }, { multi: true }) + .then(function () { + return collection.find({ key: "coucou" }); + }) + .then(function (docs: { key: string, value: number }[]) { + Assert.equal(1, docs.length); + Assert.equal(2, docs[0].value); + }); + }); + }); + + describe("update", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou" }); + collection.insert({ key: "hello" }); + }); + + it("should update the value", function () { + return collection.remove({ key: "coucou" }) + .then(function () { + return collection.count({}); + }) + .then(function (count: number) { + Assert.equal(1, count); + }); + }); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/nedb/NedbCollection.ts b/themes/main/server/src/lib/storage/nedb/NedbCollection.ts new file mode 100644 index 000000000..88a93ad05 --- /dev/null +++ b/themes/main/server/src/lib/storage/nedb/NedbCollection.ts @@ -0,0 +1,47 @@ +import BluebirdPromise = require("bluebird"); +import { ICollection } from "../ICollection"; +import Nedb = require("nedb"); + +declare module "nedb" { + export class NedbAsync extends Nedb { + constructor(pathOrOptions?: string | Nedb.DataStoreOptions); + updateAsync(query: any, updateQuery: any, options?: Nedb.UpdateOptions): BluebirdPromise; + findOneAsync(query: any): BluebirdPromise; + insertAsync(newDoc: T): BluebirdPromise; + removeAsync(query: any): BluebirdPromise; + countAsync(query: any): BluebirdPromise; + } +} + +export class NedbCollection implements ICollection { + private collection: Nedb.NedbAsync; + + constructor(options: Nedb.DataStoreOptions) { + this.collection = BluebirdPromise.promisifyAll(new Nedb(options)) as Nedb.NedbAsync; + } + + find(query: any, sortKeys?: any, count?: number): BluebirdPromise { + const q = this.collection.find(query).sort(sortKeys).limit(count); + return BluebirdPromise.promisify(q.exec, { context: q })(); + } + + findOne(query: any): BluebirdPromise { + return this.collection.findOneAsync(query); + } + + update(query: any, updateQuery: any, options?: any): BluebirdPromise { + return this.collection.updateAsync(query, updateQuery, options); + } + + remove(query: any): BluebirdPromise { + return this.collection.removeAsync(query); + } + + insert(document: any): BluebirdPromise { + return this.collection.insertAsync(document); + } + + count(query: any): BluebirdPromise { + return this.collection.countAsync(query); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts b/themes/main/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts new file mode 100644 index 000000000..da90c661f --- /dev/null +++ b/themes/main/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts @@ -0,0 +1,16 @@ +import Sinon = require("sinon"); +import Assert = require("assert"); + +import { NedbCollectionFactory } from "./NedbCollectionFactory"; + +describe("storage/nedb/NedbCollectionFactory", function() { + it("should create a nedb collection", function() { + const nedbOptions = { + inMemoryOnly: true + }; + const factory = new NedbCollectionFactory(nedbOptions); + + const collection = factory.build("mycollection"); + Assert(collection); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/nedb/NedbCollectionFactory.ts b/themes/main/server/src/lib/storage/nedb/NedbCollectionFactory.ts new file mode 100644 index 000000000..49c4dc853 --- /dev/null +++ b/themes/main/server/src/lib/storage/nedb/NedbCollectionFactory.ts @@ -0,0 +1,28 @@ +import { ICollection } from "../ICollection"; +import { ICollectionFactory } from "../ICollectionFactory"; +import { NedbCollection } from "./NedbCollection"; +import path = require("path"); +import Nedb = require("nedb"); + +export interface NedbOptions { + inMemoryOnly?: boolean; + directory?: string; +} + +export class NedbCollectionFactory implements ICollectionFactory { + private options: Nedb.DataStoreOptions; + + constructor(options: Nedb.DataStoreOptions) { + this.options = options; + } + + build(collectionName: string): ICollection { + const datastoreOptions: Nedb.DataStoreOptions = { + inMemoryOnly: this.options.inMemoryOnly || false, + autoload: true, + filename: (this.options.filename) ? path.resolve(this.options.filename, collectionName) : undefined + }; + + return new NedbCollection(datastoreOptions); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/stubs/express.spec.ts b/themes/main/server/src/lib/stubs/express.spec.ts new file mode 100644 index 000000000..48f15d7e1 --- /dev/null +++ b/themes/main/server/src/lib/stubs/express.spec.ts @@ -0,0 +1,103 @@ + +import sinon = require("sinon"); +import express = require("express"); + +export interface RequestMock { + app?: any; + body?: any; + session?: any; + headers?: any; + get?: any; + query?: any; + originalUrl: string; +} + +export interface ResponseMock { + send: sinon.SinonStub | sinon.SinonSpy; + sendStatus: sinon.SinonStub; + sendFile: sinon.SinonStub; + sendfile: sinon.SinonStub; + status: sinon.SinonStub | sinon.SinonSpy; + json: sinon.SinonStub | sinon.SinonSpy; + links: sinon.SinonStub; + jsonp: sinon.SinonStub; + download: sinon.SinonStub; + contentType: sinon.SinonStub; + type: sinon.SinonStub; + format: sinon.SinonStub; + attachment: sinon.SinonStub; + set: sinon.SinonStub; + header: sinon.SinonStub; + headersSent: boolean; + get: sinon.SinonStub; + clearCookie: sinon.SinonStub; + cookie: sinon.SinonStub; + location: sinon.SinonStub; + redirect: sinon.SinonStub | sinon.SinonSpy; + render: sinon.SinonStub | sinon.SinonSpy; + locals: sinon.SinonStub; + charset: string; + vary: sinon.SinonStub; + app: any; + write: sinon.SinonStub; + writeContinue: sinon.SinonStub; + writeHead: sinon.SinonStub; + statusCode: number; + statusMessage: string; + setHeader: sinon.SinonStub; + setTimeout: sinon.SinonStub; + sendDate: boolean; + getHeader: sinon.SinonStub; +} + +export function RequestMock(): RequestMock { + return { + originalUrl: "/non-api/xxx", + app: { + get: sinon.stub() + }, + headers: { + "x-forwarded-for": "127.0.0.1" + }, + session: {} + }; +} +export function ResponseMock(): ResponseMock { + return { + send: sinon.stub(), + status: sinon.stub(), + json: sinon.stub(), + sendStatus: sinon.stub(), + links: sinon.stub(), + jsonp: sinon.stub(), + sendFile: sinon.stub(), + sendfile: sinon.stub(), + download: sinon.stub(), + contentType: sinon.stub(), + type: sinon.stub(), + format: sinon.stub(), + attachment: sinon.stub(), + set: sinon.stub(), + header: sinon.stub(), + headersSent: true, + get: sinon.stub(), + clearCookie: sinon.stub(), + cookie: sinon.stub(), + location: sinon.stub(), + redirect: sinon.stub(), + render: sinon.stub(), + locals: sinon.stub(), + charset: "utf-8", + vary: sinon.stub(), + app: sinon.stub(), + write: sinon.stub(), + writeContinue: sinon.stub(), + writeHead: sinon.stub(), + statusCode: 200, + statusMessage: "message", + setHeader: sinon.stub(), + setTimeout: sinon.stub(), + sendDate: true, + getHeader: sinon.stub() + }; +} diff --git a/themes/main/server/src/lib/stubs/ldapjs.spec.ts b/themes/main/server/src/lib/stubs/ldapjs.spec.ts new file mode 100644 index 000000000..045c0e11b --- /dev/null +++ b/themes/main/server/src/lib/stubs/ldapjs.spec.ts @@ -0,0 +1,50 @@ + +import Sinon = require("sinon"); + +export class LdapjsMock { + createClientStub: sinon.SinonStub; + + constructor() { + this.createClientStub = Sinon.stub(); + } + + createClient(params: any) { + return this.createClientStub(params); + } +} + +export class LdapjsClientMock { + bindStub: sinon.SinonStub; + unbindStub: sinon.SinonStub; + searchStub: sinon.SinonStub; + modifyStub: sinon.SinonStub; + onStub: sinon.SinonStub; + + constructor() { + this.bindStub = Sinon.stub(); + this.unbindStub = Sinon.stub(); + this.searchStub = Sinon.stub(); + this.modifyStub = Sinon.stub(); + this.onStub = Sinon.stub(); + } + + bind() { + return this.bindStub(); + } + + unbind() { + return this.unbindStub(); + } + + search() { + return this.searchStub(); + } + + modify() { + return this.modifyStub(); + } + + on() { + return this.onStub(); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/stubs/speakeasy.spec.ts b/themes/main/server/src/lib/stubs/speakeasy.spec.ts new file mode 100644 index 000000000..023614dcd --- /dev/null +++ b/themes/main/server/src/lib/stubs/speakeasy.spec.ts @@ -0,0 +1,7 @@ + +import sinon = require("sinon"); + +export = { + totp: sinon.stub(), + generateSecret: sinon.stub() +}; diff --git a/themes/main/server/src/lib/stubs/u2f.spec.ts b/themes/main/server/src/lib/stubs/u2f.spec.ts new file mode 100644 index 000000000..234b28c11 --- /dev/null +++ b/themes/main/server/src/lib/stubs/u2f.spec.ts @@ -0,0 +1,16 @@ + +import sinon = require("sinon"); + +export interface U2FMock { + request: sinon.SinonStub; + checkSignature: sinon.SinonStub; + checkRegistration: sinon.SinonStub; +} + +export function U2FMock(): U2FMock { + return { + request: sinon.stub(), + checkSignature: sinon.stub(), + checkRegistration: sinon.stub() + }; +} diff --git a/themes/main/server/src/lib/utils/HashGenerator.spec.ts b/themes/main/server/src/lib/utils/HashGenerator.spec.ts new file mode 100644 index 000000000..f19619a65 --- /dev/null +++ b/themes/main/server/src/lib/utils/HashGenerator.spec.ts @@ -0,0 +1,18 @@ +import Assert = require("assert"); +import { HashGenerator } from "./HashGenerator"; + +describe("utils/HashGenerator", function () { + it("should compute correct ssha512 (password)", function () { + return HashGenerator.ssha512("password", 500000, "jgiCMRyGXzoqpxS3") + .then(function (hash: string) { + Assert.equal(hash, "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"); + }); + }); + + it("should compute correct ssha512 (test)", function () { + return HashGenerator.ssha512("test", 500000, "abcdefghijklmnop") + .then(function (hash: string) { + Assert.equal(hash, "{CRYPT}$6$rounds=500000$abcdefghijklmnop$sTlNGf0VO/HTQIOXemmaBbV28HUch/qhWOA1/4dsDj6CDQYhUgXbYSPL6gccAsWMr2zD5fFWwhKmPdG.yxphs."); + }); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/utils/HashGenerator.ts b/themes/main/server/src/lib/utils/HashGenerator.ts new file mode 100644 index 000000000..e67de32b7 --- /dev/null +++ b/themes/main/server/src/lib/utils/HashGenerator.ts @@ -0,0 +1,23 @@ +import BluebirdPromise = require("bluebird"); +import RandomString = require("randomstring"); +import Util = require("util"); +const crypt = require("crypt3"); + +export class HashGenerator { + static ssha512( + password: string, + rounds: number = 500000, + salt?: string): BluebirdPromise { + const saltSize = 16; + // $6 means SHA512 + const _salt = Util.format("$6$rounds=%d$%s", rounds, + (salt) ? salt : RandomString.generate(16)); + + const cryptAsync = BluebirdPromise.promisify(crypt); + + return cryptAsync(password, _salt) + .then(function (hash: string) { + return BluebirdPromise.resolve(Util.format("{CRYPT}%s", hash)); + }); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/utils/ObjectCloner.ts b/themes/main/server/src/lib/utils/ObjectCloner.ts new file mode 100644 index 000000000..3e125d749 --- /dev/null +++ b/themes/main/server/src/lib/utils/ObjectCloner.ts @@ -0,0 +1,6 @@ + +export class ObjectCloner { + static clone(obj: any): any { + return JSON.parse(JSON.stringify(obj)); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/utils/SafeRedirection.spec.ts b/themes/main/server/src/lib/utils/SafeRedirection.spec.ts new file mode 100644 index 000000000..4126949fd --- /dev/null +++ b/themes/main/server/src/lib/utils/SafeRedirection.spec.ts @@ -0,0 +1,33 @@ +import Assert = require("assert"); +import Sinon = require("sinon"); +import { SafeRedirector } from "./SafeRedirection"; + +describe("web_server/middlewares/SafeRedirection", () => { + describe("Url is in protected domain", () => { + before(() => { + this.redirector = new SafeRedirector("example.com"); + this.res = {redirect: Sinon.stub()}; + }); + + it("should redirect to provided url", () => { + this.redirector.redirectOrElse(this.res, + "https://mysubdomain.example.com:8080/abc", + "https://authelia.example.com"); + Assert(this.res.redirect.calledWith("https://mysubdomain.example.com:8080/abc")); + }); + + it("should redirect to default url when wrong domain", () => { + this.redirector.redirectOrElse(this.res, + "https://mysubdomain.domain.rtf:8080/abc", + "https://authelia.example.com"); + Assert(this.res.redirect.calledWith("https://authelia.example.com")); + }); + + it("should redirect to default url when not terminating by domain", () => { + this.redirector.redirectOrElse(this.res, + "https://mysubdomain.example.com.rtf:8080/abc", + "https://authelia.example.com"); + Assert(this.res.redirect.calledWith("https://authelia.example.com")); + }); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/utils/SafeRedirection.ts b/themes/main/server/src/lib/utils/SafeRedirection.ts new file mode 100644 index 000000000..9e6a32e0c --- /dev/null +++ b/themes/main/server/src/lib/utils/SafeRedirection.ts @@ -0,0 +1,22 @@ +import Express = require("express"); +import { DomainExtractor } from "../../../../shared/DomainExtractor"; +import { BelongToDomain } from "../../../../shared/BelongToDomain"; + + +export class SafeRedirector { + private domain: string; + + constructor(domain: string) { + this.domain = domain; + } + + redirectOrElse( + res: Express.Response, + url: string, + defaultUrl: string): void { + if (BelongToDomain(url, this.domain)) { + res.redirect(url); + } + res.redirect(defaultUrl); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/utils/URLDecomposer.spec.ts b/themes/main/server/src/lib/utils/URLDecomposer.spec.ts new file mode 100644 index 000000000..cbb038738 --- /dev/null +++ b/themes/main/server/src/lib/utils/URLDecomposer.spec.ts @@ -0,0 +1,46 @@ +import { URLDecomposer } from "./URLDecomposer"; +import Assert = require("assert"); + +describe("utils/URLDecomposer", function () { + describe("test fromUrl", function () { + it("should return domain from https url", function () { + const d = URLDecomposer.fromUrl("https://www.example.com/test/abc"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/test/abc"); + }); + + it("should return domain from http url", function () { + const d = URLDecomposer.fromUrl("http://www.example.com/test/abc"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/test/abc"); + }); + + it("should return domain when url contains port", function () { + const d = URLDecomposer.fromUrl("https://www.example.com:8080/test/abc"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/test/abc"); + }); + + it("should return default path when no path provided", function () { + const d = URLDecomposer.fromUrl("https://www.example.com:8080"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/"); + }); + + it("should return default path when provided", function () { + const d = URLDecomposer.fromUrl("https://www.example.com:8080/"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/"); + }); + + it("should return undefined when does not match", function () { + const d = URLDecomposer.fromUrl("https:///abc/test"); + Assert.equal(d, undefined); + }); + + it("should return undefined when does not match", function () { + const d = URLDecomposer.fromUrl("https:///abc/test"); + Assert.equal(d, undefined); + }); + }); +}); \ No newline at end of file diff --git a/themes/main/server/src/lib/utils/URLDecomposer.ts b/themes/main/server/src/lib/utils/URLDecomposer.ts new file mode 100644 index 000000000..9bdf2e9d2 --- /dev/null +++ b/themes/main/server/src/lib/utils/URLDecomposer.ts @@ -0,0 +1,15 @@ +export class URLDecomposer { + static fromUrl(url: string): {domain: string, path: string} { + if (!url) return; + const match = url.match(/https?:\/\/([a-z0-9_.-]+)(:[0-9]+)?(.*)/); + + if (!match) return; + + if (match[1] && !match[3]) { + return {domain: match[1], path: "/"}; + } else if (match[1] && match[3]) { + return {domain: match[1], path: match[3]}; + } + return; + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/web_server/Configurator.ts b/themes/main/server/src/lib/web_server/Configurator.ts new file mode 100644 index 000000000..6e404874a --- /dev/null +++ b/themes/main/server/src/lib/web_server/Configurator.ts @@ -0,0 +1,47 @@ +import { Configuration } from "../configuration/schema/Configuration"; +import { GlobalDependencies } from "../../../types/Dependencies"; +import { SessionConfigurationBuilder } from + "../configuration/SessionConfigurationBuilder"; +import Path = require("path"); +import Express = require("express"); +import * as BodyParser from "body-parser"; +import { RestApi } from "./RestApi"; +import { WithHeadersLogged } from "./middlewares/WithHeadersLogged"; +import { ServerVariables } from "../ServerVariables"; +import Helmet = require("helmet"); + +const addRequestId = require("express-request-id")(); + +// Constants +const TRUST_PROXY = "trust proxy"; +const X_POWERED_BY = "x-powered-by"; +const VIEWS = "views"; +const VIEW_ENGINE = "view engine"; +const PUG = "pug"; + +export class Configurator { + static configure(config: Configuration, + app: Express.Application, + vars: ServerVariables, + deps: GlobalDependencies): void { + const viewsDirectory = Path.resolve(__dirname, "../../views"); + const publicHtmlDirectory = Path.resolve(__dirname, "../../public_html"); + + const expressSessionOptions = SessionConfigurationBuilder.build(config, deps); + + app.use(Express.static(publicHtmlDirectory)); + app.use(BodyParser.urlencoded({ extended: false })); + app.use(BodyParser.json()); + app.use(deps.session(expressSessionOptions)); + app.use(addRequestId); + app.use(WithHeadersLogged.middleware(vars.logger)); + app.disable(X_POWERED_BY); + app.enable(TRUST_PROXY); + app.use(Helmet()); + + app.set(VIEWS, viewsDirectory); + app.set(VIEW_ENGINE, PUG); + + RestApi.setup(app, vars); + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/web_server/RestApi.ts b/themes/main/server/src/lib/web_server/RestApi.ts new file mode 100644 index 000000000..9144a15b9 --- /dev/null +++ b/themes/main/server/src/lib/web_server/RestApi.ts @@ -0,0 +1,125 @@ +import Express = require("express"); + +import FirstFactorGet = require("../routes/firstfactor/get"); +import SecondFactorGet = require("../routes/secondfactor/get"); + +import FirstFactorPost = require("../routes/firstfactor/post"); +import LogoutGet = require("../routes/logout/get"); +import VerifyGet = require("../routes/verify/get"); +import TOTPSignGet = require("../routes/secondfactor/totp/sign/post"); + +import IdentityCheckMiddleware = require("../IdentityCheckMiddleware"); + +import TOTPRegistrationIdentityHandler from "../routes/secondfactor/totp/identity/RegistrationHandler"; +import U2FRegistrationIdentityHandler from "../routes/secondfactor/u2f/identity/RegistrationHandler"; +import ResetPasswordIdentityHandler from "../routes/password-reset/identity/PasswordResetHandler"; + +import U2FSignPost = require("../routes/secondfactor/u2f/sign/post"); +import U2FSignRequestGet = require("../routes/secondfactor/u2f/sign_request/get"); + +import U2FRegisterPost = require("../routes/secondfactor/u2f/register/post"); +import U2FRegisterRequestGet = require("../routes/secondfactor/u2f/register_request/get"); + +import ResetPasswordFormPost = require("../routes/password-reset/form/post"); +import ResetPasswordRequestPost = require("../routes/password-reset/request/get"); + +import Error401Get = require("../routes/error/401/get"); +import Error403Get = require("../routes/error/403/get"); +import Error404Get = require("../routes/error/404/get"); + +import LoggedIn = require("../routes/loggedin/get"); + +import { ServerVariables } from "../ServerVariables"; +import Endpoints = require("../../../../shared/api"); +import { RequireValidatedFirstFactor } from "./middlewares/RequireValidatedFirstFactor"; + +function setupTotp(app: Express.Application, vars: ServerVariables) { + app.post(Endpoints.SECOND_FACTOR_TOTP_POST, + RequireValidatedFirstFactor.middleware(vars.logger), + TOTPSignGet.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, + RequireValidatedFirstFactor.middleware(vars.logger)); + + app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, + RequireValidatedFirstFactor.middleware(vars.logger)); + + IdentityCheckMiddleware.register(app, + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, + new TOTPRegistrationIdentityHandler(vars.logger, + vars.userDataStore, vars.totpHandler, vars.config.totp), + vars); +} + +function setupU2f(app: Express.Application, vars: ServerVariables) { + app.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, + RequireValidatedFirstFactor.middleware(vars.logger), + U2FSignRequestGet.default(vars)); + + app.post(Endpoints.SECOND_FACTOR_U2F_SIGN_POST, + RequireValidatedFirstFactor.middleware(vars.logger), + U2FSignPost.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, + RequireValidatedFirstFactor.middleware(vars.logger), + U2FRegisterRequestGet.default(vars)); + + app.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, + RequireValidatedFirstFactor.middleware(vars.logger), + U2FRegisterPost.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, + RequireValidatedFirstFactor.middleware(vars.logger)); + + app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, + RequireValidatedFirstFactor.middleware(vars.logger)); + + IdentityCheckMiddleware.register(app, + Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, + Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, + new U2FRegistrationIdentityHandler(vars.logger), vars); +} + +function setupResetPassword(app: Express.Application, vars: ServerVariables) { + IdentityCheckMiddleware.register(app, + Endpoints.RESET_PASSWORD_IDENTITY_START_GET, + Endpoints.RESET_PASSWORD_IDENTITY_FINISH_GET, + new ResetPasswordIdentityHandler(vars.logger, vars.usersDatabase), + vars); + + app.get(Endpoints.RESET_PASSWORD_REQUEST_GET, + ResetPasswordRequestPost.default); + app.post(Endpoints.RESET_PASSWORD_FORM_POST, + ResetPasswordFormPost.default(vars)); +} + +function setupErrors(app: Express.Application, vars: ServerVariables) { + app.get(Endpoints.ERROR_401_GET, Error401Get.default(vars)); + app.get(Endpoints.ERROR_403_GET, Error403Get.default(vars)); + app.get(Endpoints.ERROR_404_GET, Error404Get.default); +} + +export class RestApi { + static setup(app: Express.Application, vars: ServerVariables): void { + app.get(Endpoints.FIRST_FACTOR_GET, FirstFactorGet.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_GET, + RequireValidatedFirstFactor.middleware(vars.logger), + SecondFactorGet.default(vars)); + + app.get(Endpoints.LOGOUT_GET, LogoutGet.default(vars)); + + app.get(Endpoints.VERIFY_GET, VerifyGet.default(vars)); + app.post(Endpoints.FIRST_FACTOR_POST, FirstFactorPost.default(vars)); + + setupTotp(app, vars); + setupU2f(app, vars); + setupResetPassword(app, vars); + setupErrors(app, vars); + + app.get(Endpoints.LOGGED_IN, + RequireValidatedFirstFactor.middleware(vars.logger), + LoggedIn.default(vars)); + } +} diff --git a/themes/main/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts b/themes/main/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts new file mode 100644 index 000000000..ecfd75765 --- /dev/null +++ b/themes/main/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts @@ -0,0 +1,27 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import ErrorReplies = require("../../ErrorReplies"); +import { IRequestLogger } from "../../logging/IRequestLogger"; +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Exceptions = require("../../Exceptions"); +import { Level } from "../../authentication/Level"; + +export class RequireValidatedFirstFactor { + static middleware(logger: IRequestLogger) { + return function (req: Express.Request, res: Express.Response, + next: Express.NextFunction): BluebirdPromise { + + 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.")); + + next(); + resolve(); + }) + .catch(ErrorReplies.replyWithError401(req, res, logger)); + }; + } +} \ No newline at end of file diff --git a/themes/main/server/src/lib/web_server/middlewares/WithHeadersLogged.ts b/themes/main/server/src/lib/web_server/middlewares/WithHeadersLogged.ts new file mode 100644 index 000000000..139db1141 --- /dev/null +++ b/themes/main/server/src/lib/web_server/middlewares/WithHeadersLogged.ts @@ -0,0 +1,12 @@ +import Express = require("express"); +import { IRequestLogger } from "../../logging/IRequestLogger"; + +export class WithHeadersLogged { + static middleware(logger: IRequestLogger) { + return function (req: Express.Request, res: Express.Response, + next: Express.NextFunction): void { + logger.debug(req, "Headers = %s", JSON.stringify(req.headers)); + next(); + }; + } +} \ No newline at end of file diff --git a/themes/main/server/src/resources/email-template.ejs b/themes/main/server/src/resources/email-template.ejs new file mode 100644 index 000000000..f29d5afc0 --- /dev/null +++ b/themes/main/server/src/resources/email-template.ejs @@ -0,0 +1,254 @@ + + + + + + Simples-Minimalistic Responsive Template + + + + + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + +
 
+ + + + + + + +
+

<%= title %>

+
+ +
 
+
+
+ + + + + + + + +
+ + + + + + +
 
+
+ + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + +
 
+ + + + + + + + + + + + + + + + + + +
+ This email has been sent to you in order to validate your identity. Please ignore it if you do not know why you received it. +
 
+ <%= button_title %> +
+
 
+
+
+ + + + + + + + +
+ + + + + + + + + + + + +
 
 
 
+
+ + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + +
+ Please ignore this email if you did not initiate the process. +
+
+
+ + + + + diff --git a/themes/main/server/src/views/already-logged-in.pug b/themes/main/server/src/views/already-logged-in.pug new file mode 100644 index 000000000..137bbea30 --- /dev/null +++ b/themes/main/server/src/views/already-logged-in.pug @@ -0,0 +1,14 @@ +extends layout/layout.pug + +block form-header + h1 Sign in + +block content + img(class="header-img" src="/img/success.png" alt="success") + if redirection_url + p You are already logged in as #{ username }.

+ | If you are not redirected in few seconds, click here.

+ | Otherwise, click here to log off. + else + p You are already logged in as #{ username }.

+ | Click here to log off. diff --git a/themes/main/server/src/views/errors/.directory b/themes/main/server/src/views/errors/.directory new file mode 100644 index 000000000..d51d6cb47 --- /dev/null +++ b/themes/main/server/src/views/errors/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,59,58 +Version=3 +ViewMode=1 diff --git a/themes/main/server/src/views/errors/401.pug b/themes/main/server/src/views/errors/401.pug new file mode 100644 index 000000000..b7a222ad0 --- /dev/null +++ b/themes/main/server/src/views/errors/401.pug @@ -0,0 +1,16 @@ +extends ../layout/layout.pug + +block variables + - page_classname = "error-401"; + +block form-header + h1 Error 401 + +block content + img(class="header-img" src="/img/warning.png" alt="warning") + if redirection_url + p You are not authorized to access this resource.

+ | Please click here if you are not + | redirected in few seconds. + else + p You are not authorized to access this resource. \ No newline at end of file diff --git a/themes/main/server/src/views/errors/403.pug b/themes/main/server/src/views/errors/403.pug new file mode 100644 index 000000000..f4b5ca8a2 --- /dev/null +++ b/themes/main/server/src/views/errors/403.pug @@ -0,0 +1,16 @@ +extends ../layout/layout.pug + +block variables + - page_classname = "error-403"; + +block form-header + h1 Error 403 + +block content + img(class="header-img" src="/img/warning.png" alt="warning") + if redirection_url + p You don't have enough privileges to access this resource.

+ | Please click here if you are not + | redirected in few seconds. + else + p You don't have enough privileges to access this resource. diff --git a/themes/main/server/src/views/errors/404.pug b/themes/main/server/src/views/errors/404.pug new file mode 100644 index 000000000..06d6375fc --- /dev/null +++ b/themes/main/server/src/views/errors/404.pug @@ -0,0 +1,11 @@ +extends ../layout/layout.pug + +block variables + - page_classname = "error-404"; + +block form-header +

Error 404

+ +block content + img(class="header-img" src="/img/warning.png" alt="warning") + p Page not found. diff --git a/themes/main/server/src/views/firstfactor.pug b/themes/main/server/src/views/firstfactor.pug new file mode 100644 index 000000000..046b8c4c1 --- /dev/null +++ b/themes/main/server/src/views/firstfactor.pug @@ -0,0 +1,23 @@ +extends layout/layout.pug + +block variables + - page_classname = "firstfactor"; + +block form-header + h1 Sign in + +block content + div(class="notification") + img(class="header-img" src="/img/user.png" alt="user profile") + p Enter your credentials to sign in + form(class="form-signin") + div(class="form-inputs") + input(type="text" class="form-control" id="username" placeholder="Username" required autofocus) + input(type="password" class="form-control" id="password" placeholder="Password" required) + button(id="signin" class="btn btn-lg btn-primary btn-block" type="submit") Sign in + div(class="keep-me-logged-in pull-left") + input(type="checkbox" id="keep_me_logged_in" name="keep_me_logged_in" value="true") + label(for="keep_me_logged_in") Keep me logged in + div(class="bottom-right-links pull-right") + a(href=reset_password_request_endpoint, class="link forgot-password") Forgot password? + span(class="clearfix") diff --git a/themes/main/server/src/views/layout/layout.pug b/themes/main/server/src/views/layout/layout.pug new file mode 100644 index 000000000..1d845be49 --- /dev/null +++ b/themes/main/server/src/views/layout/layout.pug @@ -0,0 +1,30 @@ +block variables + +doctype html +html + head + title Authelia - 2FA + meta(name="viewport", content="width=device-width, initial-scale=1.0") + meta(name="robots", content="noindex, nofollow, nosnippet, noarchive") + meta(http-equiv="Content-Security-Policy", content="default-src 'self'; img-src 'self' data:;") + link(rel="icon", href="/img/icon.png" type="image/png" sizes="32x32") + link(rel="stylesheet", type="text/css", href="/css/authelia.css") + if redirection_url + meta(http-equiv="refresh" content="4;url=" + redirection_url) + body + canvas#canvas(width='400', height='300') + script(src='/js/matrix.js') + div(class="container") + div(class="row") + div(class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-6 col-lg-offset-3") + div(class="account-wall " + page_classname) + div(class="row header") + block form-header + div(class="row body") + div(class="form col-xs-10 col-xs-offset-1 col-sm-8 col-sm-offset-2 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2") + block content + div(class="row footer poweredby-block") + div(class="poweredby col-xs-6 col-xs-offset-4 col-sm-6 col-sm-offset-4 col-md-6 col-md-offset-4") + | Powered by Authelia + block entrypoint + script(src="/js/authelia.js", type="text/javascript") diff --git a/themes/main/server/src/views/need-identity-validation.pug b/themes/main/server/src/views/need-identity-validation.pug new file mode 100644 index 000000000..4cfd62717 --- /dev/null +++ b/themes/main/server/src/views/need-identity-validation.pug @@ -0,0 +1,12 @@ +extends layout/layout.pug + +block variables + - page_classname = "identity-validation"; + +block form-header + h1 Registration + +block content + img(class="header-img" src="/img/mail.png" alt="mail") + p A confirmation email has been sent to your mailbox. + | Please open it and click on the link within 15 minutes to confirm the registration. diff --git a/themes/main/server/src/views/password-reset-form.pug b/themes/main/server/src/views/password-reset-form.pug new file mode 100644 index 000000000..07f0baa7b --- /dev/null +++ b/themes/main/server/src/views/password-reset-form.pug @@ -0,0 +1,18 @@ +extends layout/layout.pug + +block variables + - page_classname = "password-reset-form"; + +block form-header + h1 Reset password + +block content + div(class="notification") + img(class="header-img" src="/img/password.png" alt="password") + p Set your new password and confirm it. + form(class="form-signin") + div(class="form-inputs") + input(class="form-control" type="password" name="password1" id="password1" placeholder="New password" required="required") + input(class="form-control" type="password" name="password2" id="password2" placeholder="Password confirmation" required="required") + button(id="reset-password-button" class="btn btn-lg btn-primary btn-block" type="submit") Reset Password + span(class="clearfix") diff --git a/themes/main/server/src/views/password-reset-request.pug b/themes/main/server/src/views/password-reset-request.pug new file mode 100644 index 000000000..21746af9e --- /dev/null +++ b/themes/main/server/src/views/password-reset-request.pug @@ -0,0 +1,18 @@ +extends layout/layout.pug + +block variables + - page_classname = "password-reset-request"; + +block form-header + h1 Reset password + +block content + div(class="notification") + div + img(class="header-img" src="/img/password.png" alt="password") + p After giving your username, you will receive an email to change your password. + form(class="form-signin") + div(class="form-inputs") + input(type="text" class="form-control" name="username" id="username" placeholder="Your username" required="required") + button(id="reset-password-button" class="btn btn-lg btn-primary btn-block" type="submit") Reset Password + span(class="clearfix") diff --git a/themes/main/server/src/views/secondfactor.pug b/themes/main/server/src/views/secondfactor.pug new file mode 100644 index 000000000..4df8ec257 --- /dev/null +++ b/themes/main/server/src/views/secondfactor.pug @@ -0,0 +1,30 @@ +extends layout/layout.pug + +block variables + - page_classname = "secondfactor"; + +block form-header + h1 Sign in + +block content + div + div(class="notification") + h3 Hi #{username} + div(class="row") + div(class="u2f-token") + img(src="/img/pendrive.png", alt="security key") + p + | Please, touch your security key
+ b Or
+ | Get a one-time password + form(class="form-signin totp") + div(class="form-inputs") + input(type="text" autocomplete="off" class="form-control" id="token" placeholder="Token" required autofocus) + button(class="btn btn-lg btn-primary btn-block totp-button" type="submit") Sign in + div(class="pull-right bottom-right-links") + div Need to register? + div + a(href=u2f_identity_start_endpoint, class="link register-u2f", data-toggle="tooltip", title="A security key is required to register.") Security key + | | + a(href=totp_identity_start_endpoint, class="link register-totp") Google Authenticator + span(class="clearfix") \ No newline at end of file diff --git a/themes/main/server/src/views/totp-register.pug b/themes/main/server/src/views/totp-register.pug new file mode 100644 index 000000000..1b4d98354 --- /dev/null +++ b/themes/main/server/src/views/totp-register.pug @@ -0,0 +1,25 @@ +extends layout/layout.pug + +block variables + - page_classname = "totp-register"; + +block form-header + h1 One-time passwords + +block content + p Open Google Authenticator and add this entry + p(id="secret") #{ base32_secret } + p or scan this barcode + div(id="qrcode") #{ otpauth_url } + p + a(href=login_endpoint, id="login-button") Login + div(class="need-google-authenticator") + | Need Google Authenticator? + div(class="store-badges") + a(href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1', target="_blank") + img(alt='Get it on Google Play', src='/img/stores/googleplay-badge.svg', class="store-badge") + a(href='https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8', target="_blank") + img(alt='Get it on Apple Store' src='/img/stores/applestore-badge.svg' class="store-badge") + +block entrypoint + script(src="/js/qrcode.min.js", type="text/javascript" ) diff --git a/themes/main/server/src/views/u2f-register.pug b/themes/main/server/src/views/u2f-register.pug new file mode 100644 index 000000000..5e24bc707 --- /dev/null +++ b/themes/main/server/src/views/u2f-register.pug @@ -0,0 +1,11 @@ +extends layout/layout.pug + +block variables + - page_classname = "u2f-register"; + +block form-header + h1 Register your security key + +block content + p Touch the token to register your security key. + img(src="/img/pendrive.png" alt="pendrive") \ No newline at end of file diff --git a/themes/main/server/test/requests.ts b/themes/main/server/test/requests.ts new file mode 100644 index 000000000..93fa0de47 --- /dev/null +++ b/themes/main/server/test/requests.ts @@ -0,0 +1,94 @@ + +import BluebirdPromise = require("bluebird"); +import request = require("request"); +import assert = require("assert"); +import express = require("express"); +import nodemailer = require("nodemailer"); +import Endpoints = require("../../shared/api"); + +declare module "request" { + export interface RequestAPI { + getAsync(uri: string, options?: RequiredUriUrl): BluebirdPromise; + getAsync(uri: string): BluebirdPromise; + getAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; + + postAsync(uri: string, options?: CoreOptions): BluebirdPromise; + postAsync(uri: string): BluebirdPromise; + postAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; + } +} + +const requestAsync: typeof request = BluebirdPromise.promisifyAll(request) as typeof request; + +export = function (port: number) { + const PORT = port; + const BASE_URL = "http://localhost:" + PORT; + + function execute_totp(jar: request.CookieJar, token: string) { + return requestAsync.postAsync({ + url: BASE_URL + Endpoints.SECOND_FACTOR_TOTP_POST, + jar: jar, + form: { + token: token + } + }); + } + + function execute_u2f_authentication(jar: request.CookieJar) { + return requestAsync.getAsync({ + url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, + jar: jar + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200); + return requestAsync.postAsync({ + url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_POST, + jar: jar, + form: { + } + }); + }); + } + + function execute_verification(jar: request.CookieJar) { + return requestAsync.getAsync({ url: BASE_URL + Endpoints.VERIFY_GET, jar: jar }); + } + + function execute_login(jar: request.CookieJar) { + return requestAsync.getAsync({ url: BASE_URL + Endpoints.FIRST_FACTOR_GET, jar: jar }); + } + + function execute_first_factor(jar: request.CookieJar) { + return requestAsync.postAsync({ + url: BASE_URL + Endpoints.FIRST_FACTOR_POST, + jar: jar, + form: { + username: "test_ok", + password: "password" + } + }); + } + + function execute_failing_first_factor(jar: request.CookieJar) { + return requestAsync.postAsync({ + url: BASE_URL + Endpoints.FIRST_FACTOR_POST, + jar: jar, + form: { + username: "test_nok", + password: "password" + } + }); + } + + return { + login: execute_login, + verify: execute_verification, + u2f_authentication: execute_u2f_authentication, + first_factor: execute_first_factor, + failing_first_factor: execute_failing_first_factor, + totp: execute_totp, + }; +}; + diff --git a/themes/main/server/tsconfig.json b/themes/main/server/tsconfig.json new file mode 100644 index 000000000..ebe98c5ed --- /dev/null +++ b/themes/main/server/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "moduleResolution": "node", + "noImplicitAny": true, + "sourceMap": true, + "removeComments": true, + "outDir": "../dist", + "baseUrl": ".", + "paths": { + "*": [ + "./types/*", + "../shared/types/*" + ] + } + }, + "exclude": [ + "src/**/*.spec.ts" + ] +} diff --git a/themes/main/server/tslint.json b/themes/main/server/tslint.json new file mode 100644 index 000000000..c2c1b7501 --- /dev/null +++ b/themes/main/server/tslint.json @@ -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 + } +} diff --git a/themes/main/server/types/.directory b/themes/main/server/types/.directory new file mode 100644 index 000000000..63f8e11d5 --- /dev/null +++ b/themes/main/server/types/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,58,28 +Version=3 +ViewMode=1 diff --git a/themes/main/server/types/AuthenticationSession.ts b/themes/main/server/types/AuthenticationSession.ts new file mode 100644 index 000000000..bbed0e715 --- /dev/null +++ b/themes/main/server/types/AuthenticationSession.ts @@ -0,0 +1,18 @@ +import U2f = require("u2f"); +import { Level } from "../src/lib/authentication/Level"; + +export interface AuthenticationSession { + userid: string; + authentication_level: Level; + keep_me_logged_in: boolean; + last_activity_datetime: number; + identity_check?: { + challenge: string; + userid: string; + }; + register_request?: U2f.Request; + sign_request?: U2f.Request; + email: string; + groups: string[]; + redirect?: string; +} \ No newline at end of file diff --git a/themes/main/server/types/Dependencies.ts b/themes/main/server/types/Dependencies.ts new file mode 100644 index 000000000..f20404dbc --- /dev/null +++ b/themes/main/server/types/Dependencies.ts @@ -0,0 +1,29 @@ +import winston = require("winston"); +import speakeasy = require("speakeasy"); +import nodemailer = require("nodemailer"); +import session = require("express-session"); +import nedb = require("nedb"); +import ldapjs = require("ldapjs"); +import u2f = require("u2f"); +import RedisSession = require("connect-redis"); +import Redis = require("redis"); + +export type Speakeasy = typeof speakeasy; +export type Winston = typeof winston; +export type Session = typeof session; +export type Nedb = typeof nedb; +export type Ldapjs = typeof ldapjs; +export type U2f = typeof u2f; +export type ConnectRedis = typeof RedisSession; +export type Redis = typeof Redis; + +export interface GlobalDependencies { + u2f: U2f; + ldapjs: Ldapjs; + session: Session; + Redis: Redis; + ConnectRedis: ConnectRedis; + winston: Winston; + speakeasy: Speakeasy; + nedb: Nedb; +} \ No newline at end of file diff --git a/themes/main/server/types/Identity.ts b/themes/main/server/types/Identity.ts new file mode 100644 index 000000000..e985984e2 --- /dev/null +++ b/themes/main/server/types/Identity.ts @@ -0,0 +1,6 @@ + + +export interface Identity { + userid: string; + email: string; +} \ No newline at end of file diff --git a/themes/main/server/types/TOTPSecret.ts b/themes/main/server/types/TOTPSecret.ts new file mode 100644 index 000000000..d6775f2f0 --- /dev/null +++ b/themes/main/server/types/TOTPSecret.ts @@ -0,0 +1,11 @@ + +export interface TOTPSecret { + ascii: string; + hex: string; + base32: string; + qr_code_ascii: string; + qr_code_hex: string; + qr_code_base32: string; + google_auth_qr: string; + otpauth_url: string; + } \ No newline at end of file diff --git a/themes/main/server/types/U2FRegistration.ts b/themes/main/server/types/U2FRegistration.ts new file mode 100644 index 000000000..b6080af07 --- /dev/null +++ b/themes/main/server/types/U2FRegistration.ts @@ -0,0 +1,5 @@ + +export interface U2FRegistration { + keyHandle: string; + publicKey: string; +} \ No newline at end of file diff --git a/themes/main/server/types/dovehash.d.ts b/themes/main/server/types/dovehash.d.ts new file mode 100644 index 000000000..c354609c0 --- /dev/null +++ b/themes/main/server/types/dovehash.d.ts @@ -0,0 +1,4 @@ + +declare module "dovehash" { + function encode(algo: string, text: string): string; +} \ No newline at end of file diff --git a/themes/main/server/types/speakeasy.d.ts b/themes/main/server/types/speakeasy.d.ts new file mode 100644 index 000000000..6ea06948b --- /dev/null +++ b/themes/main/server/types/speakeasy.d.ts @@ -0,0 +1,96 @@ +declare module "speakeasy" { + export = speakeasy + + interface SharedOptions { + encoding?: string + algorithm?: string + } + + interface DigestOptions extends SharedOptions { + secret: string + counter: number + } + + interface HOTPOptions extends SharedOptions { + secret: string + counter: number + digest?: Buffer + digits?: number + } + + interface HOTPVerifyOptions extends SharedOptions { + secret: string + token: string + counter: number + digits?: number + window?: number + } + + interface TOTPOptions extends SharedOptions { + secret: string + time?: number + step?: number + epoch?: number + counter?: number + digits?: number + } + + interface TOTPVerifyOptions extends SharedOptions { + secret: string + token: string + time?: number + step?: number + epoch?: number + counter?: number + digits?: number + window?: number + } + + interface GenerateSecretOptions { + length?: number + symbols?: boolean + otpauth_url?: boolean + name?: string + issuer?: string + } + + interface GeneratedSecret { + ascii: string + hex: string + base32: string + qr_code_ascii: string + qr_code_hex: string + qr_code_base32: string + google_auth_qr: string + otpauth_url: string + } + + interface OTPAuthURLOptions extends SharedOptions { + secret: string + label: string + type?: string + counter?: number + issuer?: string + digits?: number + period?: number + } + + interface Speakeasy { + digest: (options: DigestOptions) => Buffer + hotp: { + (options: HOTPOptions): string, + verifyDelta: (options: HOTPVerifyOptions) => boolean, + verify: (options: HOTPVerifyOptions) => boolean, + } + totp: { + (options: TOTPOptions): string + verifyDelta: (options: TOTPVerifyOptions) => boolean, + verify: (options: TOTPVerifyOptions) => boolean, + } + generateSecret: (options?: GenerateSecretOptions) => GeneratedSecret + generateSecretASCII: (length?: number, symbols?: boolean) => string + otpauthURL: (options: OTPAuthURLOptions) => string + } + + const speakeasy: Speakeasy +} \ No newline at end of file diff --git a/themes/matrix/client/src/css/.directory b/themes/matrix/client/src/css/.directory new file mode 100644 index 000000000..6e4b3f639 --- /dev/null +++ b/themes/matrix/client/src/css/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,56,41 +Version=3 +ViewMode=1 diff --git a/themes/matrix/client/src/css/00-bootstrap.min.css b/themes/matrix/client/src/css/00-bootstrap.min.css new file mode 100644 index 000000000..7ff40a282 --- /dev/null +++ b/themes/matrix/client/src/css/00-bootstrap.min.css @@ -0,0 +1,5770 @@ +/*! * Bootstrap v3.3.7 (http://getbootstrap.com) * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ +html{ + font-family:sans-serif; + -webkit-text-size-adjust:100%; + -ms-text-size-adjust:100% +} +body{ + margin:0; + height: 100%; + width: 100% +} +article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{ + display:block +} +audio,canvas,progress,video{ + display:inline-block; + vertical-align:baseline +} +audio:not([controls]){ + display:none; + height:0 +} +[hidden],template{ + display:none +} +a{ + background-color:transparent +} +a:active,a:hover{ + outline:0 +} +abbr[title]{ + border-bottom:1px dotted +} +b,strong{ + font-weight:700 +} +dfn{ + font-style:italic +} +h1{ + margin:.67em 0; + font-size:2em +} +mark{ + color:#000; + background:#ff0 +} +small{ + font-size:80% +} +sub,sup{ + position:relative; + font-size:75%; + line-height:0; + vertical-align:baseline +} +sup{ + top:-.5em +} +sub{ + bottom:-.25em +} +img{ + border:0 +} +svg:not(:root){ + overflow:hidden +} +figure{ + margin:1em 40px +} +hr{ + height:0; + -webkit-box-sizing:content-box; + -moz-box-sizing:content-box; + box-sizing:content-box +} +pre{ + overflow:auto +} +code,kbd,pre,samp{ + font-family:monospace,monospace; + font-size:1em +} +button,input,optgroup,select,textarea{ + margin:0; + font:inherit; + color:inherit +} +button{ + overflow:visible +} +button,select{ + text-transform:none +} +button,html input[type=button],input[type=reset],input[type=submit]{ + -webkit-appearance:button; + cursor:pointer +} +button[disabled],html input[disabled]{ + cursor:default +} +button::-moz-focus-inner,input::-moz-focus-inner{ + padding:0; + border:0 +} +input{ + line-height:normal +} +input[type=checkbox],input[type=radio]{ + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + padding:0 +} +input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{ + height:auto +} +input[type=search]{ + -webkit-box-sizing:content-box; + -moz-box-sizing:content-box; + box-sizing:content-box; + -webkit-appearance:textfield +} +input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{ + -webkit-appearance:none +} +fieldset{ + padding:.35em .625em .75em; + margin:0 2px; + border:1px solid silver +} +legend{ + padding:0; + border:0 +} +textarea{ + overflow:auto +} +optgroup{ + font-weight:700 +} +table{ + border-spacing:0; + border-collapse:collapse +} +td,th{ + padding:0 +} +/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ +@media print{ + *,:after,:before{ + color:#000!important; + text-shadow:none!important; + background:0 0!important; + -webkit-box-shadow:none!important; + box-shadow:none!important + } + a,a:visited{ + text-decoration:underline + } + a[href]:after{ + content:" (" attr(href) ")" + } + abbr[title]:after{ + content:" (" attr(title) ")" + } + a[href^="javascript:"]:after,a[href^="#"]:after{ + content:"" + } + blockquote,pre{ + border:1px solid #999; + page-break-inside:avoid + } + thead{ + display:table-header-group + } + img,tr{ + page-break-inside:avoid + } + img{ + max-width:100%!important + } + h2,h3,p{ + orphans:3; + widows:3 + } + h2,h3{ + page-break-after:avoid + } + .navbar{ + display:none + } + .btn>.caret,.dropup>.btn>.caret{ + border-top-color:#000!important + } + .label{ + border:1px solid #000 + } + .table{ + border-collapse:collapse!important + } + .table td,.table th{ + background-color:#fff!important + } + .table-bordered td,.table-bordered th{ + border:1px solid #ddd!important + } +} +@font-face{ + font-family:'Glyphicons Halflings'; + src:url(../fonts/glyphicons-halflings-regular.eot); + src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg') +} +.glyphicon{ + position:relative; + top:1px; + display:inline-block; + font-family:'Glyphicons Halflings'; + font-style:normal; + font-weight:400; + line-height:1; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale +} +.glyphicon-asterisk:before{ + content:"\002a" +} +.glyphicon-plus:before{ + content:"\002b" +} +.glyphicon-eur:before,.glyphicon-euro:before{ + content:"\20ac" +} +.glyphicon-minus:before{ + content:"\2212" +} +.glyphicon-cloud:before{ + content:"\2601" +} +.glyphicon-envelope:before{ + content:"\2709" +} +.glyphicon-pencil:before{ + content:"\270f" +} +.glyphicon-glass:before{ + content:"\e001" +} +.glyphicon-music:before{ + content:"\e002" +} +.glyphicon-search:before{ + content:"\e003" +} +.glyphicon-heart:before{ + content:"\e005" +} +.glyphicon-star:before{ + content:"\e006" +} +.glyphicon-star-empty:before{ + content:"\e007" +} +.glyphicon-user:before{ + content:"\e008" +} +.glyphicon-film:before{ + content:"\e009" +} +.glyphicon-th-large:before{ + content:"\e010" +} +.glyphicon-th:before{ + content:"\e011" +} +.glyphicon-th-list:before{ + content:"\e012" +} +.glyphicon-ok:before{ + content:"\e013" +} +.glyphicon-remove:before{ + content:"\e014" +} +.glyphicon-zoom-in:before{ + content:"\e015" +} +.glyphicon-zoom-out:before{ + content:"\e016" +} +.glyphicon-off:before{ + content:"\e017" +} +.glyphicon-signal:before{ + content:"\e018" +} +.glyphicon-cog:before{ + content:"\e019" +} +.glyphicon-trash:before{ + content:"\e020" +} +.glyphicon-home:before{ + content:"\e021" +} +.glyphicon-file:before{ + content:"\e022" +} +.glyphicon-time:before{ + content:"\e023" +} +.glyphicon-road:before{ + content:"\e024" +} +.glyphicon-download-alt:before{ + content:"\e025" +} +.glyphicon-download:before{ + content:"\e026" +} +.glyphicon-upload:before{ + content:"\e027" +} +.glyphicon-inbox:before{ + content:"\e028" +} +.glyphicon-play-circle:before{ + content:"\e029" +} +.glyphicon-repeat:before{ + content:"\e030" +} +.glyphicon-refresh:before{ + content:"\e031" +} +.glyphicon-list-alt:before{ + content:"\e032" +} +.glyphicon-lock:before{ + content:"\e033" +} +.glyphicon-flag:before{ + content:"\e034" +} +.glyphicon-headphones:before{ + content:"\e035" +} +.glyphicon-volume-off:before{ + content:"\e036" +} +.glyphicon-volume-down:before{ + content:"\e037" +} +.glyphicon-volume-up:before{ + content:"\e038" +} +.glyphicon-qrcode:before{ + content:"\e039" +} +.glyphicon-barcode:before{ + content:"\e040" +} +.glyphicon-tag:before{ + content:"\e041" +} +.glyphicon-tags:before{ + content:"\e042" +} +.glyphicon-book:before{ + content:"\e043" +} +.glyphicon-bookmark:before{ + content:"\e044" +} +.glyphicon-print:before{ + content:"\e045" +} +.glyphicon-camera:before{ + content:"\e046" +} +.glyphicon-font:before{ + content:"\e047" +} +.glyphicon-bold:before{ + content:"\e048" +} +.glyphicon-italic:before{ + content:"\e049" +} +.glyphicon-text-height:before{ + content:"\e050" +} +.glyphicon-text-width:before{ + content:"\e051" +} +.glyphicon-align-left:before{ + content:"\e052" +} +.glyphicon-align-center:before{ + content:"\e053" +} +.glyphicon-align-right:before{ + content:"\e054" +} +.glyphicon-align-justify:before{ + content:"\e055" +} +.glyphicon-list:before{ + content:"\e056" +} +.glyphicon-indent-left:before{ + content:"\e057" +} +.glyphicon-indent-right:before{ + content:"\e058" +} +.glyphicon-facetime-video:before{ + content:"\e059" +} +.glyphicon-picture:before{ + content:"\e060" +} +.glyphicon-map-marker:before{ + content:"\e062" +} +.glyphicon-adjust:before{ + content:"\e063" +} +.glyphicon-tint:before{ + content:"\e064" +} +.glyphicon-edit:before{ + content:"\e065" +} +.glyphicon-share:before{ + content:"\e066" +} +.glyphicon-check:before{ + content:"\e067" +} +.glyphicon-move:before{ + content:"\e068" +} +.glyphicon-step-backward:before{ + content:"\e069" +} +.glyphicon-fast-backward:before{ + content:"\e070" +} +.glyphicon-backward:before{ + content:"\e071" +} +.glyphicon-play:before{ + content:"\e072" +} +.glyphicon-pause:before{ + content:"\e073" +} +.glyphicon-stop:before{ + content:"\e074" +} +.glyphicon-forward:before{ + content:"\e075" +} +.glyphicon-fast-forward:before{ + content:"\e076" +} +.glyphicon-step-forward:before{ + content:"\e077" +} +.glyphicon-eject:before{ + content:"\e078" +} +.glyphicon-chevron-left:before{ + content:"\e079" +} +.glyphicon-chevron-right:before{ + content:"\e080" +} +.glyphicon-plus-sign:before{ + content:"\e081" +} +.glyphicon-minus-sign:before{ + content:"\e082" +} +.glyphicon-remove-sign:before{ + content:"\e083" +} +.glyphicon-ok-sign:before{ + content:"\e084" +} +.glyphicon-question-sign:before{ + content:"\e085" +} +.glyphicon-info-sign:before{ + content:"\e086" +} +.glyphicon-screenshot:before{ + content:"\e087" +} +.glyphicon-remove-circle:before{ + content:"\e088" +} +.glyphicon-ok-circle:before{ + content:"\e089" +} +.glyphicon-ban-circle:before{ + content:"\e090" +} +.glyphicon-arrow-left:before{ + content:"\e091" +} +.glyphicon-arrow-right:before{ + content:"\e092" +} +.glyphicon-arrow-up:before{ + content:"\e093" +} +.glyphicon-arrow-down:before{ + content:"\e094" +} +.glyphicon-share-alt:before{ + content:"\e095" +} +.glyphicon-resize-full:before{ + content:"\e096" +} +.glyphicon-resize-small:before{ + content:"\e097" +} +.glyphicon-exclamation-sign:before{ + content:"\e101" +} +.glyphicon-gift:before{ + content:"\e102" +} +.glyphicon-leaf:before{ + content:"\e103" +} +.glyphicon-fire:before{ + content:"\e104" +} +.glyphicon-eye-open:before{ + content:"\e105" +} +.glyphicon-eye-close:before{ + content:"\e106" +} +.glyphicon-warning-sign:before{ + content:"\e107" +} +.glyphicon-plane:before{ + content:"\e108" +} +.glyphicon-calendar:before{ + content:"\e109" +} +.glyphicon-random:before{ + content:"\e110" +} +.glyphicon-comment:before{ + content:"\e111" +} +.glyphicon-magnet:before{ + content:"\e112" +} +.glyphicon-chevron-up:before{ + content:"\e113" +} +.glyphicon-chevron-down:before{ + content:"\e114" +} +.glyphicon-retweet:before{ + content:"\e115" +} +.glyphicon-shopping-cart:before{ + content:"\e116" +} +.glyphicon-folder-close:before{ + content:"\e117" +} +.glyphicon-folder-open:before{ + content:"\e118" +} +.glyphicon-resize-vertical:before{ + content:"\e119" +} +.glyphicon-resize-horizontal:before{ + content:"\e120" +} +.glyphicon-hdd:before{ + content:"\e121" +} +.glyphicon-bullhorn:before{ + content:"\e122" +} +.glyphicon-bell:before{ + content:"\e123" +} +.glyphicon-certificate:before{ + content:"\e124" +} +.glyphicon-thumbs-up:before{ + content:"\e125" +} +.glyphicon-thumbs-down:before{ + content:"\e126" +} +.glyphicon-hand-right:before{ + content:"\e127" +} +.glyphicon-hand-left:before{ + content:"\e128" +} +.glyphicon-hand-up:before{ + content:"\e129" +} +.glyphicon-hand-down:before{ + content:"\e130" +} +.glyphicon-circle-arrow-right:before{ + content:"\e131" +} +.glyphicon-circle-arrow-left:before{ + content:"\e132" +} +.glyphicon-circle-arrow-up:before{ + content:"\e133" +} +.glyphicon-circle-arrow-down:before{ + content:"\e134" +} +.glyphicon-globe:before{ + content:"\e135" +} +.glyphicon-wrench:before{ + content:"\e136" +} +.glyphicon-tasks:before{ + content:"\e137" +} +.glyphicon-filter:before{ + content:"\e138" +} +.glyphicon-briefcase:before{ + content:"\e139" +} +.glyphicon-fullscreen:before{ + content:"\e140" +} +.glyphicon-dashboard:before{ + content:"\e141" +} +.glyphicon-paperclip:before{ + content:"\e142" +} +.glyphicon-heart-empty:before{ + content:"\e143" +} +.glyphicon-link:before{ + content:"\e144" +} +.glyphicon-phone:before{ + content:"\e145" +} +.glyphicon-pushpin:before{ + content:"\e146" +} +.glyphicon-usd:before{ + content:"\e148" +} +.glyphicon-gbp:before{ + content:"\e149" +} +.glyphicon-sort:before{ + content:"\e150" +} +.glyphicon-sort-by-alphabet:before{ + content:"\e151" +} +.glyphicon-sort-by-alphabet-alt:before{ + content:"\e152" +} +.glyphicon-sort-by-order:before{ + content:"\e153" +} +.glyphicon-sort-by-order-alt:before{ + content:"\e154" +} +.glyphicon-sort-by-attributes:before{ + content:"\e155" +} +.glyphicon-sort-by-attributes-alt:before{ + content:"\e156" +} +.glyphicon-unchecked:before{ + content:"\e157" +} +.glyphicon-expand:before{ + content:"\e158" +} +.glyphicon-collapse-down:before{ + content:"\e159" +} +.glyphicon-collapse-up:before{ + content:"\e160" +} +.glyphicon-log-in:before{ + content:"\e161" +} +.glyphicon-flash:before{ + content:"\e162" +} +.glyphicon-log-out:before{ + content:"\e163" +} +.glyphicon-new-window:before{ + content:"\e164" +} +.glyphicon-record:before{ + content:"\e165" +} +.glyphicon-save:before{ + content:"\e166" +} +.glyphicon-open:before{ + content:"\e167" +} +.glyphicon-saved:before{ + content:"\e168" +} +.glyphicon-import:before{ + content:"\e169" +} +.glyphicon-export:before{ + content:"\e170" +} +.glyphicon-send:before{ + content:"\e171" +} +.glyphicon-floppy-disk:before{ + content:"\e172" +} +.glyphicon-floppy-saved:before{ + content:"\e173" +} +.glyphicon-floppy-remove:before{ + content:"\e174" +} +.glyphicon-floppy-save:before{ + content:"\e175" +} +.glyphicon-floppy-open:before{ + content:"\e176" +} +.glyphicon-credit-card:before{ + content:"\e177" +} +.glyphicon-transfer:before{ + content:"\e178" +} +.glyphicon-cutlery:before{ + content:"\e179" +} +.glyphicon-header:before{ + content:"\e180" +} +.glyphicon-compressed:before{ + content:"\e181" +} +.glyphicon-earphone:before{ + content:"\e182" +} +.glyphicon-phone-alt:before{ + content:"\e183" +} +.glyphicon-tower:before{ + content:"\e184" +} +.glyphicon-stats:before{ + content:"\e185" +} +.glyphicon-sd-video:before{ + content:"\e186" +} +.glyphicon-hd-video:before{ + content:"\e187" +} +.glyphicon-subtitles:before{ + content:"\e188" +} +.glyphicon-sound-stereo:before{ + content:"\e189" +} +.glyphicon-sound-dolby:before{ + content:"\e190" +} +.glyphicon-sound-5-1:before{ + content:"\e191" +} +.glyphicon-sound-6-1:before{ + content:"\e192" +} +.glyphicon-sound-7-1:before{ + content:"\e193" +} +.glyphicon-copyright-mark:before{ + content:"\e194" +} +.glyphicon-registration-mark:before{ + content:"\e195" +} +.glyphicon-cloud-download:before{ + content:"\e197" +} +.glyphicon-cloud-upload:before{ + content:"\e198" +} +.glyphicon-tree-conifer:before{ + content:"\e199" +} +.glyphicon-tree-deciduous:before{ + content:"\e200" +} +.glyphicon-cd:before{ + content:"\e201" +} +.glyphicon-save-file:before{ + content:"\e202" +} +.glyphicon-open-file:before{ + content:"\e203" +} +.glyphicon-level-up:before{ + content:"\e204" +} +.glyphicon-copy:before{ + content:"\e205" +} +.glyphicon-paste:before{ + content:"\e206" +} +.glyphicon-alert:before{ + content:"\e209" +} +.glyphicon-equalizer:before{ + content:"\e210" +} +.glyphicon-king:before{ + content:"\e211" +} +.glyphicon-queen:before{ + content:"\e212" +} +.glyphicon-pawn:before{ + content:"\e213" +} +.glyphicon-bishop:before{ + content:"\e214" +} +.glyphicon-knight:before{ + content:"\e215" +} +.glyphicon-baby-formula:before{ + content:"\e216" +} +.glyphicon-tent:before{ + content:"\26fa" +} +.glyphicon-blackboard:before{ + content:"\e218" +} +.glyphicon-bed:before{ + content:"\e219" +} +.glyphicon-apple:before{ + content:"\f8ff" +} +.glyphicon-erase:before{ + content:"\e221" +} +.glyphicon-hourglass:before{ + content:"\231b" +} +.glyphicon-lamp:before{ + content:"\e223" +} +.glyphicon-duplicate:before{ + content:"\e224" +} +.glyphicon-piggy-bank:before{ + content:"\e225" +} +.glyphicon-scissors:before{ + content:"\e226" +} +.glyphicon-bitcoin:before{ + content:"\e227" +} +.glyphicon-btc:before{ + content:"\e227" +} +.glyphicon-xbt:before{ + content:"\e227" +} +.glyphicon-yen:before{ + content:"\00a5" +} +.glyphicon-jpy:before{ + content:"\00a5" +} +.glyphicon-ruble:before{ + content:"\20bd" +} +.glyphicon-rub:before{ + content:"\20bd" +} +.glyphicon-scale:before{ + content:"\e230" +} +.glyphicon-ice-lolly:before{ + content:"\e231" +} +.glyphicon-ice-lolly-tasted:before{ + content:"\e232" +} +.glyphicon-education:before{ + content:"\e233" +} +.glyphicon-option-horizontal:before{ + content:"\e234" +} +.glyphicon-option-vertical:before{ + content:"\e235" +} +.glyphicon-menu-hamburger:before{ + content:"\e236" +} +.glyphicon-modal-window:before{ + content:"\e237" +} +.glyphicon-oil:before{ + content:"\e238" +} +.glyphicon-grain:before{ + content:"\e239" +} +.glyphicon-sunglasses:before{ + content:"\e240" +} +.glyphicon-text-size:before{ + content:"\e241" +} +.glyphicon-text-color:before{ + content:"\e242" +} +.glyphicon-text-background:before{ + content:"\e243" +} +.glyphicon-object-align-top:before{ + content:"\e244" +} +.glyphicon-object-align-bottom:before{ + content:"\e245" +} +.glyphicon-object-align-horizontal:before{ + content:"\e246" +} +.glyphicon-object-align-left:before{ + content:"\e247" +} +.glyphicon-object-align-vertical:before{ + content:"\e248" +} +.glyphicon-object-align-right:before{ + content:"\e249" +} +.glyphicon-triangle-right:before{ + content:"\e250" +} +.glyphicon-triangle-left:before{ + content:"\e251" +} +.glyphicon-triangle-bottom:before{ + content:"\e252" +} +.glyphicon-triangle-top:before{ + content:"\e253" +} +.glyphicon-console:before{ + content:"\e254" +} +.glyphicon-superscript:before{ + content:"\e255" +} +.glyphicon-subscript:before{ + content:"\e256" +} +.glyphicon-menu-left:before{ + content:"\e257" +} +.glyphicon-menu-right:before{ + content:"\e258" +} +.glyphicon-menu-down:before{ + content:"\e259" +} +.glyphicon-menu-up:before{ + content:"\e260" +} +*{ + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box +} +:after,:before{ + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box +} +html{ + font-size:10px; + -webkit-tap-highlight-color:rgba(0,0,0,0) +} +body{ + font-family:"Helvetica Neue",Helvetica,Arial,sans-serif; + font-size:14px; + line-height:1.42857143; + color:#333; + background-color:#fff +} +button,input,select,textarea{ + font-family:inherit; + font-size:inherit; + line-height:inherit +} +a{ + color:#337ab7; + text-decoration:none +} +a:focus,a:hover{ + color:#23527c; + text-decoration:underline +} +a:focus{ + outline:5px auto -webkit-focus-ring-color; + outline-offset:-2px +} +figure{ + margin:0 +} +img{ + vertical-align:middle +} +.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{ + display:block; + max-width:100%; + height:auto +} +.img-rounded{ + border-radius:6px +} +.img-thumbnail{ + display:inline-block; + max-width:100%; + height:auto; + padding:4px; + line-height:1.42857143; + background-color:#fff; + border:1px solid #ddd; + border-radius:4px; + -webkit-transition:all .2s ease-in-out; + -o-transition:all .2s ease-in-out; + transition:all .2s ease-in-out +} +.img-circle{ + border-radius:50% +} +hr{ + margin-top:20px; + margin-bottom:20px; + border:0; + border-top:1px solid #eee +} +.sr-only{ + position:absolute; + width:1px; + height:1px; + padding:0; + margin:-1px; + overflow:hidden; + clip:rect(0,0,0,0); + border:0 +} +.sr-only-focusable:active,.sr-only-focusable:focus{ + position:static; + width:auto; + height:auto; + margin:0; + overflow:visible; + clip:auto +} +[role=button]{ + cursor:pointer +} +.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{ + font-family:inherit; + font-weight:500; + line-height:1.1; + color:inherit +} +.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{ + font-weight:400; + line-height:1; + color:#777 +} +.h1,.h2,.h3,h1,h2,h3{ + margin-top:20px; + margin-bottom:10px +} +.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{ + font-size:65% +} +.h4,.h5,.h6,h4,h5,h6{ + margin-top:10px; + margin-bottom:10px +} +.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{ + font-size:75% +} +.h1,h1{ + font-size:36px +} +.h2,h2{ + font-size:30px +} +.h3,h3{ + font-size:24px +} +.h4,h4{ + font-size:18px +} +.h5,h5{ + font-size:14px +} +.h6,h6{ + font-size:12px +} +p{ + margin:0 0 10px +} +.lead{ + margin-bottom:20px; + font-size:16px; + font-weight:300; + line-height:1.4 +} +@media (min-width:768px){ + .lead{ + font-size:21px + } +} +.small,small{ + font-size:85% +} +.mark,mark{ + padding:.2em; + background-color:#fcf8e3 +} +.text-left{ + text-align:left +} +.text-right{ + text-align:right +} +.text-center{ + text-align:center +} +.text-justify{ + text-align:justify +} +.text-nowrap{ + white-space:nowrap +} +.text-lowercase{ + text-transform:lowercase +} +.text-uppercase{ + text-transform:uppercase +} +.text-capitalize{ + text-transform:capitalize +} +.text-muted{ + color:#777 +} +.text-primary{ + color:#337ab7 +} +a.text-primary:focus,a.text-primary:hover{ + color:#286090 +} +.text-success{ + color:#3c763d +} +a.text-success:focus,a.text-success:hover{ + color:#2b542c +} +.text-info{ + color:#31708f +} +a.text-info:focus,a.text-info:hover{ + color:#245269 +} +.text-warning{ + color:#8a6d3b +} +a.text-warning:focus,a.text-warning:hover{ + color:#66512c +} +.text-danger{ + color:#a94442 +} +a.text-danger:focus,a.text-danger:hover{ + color:#843534 +} +.bg-primary{ + color:#fff; + background-color:#337ab7 +} +a.bg-primary:focus,a.bg-primary:hover{ + background-color:#286090 +} +.bg-success{ + background-color:#dff0d8 +} +a.bg-success:focus,a.bg-success:hover{ + background-color:#c1e2b3 +} +.bg-info{ + background-color:#d9edf7 +} +a.bg-info:focus,a.bg-info:hover{ + background-color:#afd9ee +} +.bg-warning{ + background-color:#fcf8e3 +} +a.bg-warning:focus,a.bg-warning:hover{ + background-color:#f7ecb5 +} +.bg-danger{ + background-color:#f2dede +} +a.bg-danger:focus,a.bg-danger:hover{ + background-color:#e4b9b9 +} +.page-header{ + padding-bottom:9px; + margin:40px 0 20px; + border-bottom:1px solid #eee +} +ol,ul{ + margin-top:0; + margin-bottom:10px +} +ol ol,ol ul,ul ol,ul ul{ + margin-bottom:0 +} +.list-unstyled{ + padding-left:0; + list-style:none +} +.list-inline{ + padding-left:0; + margin-left:-5px; + list-style:none +} +.list-inline>li{ + display:inline-block; + padding-right:5px; + padding-left:5px +} +dl{ + margin-top:0; + margin-bottom:20px +} +dd,dt{ + line-height:1.42857143 +} +dt{ + font-weight:700 +} +dd{ + margin-left:0 +} +@media (min-width:768px){ + .dl-horizontal dt{ + float:left; + width:160px; + overflow:hidden; + clear:left; + text-align:right; + text-overflow:ellipsis; + white-space:nowrap + } + .dl-horizontal dd{ + margin-left:180px + } +} +abbr[data-original-title],abbr[title]{ + cursor:help; + border-bottom:1px dotted #777 +} +.initialism{ + font-size:90%; + text-transform:uppercase +} +blockquote{ + padding:10px 20px; + margin:0 0 20px; + font-size:17.5px; + border-left:5px solid #eee +} +blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{ + margin-bottom:0 +} +blockquote .small,blockquote footer,blockquote small{ + display:block; + font-size:80%; + line-height:1.42857143; + color:#777 +} +blockquote .small:before,blockquote footer:before,blockquote small:before{ + content:'\2014 \00A0' +} +.blockquote-reverse,blockquote.pull-right{ + padding-right:15px; + padding-left:0; + text-align:right; + border-right:5px solid #eee; + border-left:0 +} +.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{ + content:'' +} +.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{ + content:'\00A0 \2014' +} +address{ + margin-bottom:20px; + font-style:normal; + line-height:1.42857143 +} +code,kbd,pre,samp{ + font-family:Menlo,Monaco,Consolas,"Courier New",monospace +} +code{ + padding:2px 4px; + font-size:90%; + color:#c7254e; + background-color:#f9f2f4; + border-radius:4px +} +kbd{ + padding:2px 4px; + font-size:90%; + color:#fff; + background-color:#333; + border-radius:3px; + -webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25); + box-shadow:inset 0 -1px 0 rgba(0,0,0,.25) +} +kbd kbd{ + padding:0; + font-size:100%; + font-weight:700; + -webkit-box-shadow:none; + box-shadow:none +} +pre{ + display:block; + padding:9.5px; + margin:0 0 10px; + font-size:13px; + line-height:1.42857143; + color:#333; + word-break:break-all; + word-wrap:break-word; + background-color:#f5f5f5; + border:1px solid #ccc; + border-radius:4px +} +pre code{ + padding:0; + font-size:inherit; + color:inherit; + white-space:pre-wrap; + background-color:transparent; + border-radius:0 +} +.pre-scrollable{ + max-height:340px; + overflow-y:scroll +} +.container{ + padding-right:15px; + padding-left:15px; + margin-right:auto; + margin-left:auto +} +@media (min-width:768px){ + .container{ + width:750px + } +} +@media (min-width:992px){ + .container{ + width:970px + } +} +@media (min-width:1200px){ + .container{ + width:1170px + } +} +.container-fluid{ + padding-right:15px; + padding-left:15px; + margin-right:auto; + margin-left:auto +} +.row{ + margin-right:-15px; + margin-left:-15px +} +.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{ + position:relative; + min-height:1px; + padding-right:15px; + padding-left:15px +} +.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{ + float:left +} +.col-xs-12{ + width:100% +} +.col-xs-11{ + width:91.66666667% +} +.col-xs-10{ + width:83.33333333% +} +.col-xs-9{ + width:75% +} +.col-xs-8{ + width:66.66666667% +} +.col-xs-7{ + width:58.33333333% +} +.col-xs-6{ + width:50% +} +.col-xs-5{ + width:41.66666667% +} +.col-xs-4{ + width:33.33333333% +} +.col-xs-3{ + width:25% +} +.col-xs-2{ + width:16.66666667% +} +.col-xs-1{ + width:8.33333333% +} +.col-xs-pull-12{ + right:100% +} +.col-xs-pull-11{ + right:91.66666667% +} +.col-xs-pull-10{ + right:83.33333333% +} +.col-xs-pull-9{ + right:75% +} +.col-xs-pull-8{ + right:66.66666667% +} +.col-xs-pull-7{ + right:58.33333333% +} +.col-xs-pull-6{ + right:50% +} +.col-xs-pull-5{ + right:41.66666667% +} +.col-xs-pull-4{ + right:33.33333333% +} +.col-xs-pull-3{ + right:25% +} +.col-xs-pull-2{ + right:16.66666667% +} +.col-xs-pull-1{ + right:8.33333333% +} +.col-xs-pull-0{ + right:auto +} +.col-xs-push-12{ + left:100% +} +.col-xs-push-11{ + left:91.66666667% +} +.col-xs-push-10{ + left:83.33333333% +} +.col-xs-push-9{ + left:75% +} +.col-xs-push-8{ + left:66.66666667% +} +.col-xs-push-7{ + left:58.33333333% +} +.col-xs-push-6{ + left:50% +} +.col-xs-push-5{ + left:41.66666667% +} +.col-xs-push-4{ + left:33.33333333% +} +.col-xs-push-3{ + left:25% +} +.col-xs-push-2{ + left:16.66666667% +} +.col-xs-push-1{ + left:8.33333333% +} +.col-xs-push-0{ + left:auto +} +.col-xs-offset-12{ + margin-left:100% +} +.col-xs-offset-11{ + margin-left:91.66666667% +} +.col-xs-offset-10{ + margin-left:83.33333333% +} +.col-xs-offset-9{ + margin-left:75% +} +.col-xs-offset-8{ + margin-left:66.66666667% +} +.col-xs-offset-7{ + margin-left:58.33333333% +} +.col-xs-offset-6{ + margin-left:50% +} +.col-xs-offset-5{ + margin-left:41.66666667% +} +.col-xs-offset-4{ + margin-left:33.33333333% +} +.col-xs-offset-3{ + margin-left:25% +} +.col-xs-offset-2{ + margin-left:16.66666667% +} +.col-xs-offset-1{ + margin-left:8.33333333% +} +.col-xs-offset-0{ + margin-left:0 +} +@media (min-width:768px){ + .col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{ + float:left + } + .col-sm-12{ + width:100% + } + .col-sm-11{ + width:91.66666667% + } + .col-sm-10{ + width:83.33333333% + } + .col-sm-9{ + width:75% + } + .col-sm-8{ + width:66.66666667% + } + .col-sm-7{ + width:58.33333333% + } + .col-sm-6{ + width:50% + } + .col-sm-5{ + width:41.66666667% + } + .col-sm-4{ + width:33.33333333% + } + .col-sm-3{ + width:25% + } + .col-sm-2{ + width:16.66666667% + } + .col-sm-1{ + width:8.33333333% + } + .col-sm-pull-12{ + right:100% + } + .col-sm-pull-11{ + right:91.66666667% + } + .col-sm-pull-10{ + right:83.33333333% + } + .col-sm-pull-9{ + right:75% + } + .col-sm-pull-8{ + right:66.66666667% + } + .col-sm-pull-7{ + right:58.33333333% + } + .col-sm-pull-6{ + right:50% + } + .col-sm-pull-5{ + right:41.66666667% + } + .col-sm-pull-4{ + right:33.33333333% + } + .col-sm-pull-3{ + right:25% + } + .col-sm-pull-2{ + right:16.66666667% + } + .col-sm-pull-1{ + right:8.33333333% + } + .col-sm-pull-0{ + right:auto + } + .col-sm-push-12{ + left:100% + } + .col-sm-push-11{ + left:91.66666667% + } + .col-sm-push-10{ + left:83.33333333% + } + .col-sm-push-9{ + left:75% + } + .col-sm-push-8{ + left:66.66666667% + } + .col-sm-push-7{ + left:58.33333333% + } + .col-sm-push-6{ + left:50% + } + .col-sm-push-5{ + left:41.66666667% + } + .col-sm-push-4{ + left:33.33333333% + } + .col-sm-push-3{ + left:25% + } + .col-sm-push-2{ + left:16.66666667% + } + .col-sm-push-1{ + left:8.33333333% + } + .col-sm-push-0{ + left:auto + } + .col-sm-offset-12{ + margin-left:100% + } + .col-sm-offset-11{ + margin-left:91.66666667% + } + .col-sm-offset-10{ + margin-left:83.33333333% + } + .col-sm-offset-9{ + margin-left:75% + } + .col-sm-offset-8{ + margin-left:66.66666667% + } + .col-sm-offset-7{ + margin-left:58.33333333% + } + .col-sm-offset-6{ + margin-left:50% + } + .col-sm-offset-5{ + margin-left:41.66666667% + } + .col-sm-offset-4{ + margin-left:33.33333333% + } + .col-sm-offset-3{ + margin-left:25% + } + .col-sm-offset-2{ + margin-left:16.66666667% + } + .col-sm-offset-1{ + margin-left:8.33333333% + } + .col-sm-offset-0{ + margin-left:0 + } +} +@media (min-width:992px){ + .col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{ + float:left + } + .col-md-12{ + width:100% + } + .col-md-11{ + width:91.66666667% + } + .col-md-10{ + width:83.33333333% + } + .col-md-9{ + width:75% + } + .col-md-8{ + width:66.66666667% + } + .col-md-7{ + width:58.33333333% + } + .col-md-6{ + width:50% + } + .col-md-5{ + width:41.66666667% + } + .col-md-4{ + width:33.33333333% + } + .col-md-3{ + width:25% + } + .col-md-2{ + width:16.66666667% + } + .col-md-1{ + width:8.33333333% + } + .col-md-pull-12{ + right:100% + } + .col-md-pull-11{ + right:91.66666667% + } + .col-md-pull-10{ + right:83.33333333% + } + .col-md-pull-9{ + right:75% + } + .col-md-pull-8{ + right:66.66666667% + } + .col-md-pull-7{ + right:58.33333333% + } + .col-md-pull-6{ + right:50% + } + .col-md-pull-5{ + right:41.66666667% + } + .col-md-pull-4{ + right:33.33333333% + } + .col-md-pull-3{ + right:25% + } + .col-md-pull-2{ + right:16.66666667% + } + .col-md-pull-1{ + right:8.33333333% + } + .col-md-pull-0{ + right:auto + } + .col-md-push-12{ + left:100% + } + .col-md-push-11{ + left:91.66666667% + } + .col-md-push-10{ + left:83.33333333% + } + .col-md-push-9{ + left:75% + } + .col-md-push-8{ + left:66.66666667% + } + .col-md-push-7{ + left:58.33333333% + } + .col-md-push-6{ + left:50% + } + .col-md-push-5{ + left:41.66666667% + } + .col-md-push-4{ + left:33.33333333% + } + .col-md-push-3{ + left:25% + } + .col-md-push-2{ + left:16.66666667% + } + .col-md-push-1{ + left:8.33333333% + } + .col-md-push-0{ + left:auto + } + .col-md-offset-12{ + margin-left:100% + } + .col-md-offset-11{ + margin-left:91.66666667% + } + .col-md-offset-10{ + margin-left:83.33333333% + } + .col-md-offset-9{ + margin-left:75% + } + .col-md-offset-8{ + margin-left:66.66666667% + } + .col-md-offset-7{ + margin-left:58.33333333% + } + .col-md-offset-6{ + margin-left:50% + } + .col-md-offset-5{ + margin-left:41.66666667% + } + .col-md-offset-4{ + margin-left:33.33333333% + } + .col-md-offset-3{ + margin-left:25% + } + .col-md-offset-2{ + margin-left:16.66666667% + } + .col-md-offset-1{ + margin-left:8.33333333% + } + .col-md-offset-0{ + margin-left:0 + } +} +@media (min-width:1200px){ + .col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{ + float:left + } + .col-lg-12{ + width:100% + } + .col-lg-11{ + width:91.66666667% + } + .col-lg-10{ + width:83.33333333% + } + .col-lg-9{ + width:75% + } + .col-lg-8{ + width:66.66666667% + } + .col-lg-7{ + width:58.33333333% + } + .col-lg-6{ + width:50% + } + .col-lg-5{ + width:41.66666667% + } + .col-lg-4{ + width:33.33333333% + } + .col-lg-3{ + width:25% + } + .col-lg-2{ + width:16.66666667% + } + .col-lg-1{ + width:8.33333333% + } + .col-lg-pull-12{ + right:100% + } + .col-lg-pull-11{ + right:91.66666667% + } + .col-lg-pull-10{ + right:83.33333333% + } + .col-lg-pull-9{ + right:75% + } + .col-lg-pull-8{ + right:66.66666667% + } + .col-lg-pull-7{ + right:58.33333333% + } + .col-lg-pull-6{ + right:50% + } + .col-lg-pull-5{ + right:41.66666667% + } + .col-lg-pull-4{ + right:33.33333333% + } + .col-lg-pull-3{ + right:25% + } + .col-lg-pull-2{ + right:16.66666667% + } + .col-lg-pull-1{ + right:8.33333333% + } + .col-lg-pull-0{ + right:auto + } + .col-lg-push-12{ + left:100% + } + .col-lg-push-11{ + left:91.66666667% + } + .col-lg-push-10{ + left:83.33333333% + } + .col-lg-push-9{ + left:75% + } + .col-lg-push-8{ + left:66.66666667% + } + .col-lg-push-7{ + left:58.33333333% + } + .col-lg-push-6{ + left:50% + } + .col-lg-push-5{ + left:41.66666667% + } + .col-lg-push-4{ + left:33.33333333% + } + .col-lg-push-3{ + left:25% + } + .col-lg-push-2{ + left:16.66666667% + } + .col-lg-push-1{ + left:8.33333333% + } + .col-lg-push-0{ + left:auto + } + .col-lg-offset-12{ + margin-left:100% + } + .col-lg-offset-11{ + margin-left:91.66666667% + } + .col-lg-offset-10{ + margin-left:83.33333333% + } + .col-lg-offset-9{ + margin-left:75% + } + .col-lg-offset-8{ + margin-left:66.66666667% + } + .col-lg-offset-7{ + margin-left:58.33333333% + } + .col-lg-offset-6{ + margin-left:50% + } + .col-lg-offset-5{ + margin-left:41.66666667% + } + .col-lg-offset-4{ + margin-left:33.33333333% + } + .col-lg-offset-3{ + margin-left:25% + } + .col-lg-offset-2{ + margin-left:16.66666667% + } + .col-lg-offset-1{ + margin-left:8.33333333% + } + .col-lg-offset-0{ + margin-left:0 + } +} +table{ + background-color:transparent +} +caption{ + padding-top:8px; + padding-bottom:8px; + color:#777; + text-align:left +} +th{ + text-align:left +} +.table{ + width:100%; + max-width:100%; + margin-bottom:20px +} +.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{ + padding:8px; + line-height:1.42857143; + vertical-align:top; + border-top:1px solid #ddd +} +.table>thead>tr>th{ + vertical-align:bottom; + border-bottom:2px solid #ddd +} +.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{ + border-top:0 +} +.table>tbody+tbody{ + border-top:2px solid #ddd +} +.table .table{ + background-color:#fff +} +.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{ + padding:5px +} +.table-bordered{ + border:1px solid #ddd +} +.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{ + border:1px solid #ddd +} +.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{ + border-bottom-width:2px +} +.table-striped>tbody>tr:nth-of-type(odd){ + background-color:#f9f9f9 +} +.table-hover>tbody>tr:hover{ + background-color:#f5f5f5 +} +table col[class*=col-]{ + position:static; + display:table-column; + float:none +} +table td[class*=col-],table th[class*=col-]{ + position:static; + display:table-cell; + float:none +} +.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{ + background-color:#f5f5f5 +} +.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{ + background-color:#e8e8e8 +} +.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{ + background-color:#dff0d8 +} +.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{ + background-color:#d0e9c6 +} +.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{ + background-color:#d9edf7 +} +.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{ + background-color:#c4e3f3 +} +.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{ + background-color:#fcf8e3 +} +.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{ + background-color:#faf2cc +} +.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{ + background-color:#f2dede +} +.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{ + background-color:#ebcccc +} +.table-responsive{ + min-height:.01%; + overflow-x:auto +} +@media screen and (max-width:767px){ + .table-responsive{ + width:100%; + margin-bottom:15px; + overflow-y:hidden; + -ms-overflow-style:-ms-autohiding-scrollbar; + border:1px solid #ddd + } + .table-responsive>.table{ + margin-bottom:0 + } + .table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{ + white-space:nowrap + } + .table-responsive>.table-bordered{ + border:0 + } + .table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{ + border-left:0 + } + .table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{ + border-right:0 + } + .table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{ + border-bottom:0 + } +} +fieldset{ + min-width:0; + padding:0; + margin:0; + border:0 +} +legend{ + display:block; + width:100%; + padding:0; + margin-bottom:20px; + font-size:21px; + line-height:inherit; + color:#333; + border:0; + border-bottom:1px solid #e5e5e5 +} +label{ + display:inline-block; + max-width:100%; + margin-bottom:5px; + font-weight:700 +} +input[type=search]{ + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box +} +input[type=checkbox],input[type=radio]{ + margin:4px 0 0; + margin-top:1px\9; + line-height:normal +} +input[type=file]{ + display:block +} +input[type=range]{ + display:block; + width:100% +} +select[multiple],select[size]{ + height:auto +} +input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{ + outline:5px auto -webkit-focus-ring-color; + outline-offset:-2px +} +output{ + display:block; + padding-top:7px; + font-size:14px; + line-height:1.42857143; + color:#555 +} +.form-control{ + display:block; + width:100%; + height:34px; + padding:6px 12px; + font-size:14px; + line-height:1.42857143; + color:#555; + background-color:#fff; + background-image:none; + border:1px solid #ccc; + border-radius:4px; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075); + box-shadow:inset 0 1px 1px rgba(0,0,0,.075); + -webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s; + -o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s; + transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s +} +.form-control:focus{ + border-color:#66afe9; + outline:0; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6); + box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6) +} +.form-control::-moz-placeholder{ + color:#999; + opacity:1 +} +.form-control:-ms-input-placeholder{ + color:#999 +} +.form-control::-webkit-input-placeholder{ + color:#999 +} +.form-control::-ms-expand{ + background-color:transparent; + border:0 +} +.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{ + background-color:#eee; + opacity:1 +} +.form-control[disabled],fieldset[disabled] .form-control{ + cursor:not-allowed +} +textarea.form-control{ + height:auto +} +input[type=search]{ + -webkit-appearance:none +} +@media screen and (-webkit-min-device-pixel-ratio:0){ + input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{ + line-height:34px + } + .input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{ + line-height:30px + } + .input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{ + line-height:46px + } +} +.form-group{ + margin-bottom:15px +} +.checkbox,.radio{ + position:relative; + display:block; + margin-top:10px; + margin-bottom:10px +} +.checkbox label,.radio label{ + min-height:20px; + padding-left:20px; + margin-bottom:0; + font-weight:400; + cursor:pointer +} +.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{ + position:absolute; + margin-top:4px\9; + margin-left:-20px +} +.checkbox+.checkbox,.radio+.radio{ + margin-top:-5px +} +.checkbox-inline,.radio-inline{ + position:relative; + display:inline-block; + padding-left:20px; + margin-bottom:0; + font-weight:400; + vertical-align:middle; + cursor:pointer +} +.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{ + margin-top:0; + margin-left:10px +} +fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{ + cursor:not-allowed +} +.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{ + cursor:not-allowed +} +.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{ + cursor:not-allowed +} +.form-control-static{ + min-height:34px; + padding-top:7px; + padding-bottom:7px; + margin-bottom:0 +} +.form-control-static.input-lg,.form-control-static.input-sm{ + padding-right:0; + padding-left:0 +} +.input-sm{ + height:30px; + padding:5px 10px; + font-size:12px; + line-height:1.5; + border-radius:3px +} +select.input-sm{ + height:30px; + line-height:30px +} +select[multiple].input-sm,textarea.input-sm{ + height:auto +} +.form-group-sm .form-control{ + height:30px; + padding:5px 10px; + font-size:12px; + line-height:1.5; + border-radius:3px +} +.form-group-sm select.form-control{ + height:30px; + line-height:30px +} +.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{ + height:auto +} +.form-group-sm .form-control-static{ + height:30px; + min-height:32px; + padding:6px 10px; + font-size:12px; + line-height:1.5 +} +.input-lg{ + height:46px; + padding:10px 16px; + font-size:18px; + line-height:1.3333333; + border-radius:6px +} +select.input-lg{ + height:46px; + line-height:46px +} +select[multiple].input-lg,textarea.input-lg{ + height:auto +} +.form-group-lg .form-control{ + height:46px; + padding:10px 16px; + font-size:18px; + line-height:1.3333333; + border-radius:6px +} +.form-group-lg select.form-control{ + height:46px; + line-height:46px +} +.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{ + height:auto +} +.form-group-lg .form-control-static{ + height:46px; + min-height:38px; + padding:11px 16px; + font-size:18px; + line-height:1.3333333 +} +.has-feedback{ + position:relative +} +.has-feedback .form-control{ + padding-right:42.5px +} +.form-control-feedback{ + position:absolute; + top:0; + right:0; + z-index:2; + display:block; + width:34px; + height:34px; + line-height:34px; + text-align:center; + pointer-events:none +} +.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{ + width:46px; + height:46px; + line-height:46px +} +.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{ + width:30px; + height:30px; + line-height:30px +} +.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{ + color:#3c763d +} +.has-success .form-control{ + border-color:#3c763d; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075); + box-shadow:inset 0 1px 1px rgba(0,0,0,.075) +} +.has-success .form-control:focus{ + border-color:#2b542c; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168; + box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168 +} +.has-success .input-group-addon{ + color:#3c763d; + background-color:#dff0d8; + border-color:#3c763d +} +.has-success .form-control-feedback{ + color:#3c763d +} +.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{ + color:#8a6d3b +} +.has-warning .form-control{ + border-color:#8a6d3b; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075); + box-shadow:inset 0 1px 1px rgba(0,0,0,.075) +} +.has-warning .form-control:focus{ + border-color:#66512c; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b; + box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b +} +.has-warning .input-group-addon{ + color:#8a6d3b; + background-color:#fcf8e3; + border-color:#8a6d3b +} +.has-warning .form-control-feedback{ + color:#8a6d3b +} +.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{ + color:#a94442 +} +.has-error .form-control{ + border-color:#a94442; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075); + box-shadow:inset 0 1px 1px rgba(0,0,0,.075) +} +.has-error .form-control:focus{ + border-color:#843534; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483; + box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483 +} +.has-error .input-group-addon{ + color:#a94442; + background-color:#f2dede; + border-color:#a94442 +} +.has-error .form-control-feedback{ + color:#a94442 +} +.has-feedback label~.form-control-feedback{ + top:25px +} +.has-feedback label.sr-only~.form-control-feedback{ + top:0 +} +.help-block{ + display:block; + margin-top:5px; + margin-bottom:10px; + color:#737373 +} +@media (min-width:768px){ + .form-inline .form-group{ + display:inline-block; + margin-bottom:0; + vertical-align:middle + } + .form-inline .form-control{ + display:inline-block; + width:auto; + vertical-align:middle + } + .form-inline .form-control-static{ + display:inline-block + } + .form-inline .input-group{ + display:inline-table; + vertical-align:middle + } + .form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{ + width:auto + } + .form-inline .input-group>.form-control{ + width:100% + } + .form-inline .control-label{ + margin-bottom:0; + vertical-align:middle + } + .form-inline .checkbox,.form-inline .radio{ + display:inline-block; + margin-top:0; + margin-bottom:0; + vertical-align:middle + } + .form-inline .checkbox label,.form-inline .radio label{ + padding-left:0 + } + .form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{ + position:relative; + margin-left:0 + } + .form-inline .has-feedback .form-control-feedback{ + top:0 + } +} +.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{ + padding-top:7px; + margin-top:0; + margin-bottom:0 +} +.form-horizontal .checkbox,.form-horizontal .radio{ + min-height:27px +} +.form-horizontal .form-group{ + margin-right:-15px; + margin-left:-15px +} +@media (min-width:768px){ + .form-horizontal .control-label{ + padding-top:7px; + margin-bottom:0; + text-align:right + } +} +.form-horizontal .has-feedback .form-control-feedback{ + right:15px +} +@media (min-width:768px){ + .form-horizontal .form-group-lg .control-label{ + padding-top:11px; + font-size:18px + } +} +@media (min-width:768px){ + .form-horizontal .form-group-sm .control-label{ + padding-top:6px; + font-size:12px + } +} +.btn{ + display:inline-block; + padding:6px 12px; + margin-bottom:0; + font-size:14px; + font-weight:400; + line-height:1.42857143; + text-align:center; + white-space:nowrap; + vertical-align:middle; + -ms-touch-action:manipulation; + touch-action:manipulation; + cursor:pointer; + -webkit-user-select:none; + -moz-user-select:none; + -ms-user-select:none; + user-select:none; + background-image:none; + border:1px solid transparent; + border-radius:4px +} +.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{ + outline:5px auto -webkit-focus-ring-color; + outline-offset:-2px +} +.btn.focus,.btn:focus,.btn:hover{ + color:#333; + text-decoration:none +} +.btn.active,.btn:active{ + background-image:none; + outline:0; + -webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125); + box-shadow:inset 0 3px 5px rgba(0,0,0,.125) +} +.btn.disabled,.btn[disabled],fieldset[disabled] .btn{ + cursor:not-allowed; + filter:alpha(opacity=65); + -webkit-box-shadow:none; + box-shadow:none; + opacity:.65 +} +a.btn.disabled,fieldset[disabled] a.btn{ + pointer-events:none +} +.btn-default{ + color:#333; + background-color:#fff; + border-color:#ccc +} +.btn-default.focus,.btn-default:focus{ + color:#333; + background-color:#e6e6e6; + border-color:#8c8c8c +} +.btn-default:hover{ + color:#333; + background-color:#e6e6e6; + border-color:#adadad +} +.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{ + color:#333; + background-color:#e6e6e6; + border-color:#adadad +} +.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{ + color:#333; + background-color:#d4d4d4; + border-color:#8c8c8c +} +.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{ + background-image:none +} +.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{ + background-color:#fff; + border-color:#ccc +} +.btn-default .badge{ + color:#fff; + background-color:#333 +} +.btn-primary{ + color:#fff; + background-color:#03b703; + border-color:#009100 +} +.btn-primary.focus,.btn-primary:focus{ + color:#fff; + background-color:#067906; + border-color:#009100 +} +.btn-primary:hover{ + color:#fff; + background-color:#067906; + border-color:#009100 +} +.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{ + color:#fff; + background-color:#067906; + border-color:#009100 +} +.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{ + color:#fff; + background-color:#067906; + border-color:#009100 +} +.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{ + background-image:none +} +.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{ + background-color:#067906; + border-color:#009100 +} +.btn-primary .badge{ + color:#337ab7; + background-color:#fff +} +.btn-success{ + color:#fff; + background-color:#5cb85c; + border-color:#4cae4c +} +.btn-success.focus,.btn-success:focus{ + color:#fff; + background-color:#449d44; + border-color:#255625 +} +.btn-success:hover{ + color:#fff; + background-color:#449d44; + border-color:#398439 +} +.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{ + color:#fff; + background-color:#449d44; + border-color:#398439 +} +.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{ + color:#fff; + background-color:#398439; + border-color:#255625 +} +.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{ + background-image:none +} +.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{ + background-color:#5cb85c; + border-color:#4cae4c +} +.btn-success .badge{ + color:#5cb85c; + background-color:#fff +} +.btn-info{ + color:#fff; + background-color:#5bc0de; + border-color:#46b8da +} +.btn-info.focus,.btn-info:focus{ + color:#fff; + background-color:#31b0d5; + border-color:#1b6d85 +} +.btn-info:hover{ + color:#fff; + background-color:#31b0d5; + border-color:#269abc +} +.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{ + color:#fff; + background-color:#31b0d5; + border-color:#269abc +} +.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{ + color:#fff; + background-color:#269abc; + border-color:#1b6d85 +} +.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{ + background-image:none +} +.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{ + background-color:#5bc0de; + border-color:#46b8da +} +.btn-info .badge{ + color:#5bc0de; + background-color:#fff +} +.btn-warning{ + color:#fff; + background-color:#f0ad4e; + border-color:#eea236 +} +.btn-warning.focus,.btn-warning:focus{ + color:#fff; + background-color:#ec971f; + border-color:#985f0d +} +.btn-warning:hover{ + color:#fff; + background-color:#ec971f; + border-color:#d58512 +} +.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{ + color:#fff; + background-color:#ec971f; + border-color:#d58512 +} +.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{ + color:#fff; + background-color:#d58512; + border-color:#985f0d +} +.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{ + background-image:none +} +.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{ + background-color:#f0ad4e; + border-color:#eea236 +} +.btn-warning .badge{ + color:#f0ad4e; + background-color:#fff +} +.btn-danger{ + color:#fff; + background-color:#d9534f; + border-color:#d43f3a +} +.btn-danger.focus,.btn-danger:focus{ + color:#fff; + background-color:#c9302c; + border-color:#761c19 +} +.btn-danger:hover{ + color:#fff; + background-color:#c9302c; + border-color:#ac2925 +} +.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{ + color:#fff; + background-color:#c9302c; + border-color:#ac2925 +} +.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{ + color:#fff; + background-color:#ac2925; + border-color:#761c19 +} +.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{ + background-image:none +} +.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{ + background-color:#d9534f; + border-color:#d43f3a +} +.btn-danger .badge{ + color:#d9534f; + background-color:#fff +} +.btn-link{ + font-weight:400; + color:#337ab7; + border-radius:0 +} +.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{ + background-color:transparent; + -webkit-box-shadow:none; + box-shadow:none +} +.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{ + border-color:transparent +} +.btn-link:focus,.btn-link:hover{ + color:#23527c; + text-decoration:underline; + background-color:transparent +} +.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{ + color:#777; + text-decoration:none +} +.btn-group-lg>.btn,.btn-lg{ + padding:10px 16px; + font-size:18px; + line-height:1.3333333; + border-radius:6px +} +.btn-group-sm>.btn,.btn-sm{ + padding:5px 10px; + font-size:12px; + line-height:1.5; + border-radius:3px +} +.btn-group-xs>.btn,.btn-xs{ + padding:1px 5px; + font-size:12px; + line-height:1.5; + border-radius:3px +} +.btn-block{ + display:block; + width:100% +} +.btn-block+.btn-block{ + margin-top:5px +} +input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{ + width:100% +} +.fade{ + opacity:0; + -webkit-transition:opacity .15s linear; + -o-transition:opacity .15s linear; + transition:opacity .15s linear +} +.fade.in{ + opacity:1 +} +.collapse{ + display:none +} +.collapse.in{ + display:block +} +tr.collapse.in{ + display:table-row +} +tbody.collapse.in{ + display:table-row-group +} +.collapsing{ + position:relative; + height:0; + overflow:hidden; + -webkit-transition-timing-function:ease; + -o-transition-timing-function:ease; + transition-timing-function:ease; + -webkit-transition-duration:.35s; + -o-transition-duration:.35s; + transition-duration:.35s; + -webkit-transition-property:height,visibility; + -o-transition-property:height,visibility; + transition-property:height,visibility +} +.caret{ + display:inline-block; + width:0; + height:0; + margin-left:2px; + vertical-align:middle; + border-top:4px dashed; + border-top:4px solid\9; + border-right:4px solid transparent; + border-left:4px solid transparent +} +.dropdown,.dropup{ + position:relative +} +.dropdown-toggle:focus{ + outline:0 +} +.dropdown-menu{ + position:absolute; + top:100%; + left:0; + z-index:1000; + display:none; + float:left; + min-width:160px; + padding:5px 0; + margin:2px 0 0; + font-size:14px; + text-align:left; + list-style:none; + background-color:#fff; + -webkit-background-clip:padding-box; + background-clip:padding-box; + border:1px solid #ccc; + border:1px solid rgba(0,0,0,.15); + border-radius:4px; + -webkit-box-shadow:0 6px 12px rgba(0,0,0,.175); + box-shadow:0 6px 12px rgba(0,0,0,.175) +} +.dropdown-menu.pull-right{ + right:0; + left:auto +} +.dropdown-menu .divider{ + height:1px; + margin:9px 0; + overflow:hidden; + background-color:#e5e5e5 +} +.dropdown-menu>li>a{ + display:block; + padding:3px 20px; + clear:both; + font-weight:400; + line-height:1.42857143; + color:#333; + white-space:nowrap +} +.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{ + color:#262626; + text-decoration:none; + background-color:#f5f5f5 +} +.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{ + color:#fff; + text-decoration:none; + background-color:#337ab7; + outline:0 +} +.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{ + color:#777 +} +.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{ + text-decoration:none; + cursor:not-allowed; + background-color:transparent; + background-image:none; + filter:progid:DXImageTransform.Microsoft.gradient(enabled=false) +} +.open>.dropdown-menu{ + display:block +} +.open>a{ + outline:0 +} +.dropdown-menu-right{ + right:0; + left:auto +} +.dropdown-menu-left{ + right:auto; + left:0 +} +.dropdown-header{ + display:block; + padding:3px 20px; + font-size:12px; + line-height:1.42857143; + color:#777; + white-space:nowrap +} +.dropdown-backdrop{ + position:fixed; + top:0; + right:0; + bottom:0; + left:0; + z-index:990 +} +.pull-right>.dropdown-menu{ + right:0; + left:auto +} +.dropup .caret,.navbar-fixed-bottom .dropdown .caret{ + content:""; + border-top:0; + border-bottom:4px dashed; + border-bottom:4px solid\9 +} +.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{ + top:auto; + bottom:100%; + margin-bottom:2px +} +@media (min-width:768px){ + .navbar-right .dropdown-menu{ + right:0; + left:auto + } + .navbar-right .dropdown-menu-left{ + right:auto; + left:0 + } +} +.btn-group,.btn-group-vertical{ + position:relative; + display:inline-block; + vertical-align:middle +} +.btn-group-vertical>.btn,.btn-group>.btn{ + position:relative; + float:left +} +.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{ + z-index:2 +} +.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{ + margin-left:-1px +} +.btn-toolbar{ + margin-left:-5px +} +.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{ + float:left +} +.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{ + margin-left:5px +} +.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){ + border-radius:0 +} +.btn-group>.btn:first-child{ + margin-left:0 +} +.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){ + border-top-right-radius:0; + border-bottom-right-radius:0 +} +.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){ + border-top-left-radius:0; + border-bottom-left-radius:0 +} +.btn-group>.btn-group{ + float:left +} +.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{ + border-radius:0 +} +.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{ + border-top-right-radius:0; + border-bottom-right-radius:0 +} +.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{ + border-top-left-radius:0; + border-bottom-left-radius:0 +} +.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{ + outline:0 +} +.btn-group>.btn+.dropdown-toggle{ + padding-right:8px; + padding-left:8px +} +.btn-group>.btn-lg+.dropdown-toggle{ + padding-right:12px; + padding-left:12px +} +.btn-group.open .dropdown-toggle{ + -webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125); + box-shadow:inset 0 3px 5px rgba(0,0,0,.125) +} +.btn-group.open .dropdown-toggle.btn-link{ + -webkit-box-shadow:none; + box-shadow:none +} +.btn .caret{ + margin-left:0 +} +.btn-lg .caret{ + border-width:5px 5px 0; + border-bottom-width:0 +} +.dropup .btn-lg .caret{ + border-width:0 5px 5px +} +.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{ + display:block; + float:none; + width:100%; + max-width:100% +} +.btn-group-vertical>.btn-group>.btn{ + float:none +} +.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{ + margin-top:-1px; + margin-left:0 +} +.btn-group-vertical>.btn:not(:first-child):not(:last-child){ + border-radius:0 +} +.btn-group-vertical>.btn:first-child:not(:last-child){ + border-top-left-radius:4px; + border-top-right-radius:4px; + border-bottom-right-radius:0; + border-bottom-left-radius:0 +} +.btn-group-vertical>.btn:last-child:not(:first-child){ + border-top-left-radius:0; + border-top-right-radius:0; + border-bottom-right-radius:4px; + border-bottom-left-radius:4px +} +.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{ + border-radius:0 +} +.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{ + border-bottom-right-radius:0; + border-bottom-left-radius:0 +} +.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{ + border-top-left-radius:0; + border-top-right-radius:0 +} +.btn-group-justified{ + display:table; + width:100%; + table-layout:fixed; + border-collapse:separate +} +.btn-group-justified>.btn,.btn-group-justified>.btn-group{ + display:table-cell; + float:none; + width:1% +} +.btn-group-justified>.btn-group .btn{ + width:100% +} +.btn-group-justified>.btn-group .dropdown-menu{ + left:auto +} +[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{ + position:absolute; + clip:rect(0,0,0,0); + pointer-events:none +} +.input-group{ + position:relative; + display:table; + border-collapse:separate +} +.input-group[class*=col-]{ + float:none; + padding-right:0; + padding-left:0 +} +.input-group .form-control{ + position:relative; + z-index:2; + float:left; + width:100%; + margin-bottom:0 +} +.input-group .form-control:focus{ + z-index:3 +} +.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{ + height:46px; + padding:10px 16px; + font-size:18px; + line-height:1.3333333; + border-radius:6px +} +select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{ + height:46px; + line-height:46px +} +select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{ + height:auto +} +.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{ + height:30px; + padding:5px 10px; + font-size:12px; + line-height:1.5; + border-radius:3px +} +select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{ + height:30px; + line-height:30px +} +select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{ + height:auto +} +.input-group .form-control,.input-group-addon,.input-group-btn{ + display:table-cell +} +.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){ + border-radius:0 +} +.input-group-addon,.input-group-btn{ + width:1%; + white-space:nowrap; + vertical-align:middle +} +.input-group-addon{ + padding:6px 12px; + font-size:14px; + font-weight:400; + line-height:1; + color:#555; + text-align:center; + background-color:#eee; + border:1px solid #ccc; + border-radius:4px +} +.input-group-addon.input-sm{ + padding:5px 10px; + font-size:12px; + border-radius:3px +} +.input-group-addon.input-lg{ + padding:10px 16px; + font-size:18px; + border-radius:6px +} +.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{ + margin-top:0 +} +.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){ + border-top-right-radius:0; + border-bottom-right-radius:0 +} +.input-group-addon:first-child{ + border-right:0 +} +.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{ + border-top-left-radius:0; + border-bottom-left-radius:0 +} +.input-group-addon:last-child{ + border-left:0 +} +.input-group-btn{ + position:relative; + font-size:0; + white-space:nowrap +} +.input-group-btn>.btn{ + position:relative +} +.input-group-btn>.btn+.btn{ + margin-left:-1px +} +.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{ + z-index:2 +} +.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{ + margin-right:-1px +} +.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{ + z-index:2; + margin-left:-1px +} +.nav{ + padding-left:0; + margin-bottom:0; + list-style:none +} +.nav>li{ + position:relative; + display:block +} +.nav>li>a{ + position:relative; + display:block; + padding:10px 15px +} +.nav>li>a:focus,.nav>li>a:hover{ + text-decoration:none; + background-color:#eee +} +.nav>li.disabled>a{ + color:#777 +} +.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{ + color:#777; + text-decoration:none; + cursor:not-allowed; + background-color:transparent +} +.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{ + background-color:#eee; + border-color:#337ab7 +} +.nav .nav-divider{ + height:1px; + margin:9px 0; + overflow:hidden; + background-color:#e5e5e5 +} +.nav>li>a>img{ + max-width:none +} +.nav-tabs{ + border-bottom:1px solid #ddd +} +.nav-tabs>li{ + float:left; + margin-bottom:-1px +} +.nav-tabs>li>a{ + margin-right:2px; + line-height:1.42857143; + border:1px solid transparent; + border-radius:4px 4px 0 0 +} +.nav-tabs>li>a:hover{ + border-color:#eee #eee #ddd +} +.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{ + color:#555; + cursor:default; + background-color:#fff; + border:1px solid #ddd; + border-bottom-color:transparent +} +.nav-tabs.nav-justified{ + width:100%; + border-bottom:0 +} +.nav-tabs.nav-justified>li{ + float:none +} +.nav-tabs.nav-justified>li>a{ + margin-bottom:5px; + text-align:center +} +.nav-tabs.nav-justified>.dropdown .dropdown-menu{ + top:auto; + left:auto +} +@media (min-width:768px){ + .nav-tabs.nav-justified>li{ + display:table-cell; + width:1% + } + .nav-tabs.nav-justified>li>a{ + margin-bottom:0 + } +} +.nav-tabs.nav-justified>li>a{ + margin-right:0; + border-radius:4px +} +.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{ + border:1px solid #ddd +} +@media (min-width:768px){ + .nav-tabs.nav-justified>li>a{ + border-bottom:1px solid #ddd; + border-radius:4px 4px 0 0 + } + .nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{ + border-bottom-color:#fff + } +} +.nav-pills>li{ + float:left +} +.nav-pills>li>a{ + border-radius:4px +} +.nav-pills>li+li{ + margin-left:2px +} +.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{ + color:#fff; + background-color:#337ab7 +} +.nav-stacked>li{ + float:none +} +.nav-stacked>li+li{ + margin-top:2px; + margin-left:0 +} +.nav-justified{ + width:100% +} +.nav-justified>li{ + float:none +} +.nav-justified>li>a{ + margin-bottom:5px; + text-align:center +} +.nav-justified>.dropdown .dropdown-menu{ + top:auto; + left:auto +} +@media (min-width:768px){ + .nav-justified>li{ + display:table-cell; + width:1% + } + .nav-justified>li>a{ + margin-bottom:0 + } +} +.nav-tabs-justified{ + border-bottom:0 +} +.nav-tabs-justified>li>a{ + margin-right:0; + border-radius:4px +} +.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{ + border:1px solid #ddd +} +@media (min-width:768px){ + .nav-tabs-justified>li>a{ + border-bottom:1px solid #ddd; + border-radius:4px 4px 0 0 + } + .nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{ + border-bottom-color:#fff + } +} +.tab-content>.tab-pane{ + display:none +} +.tab-content>.active{ + display:block +} +.nav-tabs .dropdown-menu{ + margin-top:-1px; + border-top-left-radius:0; + border-top-right-radius:0 +} +.navbar{ + position:relative; + min-height:50px; + margin-bottom:20px; + border:1px solid transparent +} +@media (min-width:768px){ + .navbar{ + border-radius:4px + } +} +@media (min-width:768px){ + .navbar-header{ + float:left + } +} +.navbar-collapse{ + padding-right:15px; + padding-left:15px; + overflow-x:visible; + -webkit-overflow-scrolling:touch; + border-top:1px solid transparent; + -webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1); + box-shadow:inset 0 1px 0 rgba(255,255,255,.1) +} +.navbar-collapse.in{ + overflow-y:auto +} +@media (min-width:768px){ + .navbar-collapse{ + width:auto; + border-top:0; + -webkit-box-shadow:none; + box-shadow:none + } + .navbar-collapse.collapse{ + display:block!important; + height:auto!important; + padding-bottom:0; + overflow:visible!important + } + .navbar-collapse.in{ + overflow-y:visible + } + .navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{ + padding-right:0; + padding-left:0 + } +} +.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{ + max-height:340px +} +@media (max-device-width:480px) and (orientation:landscape){ + .navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{ + max-height:200px + } +} +.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{ + margin-right:-15px; + margin-left:-15px +} +@media (min-width:768px){ + .container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{ + margin-right:0; + margin-left:0 + } +} +.navbar-static-top{ + z-index:1000; + border-width:0 0 1px +} +@media (min-width:768px){ + .navbar-static-top{ + border-radius:0 + } +} +.navbar-fixed-bottom,.navbar-fixed-top{ + position:fixed; + right:0; + left:0; + z-index:1030 +} +@media (min-width:768px){ + .navbar-fixed-bottom,.navbar-fixed-top{ + border-radius:0 + } +} +.navbar-fixed-top{ + top:0; + border-width:0 0 1px +} +.navbar-fixed-bottom{ + bottom:0; + margin-bottom:0; + border-width:1px 0 0 +} +.navbar-brand{ + float:left; + height:50px; + padding:15px 15px; + font-size:18px; + line-height:20px +} +.navbar-brand:focus,.navbar-brand:hover{ + text-decoration:none +} +.navbar-brand>img{ + display:block +} +@media (min-width:768px){ + .navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{ + margin-left:-15px + } +} +.navbar-toggle{ + position:relative; + float:right; + padding:9px 10px; + margin-top:8px; + margin-right:15px; + margin-bottom:8px; + background-color:transparent; + background-image:none; + border:1px solid transparent; + border-radius:4px +} +.navbar-toggle:focus{ + outline:0 +} +.navbar-toggle .icon-bar{ + display:block; + width:22px; + height:2px; + border-radius:1px +} +.navbar-toggle .icon-bar+.icon-bar{ + margin-top:4px +} +@media (min-width:768px){ + .navbar-toggle{ + display:none + } +} +.navbar-nav{ + margin:7.5px -15px +} +.navbar-nav>li>a{ + padding-top:10px; + padding-bottom:10px; + line-height:20px +} +@media (max-width:767px){ + .navbar-nav .open .dropdown-menu{ + position:static; + float:none; + width:auto; + margin-top:0; + background-color:transparent; + border:0; + -webkit-box-shadow:none; + box-shadow:none + } + .navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{ + padding:5px 15px 5px 25px + } + .navbar-nav .open .dropdown-menu>li>a{ + line-height:20px + } + .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{ + background-image:none + } +} +@media (min-width:768px){ + .navbar-nav{ + float:left; + margin:0 + } + .navbar-nav>li{ + float:left + } + .navbar-nav>li>a{ + padding-top:15px; + padding-bottom:15px + } +} +.navbar-form{ + padding:10px 15px; + margin-top:8px; + margin-right:-15px; + margin-bottom:8px; + margin-left:-15px; + border-top:1px solid transparent; + border-bottom:1px solid transparent; + -webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1); + box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1) +} +@media (min-width:768px){ + .navbar-form .form-group{ + display:inline-block; + margin-bottom:0; + vertical-align:middle + } + .navbar-form .form-control{ + display:inline-block; + width:auto; + vertical-align:middle + } + .navbar-form .form-control-static{ + display:inline-block + } + .navbar-form .input-group{ + display:inline-table; + vertical-align:middle + } + .navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{ + width:auto + } + .navbar-form .input-group>.form-control{ + width:100% + } + .navbar-form .control-label{ + margin-bottom:0; + vertical-align:middle + } + .navbar-form .checkbox,.navbar-form .radio{ + display:inline-block; + margin-top:0; + margin-bottom:0; + vertical-align:middle + } + .navbar-form .checkbox label,.navbar-form .radio label{ + padding-left:0 + } + .navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{ + position:relative; + margin-left:0 + } + .navbar-form .has-feedback .form-control-feedback{ + top:0 + } +} +@media (max-width:767px){ + .navbar-form .form-group{ + margin-bottom:5px + } + .navbar-form .form-group:last-child{ + margin-bottom:0 + } +} +@media (min-width:768px){ + .navbar-form{ + width:auto; + padding-top:0; + padding-bottom:0; + margin-right:0; + margin-left:0; + border:0; + -webkit-box-shadow:none; + box-shadow:none + } +} +.navbar-nav>li>.dropdown-menu{ + margin-top:0; + border-top-left-radius:0; + border-top-right-radius:0 +} +.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{ + margin-bottom:0; + border-top-left-radius:4px; + border-top-right-radius:4px; + border-bottom-right-radius:0; + border-bottom-left-radius:0 +} +.navbar-btn{ + margin-top:8px; + margin-bottom:8px +} +.navbar-btn.btn-sm{ + margin-top:10px; + margin-bottom:10px +} +.navbar-btn.btn-xs{ + margin-top:14px; + margin-bottom:14px +} +.navbar-text{ + margin-top:15px; + margin-bottom:15px +} +@media (min-width:768px){ + .navbar-text{ + float:left; + margin-right:15px; + margin-left:15px + } +} +@media (min-width:768px){ + .navbar-left{ + float:left!important + } + .navbar-right{ + float:right!important; + margin-right:-15px + } + .navbar-right~.navbar-right{ + margin-right:0 + } +} +.navbar-default{ + background-color:#f8f8f8; + border-color:#e7e7e7 +} +.navbar-default .navbar-brand{ + color:#777 +} +.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{ + color:#5e5e5e; + background-color:transparent +} +.navbar-default .navbar-text{ + color:#777 +} +.navbar-default .navbar-nav>li>a{ + color:#777 +} +.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{ + color:#333; + background-color:transparent +} +.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{ + color:#555; + background-color:#e7e7e7 +} +.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{ + color:#ccc; + background-color:transparent +} +.navbar-default .navbar-toggle{ + border-color:#ddd +} +.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{ + background-color:#ddd +} +.navbar-default .navbar-toggle .icon-bar{ + background-color:#888 +} +.navbar-default .navbar-collapse,.navbar-default .navbar-form{ + border-color:#e7e7e7 +} +.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{ + color:#555; + background-color:#e7e7e7 +} +@media (max-width:767px){ + .navbar-default .navbar-nav .open .dropdown-menu>li>a{ + color:#777 + } + .navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{ + color:#333; + background-color:transparent + } + .navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{ + color:#555; + background-color:#e7e7e7 + } + .navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{ + color:#ccc; + background-color:transparent + } +} +.navbar-default .navbar-link{ + color:#777 +} +.navbar-default .navbar-link:hover{ + color:#333 +} +.navbar-default .btn-link{ + color:#777 +} +.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{ + color:#333 +} +.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{ + color:#ccc +} +.navbar-inverse{ + background-color:#222; + border-color:#080808 +} +.navbar-inverse .navbar-brand{ + color:#9d9d9d +} +.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{ + color:#fff; + background-color:transparent +} +.navbar-inverse .navbar-text{ + color:#9d9d9d +} +.navbar-inverse .navbar-nav>li>a{ + color:#9d9d9d +} +.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{ + color:#fff; + background-color:transparent +} +.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{ + color:#fff; + background-color:#080808 +} +.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{ + color:#444; + background-color:transparent +} +.navbar-inverse .navbar-toggle{ + border-color:#333 +} +.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{ + background-color:#333 +} +.navbar-inverse .navbar-toggle .icon-bar{ + background-color:#fff +} +.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{ + border-color:#101010 +} +.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{ + color:#fff; + background-color:#080808 +} +@media (max-width:767px){ + .navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{ + border-color:#080808 + } + .navbar-inverse .navbar-nav .open .dropdown-menu .divider{ + background-color:#080808 + } + .navbar-inverse .navbar-nav .open .dropdown-menu>li>a{ + color:#9d9d9d + } + .navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{ + color:#fff; + background-color:transparent + } + .navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{ + color:#fff; + background-color:#080808 + } + .navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{ + color:#444; + background-color:transparent + } +} +.navbar-inverse .navbar-link{ + color:#9d9d9d +} +.navbar-inverse .navbar-link:hover{ + color:#fff +} +.navbar-inverse .btn-link{ + color:#9d9d9d +} +.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{ + color:#fff +} +.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{ + color:#444 +} +.breadcrumb{ + padding:8px 15px; + margin-bottom:20px; + list-style:none; + background-color:#f5f5f5; + border-radius:4px +} +.breadcrumb>li{ + display:inline-block +} +.breadcrumb>li+li:before{ + padding:0 5px; + color:#ccc; + content:"/\00a0" +} +.breadcrumb>.active{ + color:#777 +} +.pagination{ + display:inline-block; + padding-left:0; + margin:20px 0; + border-radius:4px +} +.pagination>li{ + display:inline +} +.pagination>li>a,.pagination>li>span{ + position:relative; + float:left; + padding:6px 12px; + margin-left:-1px; + line-height:1.42857143; + color:#337ab7; + text-decoration:none; + background-color:#fff; + border:1px solid #ddd +} +.pagination>li:first-child>a,.pagination>li:first-child>span{ + margin-left:0; + border-top-left-radius:4px; + border-bottom-left-radius:4px +} +.pagination>li:last-child>a,.pagination>li:last-child>span{ + border-top-right-radius:4px; + border-bottom-right-radius:4px +} +.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{ + z-index:2; + color:#23527c; + background-color:#eee; + border-color:#ddd +} +.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{ + z-index:3; + color:#fff; + cursor:default; + background-color:#337ab7; + border-color:#337ab7 +} +.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{ + color:#777; + cursor:not-allowed; + background-color:#fff; + border-color:#ddd +} +.pagination-lg>li>a,.pagination-lg>li>span{ + padding:10px 16px; + font-size:18px; + line-height:1.3333333 +} +.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{ + border-top-left-radius:6px; + border-bottom-left-radius:6px +} +.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{ + border-top-right-radius:6px; + border-bottom-right-radius:6px +} +.pagination-sm>li>a,.pagination-sm>li>span{ + padding:5px 10px; + font-size:12px; + line-height:1.5 +} +.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{ + border-top-left-radius:3px; + border-bottom-left-radius:3px +} +.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{ + border-top-right-radius:3px; + border-bottom-right-radius:3px +} +.pager{ + padding-left:0; + margin:20px 0; + text-align:center; + list-style:none +} +.pager li{ + display:inline +} +.pager li>a,.pager li>span{ + display:inline-block; + padding:5px 14px; + background-color:#fff; + border:1px solid #ddd; + border-radius:15px +} +.pager li>a:focus,.pager li>a:hover{ + text-decoration:none; + background-color:#eee +} +.pager .next>a,.pager .next>span{ + float:right +} +.pager .previous>a,.pager .previous>span{ + float:left +} +.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{ + color:#777; + cursor:not-allowed; + background-color:#fff +} +.label{ + display:inline; + padding:.2em .6em .3em; + font-size:75%; + font-weight:700; + line-height:1; + color:#fff; + text-align:center; + white-space:nowrap; + vertical-align:baseline; + border-radius:.25em +} +a.label:focus,a.label:hover{ + color:#fff; + text-decoration:none; + cursor:pointer +} +.label:empty{ + display:none +} +.btn .label{ + position:relative; + top:-1px +} +.label-default{ + background-color:#777 +} +.label-default[href]:focus,.label-default[href]:hover{ + background-color:#5e5e5e +} +.label-primary{ + background-color:#337ab7 +} +.label-primary[href]:focus,.label-primary[href]:hover{ + background-color:#286090 +} +.label-success{ + background-color:#5cb85c +} +.label-success[href]:focus,.label-success[href]:hover{ + background-color:#449d44 +} +.label-info{ + background-color:#5bc0de +} +.label-info[href]:focus,.label-info[href]:hover{ + background-color:#31b0d5 +} +.label-warning{ + background-color:#f0ad4e +} +.label-warning[href]:focus,.label-warning[href]:hover{ + background-color:#ec971f +} +.label-danger{ + background-color:#d9534f +} +.label-danger[href]:focus,.label-danger[href]:hover{ + background-color:#c9302c +} +.badge{ + display:inline-block; + min-width:10px; + padding:3px 7px; + font-size:12px; + font-weight:700; + line-height:1; + color:#fff; + text-align:center; + white-space:nowrap; + vertical-align:middle; + background-color:#777; + border-radius:10px +} +.badge:empty{ + display:none +} +.btn .badge{ + position:relative; + top:-1px +} +.btn-group-xs>.btn .badge,.btn-xs .badge{ + top:0; + padding:1px 5px +} +a.badge:focus,a.badge:hover{ + color:#fff; + text-decoration:none; + cursor:pointer +} +.list-group-item.active>.badge,.nav-pills>.active>a>.badge{ + color:#337ab7; + background-color:#fff +} +.list-group-item>.badge{ + float:right +} +.list-group-item>.badge+.badge{ + margin-right:5px +} +.nav-pills>li>a>.badge{ + margin-left:3px +} +.jumbotron{ + padding-top:30px; + padding-bottom:30px; + margin-bottom:30px; + color:inherit; + background-color:#eee +} +.jumbotron .h1,.jumbotron h1{ + color:inherit +} +.jumbotron p{ + margin-bottom:15px; + font-size:21px; + font-weight:200 +} +.jumbotron>hr{ + border-top-color:#d5d5d5 +} +.container .jumbotron,.container-fluid .jumbotron{ + padding-right:15px; + padding-left:15px; + border-radius:6px +} +.jumbotron .container{ + max-width:100% +} +@media screen and (min-width:768px){ + .jumbotron{ + padding-top:48px; + padding-bottom:48px + } + .container .jumbotron,.container-fluid .jumbotron{ + padding-right:60px; + padding-left:60px + } + .jumbotron .h1,.jumbotron h1{ + font-size:63px + } +} +.thumbnail{ + display:block; + padding:4px; + margin-bottom:20px; + line-height:1.42857143; + background-color:#fff; + border:1px solid #ddd; + border-radius:4px; + -webkit-transition:border .2s ease-in-out; + -o-transition:border .2s ease-in-out; + transition:border .2s ease-in-out +} +.thumbnail a>img,.thumbnail>img{ + margin-right:auto; + margin-left:auto +} +a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{ + border-color:#337ab7 +} +.thumbnail .caption{ + padding:9px; + color:#333 +} +.alert{ + padding:15px; + margin-bottom:20px; + border:1px solid transparent; + border-radius:4px +} +.alert h4{ + margin-top:0; + color:inherit +} +.alert .alert-link{ + font-weight:700 +} +.alert>p,.alert>ul{ + margin-bottom:0 +} +.alert>p+p{ + margin-top:5px +} +.alert-dismissable,.alert-dismissible{ + padding-right:35px +} +.alert-dismissable .close,.alert-dismissible .close{ + position:relative; + top:-2px; + right:-21px; + color:inherit +} +.alert-success{ + color:#3c763d; + background-color:#dff0d8; + border-color:#d6e9c6 +} +.alert-success hr{ + border-top-color:#c9e2b3 +} +.alert-success .alert-link{ + color:#2b542c +} +.alert-info{ + color:#31708f; + background-color:#d9edf7; + border-color:#bce8f1 +} +.alert-info hr{ + border-top-color:#a6e1ec +} +.alert-info .alert-link{ + color:#245269 +} +.alert-warning{ + color:#8a6d3b; + background-color:#fcf8e3; + border-color:#faebcc +} +.alert-warning hr{ + border-top-color:#f7e1b5 +} +.alert-warning .alert-link{ + color:#66512c +} +.alert-danger{ + color:#a94442; + background-color:#f2dede; + border-color:#ebccd1 +} +.alert-danger hr{ + border-top-color:#e4b9c0 +} +.alert-danger .alert-link{ + color:#843534 +} +@-webkit-keyframes progress-bar-stripes{ + from{ + background-position:40px 0 + } + to{ + background-position:0 0 + } +} +@-o-keyframes progress-bar-stripes{ + from{ + background-position:40px 0 + } + to{ + background-position:0 0 + } +} +@keyframes progress-bar-stripes{ + from{ + background-position:40px 0 + } + to{ + background-position:0 0 + } +} +.progress{ + height:20px; + margin-bottom:20px; + overflow:hidden; + background-color:#f5f5f5; + border-radius:4px; + -webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1); + box-shadow:inset 0 1px 2px rgba(0,0,0,.1) +} +.progress-bar{ + float:left; + width:0; + height:100%; + font-size:12px; + line-height:20px; + color:#fff; + text-align:center; + background-color:#337ab7; + -webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15); + box-shadow:inset 0 -1px 0 rgba(0,0,0,.15); + -webkit-transition:width .6s ease; + -o-transition:width .6s ease; + transition:width .6s ease +} +.progress-bar-striped,.progress-striped .progress-bar{ + background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + -webkit-background-size:40px 40px; + background-size:40px 40px +} +.progress-bar.active,.progress.active .progress-bar{ + -webkit-animation:progress-bar-stripes 2s linear infinite; + -o-animation:progress-bar-stripes 2s linear infinite; + animation:progress-bar-stripes 2s linear infinite +} +.progress-bar-success{ + background-color:#5cb85c +} +.progress-striped .progress-bar-success{ + background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent) +} +.progress-bar-info{ + background-color:#5bc0de +} +.progress-striped .progress-bar-info{ + background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent) +} +.progress-bar-warning{ + background-color:#f0ad4e +} +.progress-striped .progress-bar-warning{ + background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent) +} +.progress-bar-danger{ + background-color:#d9534f +} +.progress-striped .progress-bar-danger{ + background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent) +} +.media{ + margin-top:15px +} +.media:first-child{ + margin-top:0 +} +.media,.media-body{ + overflow:hidden; + zoom:1 +} +.media-body{ + width:10000px +} +.media-object{ + display:block +} +.media-object.img-thumbnail{ + max-width:none +} +.media-right,.media>.pull-right{ + padding-left:10px +} +.media-left,.media>.pull-left{ + padding-right:10px +} +.media-body,.media-left,.media-right{ + display:table-cell; + vertical-align:top +} +.media-middle{ + vertical-align:middle +} +.media-bottom{ + vertical-align:bottom +} +.media-heading{ + margin-top:0; + margin-bottom:5px +} +.media-list{ + padding-left:0; + list-style:none +} +.list-group{ + padding-left:0; + margin-bottom:20px +} +.list-group-item{ + position:relative; + display:block; + padding:10px 15px; + margin-bottom:-1px; + background-color:#fff; + border:1px solid #ddd +} +.list-group-item:first-child{ + border-top-left-radius:4px; + border-top-right-radius:4px +} +.list-group-item:last-child{ + margin-bottom:0; + border-bottom-right-radius:4px; + border-bottom-left-radius:4px +} +a.list-group-item,button.list-group-item{ + color:#555 +} +a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{ + color:#333 +} +a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{ + color:#555; + text-decoration:none; + background-color:#f5f5f5 +} +button.list-group-item{ + width:100%; + text-align:left +} +.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{ + color:#777; + cursor:not-allowed; + background-color:#eee +} +.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{ + color:inherit +} +.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{ + color:#777 +} +.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{ + z-index:2; + color:#fff; + background-color:#337ab7; + border-color:#337ab7 +} +.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{ + color:inherit +} +.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{ + color:#c7ddef +} +.list-group-item-success{ + color:#3c763d; + background-color:#dff0d8 +} +a.list-group-item-success,button.list-group-item-success{ + color:#3c763d +} +a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{ + color:inherit +} +a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{ + color:#3c763d; + background-color:#d0e9c6 +} +a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{ + color:#fff; + background-color:#3c763d; + border-color:#3c763d +} +.list-group-item-info{ + color:#31708f; + background-color:#d9edf7 +} +a.list-group-item-info,button.list-group-item-info{ + color:#31708f +} +a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{ + color:inherit +} +a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{ + color:#31708f; + background-color:#c4e3f3 +} +a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{ + color:#fff; + background-color:#31708f; + border-color:#31708f +} +.list-group-item-warning{ + color:#8a6d3b; + background-color:#fcf8e3 +} +a.list-group-item-warning,button.list-group-item-warning{ + color:#8a6d3b +} +a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{ + color:inherit +} +a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{ + color:#8a6d3b; + background-color:#faf2cc +} +a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{ + color:#fff; + background-color:#8a6d3b; + border-color:#8a6d3b +} +.list-group-item-danger{ + color:#a94442; + background-color:#f2dede +} +a.list-group-item-danger,button.list-group-item-danger{ + color:#a94442 +} +a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{ + color:inherit +} +a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{ + color:#a94442; + background-color:#ebcccc +} +a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{ + color:#fff; + background-color:#a94442; + border-color:#a94442 +} +.list-group-item-heading{ + margin-top:0; + margin-bottom:5px +} +.list-group-item-text{ + margin-bottom:0; + line-height:1.3 +} +.panel{ + margin-bottom:20px; + background-color:#fff; + border:1px solid transparent; + border-radius:4px; + -webkit-box-shadow:0 1px 1px rgba(0,0,0,.05); + box-shadow:0 1px 1px rgba(0,0,0,.05) +} +.panel-body{ + padding:15px +} +.panel-heading{ + padding:10px 15px; + border-bottom:1px solid transparent; + border-top-left-radius:3px; + border-top-right-radius:3px +} +.panel-heading>.dropdown .dropdown-toggle{ + color:inherit +} +.panel-title{ + margin-top:0; + margin-bottom:0; + font-size:16px; + color:inherit +} +.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{ + color:inherit +} +.panel-footer{ + padding:10px 15px; + background-color:#f5f5f5; + border-top:1px solid #ddd; + border-bottom-right-radius:3px; + border-bottom-left-radius:3px +} +.panel>.list-group,.panel>.panel-collapse>.list-group{ + margin-bottom:0 +} +.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{ + border-width:1px 0; + border-radius:0 +} +.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{ + border-top:0; + border-top-left-radius:3px; + border-top-right-radius:3px +} +.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{ + border-bottom:0; + border-bottom-right-radius:3px; + border-bottom-left-radius:3px +} +.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{ + border-top-left-radius:0; + border-top-right-radius:0 +} +.panel-heading+.list-group .list-group-item:first-child{ + border-top-width:0 +} +.list-group+.panel-footer{ + border-top-width:0 +} +.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{ + margin-bottom:0 +} +.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{ + padding-right:15px; + padding-left:15px +} +.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{ + border-top-left-radius:3px; + border-top-right-radius:3px +} +.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{ + border-top-left-radius:3px; + border-top-right-radius:3px +} +.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{ + border-top-left-radius:3px +} +.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{ + border-top-right-radius:3px +} +.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{ + border-bottom-right-radius:3px; + border-bottom-left-radius:3px +} +.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{ + border-bottom-right-radius:3px; + border-bottom-left-radius:3px +} +.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{ + border-bottom-left-radius:3px +} +.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{ + border-bottom-right-radius:3px +} +.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{ + border-top:1px solid #ddd +} +.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{ + border-top:0 +} +.panel>.table-bordered,.panel>.table-responsive>.table-bordered{ + border:0 +} +.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{ + border-left:0 +} +.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{ + border-right:0 +} +.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{ + border-bottom:0 +} +.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{ + border-bottom:0 +} +.panel>.table-responsive{ + margin-bottom:0; + border:0 +} +.panel-group{ + margin-bottom:20px +} +.panel-group .panel{ + margin-bottom:0; + border-radius:4px +} +.panel-group .panel+.panel{ + margin-top:5px +} +.panel-group .panel-heading{ + border-bottom:0 +} +.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{ + border-top:1px solid #ddd +} +.panel-group .panel-footer{ + border-top:0 +} +.panel-group .panel-footer+.panel-collapse .panel-body{ + border-bottom:1px solid #ddd +} +.panel-default{ + border-color:#ddd +} +.panel-default>.panel-heading{ + color:#333; + background-color:#f5f5f5; + border-color:#ddd +} +.panel-default>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#ddd +} +.panel-default>.panel-heading .badge{ + color:#f5f5f5; + background-color:#333 +} +.panel-default>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#ddd +} +.panel-primary{ + border-color:#337ab7 +} +.panel-primary>.panel-heading{ + color:#fff; + background-color:#337ab7; + border-color:#337ab7 +} +.panel-primary>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#337ab7 +} +.panel-primary>.panel-heading .badge{ + color:#337ab7; + background-color:#fff +} +.panel-primary>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#337ab7 +} +.panel-success{ + border-color:#d6e9c6 +} +.panel-success>.panel-heading{ + color:#3c763d; + background-color:#dff0d8; + border-color:#d6e9c6 +} +.panel-success>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#d6e9c6 +} +.panel-success>.panel-heading .badge{ + color:#dff0d8; + background-color:#3c763d +} +.panel-success>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#d6e9c6 +} +.panel-info{ + border-color:#bce8f1 +} +.panel-info>.panel-heading{ + color:#31708f; + background-color:#d9edf7; + border-color:#bce8f1 +} +.panel-info>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#bce8f1 +} +.panel-info>.panel-heading .badge{ + color:#d9edf7; + background-color:#31708f +} +.panel-info>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#bce8f1 +} +.panel-warning{ + border-color:#faebcc +} +.panel-warning>.panel-heading{ + color:#8a6d3b; + background-color:#fcf8e3; + border-color:#faebcc +} +.panel-warning>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#faebcc +} +.panel-warning>.panel-heading .badge{ + color:#fcf8e3; + background-color:#8a6d3b +} +.panel-warning>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#faebcc +} +.panel-danger{ + border-color:#ebccd1 +} +.panel-danger>.panel-heading{ + color:#a94442; + background-color:#f2dede; + border-color:#ebccd1 +} +.panel-danger>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#ebccd1 +} +.panel-danger>.panel-heading .badge{ + color:#f2dede; + background-color:#a94442 +} +.panel-danger>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#ebccd1 +} +.embed-responsive{ + position:relative; + display:block; + height:0; + padding:0; + overflow:hidden +} +.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{ + position:absolute; + top:0; + bottom:0; + left:0; + width:100%; + height:100%; + border:0 +} +.embed-responsive-16by9{ + padding-bottom:56.25% +} +.embed-responsive-4by3{ + padding-bottom:75% +} +.well{ + min-height:20px; + padding:19px; + margin-bottom:20px; + background-color:#f5f5f5; + border:1px solid #e3e3e3; + border-radius:4px; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05); + box-shadow:inset 0 1px 1px rgba(0,0,0,.05) +} +.well blockquote{ + border-color:#ddd; + border-color:rgba(0,0,0,.15) +} +.well-lg{ + padding:24px; + border-radius:6px +} +.well-sm{ + padding:9px; + border-radius:3px +} +.close{ + float:right; + font-size:21px; + font-weight:700; + line-height:1; + color:#000; + text-shadow:0 1px 0 #fff; + filter:alpha(opacity=20); + opacity:.2 +} +.close:focus,.close:hover{ + color:#000; + text-decoration:none; + cursor:pointer; + filter:alpha(opacity=50); + opacity:.5 +} +button.close{ + -webkit-appearance:none; + padding:0; + cursor:pointer; + background:0 0; + border:0 +} +.modal-open{ + overflow:hidden +} +.modal{ + position:fixed; + top:0; + right:0; + bottom:0; + left:0; + z-index:1050; + display:none; + overflow:hidden; + -webkit-overflow-scrolling:touch; + outline:0 +} +.modal.fade .modal-dialog{ + -webkit-transition:-webkit-transform .3s ease-out; + -o-transition:-o-transform .3s ease-out; + transition:transform .3s ease-out; + -webkit-transform:translate(0,-25%); + -ms-transform:translate(0,-25%); + -o-transform:translate(0,-25%); + transform:translate(0,-25%) +} +.modal.in .modal-dialog{ + -webkit-transform:translate(0,0); + -ms-transform:translate(0,0); + -o-transform:translate(0,0); + transform:translate(0,0) +} +.modal-open .modal{ + overflow-x:hidden; + overflow-y:auto +} +.modal-dialog{ + position:relative; + width:auto; + margin:10px +} +.modal-content{ + position:relative; + background-color:#fff; + -webkit-background-clip:padding-box; + background-clip:padding-box; + border:1px solid #999; + border:1px solid rgba(0,0,0,.2); + border-radius:6px; + outline:0; + -webkit-box-shadow:0 3px 9px rgba(0,0,0,.5); + box-shadow:0 3px 9px rgba(0,0,0,.5) +} +.modal-backdrop{ + position:fixed; + top:0; + right:0; + bottom:0; + left:0; + z-index:1040; + background-color:#000 +} +.modal-backdrop.fade{ + filter:alpha(opacity=0); + opacity:0 +} +.modal-backdrop.in{ + filter:alpha(opacity=50); + opacity:.5 +} +.modal-header{ + padding:15px; + border-bottom:1px solid #e5e5e5 +} +.modal-header .close{ + margin-top:-2px +} +.modal-title{ + margin:0; + line-height:1.42857143 +} +.modal-body{ + position:relative; + padding:15px +} +.modal-footer{ + padding:15px; + text-align:right; + border-top:1px solid #e5e5e5 +} +.modal-footer .btn+.btn{ + margin-bottom:0; + margin-left:5px +} +.modal-footer .btn-group .btn+.btn{ + margin-left:-1px +} +.modal-footer .btn-block+.btn-block{ + margin-left:0 +} +.modal-scrollbar-measure{ + position:absolute; + top:-9999px; + width:50px; + height:50px; + overflow:scroll +} +@media (min-width:768px){ + .modal-dialog{ + width:600px; + margin:30px auto + } + .modal-content{ + -webkit-box-shadow:0 5px 15px rgba(0,0,0,.5); + box-shadow:0 5px 15px rgba(0,0,0,.5) + } + .modal-sm{ + width:300px + } +} +@media (min-width:992px){ + .modal-lg{ + width:900px + } +} +.tooltip{ + position:absolute; + z-index:1070; + display:block; + font-family:"Helvetica Neue",Helvetica,Arial,sans-serif; + font-size:12px; + font-style:normal; + font-weight:400; + line-height:1.42857143; + text-align:left; + text-align:start; + text-decoration:none; + text-shadow:none; + text-transform:none; + letter-spacing:normal; + word-break:normal; + word-spacing:normal; + word-wrap:normal; + white-space:normal; + filter:alpha(opacity=0); + opacity:0; + line-break:auto +} +.tooltip.in{ + filter:alpha(opacity=90); + opacity:.9 +} +.tooltip.top{ + padding:5px 0; + margin-top:-3px +} +.tooltip.right{ + padding:0 5px; + margin-left:3px +} +.tooltip.bottom{ + padding:5px 0; + margin-top:3px +} +.tooltip.left{ + padding:0 5px; + margin-left:-3px +} +.tooltip-inner{ + max-width:200px; + padding:3px 8px; + color:#fff; + text-align:center; + background-color:#000; + border-radius:4px +} +.tooltip-arrow{ + position:absolute; + width:0; + height:0; + border-color:transparent; + border-style:solid +} +.tooltip.top .tooltip-arrow{ + bottom:0; + left:50%; + margin-left:-5px; + border-width:5px 5px 0; + border-top-color:#000 +} +.tooltip.top-left .tooltip-arrow{ + right:5px; + bottom:0; + margin-bottom:-5px; + border-width:5px 5px 0; + border-top-color:#000 +} +.tooltip.top-right .tooltip-arrow{ + bottom:0; + left:5px; + margin-bottom:-5px; + border-width:5px 5px 0; + border-top-color:#000 +} +.tooltip.right .tooltip-arrow{ + top:50%; + left:0; + margin-top:-5px; + border-width:5px 5px 5px 0; + border-right-color:#000 +} +.tooltip.left .tooltip-arrow{ + top:50%; + right:0; + margin-top:-5px; + border-width:5px 0 5px 5px; + border-left-color:#000 +} +.tooltip.bottom .tooltip-arrow{ + top:0; + left:50%; + margin-left:-5px; + border-width:0 5px 5px; + border-bottom-color:#000 +} +.tooltip.bottom-left .tooltip-arrow{ + top:0; + right:5px; + margin-top:-5px; + border-width:0 5px 5px; + border-bottom-color:#000 +} +.tooltip.bottom-right .tooltip-arrow{ + top:0; + left:5px; + margin-top:-5px; + border-width:0 5px 5px; + border-bottom-color:#000 +} +.popover{ + position:absolute; + top:0; + left:0; + z-index:1060; + display:none; + max-width:276px; + padding:1px; + font-family:"Helvetica Neue",Helvetica,Arial,sans-serif; + font-size:14px; + font-style:normal; + font-weight:400; + line-height:1.42857143; + text-align:left; + text-align:start; + text-decoration:none; + text-shadow:none; + text-transform:none; + letter-spacing:normal; + word-break:normal; + word-spacing:normal; + word-wrap:normal; + white-space:normal; + background-color:#fff; + -webkit-background-clip:padding-box; + background-clip:padding-box; + border:1px solid #ccc; + border:1px solid rgba(0,0,0,.2); + border-radius:6px; + -webkit-box-shadow:0 5px 10px rgba(0,0,0,.2); + box-shadow:0 5px 10px rgba(0,0,0,.2); + line-break:auto +} +.popover.top{ + margin-top:-10px +} +.popover.right{ + margin-left:10px +} +.popover.bottom{ + margin-top:10px +} +.popover.left{ + margin-left:-10px +} +.popover-title{ + padding:8px 14px; + margin:0; + font-size:14px; + background-color:#f7f7f7; + border-bottom:1px solid #ebebeb; + border-radius:5px 5px 0 0 +} +.popover-content{ + padding:9px 14px +} +.popover>.arrow,.popover>.arrow:after{ + position:absolute; + display:block; + width:0; + height:0; + border-color:transparent; + border-style:solid +} +.popover>.arrow{ + border-width:11px +} +.popover>.arrow:after{ + content:""; + border-width:10px +} +.popover.top>.arrow{ + bottom:-11px; + left:50%; + margin-left:-11px; + border-top-color:#999; + border-top-color:rgba(0,0,0,.25); + border-bottom-width:0 +} +.popover.top>.arrow:after{ + bottom:1px; + margin-left:-10px; + content:" "; + border-top-color:#fff; + border-bottom-width:0 +} +.popover.right>.arrow{ + top:50%; + left:-11px; + margin-top:-11px; + border-right-color:#999; + border-right-color:rgba(0,0,0,.25); + border-left-width:0 +} +.popover.right>.arrow:after{ + bottom:-10px; + left:1px; + content:" "; + border-right-color:#fff; + border-left-width:0 +} +.popover.bottom>.arrow{ + top:-11px; + left:50%; + margin-left:-11px; + border-top-width:0; + border-bottom-color:#999; + border-bottom-color:rgba(0,0,0,.25) +} +.popover.bottom>.arrow:after{ + top:1px; + margin-left:-10px; + content:" "; + border-top-width:0; + border-bottom-color:#fff +} +.popover.left>.arrow{ + top:50%; + right:-11px; + margin-top:-11px; + border-right-width:0; + border-left-color:#999; + border-left-color:rgba(0,0,0,.25) +} +.popover.left>.arrow:after{ + right:1px; + bottom:-10px; + content:" "; + border-right-width:0; + border-left-color:#fff +} +.carousel{ + position:relative +} +.carousel-inner{ + position:relative; + width:100%; + overflow:hidden +} +.carousel-inner>.item{ + position:relative; + display:none; + -webkit-transition:.6s ease-in-out left; + -o-transition:.6s ease-in-out left; + transition:.6s ease-in-out left +} +.carousel-inner>.item>a>img,.carousel-inner>.item>img{ + line-height:1 +} +@media all and (transform-3d),(-webkit-transform-3d){ + .carousel-inner>.item{ + -webkit-transition:-webkit-transform .6s ease-in-out; + -o-transition:-o-transform .6s ease-in-out; + transition:transform .6s ease-in-out; + -webkit-backface-visibility:hidden; + backface-visibility:hidden; + -webkit-perspective:1000px; + perspective:1000px + } + .carousel-inner>.item.active.right,.carousel-inner>.item.next{ + left:0; + -webkit-transform:translate3d(100%,0,0); + transform:translate3d(100%,0,0) + } + .carousel-inner>.item.active.left,.carousel-inner>.item.prev{ + left:0; + -webkit-transform:translate3d(-100%,0,0); + transform:translate3d(-100%,0,0) + } + .carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{ + left:0; + -webkit-transform:translate3d(0,0,0); + transform:translate3d(0,0,0) + } +} +.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{ + display:block +} +.carousel-inner>.active{ + left:0 +} +.carousel-inner>.next,.carousel-inner>.prev{ + position:absolute; + top:0; + width:100% +} +.carousel-inner>.next{ + left:100% +} +.carousel-inner>.prev{ + left:-100% +} +.carousel-inner>.next.left,.carousel-inner>.prev.right{ + left:0 +} +.carousel-inner>.active.left{ + left:-100% +} +.carousel-inner>.active.right{ + left:100% +} +.carousel-control{ + position:absolute; + top:0; + bottom:0; + left:0; + width:15%; + font-size:20px; + color:#fff; + text-align:center; + text-shadow:0 1px 2px rgba(0,0,0,.6); + background-color:rgba(0,0,0,0); + filter:alpha(opacity=50); + opacity:.5 +} +.carousel-control.left{ + background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%); + background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%); + background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001))); + background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); + background-repeat:repeat-x +} +.carousel-control.right{ + right:0; + left:auto; + background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%); + background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%); + background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5))); + background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); + background-repeat:repeat-x +} +.carousel-control:focus,.carousel-control:hover{ + color:#fff; + text-decoration:none; + filter:alpha(opacity=90); + outline:0; + opacity:.9 +} +.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{ + position:absolute; + top:50%; + z-index:5; + display:inline-block; + margin-top:-10px +} +.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{ + left:50%; + margin-left:-10px +} +.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{ + right:50%; + margin-right:-10px +} +.carousel-control .icon-next,.carousel-control .icon-prev{ + width:20px; + height:20px; + font-family:serif; + line-height:1 +} +.carousel-control .icon-prev:before{ + content:'\2039' +} +.carousel-control .icon-next:before{ + content:'\203a' +} +.carousel-indicators{ + position:absolute; + bottom:10px; + left:50%; + z-index:15; + width:60%; + padding-left:0; + margin-left:-30%; + text-align:center; + list-style:none +} +.carousel-indicators li{ + display:inline-block; + width:10px; + height:10px; + margin:1px; + text-indent:-999px; + cursor:pointer; + background-color:#000\9; + background-color:rgba(0,0,0,0); + border:1px solid #fff; + border-radius:10px +} +.carousel-indicators .active{ + width:12px; + height:12px; + margin:0; + background-color:#fff +} +.carousel-caption{ + position:absolute; + right:15%; + bottom:20px; + left:15%; + z-index:10; + padding-top:20px; + padding-bottom:20px; + color:#fff; + text-align:center; + text-shadow:0 1px 2px rgba(0,0,0,.6) +} +.carousel-caption .btn{ + text-shadow:none +} +@media screen and (min-width:768px){ + .carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{ + width:30px; + height:30px; + margin-top:-10px; + font-size:30px + } + .carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{ + margin-left:-10px + } + .carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{ + margin-right:-10px + } + .carousel-caption{ + right:20%; + left:20%; + padding-bottom:30px + } + .carousel-indicators{ + bottom:20px + } +} +.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{ + display:table; + content:" " +} +.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{ + clear:both +} +.center-block{ + display:block; + margin-right:auto; + margin-left:auto +} +.pull-right{ + float:right!important +} +.pull-left{ + float:left!important +} +.hide{ + display:none!important +} +.show{ + display:block!important +} +.invisible{ + visibility:hidden +} +.text-hide{ + font:0/0 a; + color:transparent; + text-shadow:none; + background-color:transparent; + border:0 +} +.hidden{ + display:none!important +} +.affix{ + position:fixed +} +@-ms-viewport{ + width:device-width +} +.visible-lg,.visible-md,.visible-sm,.visible-xs{ + display:none!important +} +.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{ + display:none!important +} +@media (max-width:767px){ + .visible-xs{ + display:block!important + } + table.visible-xs{ + display:table!important + } + tr.visible-xs{ + display:table-row!important + } + td.visible-xs,th.visible-xs{ + display:table-cell!important + } +} +@media (max-width:767px){ + .visible-xs-block{ + display:block!important + } +} +@media (max-width:767px){ + .visible-xs-inline{ + display:inline!important + } +} +@media (max-width:767px){ + .visible-xs-inline-block{ + display:inline-block!important + } +} +@media (min-width:768px) and (max-width:991px){ + .visible-sm{ + display:block!important + } + table.visible-sm{ + display:table!important + } + tr.visible-sm{ + display:table-row!important + } + td.visible-sm,th.visible-sm{ + display:table-cell!important + } +} +@media (min-width:768px) and (max-width:991px){ + .visible-sm-block{ + display:block!important + } +} +@media (min-width:768px) and (max-width:991px){ + .visible-sm-inline{ + display:inline!important + } +} +@media (min-width:768px) and (max-width:991px){ + .visible-sm-inline-block{ + display:inline-block!important + } +} +@media (min-width:992px) and (max-width:1199px){ + .visible-md{ + display:block!important + } + table.visible-md{ + display:table!important + } + tr.visible-md{ + display:table-row!important + } + td.visible-md,th.visible-md{ + display:table-cell!important + } +} +@media (min-width:992px) and (max-width:1199px){ + .visible-md-block{ + display:block!important + } +} +@media (min-width:992px) and (max-width:1199px){ + .visible-md-inline{ + display:inline!important + } +} +@media (min-width:992px) and (max-width:1199px){ + .visible-md-inline-block{ + display:inline-block!important + } +} +@media (min-width:1200px){ + .visible-lg{ + display:block!important + } + table.visible-lg{ + display:table!important + } + tr.visible-lg{ + display:table-row!important + } + td.visible-lg,th.visible-lg{ + display:table-cell!important + } +} +@media (min-width:1200px){ + .visible-lg-block{ + display:block!important + } +} +@media (min-width:1200px){ + .visible-lg-inline{ + display:inline!important + } +} +@media (min-width:1200px){ + .visible-lg-inline-block{ + display:inline-block!important + } +} +@media (max-width:767px){ + .hidden-xs{ + display:none!important + } +} +@media (min-width:768px) and (max-width:991px){ + .hidden-sm{ + display:none!important + } +} +@media (min-width:992px) and (max-width:1199px){ + .hidden-md{ + display:none!important + } +} +@media (min-width:1200px){ + .hidden-lg{ + display:none!important + } +} +.visible-print{ + display:none!important +} +@media print{ + .visible-print{ + display:block!important + } + table.visible-print{ + display:table!important + } + tr.visible-print{ + display:table-row!important + } + td.visible-print,th.visible-print{ + display:table-cell!important + } +} +.visible-print-block{ + display:none!important +} +@media print{ + .visible-print-block{ + display:block!important + } +} +.visible-print-inline{ + display:none!important +} +@media print{ + .visible-print-inline{ + display:inline!important + } +} +.visible-print-inline-block{ + display:none!important +} +@media print{ + .visible-print-inline-block{ + display:inline-block!important + } +} +@media print{ + .hidden-print{ + display:none!important + } +} +/*# sourceMappingURL=bootstrap.min.css.map */ + diff --git a/themes/matrix/client/src/css/01-main.css b/themes/matrix/client/src/css/01-main.css new file mode 100644 index 000000000..318b90e25 --- /dev/null +++ b/themes/matrix/client/src/css/01-main.css @@ -0,0 +1,77 @@ +body { + /*background-image: url("//*img//*LargeTriangles.svg");*/ + /*background-image: url("//*img//*RandomizedPattern.svg");*/ + /*background-image: url("//*img//*background.svg");*/ + background-color:#000000;*/ +} +canvas{ + position:absolute; + top:0; + left:0; +} +.authelia-brand { + font-weight: bold; + font-style: italic; + color: #ffffff +} +.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: white; +} +/* 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; + color: white; +} +.header { + background-color: #000000; + color: white; + margin: 0px; +} +.body { + padding: 10px; +} +h1 { + font-size: 25px; +} diff --git a/themes/matrix/client/src/css/02-login.css b/themes/matrix/client/src/css/02-login.css new file mode 100644 index 000000000..a6984267b --- /dev/null +++ b/themes/matrix/client/src/css/02-login.css @@ -0,0 +1,136 @@ +.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 #000; + margin-top: 20px; + padding-bottom: 20px; + background-color: #000000; + -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 1); + -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 1); + box-shadow: 0px 2px 2px rgba(0, 0, 0, 1); +} +.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; + color: white; +} +.account-wall .form-inputs +{ + margin-bottom: 10px; + border-color: #b20c0c; +} +.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; + color: white; +} + +.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; + color: white; +} + +.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 */ +} diff --git a/themes/matrix/client/src/css/03-errors.css b/themes/matrix/client/src/css/03-errors.css new file mode 100644 index 000000000..e9f97f33b --- /dev/null +++ b/themes/matrix/client/src/css/03-errors.css @@ -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%; +} \ No newline at end of file diff --git a/themes/matrix/client/src/css/03-password-reset-form.css b/themes/matrix/client/src/css/03-password-reset-form.css new file mode 100644 index 000000000..34066bc24 --- /dev/null +++ b/themes/matrix/client/src/css/03-password-reset-form.css @@ -0,0 +1,4 @@ + +.password-reset-form .header-img { + border-radius: 0%; +} diff --git a/themes/matrix/client/src/css/03-password-reset-request.css b/themes/matrix/client/src/css/03-password-reset-request.css new file mode 100644 index 000000000..1a2ad4df8 --- /dev/null +++ b/themes/matrix/client/src/css/03-password-reset-request.css @@ -0,0 +1,4 @@ + +.password-reset-request .header-img { + border-radius: 0%; +} diff --git a/themes/matrix/client/src/css/03-totp-register.css b/themes/matrix/client/src/css/03-totp-register.css new file mode 100644 index 000000000..cb76720ad --- /dev/null +++ b/themes/matrix/client/src/css/03-totp-register.css @@ -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; +} \ No newline at end of file diff --git a/themes/matrix/client/src/css/03-u2f-register.css b/themes/matrix/client/src/css/03-u2f-register.css new file mode 100644 index 000000000..e54cddf89 --- /dev/null +++ b/themes/matrix/client/src/css/03-u2f-register.css @@ -0,0 +1,5 @@ + +.u2f-register img { + display: block; + margin: 20px auto; +} \ No newline at end of file diff --git a/themes/matrix/client/src/img/background.jpg b/themes/matrix/client/src/img/background.jpg new file mode 100644 index 0000000000000000000000000000000000000000..974ea273fa87adccec4b4433fc4d097c04ae4c31 GIT binary patch literal 587 zcmb7za3BG;m3jxdn?P_E@h=1|8$YR;59+9 zW`I5}XRiI4&WC@yzle_&cQR_(0pU8Ji5j{Gs2||wia~Q)aTB2CVR=3aT5jO?)niY+ P^@QfW&?n$dCIR+GQO8ew literal 0 HcmV?d00001 diff --git a/themes/matrix/client/src/img/icon.png b/themes/matrix/client/src/img/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..040d10c1ab5feaf6720fa8e8c0a0aa336402894e GIT binary patch literal 1461 zcmV;m1xosfP)e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00kXML_t(o!_`+?Y*bYgUHhE- zn3=ZI7qzh{%0mzcVu-|44KcBlMg%4JNF)*ye@ry-m;Ooo@C(p{@FNm6K%#tzfV8Dp z(gHC?u?eAyEkY@6v9v&$K4#|5J!kuI=XL2!XD|}uPVUd$_w2p)Ui)z_@E;5PlS6Y? zzD$W&;aqBgCCWIgr3%#B17TU=LfaFniBfPfGHk@o-q0}>pSg3elo_&jXcox?pxuirC( z+64_w<1wcpt~Ct9s1v|4#r%X~rnEyMkPy>M!hxWQY!F}#Xodt_W_^yC&$ChylYpwS z&=(^YTQB4VP`#+(neoZ!X+^?|Fo&2@Mx>OVR#uekyFNbs3W#>4vjG6i{zq#-D?lmO zgwb%&er)to`<;|MBis1xiRpQoq=1lspman6NDy#26twqNR#fgA_~q+c*8$*>20$i( zX43)A%7y?m0U*TPM9rNfXHy+Yt03Y#2NpCw4d56E zr8CNk8ARxwwFZ)MCdHT&Pdu5eCDZpn@^ zg!ri-09%OTsSF5HzMiccym0J>)_N^~%UQkOi}{T!LDU98Rq07el%z1z(&37?5ok7xHr~3h{1!EAv9uU=n1bkaDm3plKtq}^CpAB|@>l6g=Th{?L zO><|;%(Uw^S3mw@#S9R`U6(-E0l-S{0N(|L(*w#PM1vt?cfmoFzLeC?=_s+x9)RJv zx_T-a+gbFrCOycqq7T~N44pf2vuFUnckNe}Et`c$GGYn$Em5~-bAIoXqv}29Cef4~ z2yM@YUF1_#SCxNnnX)rE+6@p6Qr(*6e&3X1gC-+a8c+&BL+%*qK6K@t0rdTNNQVOU z`=%5Qz|c$<11C_oX0Z=|Z|P*x4y9laU$!iJR{F$j%3FW5_5>uoM=4&3>Oj<9vSgh% zpy=+qV9L1*!8#Ez-`$zbhb**wobmo&McKSC1NF&>%U#7=0}m|ebs1q0HD!v9 zy$h(TT*3Ty{Ax#AA^WnJjZ3BTP6{EanRzux5Qt1y^Q$1#gAm97P#~o>v3^Gnb?z_5 zc2NL$a)2xAmbJ20*BIQu3@cgwp7UhUS%QW_rq~p}dg}0f`!}4kbp4`eG`2hHsz#kW zZxFdbQ|=801KV!&9J_Phe@-7=xryR#eBI=9yqTE?O9J-xk&7+8_&>_uokeL63{>@u P00000NkvXXu0mjfXm6c7 literal 0 HcmV?d00001 diff --git a/themes/matrix/client/src/img/mail.png b/themes/matrix/client/src/img/mail.png new file mode 100644 index 0000000000000000000000000000000000000000..834bfce9107a94be10da4c011fb78e37a192888f GIT binary patch literal 3545 zcmWkxc{CJ$5dZF4tXtNx*0s3`#S$s!5_0FZ2?r0sk5=BUk&_Rfm`^vTD z%#kBkY*-}m^PBf(=FQA|pU;~&f6Pq0iSbnyMie6efJIMN%k0Dx{#Q8NiEUML>OFC= zAWc0B_=((uyT+Zw3^#Rcf&ici|6d_HOJ!{*N#0;>T(G&ndvJ(Tpc@DY36b{n^A2)x zy6GnEAL#LPLj`rhV5O&}VG%m<>)Dz)r!}gh`sIE>yJEBH0oyW)_DS6=t)VW~UUAj$ zm+hiLcfH+H{fex~j_(uClb=^=A>;#1uWMUy{+4Tb4*!BdqZuD{5fW-KrVz0}Qw(L} za@q)nnRz60-@;pCi~Hhm>IUq&0(@ai{WjCs-tr zACjZUSIC+XRz?w>hDYM}b7}IWaF45pAB@ivor$T$1Y)i(IxTkrrMHkX&kWKPRp1hg zo=mAMrPeyF;iZ{Vt?q%>H0ZzDPZKJ|(U#Jl;f z06i6&8@wn?^&SkpMos(hkQeM4T*f2MgzTaNS1mA$!umiOb5LgSwFu%u(u zxl5l!lfKwD2^vQ@s^A+-wh}8pN2y1)F~TmnSLe`z?}x~jT2_L8%H|axd=D5X`;&{> zP>X5~|M;ND8k#(ZG=gC1d}?gqKl-%2@dGXVHzKB%M(%~U@vF1V8GNgTYJRv*V-%RB zE?7#4CY`tCW6Rh=X>>G{9qOo_vpc4hVxP@Q_CSH)2MD614Seip{M{ETb1p+mL1AN4 zP=*G!s#NrkF=IRj2zEi(Am6vfJ0yS9t@)iN$CIJClYjK3=j^;zF+d;xVL2xn_C8sB zbt&j|^EUgp$dr>75LtlGxGnY7c;~jzrPG&&5M|^-kJE=CYq(H>N7h^*^;cOA<@|L7 zo3*HDm~%9f-e&kdFH^h|Q4$a%B0dOms8)v!9&#R~<$lrR33!A8q6wc?Y!XMKRwfOH z4*XY5U)3mL|0YU{TbM`kq>j85d(Y6R1%<^YK(Rdy|IFlW%_GBRvvkXm9gFbA>2zT4%~wgv zAa{%nEl}FuVcZ8qkO%y;u}g;FXX8CA0(V zHo$2?uxjL&5mzp7SH^_L2C;OiCX>rmMyoy;vTG$k2nW{n0cDEV02>9Bh=VB)tdf$W zrorSuy@_@0rUOW3$ZL-KDckegvIY&h_WoG-|1ck^9o(Uuv9{`?2a#*EvstqrcU;OkT%&O z&?2q|bdqN?hn4Vf7m}CLABr98mRx8-;?q+v5XK08(*u@sx_(3j* zwBweN+hiAHpXTP~5Ms>Mi#JY+7E~4JK=DjQpvuacJBq6F3_{QaKJQs^b}qae=K8>g zmjUqkf+}w1a*BP5n{MVHzz2hy<-_{Dc)dbjoQebl&j$rIfuF2d!KJx{Xan~57;aKY z*cp0oD@2eKqn|i>$qf2D({4472CcKI7Y@N>p{=|McnE1GMUYE&cW91q>S4?h5CYdc zD>jt_+|+-^so@x@KH>st-l0mkN2n*Xf7vo~s4j$KQNxqukdo#tn*4rG<1cq8nW0=t~jx@i5$Yzh}-9R7YT}D7A6DYU5 zB}Sommbv#GGl+$48vKt(LSc7%-B7@|WNpltJ)j^na_{1~R9U)o$SYR#Q*m!7C{I!- zq6B&H1TBRWX!)D}Qy8DT#!Dy$H9;TT!7N8x&)vOj*_r9}hi)PIT%O8qz!uuIyOIMX zmf!B#>W3K9xc`td+{ymLd&R7@QovuOEPBz^2?*eD1G^Svw3mY z--Ts5TPh5EQ{L4ab8gTT(`bvI&U?)fx@UC?;hR>-?)nV@{H=LNX&YboU+{G`mW3mT4zkU=M{34>Ue(w*XT0L=|s)EEY=NJQB zyq{(qvcLA3&T++b|4(_N8|OlznVJ6QlY% zd}XHHJ#=tl^)=sY=cnyIL1U&?XP5cYh7^8gE_ewLFyi9T36^c`=bVb~+_)H4GN63; ze!01GwqHW2(hWpeKI4|we%qg&+E-E<7bRi~BbBbko-X)$BihDeI7>Ei*3(%H9CW6?+!kwP)K}$%-iX&Fx-dO8OayE0>WtE?}Ceu*lDoW3u)c+rZ+>D*wT{+UNT4v z7y!ad%qetWFY*OwQ>w&{#d9_*L_QC`=Oxtk0(TaG=JPJtgT10hh7L^Ni=L+Dk-}j_Zb{DeRbsUtukh*#)(O2maD^ebRpzH&k zc1-{#JRWW@uk?3xYia$^u=P^Uf$z}^$Eya-26HZIyOKl{fJMO_#eP&R3VmL0WNyWL z>|hHtJ009@t0p!BZ))12BVh4c+K$V^w=fsnlpnfpM6JX!51A~t*kfGwF1j9})Q*Uo=fMgwFn^O>KYtsCNIrx$_O6?4zks$w|$aiP)S7HbUnC?4#0( z?9RyvUybw@hfr{p?rAz#dHK3NU30$NVMv4d%yC=7jQgzx)^|a$^*x7mKbVyrWFU8GbZ8()Nlj2>E@cM*03ZNKL_t(|+U&e(l%>gC z=J$(;cfD)gn|oGfW$k-+S8vs=mb%qy)M!C1w9vw!BMAv%jIr5)Rn=8ldsSvuZg;=$ebfw`r{k`&r+VuEGcPz}!EZ+9YuRr_7;!<*7zA5(l_kuH1 zCc6?7)+@yA0ge3=^e%QWHHnB4%vDfGntIovK$RmRqMc*Z+^bxK%+uu-{bYDZW`Y`jJyV!Y6P0Hvqg~i~2u({5yZ_ z^0}q&U;5O!dh^({S8Qr&Hb=x4M#wuuCcm|Uks+PW53sUNaCe>hRGIYIgsszE%D0w@ zpB-Ww8nlPA4*9S^T1#+il`PLO*AsNBKv;vd5@$?N3@wf?F?Gk-R+8CFw3QN{(1ZLJ zjAii6_Mqf9^5(wp@=yQu=O;HGKXKygZ~a$0KDqzQ2S5LYu2T#D+2C3{Z8r$Kfs~a! z9`P{7`VQr4Nb3(eM5hMS54-qFCNT0bJ^V8v6sjtf7bmWK(+nm(U2y)?WxT{N zOnNN5d6C*=owcv7;0Q_cduPdBj1dikTlX1`QbtQ1)LBh>#o*Kga?)Yq)&+WMfLZDi z$TB%8gVR043KTaJY{_8S2H{(bV{vOCw#adt4q00hB}4qo!DI%l3^G+y*&uqo((Xrt zLzN$_yl(nR=114;MV$jUM|{kt{znG*o7O+={4$@v`;&a{`Tud@*kkv8uI|@w43pmP z$E{(Lfna!J#PIoR1d|hFrI@^z;dEWv!#17u9we4vM};g+8J!toB1wK_#Eu7d5X_b6 z42Cq{SjLnEg{K*D#LXov|MR1e}y5rhtAXi>&uj6iCMT5!NBl9d<=ONo%& z0Ky_e5A6!vp@G66JUe;YBX|9Q*(h!}bI#|OQ9FvW=|3{S|9QFZD`U>&0Gw=n{gyZV z;(LDS{PN4ck%;t=o4DbvyHD`^D^F3{QK9|#2Cgcz{*|kES0&kI0X0SQ;c1dVN_>6* zBT2p-W80e29W|U-(tfOkiUs*hhLw`q9TSjb?0#?`dfuhjikLh!$tcbdagNC>7DLbo zDK>LN4itjm^&WYY5*(|NzZRqW38iB-3Y`-?FhOx?fDI%Vh2ts;lQI|#Lpt6eRp!Uk zm~%js>}5N!YyZdqz^zZdiTu+Ml^>s}-1+z;FI7tZDK*=C|0s;r~=gaSyewH`YY5+5j9{m3w z0Cs)p@V3T(ajib~zwG$6{ksngK0N=CWzLHYkGtK4R z64^~bc*w)u?NNW*1Y0+HR0b7v+kh5K+%rqi2^c=zX6)_|bEHa9$_e(kXAzXAzW0gT@NqRjejWTj$ zkadZ41u_Uwp<(HK79a(3%uff{bA7%{j@Kf*P%mnS4)ZGs2zkC&?9kO1Gk%pqH5wT@(vWxB8 z?KIF;RNqx)^y+}HTqfJdKx>q8NiQcz$Hf&cS(GCT5DSdf6ulhf3vA2a%sbewgl>)r z1$i&R4L$Z8IrP5S#rb#lA5fpZ@>5U7{~!Ty`4i{4_r-UA&}l|b1{1;b>g5}_6II3* zny8v&>E>(PeEb4)-?5KyA;4`a#BzuYQ{=eI+>u4vPqY|2HckIj2fLL(P0@L-g7mbN&9(wmXxUhbjJklsf;WQ+9p|ODL2$Gc?Th39d4!yHOw9Jr>LoqT8 zpY0pEB<|pb4(>F$!x-b2M0l?HdW{8%Cl-?SC zEs6&}Y*S5S60)l?_nzj(PR%kRPDJC>=9^)@N=%k>3s!!TZIDhgC)uRofueZ^og8Y)^_Mf?j_F#kN zz0=etN{mVxc|?&u)kQ5j_=kP6QzLBKF#nF7v`@ECTYwahL+u@PVmYL|Yl7s32#LmN zy5xO>Yz`6Dr z6?=1P&Ai*7fV`QSltwKN{9ucD()wV>4BfAfY)w zO?k4yfw{YQ!amN8PoH7p-Mc6{8GhTtd0mB~zg?GKeR&lfS!lQnp6@a0k*^q zhEMj8(nAkZWKH2z73uX6V$z^j`dGkar|@EysynoLw&I zC_x53$}o;bL4DUG?bkLLT_2LK#5C^TLF`1> zvO&3k23dn_DMMB~%w&ql1V&488xzhnF-b;P^=H?vcRyTdH=h_??5qa&H7Rby-#kH} zzgUB$KzQ<9I*XsGH5)V9#Ec!EBMtgQGDUC3Shs{MD>hn}dEw8VX6$X_m=Z8$sNOP8 ze0qzU_1C!sGJk|c@$pV}yhxA$>HSYZi*8EY5NB?XGr{R%! zF=Ajb{hTx}5d9&#pR*+gI9bl{g^13VZZI)XqwkDJ<`QmPxI*WJ4U(5TwEnV->bQ)) z-lhG?Wv=|r8A^*4L|72DV!#qz9I^EuZelu;)jxifuq1Ijhy12x-=TvzmjxT2U4xz_ z?iZ*#TpABf5a$Ww@0!88+oOC}Nae&B^*ic_TV1RRfeILZU=n9n2uffih{7zvhIVl5Lf4+*`U8a7pOz-7_eAGvd2MnGcaQg#yv+ww=tR@%eeYK0z^l%p)yd5=6 zKOtMmQISGu@aF?`X0Ta-nhz-u=vI!%ECbOcz7nB(hG372URLCTl;}%+^w5%C&&bcj z6cGdqWmF`1`?r55XTJObQYs=bKn#J{ps!~b9D3Kc&|2Un5_Qa_y|RUFXJj40xBkF8 zId$qOkm+!9mltdBp3{A}r_%Xsh8`28khgYJ_*(MR@|X&&O}Ls7Zx)yiBQDqL2yY-66IPj<2x20mijB z`#hWm7|(KT=`tdh=*-{!-j@))DzHo8gFiz6x<3%N;E5Nn% z&-by*g7``YlUuTWf$YYRL#}i3HBD)Mlk!BF3m-d0Fj2 zUy4x+{x=>2q+f}t+*3c8RS2AEz!zMexNa0)?TQp$HvqNWu|He~YnB- z?ygd_GD?T4MCTGjtN@EswS;%ouqZ~)w5cpq$XyB906BCpQA+D(IvCrtG5u8_S zd}0;d6DS>^wIxps?N44u=Q%dF2)m6@J1o$i07u|WmdUe>;Yy5jEJybqhH((%4%8gP zP9HOfNiU_0o*3ewFfzd=nu%L?kgpEOmq)nMRZO*@JXeFsNs25c-%4;s29xG@(b@my z&)xTHR!+PBWdR_)Z30Q%{8^#p(c#zoxVb=R$m1L?Mx@o}74$cG~&9wn)m+O><} z<`#A!IQPm6q@7LbhZ@{`w#}R0`gUIbwugzfG}eMp3Tr%!jhTFCAC-63@%K0!{?I+N zFRtOJ0O3n?D`%`fK{2qAu*BR;>8QZt9=3|V6@fe&;I47 z(9a6;r$p?r4;MAoO19 z=`)18%S7ijFZ|+HsUI367YQPd z!N?h5uNwwu`;ZsNaY<=DK+XH8hDKEj{1cPt^#nDkPz@Jn%poDcse4rC#t8R^WUE8M zg$l!AjOTiYP-4>@r9!MPA8fpL$EOFMU1Q;scm4eWKzafJD98P{aqas>E|3+Akr_f8 zgcYDI!WwV{&Rhv+zD5}MbmA6g!xp@p7S9De8)rmMRU!bf{Kt6{$|~XFRebp=dzqh(eSUe&FGH5<4^$nWAW?SZnar_7N3P3fFRMOJh13YbC-! zX*>X5ZL{69gM+4ecpMHml<#REQh{EF@OT|H)Z`nMVznS&O)%DyuWMvikZoj`k%h?$ zZ~2)IGq}(t)DB6e2^tDpm7ohu=6He8c{Oj<~xG82!w(9(I)XI!<#N6V}UI>h&hS3!=3akVr5eGbEpuk!%2BaWQ3QVeTw1ZPv3RjX`>?5`yyD`8N1;TadKe35%!9k%G zEpD}p8g~$xAH??;30_r3qWoqmMxeD(YOegL3K&M^ePZ=^=1a=Qgu2`C&(k!XaYFxnu5!j=@yf={_# zCVygp*^E(@GB2f1vvq2N-ji*_sLbV!OPDJOYvS<`>4jjRb|BgfkpqVcd3E^{B)i5T-h3)DPVB~XrwMxawe=g&9cMN8|qSFwFf z@>&E}3r1@x`Lzh$EwHZ@6fYWzL?ELKVRA$w=zXSx-2^=-Kq!O(n`neJ=)jV{CU9ct zzS_cWXxy2Q@~w5$enDltj5`q!zCJ|B03lUQlfG zD9ujbh5^$Ql}c9TarqtzIHca3no_S=U)acuGTZ~6FtUI5(k7yrB2b%w8li?pBt zoC3Q!Bt1FAnGLZHh}42EkP|*C2pC@NP&+b#8D@-%J}1WBNZ!}@NUl6_j{I~Vr|wcd z60&@Kg<>NjiwlOfjrA1_G%5t$a4q)g@F`DKaK;^&aR{mkAq}GBf%Py_r+4Mc#87u#HWWeK~HV?9CR-MgqX#=#cl{&TIP_Ga;E#udc&@0I~Zzt|%D;pU_L zrNP@O^%D2}{F^xN`TLQF;Z48!?d-gFA9*Lmj@j)1$qLkji>QIq@ELybCgrIy>dkS2 za)qp+VM4%26P+0%rxo0=w4T3;TT?8ru2J7LM&u50s)o^plz7cBe7r^Hl{VonpZIJ< z@^}O@AyPwnIbm{l14;^G4Nl#`j%k!-$6_g~#fNDA@&qL^(@w=x9 zXIwahyfTYUdgo*OoET5IT}%t&b{TaJ`2v1TGFmO@dJD ztWOX&ONc8uGS%-9P4!_~8YbPpI{>&3RfPK%H9j@l{FJD`*pYEg{`!j?DjlNvzA08N zt#JGY?&O~T`5_kHuoqbeltmPzWwz&4k zix8HguW=59=!zmUIpqTZGyh-QH(NoYDW-<bgmQ7P z6vj~f;22`ofpG_G48~Y&A+er7;ZgA_EZ)7JpZwjQLzX0tP!xWS9SX9Ql;lcEygJ0V z8ao16R@lOz2Lk!44qPmdZjIj3!zcA`^*~(Ac|+=xUdh z$FE?OiwND1(QvD5=6`n*V6J4y>F{%&k^4I@^zj{+)|2ZL-hkb2y`A&F`z&&gz;D)> zx^t4vY!y$ASrBDvOK{@w#@y}V3(+EBf(MqW#Ba|h7_+emru z5B%4x_68(ZHpnjwh@+U`t~zC}0m|SVc1g|+Xg)fDR1Vq61TiCMylITl*$5pQym1#C zgBV(r28|@YIzkRS245T?>I$t3vUP)Ml*nrym9ijdr4+B`WWyX4Sac?lQebn96#^*~ zwqIa{+|CHB!AeP6Z8B>0`Pv_Sjck1cF4;YE73jo+wadl{AEH3l#CkP=u> zP*s$URWLU+%NJJ=yBvmJ+#<~iJf#q!C0|ZafKEY9JM4VNBK6}t=)AhY#$Q}RT8orI z+PruRiT#+UD9G&pHwF+@fnQc1w(G^Wjy*C*weB)H*~gADu6*Vk?K5lK_G7PStUAg0 zufE87Yl~f9eH_R}-9GPIS6QJ)ENd*aoVu z@TclL`R9MZmES+f))N~PQs9&nNn{zc`Xn1E#TR;%?wX+YmmTaVL^}rC)3knP3pxfR z1-4@lbKo5FaBnLycBGE38Kh^iK>;GiMuxPdv0E7|4T-xsp7ao&M0zeVx7flW3xgAS zh*pNw4#u@80gfk;fkIl%H9qPXW2FO5+dtaas<;tFbt&3Ysyl#f*=@^6{HA9yF@)E5ZEuwRMvda)Tzn@e>QIf9x{Z znnoCfzLBCgEjxBiVU8KZO;CGWgc8`IK(8hg7ZRK?pWs-T{MvRAM^qG*H`meY87lY4 z;so8wQ8k~+9Tnm@W&GhOvNR$+ouZ7zJ?h~!16g0tFGUz3P!&afHbVw3G8K%{1pQiy1%eZGvgHv3K7Lgo8j7Oq zk+%li`)zM#Qw+$?bt$g)Vb)>hj@={|2h2UZpYE%hJ%hGc0$ zwK|D(OQZvf@(d;wSgSCOVe@PoHOyh?(fi{K!VlCaPgIG57~?6_UdidOUKY}d*R$yd?M1rG8Hbz(r zdI;9B2uGunlp@a&TC=fqop7cBb`7fyQoC4b@EQ({{bO`GYXonoki0rT#3^RrApI1z z=(GFLL#!2RIKv5E`r1iG-8Q36jITh>PLMYxvSPXNYcCQ``V36MjyLRO>Dnne=i3y< z;249o0V-5r5MIsw;qKYq&;7rW0J+gj7t-Mf?T`JVp{Jscpk zWa>v|2oC$W8?gRME9^MBfO|-hy9zaCnW~i-#tEae!-4tZxT3`HN`h$_q>~bi1xVxJ z$T~$!lh;!E*H>}SsKpY_9)Twd`xr4o zs2sURPFX`?z(5-5Das9MrVW*nLezSs((1 zY`9nq;q7JQv?SPD#z=$pT-s+^=#>J2%Y)zcD6*Rzlyf^hZnBt&F^#PU>#}03ZNKL_t*5=g|HG|h^>U-^K0l< zLFtwzc3+9mc-(yQ0;NK1#}CI#*qVoTK%oMOyCcATNA+hXp*ity7yh41fbD9`jQ6q3 zq_-1GgmQ4^VKhp3?delQ=MvneKyN0bTbAfz2Pd(}eIe;+2t9%6S>nwR#ZpSN9@Cj! zW9`BQ_1QB1q)%GPC>^Qc&MNAACaLW3(0$4D;VCwsTSqorvNJJh3j2O&4_ZQDGiE+? z5K*(F%Og}npxYYLHpnrMuEcaTIs)qm%o)4w;olI%?LM}zQR5D(CrOKh@_`1?7rPj# zY1}u1E^CTGgxm5+ha>Dz;_epMfkGYHE(@D5<$HhSCwS$tryy~#0vuym-hGX=CsxQh z385d-m>Q>Zv5hz3l6NwsGK6(OHYg~cs1cuy@w7l!3`LStfBiT%^EmUXCu!a?Pm;wT zT)eP?Gwu;B_o$V}@#bCP^&aCVcJs<7zl^t7CpZz1UK`=~Aw`n1>*w|oP6eb#om;kk za`kuL^x}KC`tM)++X0|_e+6^U9FckWG0!hBg@Y+HI)sUXO$L{GXq{|3XO7niELh`W zBg5c@O?JFX)6ZZ4l}pzVX(eQDX22|T9@khK$% zt2vGPs$?n$G%f^&* zBkI+Z(!L5?f4Pq7YE&Q?Jl;VWLsI0-e#b2F&4|)HO`N)qzL62`F42E!NVb*Zjrq8H z9CX(qC@K6C0nToZJ&zuxwYiSdmH5ZYG|i$ZVXXIVO+sK z``Mr8(v1tW&ur2?yULZXT!a=B{ha(_3d&NN_Q?leJcHTFF@-|ZEUM}d>?`32kLa;Y z+zvNl9*`WTao zjn(D5I)AqDJAW$x*o!Ii_S;qWC(r(2{lo;h$*~FOE(qVS__lqNk2lzH`yPhZJ9uyN zDPGnDJ8GnZ2sx&3gk*FjK{dhKYHgb}y3BGH|Q;ky(h*u)mOrhoyzY>w1FA%kY=u{8g z(&T8IwkC@O=CVOtv%K``lMK%fx0_)VKpLKY<{6?J1BzaTjJMss7J>34GRrAkMR>T5 z6$NA8KFR3$5ezlXv?Mnf6K~y#TX7j)=n^)|)Nh@l{lX2*gimlpP`bZ_no`{H=DWxv zO;Gc>`K1-aO-tcfI40QnhJB>*kkUJvyyaWp!eAz1e&?=zSIoKJ{EI< zIX1l7L$4W>19oIU2wGoRr~S2c+Fw~CUX3{P_FGBkG{Q|V1BqErsP3JhJma(Y{{3uz zdIRbO?xLh}R~fUGAy6bYdhB@T0{O6D=R-3j{S;?D#4AY__a5ZpU%WzX=Oo!ypZNR; zD_q83UuFH{Ya~~Pq^&l?r+egQQ-tuy%Q<1)W$vMQHvVK4k%PY&;M594ASf@^DBKj$ zu+-l?x!rqDbE>yih=(acD_lRMNK?W?bsGDc^j3PvuE1IFD7JFETYU6TBOHqr4z`zJ z5^!riMq5NbC)YV5HsCmDt(khy6vNXa8Xw+)PIH{HB0igto*7|!8YplUtJsl7g^J2d zgTc!iXlu!1OL4tV^=&nrodGj)p54wKUi<86Mk{@$_AIcmx=y7WQa;gO@CPkAUs%VK zB-vl~dHD-3P;b;poCsF@?&!ttJlIQxBmmfm)0s1^u4z{5%iNV(AWfQ0b@s|=$;y) zFeK0P(91c=jXp??2*viSfCf7#kg39*uaG-2);Guj?0MrZrha@U&ABn;$pUvnasR&i zX&$dI``{vJ5z`p25xEJ)(*t(CZ!dPzq1en|(xoz9r;`m>Us)#Z4gbY|QUZF9w|LTg z<)Ne@zb`u-VYS89CB@C{AC|5pU{fVsb%?^o-lP{mV7ElNN~yl<)c-yGcguJ>1dr~YsmP~8goYu5_u6xtR*arZ@M{jY{`a@j zjXLOyny~3J@xV;0f1`KO_RZfc0do%@eC){KS%@9L7&011B3%b#eu}26OBLS(CW9T&o^+>0CPPd?PnB|5Ix;Tr3&4ONX{tw zFW-c#3F+m8WVOrLm#!c(i;WFJL+_b3`2|gUy$_o?`NbS3RzyjL>nn;P!BoIr2P<-l z$l^cLM1Q4=2^7tT#z=4EIC}(k#gb(yjuePSh~IQ^#sjp@sl8#0=?CZOU2mh4oW|Y; zX)D2hQ;A|D$7;)-2M*D@wuMOy!gmmbLwdD~8wQYDbPXB@#!1{h=0b{|5saQoP%9EG zQmz*xMy z1AJM+mSe*Gbxcq|swpZiZpUIO`F7f-1XgB{DI8-M{^14;Ei#d8{qY9Hnnk{rAw7W{ z8Jzt-!{>UWM&MQzHixa>US~L)AqQZ4F6|dv2rE(2MU+xJB$q$?8iE{9*fxTSdjdt4BPDS4DK z?6z2Yas?qIW9}sH-}her_47Z+mW?pSn>f!|@<|AGd&Jk|_MM0fr&=KD2IGUQ`pC@z zjx3*W>SdLlS=av;2Cx@1Cf`2)J<+K3uvk~vVGgcD%$1PRVp0vMMF3Vxqyg(296Wko zYoi)2qNT{XDQahkkr|E`;!gP_C;L=CI!9>`GQ1RHvu%ThYC2eBA=5b368TDkD!@cJ zp6ipf3qW9o26KJH=z4;_k(0MG^5+$DL4#{4?WmA#L=@Umu9V378D^B@&WHHpAR7Do&0B-;(9F@f$C80{mC z-L{7X8lgZoJ?LwU6x0t+ply!c(u{xCEOyL6g$^>b;8^0Vq)9S^U+11OR7TVPcE7FZ@{i^=Qh53Pu`)zzH18sDNPs z!i7R?+n~n++`5mBMp!8^dp*3=B7~yo#5m(s*osNkz&~0+UyD$u3gYe%>nNbWcoty< zA~jHVFp=K=i#5l=YS3E+c2FRMhc+N{1HMHrl*q3S(H#R@iflDPCjuvvz4l&feZ%v< z-)N3kz)T^LVe#)TQJpVvTMn&MeJ#cc63||={=#v3$GVKq4^U~${K}9JX;k5n+ZDyu zoZ@PRp9R!RD9aLsLiH@|!!4qA!upvFzHLuw)hcMi~2qoxj7)Ku`Oxz_wOPcA>m0^GhQ zUREoEFYY{~sO#zgsubL4hU5;6MTsL?)ToUJSC$laW~~40X_}{7{LO#*_h~-d#Xi@7 zRfAHF_~{;@U9#}QEvjwL{`MG@BJQN9NYDkytXB1i7?jeO08)X@z}ni;1Mg8rP~8}f z#c$3b1yg9GERngO)-2Lg=&r>&i}V7vV+k9A?OW8Lgy?umv(v!a2sO;`xvI$_t&u!H z7SJPM@@FGuow-nzr)8xz7oP8z37-*FU^ z43&c$l}x{v;V|d}!JJKDNi%wLOx%o+-9Yl52JL6MG@t4at#*i~u<*XM)PLA*bpPJk z|33|M*8GxG0SSNoR|QRt>Zj0&AQqH?XsLl1kIEFft8lGAc`N5z|I`1&{AN!1tvMz{ zG>^6sFQ_2QZ{}=#_I^|XddZ;s4kr}ID$vk*f^VLA%ki3nSs$O>_3S@_UVq>-bPsyl%(*s&#hXTs#% z1g#7@2^@d+UMMT#D1}{*Z5#A{MMzVMZ{DWbDeB*|eTtXoOtvztZE<~c3$vRs8$*93 zhMpnY%xNwgn*9_V8KjY*$3mn6*}zeKZ_F?K&Oar6sKMD^ew=iz!__~y$dN~mQ@l0h z-k&&27CC0GZQJ#eYtM{-WBcD<0jocEMCJ3@|0w%VyFi7%Q&C-*6SupR!x_5YAPh^~sKCt}ssh0iJVYH!H4T*8b7t3P zBvVT@5>N_CN96;}LoKr13DHUi=_}5D@+q!<^BmG@V07(n5{rgqO?6P91hloxFO3O> zFqq!rYIGBu3%B09MU+R#?SkS~ML2+bTGH8AW%~UgX7ihw2qMd5-PCA8~90yi7aaB@Djiolow=H)%1%~i1Vl_K+s58piM-z zH>SE=B5i|Nj!`CX;^&Vsxma-ILr3wCMufRXSw(szL9fQRafT{|*69_B;gsab2COPt zD_!)72)8?jT%pE+GIJ;s5D{?op{ZA6XhcX<5=Mc|?_P(4fbs#!>w=OOU1w9HfOtdu z=_UGSdMrG#$aFEMnCA>`jJfBBPLN+5Gk#Z)Q;zQA3oM-JvcGpgW8LC>gk6Wm^F2(a zX`E^>`qlwjE3#X2C_T!?l+!sl&*d(a-IJ@@^>gB+aOgCb$Jb-=%B5 z9O0&hxNj)6BWbq?mB740v|t1zD3sLe$6Z8Fd;Jv&a@_#@$ba>jU%2w!w?D2kLx?;& zO`)tvj<@hfG-fa0wmoLWVttBgQ3o_8VBgmvDk6+in2nfnn$e`g))%i6W*%cI(#Kb5 z{hdB?afGYth&*h2jMD^D#{m)>BxqFSP_adMC`NO%70&*>53u!>OW-vj3Q*|Uztb3W z91*++rBMb*1vNEPp+E-%K~PnwE#Sr#fk5-YHlb~aPb6fwGW@8-?-lsmqjZXzIF!;* z22{5}d43F8OuQCRjY@PBkt{dyrK`=Sj0RV;5mIWx&YWPuHdCxsNY5fJ(0jZ`W248$ zW5*d>+r#CK$A0BGWD=OZazHgN;7E&GU%G%DdX|3SG%vjHL3B6a!G*KDbL$P%*fP76 z(R$d{=-phQl&8E|A%({sIQ*zWQO`I)q*hpqs$agSI=)l#ZiM=p0hr0;f1ci&9l=Sx zXsrmTN}_hcdq43kJJ)X`-a%#YILf^Y<%H%B^>G)bxRIxt7r07LLq{bLs4z`~Sxs5| zxkVNimQV`~vKvFxtr#a2#%qET@Bw2iDg~_)q!9=+k4`N#gtbSGvi;gkboAGdW`IZ` zRjAV_ALP{j~z)<5~p_ZZHJR~)`?z4b26f!E&aZIr}Chc_@-rOa> zHb-eqIhiwf?IzNV5IoJpO^RD%64m6yr|x6Kh}+{UeE-V#nZGlm*-x45%@CzfeS=(4 z=(Z+|LR~R+p}wX{!5&KR&ei|h-BU{k0W>-=azTDi0~~yB`+d65Xs^Hs(UM?{!3WPn z&pb}FTHldM2fQoLu_Z)`;>{@`vxxWTm4v9JsIrRAeJxJ@)B`;J4?f7kXI5|rIjywM z?cY65dv%G*Ofe0O51|%PL>d|vKd%VdqZ#H6No5og zDH`|oYn*FVfsM(pjL?y-?b(!|79-qBgij$}Rul-A{`edRU*DtjQ_@FTq>F7_q6jvi zGNIfr@gtA&fKpTwhiHXLg>*T^SV3XID3qy(uPCFS#tDi0y53OU(mdWmKeEK&%`GOE zN9b0QkU+LMqx2fxPEkuS5C6zhU}C_NsDwzRG&_BUZ*S3futno=o6gCYPzmkj7OLwA zKA_tgY#=Cu7>5^7UTD2<5#jDNsTCouTi0IevvnG{*gYg(VVp-Tg4jTqJ5)C!X&HX= zSN{iI__3?RC6F6;!Vea1%%GiFMi! zHAveLu~h`CeqZ9X~+#>2yB&< zqUf*oIQPoyq(?faB~9bO7U53a6EF8?xBwEMv93tOKrWaLRGo_3Z(L>e%`u{cs7h!r zrbM(^dSMkm@<>?`sscTSa8OWA0xALbcFExKfJg;&1kz5hiG_}$cc{U_`;Xx!Bfuic zVPi|2rUX~eerN?z9u_SmCDq;Hl5W>kO-U;BEFhzTrRCGK?p-8`VC}t2v>xc7k2g5= zUz{PizYo5^dySbub#q84U9AQR0j0n+BBXM4Ft`}kSCCVL%Axy~;6N`X5E+ht@+?W) zf`;k&Au_8d_XEKTeqR1Sf#fp>g`qf9vT^t%&E+=lzJ8HN41O}F+6+vu8>Q4yU0ldk8%G=H(=mD@s&|zr z{F0&jaEsy1DZ8)SMh!fE&y!!uAf~S8XPS7$LJIqTw$Cifc=~gnLiUBxAMI0oZ;siE zn0{@K>E<3As}JJxg7Jktl0&Wfm@*MWLfkSn^*ahorU$qz$F-WEzz@q>l@nm;p<}Gw zw_e8%p`vqk5$CHKAO6P0} z=RHwsIrGyG@$lh?spc745=2+%G^QFASXZlpRHcz3FdI$Lag6R+qE55 z^(N)5DTKiFKX`+D+aYP78kQ8-rbyorwPQ?T@U9{xn$U*O4?O-$&vB~tFrjd4UA%?2 zAkhLV&n)2jLi?UJo#z&?u1PhkMJTK&q^~jB60BfXEM5zG*&;1npN>xM9uJ_De!pp* zY=2~UW%M_G=2w1^{he**2Qy3uw71N!jZn9s|2J1qi<5&#J0b|4A zPo8A<&XE1Dy-T@MkQ|M0bs2^3&#yAdcA0JN;xo_w*AIv*O>8asG{>Ydz9m?rF>4W4 z#kkEJyaOYcIL7u3_TfIpIaJp$erp%;274$%6XW-DRG|n(029^yg)-DNgE&DQ2^{*! zDM$*=e&k`6m+$4F{`;9Wci6qShq8fu=qaZK4?p|_Z-3?c_`D=imU2{~ts*_$rP`Y# z-lGyr+-cTMBq8ATbL?td`y^`UKE6tMYXWgpr(oTbu%Dshl#pknN80#p@Og>y1`{c~ zcEk&o?gL#qk1w*5-)1x$v$(Xx&X=yke#Q949@VDc_Y2HUK|U?e!-{GYV5~4!QD(I{ zmx(o!g6Sws71e@~QX?v$6=;!LI@>Hx#Vy5`e*X{IzqW%4wb`TJ$x$fL=uN_LRrmlrN(v)(MC$L-ePtwgWOVG@tBq(dR(*moRAN4P#^x;JJ2>X=|P_=@EC5>ixk+GZ7s zI+YX`P)-ji;;(*{%#7KN_UV6oh0<62)!Sc& z69H8S<;LZT?MjOM)^+&CxcHu0MiP@pT% z;=^nF?ce%kLa#s{X=2wEBEssE>u8;F^gpD>dV2)VwI}_gfL+>X_>NF^Lp_-R4 zGqfJ>Gu)oC^k|Q0rN#cWAqx+7NKdwj8Zj=_0Bb;$zo^8JRG^#n(e;mh^;c+Ybn61k zfe-|6F{a(2n&(W;?V;uY(Fvn3T%#P+4@U?{5JZFk(A1D>k|$#9=|$qDn58p^snRK< zDyQ1B_{EZNMv5 zd-D#5KD|LU_au!r%biuKc}}#_frW^zuU+EG`HP5#q+=L=@qpy19;PiQ+khCNBMlUR zPzLlu3)*1sZxD>2B8^y&#?)7U$h|tu8XQlN9oa5_V6XEc`k^5+#Zqt2knP@S>bQRKo>1@dE z>zmAHQxHd#q*U1$N=uw32zL!IhJos!MCJigc~tIcKe&L640ff(^x7Vwj8J*_!ONE- z&|B!U(LGYz*z^))Ac)Ud{pll=%K;hYxIxBfzDv}O+5gHdc-Qgz|NaWa%X8$CWAfS* zDj{xD!zgKiREDrsAVy=ALb;4ERrs$~-~~PNXf#3q@ra_SRZo_66XN9vy=1AH3fs`w zZUm)5d_wlk0e)DaOJH8Raw{K*ri$*FKHVo1^l-+kERlxblqFei)E&fq&G6+RJF?HM z-@Z)IE9gAi#`i7pN(U)CDsq^a;BL>6{fZZV{R`xq87c+S5HOnH0w_bW*g!P`&4*WN zV+KGMbr)0WwEzSk7c@WFM;})lymXUnA!95DjLJE?=XTh=be%8$>winnwHduTq_{H1 z-*R*x=rf;`_@u;b7f7jSRhm#i$c3XndLMLEebN*jae^{JsRAN3dmwX7ertl82UJrb z4Goc?JlH5^`@7fq+<)>fF|&xuJCapRe5T34%li~>jmTX|Sp-xnjH`@hyNP5TnN&2| zDe8e9z4vyA+D-C{V^mYH%P}f-*o6cY>Qh*m7#z5UCyF#KR_IQxp3-&8H`bDvg|MVC z{RlM&i2@RVk3ft==^USG(r!wyfzXELJzboI{!bmEY-qxnh(TFklR*0MK6VXUsOW!a zjoF{gP@9O1P=SD;dh3NtkAH*SP27 z4>N7&*xVzt29vjjR9giLM;6$-IK;-#`pIRcdpV|K5bxM}^%go)=!PX~7^novEtEnK zMQeSDs>n!>_s~7qc;*CRG%D!&5vl?`i7*2}jKM4#&V2H{$gG69(7bPf?qZiHHW(VT zR7~L8UW+;;|)Z;YtM0k!JzH*1T!a^GQE3FT%E$`Ewjd=l@G0aPWZZs5QF^1r6_ zKp$l_C?fsP-+i*Bvj}$(@R6c_F@W^8yVD##M(|`9dS`AQ6;jdSivZi=dE>w3W zxP}lv)La{^dNRP?P)BhZ2jSF#;@;E?h9ROP=_pj-ys7JUc=oc zn4`XWv=4}_E#P)uyo50comK-$4XqQ)#7aRD&|)x&AWe@$h>ttkpI+i)KmOws7OI;< zd25J75!D^f{`fZCrA2}`oUMBflVQN`I(n!2C={iyPz^)xM^@?}@UA_t4vOkdQLkfJ zLR`~JfBEKLKu-uZfME57-+DXEua0rs8BB%AOJmCKSD+!gSwJ_?e$NU^KX#bopL!TM zGTG5r?f?;db#=WSL$a*~}luQJ-3 zk(@}WbPhdBG%8Wjtc@TtPjzmBU|AN~yTqGc{w9)XWlw}_e%6017svkKF;*mjIm0#V?D@B0YxdWw_+BHHwx?EN#P zvO<6XrEqY$t-Z>y(hWxrOQ@p~0|Q>a3P8&s$`!Za-AxJC_u6`^x-5qEP)QFt0B8u+4p>si z00@e&e)%uR&rj;g>C7vWKHvXuC)WomX{7idsD_ZN#}rpG7%0qxhP`V8#+UB!)xUU= z&Ps&_pgX6tJgsbWKPGD5CZh^zSo@pXCc*mxIak)Q@ebs-D*8`!YHF#B zY5BmysTCHEZm@G<3%o{KOSn^^Lfv*1AJCD;J7J#980_yceDql~x_;pc(k zN`~?ZX(>!*QQqU`iqvSDOI`AAKqqz3Rl9Q~{Q1B59d5pL4fCKPd8$crs)crrKpZUuNd?qNDQ%yaIekqcRO%2Bcuf?QrJRcHf!0o zp9xW>kORm@j>geG_<(Z-y-zP;VuMK(_VH$I16u}UDA+h9K4u9fV6A15Wr}N_>8PZ* zSrT%Mi4EoL5+jo=H%Z!}QX6a%ij=zq-r*cWz=A z70hb{y#G)W>kVqnGJk1Gc78^DvVj2EEl;%XK?PJC5!xl)hZi9hcK>RVpgqbsR9CV3 zfel!!YY|dcy3tLA_8zxi5w-)WTZ?#-l}4Q%tS3n9(1U>Y1+Mfge)KT*kP1d^sQ;X@ zYD%QBu_Ek)UNxu;)OtjEq`^HOe1IEYe1~dRc+Y2_W}|tOH^1--D)SI((nNMDyejcy zjovNEHgn<=4OAjTm7%xNXLEQ5Rlv^IZ;-!TU~G(b0k`95o@%3$g!WIY;q(lD(IXOQ zoaqvB4-OEAG&Mf+NFh|tp%7F7I$9$iHgTQlSUn0vf;RF^Z7^%U7qoO<#!c`@bi zbL%WFta57UKCbUwt@Q?FMY`6+xq^752|dB08EqEqjyAdQ?gfgi5gqfwY%M0KY`FBn+y*k(d{y2aAz4YYE&dCvYf zN4RN)$_253>8$~s^)8)9`+WExe2RCz_6FgO!#py#p=$?q#)hT2`d@3R2&{}M<^5iiNc6WI2zQ-8n6Gkr`kXTDGuRuJjA3et8 z`~dfzUHq=XWe!~`%B_-kIYGw)Ct~bS(7lLgBO;0#l+zq-HO2}=5fTSlrx&O;Gn6&N zM-!@nudQ9hviJHe^1XujBTolU z8+x?U1SN6Wn}+ohq^mV}gU$@eZv=mne3|%(pUz!###y-z8mZ(R_A^XaNW@ zWn93Xruj@8uRY5@y2SK*6SUE=Y>;_i?Z?&_&WE{Hnc8*Z3|UMl-Uu9j_8xQuAu{M$ z#e5VHt5GeD?#9SGV7)=Ssz+w12FlfV>G<-1D2I5O;-`)zYT`F@s$R+7=55}0`Z-qZ z8YhmQAbvE(cLaOVpbsZ({n0h5y_^z*Pc*jD1Qk#*_(_hNdDzSGGef-4;HhVSg!y<( z+>UCsZLf*WYZ)0yhH1ypsy+PDqX|CIsQV0Nqs{Vr4zqlJpM0(%RD=L34m7$c(ftlP ziO8?zbRKJ=8U_;!&EGe`rCQK=aWy)ig)40LIp3bOoXV(p7|9 zQS8q#iAGm}y>IOyg`=1W>CuFEU6U7}OGtddVr2 z+ER@&x-MnS9w8Jt6oOebOuscib_2un87ft18=;IxbwRdc+N6c{#@#WorE_mgbPWF1 zKmX6r5@RZbIw3SaxWqj_af)gfP}>DKp^-LFML-vTuW$;iS4{tE$gMxVj&uv6Q*93q14OB7lZQ5qrE(SQC3 zVWUMHm2^7|jy!sVq}ia%9Z9=^pXYdSNLi7+rIAU2pE%rRNqId--xQ*OqS%{~yAje& z=q@Po%CY|ZGW{bRnkOyyJhRT>A8Qi#W6GdmIda6N*-!IWK!B1-% z*_1-|${6JgO4rn~%RHe{sH~ojW_*C-5m-g=710W)Zi+EN<7kJF2;C<-G?J94rI~(v z7qJmiIvOVz8P8|Tw?>@#htJS^yiNQ-O6W)UQgi1E*Ex3dB>HedbSz@E(ZJ_|M^U+g z$bng_Xgt+NAG37MG@13Q+$5)yXWXWHx@=SLo_i{8e)BTQTijlSDgu5Evzr4Djj~Ww zj@hLl-div=+sA@JrIz*w7ntXB8Xbeoz;q1Vw1e4*2~)V?-sOA4mrzs1?DCYP)uftc zs8S(TBZG?Sb_PQTE)WV&C^T+6Lua1S!}P|CSHJNxEALyPGzDS{3t!{loe7iejApM%F)9f@ zP~CPl6||aC9Tat1r)#HgtU@Y5Dk1%37d_IrS%pq4#%Kr*iJ`I9qB;<4q6qsQrG@TC z4%69alkH@LBXHvV58{oZv$RTHRBZhpm+Ae`3fblqH!cXJ$Hp3I343qvGkt50>}5z* zuhXTM&e2$HQQgj%ECgE8KrMKhPqoP|IrO8JfA;JD3*SEXRetGX|AfDK<@2u_yg7~Y zA4`*jd@rMQWQpBx+(tza-jp;?cjzDQbMW>6JqYw3U1IzDS72&Up@Qh{b}`!$_&a@cqA~3zlidM+IOX(1k8|mriL5Tc_)(5AG3A?c%DG236lT@p#}0G6MNwuP ze(ogGOZznMZ7{w#X1Fs)#RW^N3EzL^Jflg@bT>m8MPnnz#t{eK8*=!g8x(0pd9gr? z!ZxGYEj=|btGUBkxH3nyg-+c73=<87)FRldo+}_jjWh&FDi%(zGIeA6PcP8vE_3gp zGpt32*)iK3{F~e4TRFxF%4$^0;8f0ge)3tusN&$2+u#%?hma_u4NG*QNpW?GDI(l7 z$K@b<6)vwCfKe}J_u3Y(UH&HXtM2nU9QIdCqNr93q{7|V%!d1n&|K>H4tA_EnGu*aferLq^;x>D)?ef$8zr}M8 ze3(2HOt9qhvNlZiRULaAIOdn#Ys?>7p>+e!-jtDKyeUCf@>x$kJJ@lEF z+_-v|s~pp5;l@Jt+602aJ1A!b2-sjy5>QzDUO+c36doo4<^fbdTdD8;j-ma-i)iK1 zr6O7~Y+l)B>6t^|ET`A+XEfbss0L)aW7ItfA?d2?~lI1wcmdW zF9z)c-Y9Uw$zOVm`S-{5)bqZhv5=75ACbJ!!YoBph3C05&yfVh>`F%U((Jp&%%h$x z>0i`AkS&e01LM~=@eY~~^eAtS@Yf1hiec`+YSelIQ+dKX)cC6eXj(`#N<54MdI6$# zM0IV9-BXBIHh%OT7CMXE&TlhF_Hb7Um@B+dBuzy%1KY7A%L&y^j+BBib#X-%9yS&U z2N_%gnbkk-t)N?)l}Fc@+!_+*f#`G_+00NqL;t6n=%W#lGq^#43xXRLm`YLZmPn;3 zH%tEHbDw8+yQ-bLZ&z?5pc)aOGK8HeoJt9!5?2LmLu0xLVrsExm{$KLa=l_z~yJK84M^%Cp z73srmSX3nMiDEt8lQ%jqKS%?!64n7uKs5iU`r%JRsuCn2_oD)9}AHU`l` zK*+ZXLg^s2sMu3dp=Wja?n3Q%+{hZ}C5s;f-0l3TcC*Rt#Sd>#-70B)bO|d8J@QCy zkwL-mrF~Rlp%2DtO7GF5oKW6XoP@f@?57nn4z;h4Q7F;i6(I;A7h0q|@z@VjjU3%$ zU7}Qz9Io$sv)W?xwIOQg37eXOZ;q(4n#b1o;sc){8njt@W(mD)p&4PYXc1H+t$Pq@?f*~H)b1(DphVD0{U9AA(ln1Y zP;o$@chI7^jGK|>(e;Vc=reL@bc2^H9soJ zU!9SipHdbTHaOz5DP|)^Eem?b5C#=;b;k5hZ{fdxa94Iz`}e2{n3VWD)Kf!|T9BX= zk{wB1E7@4nKCFJa%FjpPwNk zK@8*v73DZ1Ozs*&Xpgd?1*Un1HO1~~`2d%XP0tN3{> z?og>lx8k~0E((Ie4J(?X29p;jl+zqPDj0rsk1AxCTbkeeO?diYmi^^sGW+9kDn&jC0c7_>hU)wFcA=iwjjD@;#f~mixwT# zVo#-N{=CGqeBTP&^8=c_1hd+pe|!NoDey&!T8nWz0e#7#qln#a-N26=D2;wd)3~<@ zN+S!3#``*$F0i0s!4OG3ph|{yDb~+C>RN&Jiut7(zxI#+8GrH5e~;}yyp6e;plreN z2bO63aGT_)CWs(jqbo2A8g(jxrz6tGVpgA7A`gy6tk~V%p&<#AEU($R+`(Ltk4hv_ zL=yArZ+(-&&Y%WVMe0#+#uByT+G7ZfUDUKb)Ihp1$$NYBeqx>B+uKyRV&}pRq7B2r zEvDZY;cib+Dn`6O8bCw z2G#8eey%a|=+z&50K5O{^8alHp3#@~h#E0bGiAOLkiiUD2&88gAqfO^H|PsB#VSgn z6e#K$u}bNBm~x#J{d+62$gumZP2RuqAvQj7n)^TY5ZC|o9A>3M@A-9-PK22Td=khm z&j%2JHOvK=Wg%bdpFQ%fB*=9 zAVE^3NRgDdXrbg-nNhPs@R^f9oHxx zYp1nQD%LL2IeQ@R=?_5zyUUW%~Fcoqf|hKKm|2Glo>qdc2943s}7OAREst z^Wy*hJVFIHVQ|(!VR41$OFA6xtmVuve~a+dF}1k{>VRNtWki^l*f8hN8}A{SlW05q z@@@c_nQ&7y)V7*WV8am12ZS9-G?X+RTjH*}?}UK{r02uqCw0O&ObN~!#0c#546`=I z>58|Fz)G-A;)KTPl18&eH`_+EVv3VJHa@XJo~M)zL3A{saesr(u^J|lIN_LkcmcJf zKuXH4p*UAokZ-U(utqc9DiFgQ)(pkfA?fEv=)7R-pKT+q3;JL0p;rvfN+#}Hpd4q6 zwV`p}B%&!$3lX(DLd0}H{Vi=sE$w?}s4XT$_chSBY8L*(Bk0DME5C9EyPV_34pQ$( zMyNvjKxrvnln@JoM!;yjAUY8f>}wIvCItIqW***;YBd;aZ(v2i z3zwck)Cy#*$S!3NhFtveIf67mWCEFaT>_!Piu@w|ejpIeQ}?r#lw&`04<@wOWry37 z;Ifk8<|---a4YUhy8;ol#SZy+_`cTNjkyPY@ojX|4YD(1%3(qMt~wWf?-b=m;WYv) zW{E&nDw!4II0U(;h7uB+X)wa)2SOksftphM$oqeaOX?iAPrRD57oH~%Q#ywyDVB3s z2YQ11+Q^G13=9}YJ~9-`IrYUl$?*i41n9JYLSQ-}p|ym!Odw)|2`tS+4bqJ5iRLYnt?&oB|+U`Mh+PQ&M^7-UY7pe5>Xkl|IWiqEY6Vi z3-(MevT=04QDPjyZv#6PX(i%h#W>tIWyAxhb1y|AEy~(!p5^e}lQd^i{ zd|?Z7mt)WCm*_vafzAy-`=9+I`s2YjuYT^_Z|?>`c_TyJ9v=uAlMfG8HyHQExErZA zA~;s3|6I?jX%u*9k*n^U07R&~cb)>~B2T6q`SGm)obnXvs8i#g{M?86=*K_Gwc;Wh zCzqK%x=3DblAkxQtYJbTXJSlfa08!11kC`O8f*#a^8@UP$H2M3A=46*I@rvR^ARp} z6w>k^|Mo9)@%(xECpR&Pz>N(^ftrdvNHSK;ylpS?a1A#yn3hD$Ko|v-y&PLO%!)x; zfgL&e+qq}_v|@Bt5>MAKRwI)DGxCH{TNvVYg0b57M-<432!x~a)=5myQ0k1{nUvvL zm+oet8|yC-H=A^(W*CSqx?j6Nc`+yZRv(dq5sq*m(W@G_TH;FloIxsIfrLS2Xz24X zAw8{6*qCq+^v?A$R|<50j9K;)wojdWlB?^>ziiH?&wYnT)Wl-@BgN)`|KRWZM@;US zA#5uM001BWNkl2GyYA7pZfvmI1!hp9gd)F|G5GX0 zwyQC#W9pq2!hlnWNIJ~pcM7R266)Q^^#&MMLI=sV~k5fRPG6? zl};L_@SFo_LDr-5GXZoZa@wMnVv<7*Y;K8{nmFq# zg!=x7pe@mq1d(L0UQj&Wr@U5x!)yAO9YF_CIf7N%ik12Y(jfxhdk{(>gg`n;|LILg z1?7bi!98{KW`RQCS}Ln8HUEpj)7y6U1lazgb$<%nI{(pZl%cPWsU2yeT|v-@k@E`K zl+}H25!l_bLr8~>Aee6wOe8qrvwnigSV?;DgtQW&ONvs`|H>vWe)Ai=boQG#G&B=L zC@`TUn30_L*+&Q$Bq)J8&_V@@Y-NCO5~F?G$x|cL(ChP%VhUHM^-jNkW%EPrNB59^ql;cGF;fy@zzz+r^wEbCH)~G;0)Z_{ zpZpbmNVL;rkq(>-5k})`5qJI4UDRINpf)!};S6rek$q!?xmG}FX&$X3nu^|2ee%tm z`iYn_v?PgFD4`Q#HwxrTjB5u-1L3T~&1qa1_=p~ssEHVdKnDgghZ`8=Kr-4GczSl3 z(Nv1@#WBV@%;g-03cHnI3Qtn?11akPpMmpZ4=0e$VZ06w!UhmqTxn4!TwZ2!ejjR2#KOW}F1>gey_%6eGs30@cAm2n z2%-F|>MEQhqDltwnt_EwlvTG8gDe89@^Zk*+dBlWZzCxvmn|kMA0tQqYkmRdi6K{h z|J=7JQ>9@}91^P3cxMmRFmk;J2F8zJen5!z(P$dF&Rs7*)q(um@s!(K^IN1NC=hblFq zVTq5``Nd!RZjbJS`9ML(fm_{CV*BP9`ee~m@aN0Lw|{u@#q6^FNqc&Tbs!p%@Ae5< zeF$;WRa6LE;*cW1N$WSgdQk2aA72Fu6FOWe*^X9O)5~m}-$G<1``>(&;f-zdnK5GM z;DH9Nfa2v5dQi}Md>*&xa91oQ&k(f+c6EqLz5cDuysF02AA5%JdLKJ1D0>Cvl?>6< zjIZScxp(afdxrgQKEc+7>jY5^OW;Kzw@XCiTdE@Pf=j|0#A1xSUQ&Ns3v*iIf(nx_ zoTsZBOId<&7CW*CXMJ*5+Ul7&n#XE%M`Id$W3tthxwkHH;=%j5k=!7Ziefoqv^Au@ zRHJ`pM0s|IwXSNU$=!|KIcX&hnh_$Fh|*uNs7m$2Qpvyop`eU3ulujx!lhFeu*)Uo zhW=m4-B(R&|ILme6CYY&`*&79oj$caz$#x@Ho!F%0*7>!KAyOF3Q& zfJ9&kwWL{V@Yo&il43lNd- ztyobF-a-Yjm(;a^Z^ODoAV!X-fA>lB$(-_BiTg+9fBv^RhGgFw0vH*ohpgk6G!{`; zPzu~Y`yK#3(-hz<0%yTUMBGpKAh2DHla6w0M~TxCHWJEh zgQ`nfx3?jXURt(hdH1h>kjq!k<0cFo35j(~v7w13K(-W(x6e>KKZH&|bf|_(1pnRd z|0ZAjm!HD)G$Im2vrTkf5Hv#cpdem|y)$VEQ9H(z1&u=;#w!^cm#>qEI!Pm8<-%2Z z;gB@%QaUK}0$E-6+aJG%y{x@P5Wckb@0Fku^ualwOxwaDM%6cJuTVl+J$EBP`2HX+ zB<&5c-4eSkKkPnfp7^euLuDv_(kG#nV|Gz#ioW7sdzl$iwIY2#8dEiYUO5O5UZv7m zfzX2Ti#eyi_zYh?^Ji$4a^_p##FajIv|WR}TrzunFR~qzryg?l?*IG)*tWse161Ar zTp3mpp;CB-4(l+vA?ORz%@jL=a;v2OjV{H7A!g(V3OM)arx~9clMHHF-P-L?w|Xs|2M|rmTc1RpOHxhebF+=lCSIzWF%T3SNKzJ2^i%&%~WA`d{m!da!w6 zmBBOXPzpruIQb`ELl%-ygPm4{6ES9>Q5|mrF>zvo(Z-l~I>B@-^0qo6_m1DK{cV^I zd`GhGxb=;9a{bgY&DSK1pBwv8Yw3FxqAh4>ML+Y`fT~Gk!;xQ3E54-9{=>-tGd3g> zq5u7w5;Hagdm`*qglj134>k#dI=!?@xUWT?4!HL0RYXuyY>x?ACAGyCTNgH&J2KDO z$8O-p!n1>%AFO@ndnEo1UyJH3_Hz@d6hb&$Ln7+FLc%)otLaxpAK&_4zB}K*%=*!8 zDJ}@d-$xpRR@lq~G@P$C5zgKGfX0H79t&^mcLac}`o_-rAW%4hBN4UxCg{Gj#WSCM zg8b?Tx2I0=l`*RA7))ePkuXd>Kyg_XRPxT;<30jiS2=tai zL=Mr2a6^Mq0XB13rHPv{c`qlnA>rW`#ibEZ0WK7bH^zv*qbv;dy)~F}l-&WU(V;Wl zfk@Gun`U$^WAK@^s?>8h{ffK4x<-W%yBq(?U|_d0bOLGHDno&NsYIj->oj7RB8?;nL(FuEOB`lh5Ff9j;sDhSsqe3O@Mshv zoWK+c7ge924Cp?;jq6&{4bACqJkR(-$@uGQ3|9Kc=>$niv?rl7n!#r`J-#8;IHSlj z14<$a8CmSdtavz zf}(F2e&G_!U%$jwcMGEhHXXvOq$k%{UEN^v@HAx(wOWKrb8h_FC4`6&LL)-upPj9g zy`86*{eRzjs2E9j**78avb)Y#3H8VJA?8Blwb3I&ih)@+-&+7wrAJC4Zxd47=}PDQ ze5`Nr?4C~6B86{GNC(#XKZWqv0VxF1R0oZaUJE!5=sY+@bht%&ZbD(QIG5 zGm3pbb_?yj(@de@Xn?#eqBu3AT-Mk!Ln%w+?s???1QmiCSg0vvO;WqRLF4Wzk4Kzx zC=q+ru(9(v!cur+i^4#nAP6XRPHnb9ZN7%+q%>w5OgE=dO+|e+L?jB41kf-r8K82Y zgt%9rMiOH+@qq-{7RaTDyf;SOR!1I&JAU;MIuB0+hW7Cq&6#QL|2vO@5o|VljGi0v z;+b#KYE1H~gAa1~<@27@Asq(e&vIAcE0iBFIpJyGPTJi>+leX<6UqZ_l=CNtlvP#a zaH~1)*y8_Gd-v?Wb$`{rD*$K<)O0u`6mJ#I4_K%YJfuS=s+zJ%-_n{< zU;~`hsHKQ|KJ)-*K6RRf)*cS-zngD;?ek16PLN*BNgkRaYFLUbP<74hY`s7 zLuL@t%gmciO}U+d_9Iz8i}3yc&Qwq}=WtRWML@Kmn0{c1?wJiXpI*Vqgx=?uIroKg zFtiMx?c;Lc`F|I4%qF;QjsO7(0gywlf? z(UjQ{BCf=g1gKErWC%ftBtQg?&G`{~jx93E`z+sh5i{9fwmFNPgV59{Z{*~w8TbCe z>oJWX^Y`y%`?(b;6nbn>MMUtLgm|Gzxjkm;wR5DS(s#i!>4VT#$Q6ZG&x`|fKf`R5 zh|FLMgBm+r=v&;igLR7SF|PDFK}5dOhN-GvtXKT15I8|oqI(9bEFw3^c8qLF%C!QK zNC+f_7^7wd^YpU(}EdS~$vgcF(%&MQE@Nj#q^X_e8r@2=32ed!> zcO6R{Ua8E0XjLabVP0^l`-Nc2+X4RN0ss)RO1QrMxOCzdusc(TYSM={e2}3}2GN%4 z^Hp6#AQlK~QG}p0)D_bQTSQ67%fEM;SAX!0+%fSgp8TCp!i44EUAL3J)MxNgmn_fN zI-$@bS4W7kKsaB{dgn8VlOPhw#ACB$ms6yZm^8;V1kFQJ%Jf3)&89ISFMaaZUgOZ$CCcLHvhZI0RY^(ab&ExB>WJ8sN6EW9IKy4D-3Se zp2XpHJ5xA^D?NwMSc}skq9fS%BgaS&NddOVj@UnNEsX8XI%-p>X zQF_HMGc1v#>N`T^r=J#ITMLIPVDQws7h<*oTRL19M$h+A)-w6VCFGRF2#~dqXf~#A z2K01B>p((27c=v~1Pe>Icp3aHFD^+n-k0`fnZ46(^+~kzL|Nl=EI|xx_rq21cukfHns;^b#?=~et1|@FY zu>R}~E`QpuACg?voY?nf z4wMI2{jJMh_Qr~xjtSOTtTnrO$NmuWva~yfPp+z_c0$E*vtc9b2W0*O%SW|iQo`?ms52yHEd}f?QGapsfAND%F*VYak)FSF2H=@X6>=F$i`u#{3EP&nr> zsvtSg#5Dz?86u|>%z8$i79^b*=`2xLGJW?PwqeO*i<*t;{OBzn*WVC`p(kNZ9Bg45 z7H0)vQ{Y+>Hx&_Qg5KF{-fC7G@?cE3zfOsubt0jC-y+xk>Rp52!B%j8K4+?G$ zWW-DWvA~WD!n&Ob);E+w;T^052<33o3UxH3`QRj@?ttcjNm@YzF&UC9b$I`t_i@c! zW&6T4Oy3e7O-NcXrZ5EaHO$zeS959)%wX43oB=CygqGN`mv)!h`bJ6BR+UD09pBty zhQ{j*3Q4*?_H?#PGQ2jzjRm5##7Rtgy^HM{Kity3spXubTrLr%Lo0!l7CknEnW1)g zmhP~JUCW3csna-ICtRvCciRE-e8luai(EMQ0{PQDY!c$OGI#~ZhKf<-ccy}Z30F-4 z#m!);UVW!`j|3b%M8UF6?f0~$lE1)m#(fjShYQnhGQL#uz+I2hX-{DDlBe?*h!04@y$MDOWG)%JFvb;<;(UoKEFy$* zIHq-C0z*l?(WDGC@t!)iX>m1g87D=6QxX??Ag8sCs2;+kK<cP4VPXxg&UUydm{oZF|`0Q%2~gBo{4>X z8C+UHc47M6iv-5(bkc9$`p)fIySd8gYi9=FUeOG4RjzgONxPpzyJv;`-nUczMFK!b zg|p^qiMq{UPdF=JYlx5vJ1B2r*{WYpnu;AHK{|`9!Tfs;aN+Z(nR&w^y{k8P_Jwb9 z;R|OdiV>}SQyfYUaARQ^V>F{@`$WwcV>P1g#hJyxBBFp|WsKZZK@ zN8ipJ?|e1qpMC*5kVKQ-kt+-&Qb=rU2o5ILbpy44Fj5#*xroRRF(-YkTv!hxU8-RR z8aroEYhWXR4lJ%H(JNI$NmtVjXAmj?tx<7^QVNZ!W*d%hI>Jl|Txw~)euCiE8r$Dk zqu9zw4krXbgSpq8?B%>2+E((9UdE+9WS@a3M6zL#IY>}60*U6muKhn@py zoCHh zl*I{wOal5(uOL)Rabdvt6C>)cN@(5RVP>ug6N>g!6I#? zp;iQLPNBOQloHc}pmdlii46@u{^1XD>WQam%-4C>ue^^lr=CS@8C($}Vz3j2sdpSe zUmMfBcghn^u8j~gY}D`Tu0lmFj6hRb8zw)Lam-LkowU|LW}|HcdFJqhg2RZtDqoOs$5 z_K{Jv{5>Uo-`@bx)kz@vEn)d7q?q)4KhdC?|ae$f!T^BT_yl-!|W~d!Y z(2I`YCkI5X&i1)&qQ#ilHF#|Q!_*Eoxp3|h@k~X78VJ;_5#@T}wS_XzNK(cpoSWA+ z7+vmSwdTt6SICx!NDa=&N>IU}&x|MrC9ZEUs~R`3n2|%&@%}nPi!j2gfm!ESIa46= z0EZ^%!~|_csRUx2A%XyI5lp{!f!6JFEZn`9?TrEE#WCc9^kN@-B|y(f8pqq@XNSAb zaQFD<6{xD69M4pUYf*Jv1NjV@c^#oJl3la{3+qwwEGS)1E|2e zaXKk*f#%ikeGE0Bn3$Po`tUs4XRnjLl#|?EBRmw~>K4_MWS0xBJ#&UD)7QBAwet+W zu}(H}M3Xh_4TBzxAqCWG6_qWH;=GEu z&xJujX8zJ!_k9TfA(TJH4!PfAD1H*lv_fH>H*)Y6GXg0UqA6L^tJt=rySmNxsTR zzbf5?rn!HD>_$o;Biwe0(H5D7h|*&65-}D8iveoep-YL&4QevLt{AMg$XF3{LR{To z3WwAoQftgWU`~%HyO!~aBMdY;&X{}f7H+(JmDz){)F#?Ia`i957rSDd`|kMZDiuu_oGMI zxNwc;eM>Apet`McFHtsg(v1#5)sT=G z7+0g!x`jew(*iffAN?aQQzrs}h%BNVA?hHOOI)IGdqQL`5eFo3QKECl!W#~?XA&cmF0`bAoeDK38Od7Kc0?K)yX;aU;;Vh=lHcX|p{o89?4-^1v{4lwxR`}W`W z$gsC!c9ZNMoB(*m3fG`?zbB+P2JX%sl8aNGXJDtjV!LGabrvPXa{?7 zo8n4Nc%se3(TLGkGIUL{dF3k2=^6BMeYT!jW&PvVu>w@7$hJ$I1ra#JwD4HM!duFU zvBfS38q;;kyzo=MMvUGY;u@aTEu|xPw2c^O)ItrjT@c+8V^=k*DRB|V`51Mufw?p! zd3A^4Qi>boOuT6p*@`GPHO(V!#^(m~&i5G)QlwPKp(IUHw!gTHGa*rK$z>> z+DSw~R1R5J)L+*mJQiZxl4I|^mtk{Av?s<*DQszJ>}wOhCS>%rQ7YOL?kr~86paZmm)@|`;;R~d3lID zkzlqAR>G@Z`w-_p`5eJijUpY>xMzy^aEt76A5qBN-FNI#fczL3fl%`2U1|T}cXZc& zpaY;PV?-cu+C3uz`E}Xk9l>ln$*=Tq4a?*2`!UXa<{OwziOovvTH*DAHxu+qpVAiS zt&-wghF$YI`D#8Qiekd05G7;APi-*!mSfBwoTRK-lA|#$cTf`qOMw@eib0NyH)xEU zXYk|})$pR0eqOF{u0+g+h|-WuG_cNiCUjwutpFD()D+Z@HOSU9(d{+LzG33_N$PV= z8V^n3#)9#)15{?wy2N!$7WXZ(b^01Ql=NP_?nlsLhwhfx6@UM`e23Xv;U{I)1VU^` z#5-MS{*W-JP`=;)CHX5_PCG;_ak>4p9-4*n`R$Wp7*m^{p)AMf`6jh{n#ghLn;3CI z?cR{&RnyFW4}FFvsE z@y`Bf*4#S6YSImj7(=`mVdH?}tJ{Rik-jjXxR@gYg;|5P39dbJp8PP>It{XJiqi@PC8{H^eL?5JX$CjC)Q+?{^qwR1 z&-IY|4TEp4p|cS30(l5r4+1GM9YZvec+0wmL_}WnDYijfI&m~!djdBj*^E@ZfFjvITvU)mcY`WCZlJ;u^s{nwq~xYO zwg7OOhM)Y@Kj8m*)Bo7}#`~Z9$mF%?e0*Z+-RZ?1bOrU}G4(ghlRh^fcvFX@6H=^< z5mKRMW8_?j>tokT5PS&72QaQnSV_VB z|1AIjPW|rJ0O;t*c7JE%+vQaH8(ekAoZvRoHI(O4%tlGl3CONxko$Yn2%oLHDFN3F z>9b?Rs$(Nv=HLfkL$*2M)}u!l6e-1GLD;BaohF=(P?Ew@<0n`$0YL|^m<0o*UZ290K=CyaebJ7&mEK( zHqluQx#h$=A7=I3CCsTY!Bm}cJE!axNFf`@;9)0 zAKj+?GYjN@Fx)6!NZ*yrgnMKuUY2(!)DP5o=$GDxnvmEu#iar_c0{ug;jsvLq(*o= zzzj6mw+-L?>HmkR+B{pjM|*Ax))jJ7;37@AVGzcU-xy)?lF7r<*meNQP(RwF{+bCy zOXFGr;X#3#50K3O>kRRub+3GqhsZk>?s`NrbbRPHK1}vv52qkV1nW;;#)ct*k_^{Z z2$})J4Qf%7Z~f6HkflXL5yizVR8b;Qfp*!M#lL^Yq7(LAWNZuN8P0zbOYt|f@~4LY z?Ei^d*!axa(Dco(H{ZAS$?RL(b1R>|c%;lMLF8S))MP->D^QuDyq4mYEv7K22}w8` zl1$cUE!0>)d!6z55u%o2ZwRaus4Y#_GdMK)R*vi|Y_1WpAb)9$%Rr1BW<^0uz)A^5 z5jI1Ls|6yG=u0U=S@NOb`q$30adw4(2)&gv{pdl)%WIfzs4X-oOHEWbibl!w8;?L~ zNiTE}GYyPTpHyS{^XaqQf3W?j<-sm)&+#|56o133_d@q3;_TV(MSZ#aJv}tPUx z=Vl5iLN%(`Mk9e3ILbjuSO9Hr%3voYdtZNqm;dkuns>Fa-H`U(b)tO<+1GlArXsnw zgWDSujx@pHklFX&fxWtkna8Vrj0@_Iv=9q5N>Ucza|@#vy3`+@Laz*|KeCsJ`4$^5 zu9CEBWW6C+u){IQv1#mJ%>GAC5KlE&d-fXb*#*{4USM)+ay&fOe?}~)Z_u0hM@(A0 z%n#6#KS&?Ou3O)?AD8F!wtYz5Q9tY^-|o5eFK`!eUPs=;sUa)L}=G;9Gsh{Q|B+5JouZs1IwTFK&`d%yQ$C zD-g$|&kPA#b%uX@jg>#W$kF?bdkMNiAVdgDErw^euw%>e#mfw?T*nGSy0JBoq5RqC z&WT%XuY6EDcRpwfe$bZuLHj^vaI@)QuB0pWh3q%Ym9jCukiEs7FaHIW%buenA#o&& z3FFhNT>Z=iTyAI{ZZq}J0isz+IPLJR6CurmO;kIlIp4ud32L*mG!~nP#6qYDq6nKr z$hib*GHfp+3Kavt#e0^}!va$(#O3YFM}G9-e_o%^KQuno zZQGOCZ!Q19sT;fSiN5=R3joCr@N!*?nvJ~HZ)#qaHTlO3#Xk)fbzy~jsvf|_0`Y)>gQi4oqUbmDBI2` z3rXu^7cjjealbTN8h5Jn@L5xSdKAPh^muZ}GsSWtvxMd$EbfAy=E zo^lQOm0-I5G`T&MetY}k?r}OFVONX4RnPNpT>yMH>xBD2eB&2C_PXsqNnc}E-Cd$A zZ?E0np3=jtX4h;|JKl)0&<33o^K$h0OBm6%c4MGzsf!?ynRZD^Hr}k=-Z^XL>))%BO3pDW&@U?SP)W=! zDc?Ui-aR+*QAzPrN%B%k(lRg6F)Yt8EY2<}@KQE7hu-4Yr*R{3Rwzk*2yw|}er{!*H?d|RE?(XmJ@9|Pe z@l#6iRZa2L)$&_X^xod}eRuVKcle{8_^zq=uB!R9vH9)o`n|dOzPkFrz5Mt0{o&yK z=jQ(M@&5Su|N8m=|NkefO3nZP04a1*PE!B}2pAb5A}ud5Nl;l_gqN7AtiQp*$ji&x z+SOM_51t${QmxXRol}500C!7L_t(I%XO1yLxV67h6AluQ2_@iwQ8v|?ok!@ z-YYn2>#nW&|35*)Th&V_rjo9T ze#S;A*_l5-mq!xibJ6!wKOoo|s*V1UP3zFvtN+N2dmiZIe60TG@1IwnMAb&U!p;(& z)}V2vi81~RiCPdeuW8l@`p5cH$Duxa;$!@DOgp9kjcwZ?dHnuDpI=KyB(DLJ)l%Oiwu0v z+v3DX>XcwFHCNP3#`xu`i0+p@$j}v zvupdO%QTv77cWeCo)rccc+eBSa%GrMK}}rR1TofuJ(!M(-Rha$>YCo`p5N@L;_j;B?zHCcxaje_>G8bj z^1kZw$L#db@AlE~_S5k8)$#Y&^7q~J_~G^W`|Nj5~ z|Ns9nkp&R|000qmQchC<2M8D$ElGrzn8C=}`_N%@rX)a0Z|ZgN7D!E%EzO4BF|T#Lrx_z7o7 zd?!cSUYQljo-O%8iN{bX7`_+_qS82+VrJ&JnEfw#z64H(U-54aTjihr0J@_p!#+G! zy3)t@`pEEzh@4#i^|Mv;7Key@{s%shl_<~B(S5fjCT5W<>1t;HU|V=x3WPOt0|0}0 z;R+};#==wo_zLLBoFPTVXzf+G2XtgWM5*d=5kONq(p*3b_?p!LkqWp59$A+NrCECi zE?74ypUGPQc2z*22i4BEz^)$kGw`7J#8S(ueFE+LvgEqg05q!2ySiGp5X?S+^Vhc> zR)P_dI{|=~6(ejrC)y2Qa66arabZmMn}^G$K_mOghco%~CLizL=l=r{;7UBdz#9|* O0000yM8sXh&8)@HnE5{yMH&b zi8!)}IJ|&4vWh&ugFU{3KE8xOzlA}+hC{%IMZkzm!;DhJkyFKyRK=22#*$UWlUBZ) zR>qT8#*|sVoM_0SZO)>6)2n{dtbo+6fz_^p&$W-)xRcttr{c%C=hM&c;Lz{k((vNc z@Z;k4?d0|EQchC<2M7rb5f~XBAS*37Npg6EmzbTd zzP`c1#K_v+#*M~}1NYv0PuyGb-`g~4NNM?g=#$?2rOjRL0O0aSsuqeyLz*gifKz@Yg191z zKL>l|7_uoQdqwWkkwdyK%Y$L$qHvJ!cjbnIeZ#AxJkseoWH&ZpK4mk%m=iCbf1Qqz zTu8Qxldn8tjL$}lm15K{;^3>#m}rMo%mX9`Uwbh*KI%1IB%rcG?JhFD&iHE|>={5R z(9G!4E&e^A{EhKx2w`_Z10fQfzcWIaePgxxPWjU$B|M90H0=dj*NGj+-Ny})4&FI4Kv;zI(5W!X~C zUYq=9P~`C<;j-pFNvaznhqborUbw;E+cw@2us2MiAKF!!+j5C68*eOdG_B3iQsUC~ zf}^?Ab#S<&p*YXzQAc}KVcy|29BtJBm-*)7Ur{cM|38x literal 0 HcmV?d00001 diff --git a/themes/matrix/client/src/img/notifications/warning.png b/themes/matrix/client/src/img/notifications/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..ab8b54ffc0ea43e214c5b0a9ca13a8b590ded293 GIT binary patch literal 580 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dy%*9TgAsieWw;%dH0CG7CJR*yM z^m`CyyfQK10+1nD;u=vBoS#-wo>-L1P+nfHmzkGcoSayYs+V7sKKq@G6j0H$0G|-o z|NsBro}_YrzQU{d+OKD7zL>B3e6H?`c{%Ew#`y9xfr~3@Z1+ro2RJG>m8qL$S znrCV?&(>?6YtTC1sCB+c>jDrpYh7&Cy4w(iq8y3c@UF36%l#cL8k7YddH`2{nu zb4W<)dbUiTaqz^g$Dclby}H6Mnt_3l+0(@_#Nu>o@X4}620T-m3x$N|_$<_Zxi6UO z#sB(s?ca`0x@1-??YV86;6(=US2wog3cr4k!4t14H$f}^j_b{hPT^vg?4v($bVq(W zKm8B)w9gM~zwM7*D_4BvynO4A|3VhRk^5e2Y)<88Xg}kvq@@4Kp5cJsmq(o|Cbcpi zSpGnCqPI~3>jRC7B9V~nvt(klsYhDhGT*w&)XQlP>IKiN(4Ay_`qsC<)~{1nmVR!PD}COZ z(3kxnC7)%nN;&_XJ{5J>)4TcQI?JNIU2Zq7t-5^t_xGRmjOKwIbEe*~^8khwgQu&X J%Q~loCIDF$0tNs8 literal 0 HcmV?d00001 diff --git a/themes/matrix/client/src/img/padlock.png b/themes/matrix/client/src/img/padlock.png new file mode 100644 index 0000000000000000000000000000000000000000..31abbaeefcbc507ac59450a76b222d0f4e2ef298 GIT binary patch literal 3265 zcmX9>2{aV`6aVgk-7GeD>sK}rA(Ry*>^gE3Epmj|b<4VQ&fYA4`JLL~x$GyxygiPF4+|0N z>#b^bO(w)_&M)^CaxMsCWK&r#aHm&lGhQsda2g)u)YZ+nA*$Y=^SD?-yyum$c%Fo+ zu$ZJykg9=Rm)m9Enp(-yxPTR<#xXC42HEHB_4DZg4O#W`_q$H2B2I8p9;H-yavPzH zq>PRmoiMumG_oM7?b?9OKx=_Dkk+mM`wBnshtOJ>eU=7mz?U3Q~$Mj}SCJX1OL zyqf1?ymH|9kdKx=DX?)8JOf!9^UJ%poV#?*v99*RQ2i$@z$~vES|EhDv3k?p4arGu`0~c! zB)j#@7}HKFW*v0jh^7s5seY+tT~QL61vGi3^n&;$V?sqb^m$mcJ2D+&AnT5%LkI5`dy*61((J>5;-?QSiAbncNza z=_&RvR%{Lm(ptkSN4tVOmF{n3?6j5{zR&sjwxo5xQ|}d}l%r)b#65Pmb-wHy%d{L5 zwPQ+6{)w~M9AWrc-NtIJ$ENRvF)A{Jcr?4>y9&B4!~eLW4p`>> zhOsvg0z;G;#Cd?1bqhHtaA;*Rw-MY>E-4btGR_itUOXXU;(CuwOo62{(s@)KAuvtf z94FvGsk{$ylrmS~owI-KK@53|%_`yM&Ga8O>wLP){j(dkJHc<&j@laS3U@w@+G6&w zEevGlg6D?SCiHJ!F%BgKr*L>vX>8T2Yz8_^rTT41_xEvRdH3@!{Y6nDd=hKj!2QFr~YXu#zGc(XeTQN|WFn zQL_-O7SLo_0!Z!Uw1xG0znZF6-!;c4eL1j}u0$=vD0_f6MZe}${`8%K>BVi{5{i64 z8|m|u%LIcFxg8~xj5`zYvL?g-%vn?irwONS$jMqAk3f>u{z3D7%BMV>yDEvMtTO}d zihwHDkS-^BS)X^O)qI+wO7Cc(2D*}y<0#8~eDLL6y<=>IF)Ap{9VFsh4qS?SFhufl z(BAyj_Xz^v|A{MSeU=T;7-%0JP+Fk$q2Jf_V0xC09?A!2-q?Aq)ofD99`8HUCp^82<@{syV+N=MB$R(;352@)bTYSwwhc~HL>YeOhXS2 z7`6_89MT2B;X|FdD&I|?onT7-6g&9z@)L`P1X{VjD@l`xFt`dSWnu-|L(T!BuDGCO zoNZT=%_xMbko*hlYL}Lr*taSNRW1F@1&kj9i@TIdC=fj&0N@eyB`7fWtiKMWbATr7 zg2A&7Cg4T5(+4hN01qd{VyFg4sxm;p3HcbRj)$mUv5*IoG^Zgs!BAG-W%*=6t-UX1 zU4QHH#qWz97EB%r^T_V%r{|qM{wSWG<4a_D^Ew7a$i-0Yk9WjL_=N`g?oYJ(W=ebM z|GFOF|0d|pVM$+nV+MucRs3I=LV87{<7gYRlcx$_ud;|+Mjjfc2%;Bv>51`;2WwAO z{c#Xh5c{q-09~!XFT$9OGxf4;JiF(&p>h>ky0Ibgx8JJ};Iy=w>Y;6~e>xfhJR=#) z?X7~{(N*kOUXV%aks!{hW#9|2?9tcRK#m>F`IY_s5%sSFibr9p&|^RqvfLL<+0hfZ zQ2#zFFl5GcWlkcj8mkvA&?_^zI}S)_xCX$VFb1XnngbM-h5}-I!jWl0to`{s>4a!>rl_SvhYH` z#z%W=pEP5bHRfR-e>gHKrz_RgoAR0_lc8 z1wgm>6)?zy0O_Y4nZOkhU=5=m57R>ePM|>OJ{JSjfjgDPqai|s(%GPC-5q!&@I~B> zSVkm*1!$!dLJMNhmHgZ1#hYDkem|p7SVlnEVB!7mzhr7(ZDwave%rx4k2Hgd2yY zu*oy(^w{+B6AuMNhcISd%EV%^s3qi>3E{XaHr<;vT zLLrQgWh590jnIhuyr>83`CRZp^K;+SWri)#8qXn&eeR2GDV6$hV3 zC;|yg5QWXs$In0}S(;{3+Q)f=6U2}}MMHFMPv@82v>(iUde7*h*H5ztB$Znr8fO>y z!y`qZhxbu(c4#2?(|Dsu>wHrr5L8Mm7v}nG-NOl%x7?KV{2zXuG5c zINn@IL5?joMBh>QMAAEjtiT~pmik?HjwA;M0d{)zA}fX%gcBdHORe)+lk^9Zr_Sa` zqG{W1mf=ek9)Nz+@^^gU;jecyJU>{AcJz_GD5nPjG8kaCTN}IbF=6ffiih+nz7Y=} zg(Wv$gUXyf=OEH{vqj2}e8wO2W(9OkmWPm8m*O-uDNz}#qhmDS>q!LhLM6GcT7m~%ocUNs)9JZz20hA~qKL^0J>S_J*M z)nRkU{h6XFfO=E#6g^4IIX9ymv=`Tp0?dRuu?g;HXq3gz7`?Bzmy1d)VRWn*uU)mN zYWD;+?tYeZ=I>*4nFQYCc=YDtY9sq84;X$x+%XA_Fj)!tqt%uuX0^Y0;;2xn*cJp` zv=$LqqjK7*r)2+UuQcF?iwJcHvD+aWYZ@o41Yr=8*nku=w5P9*7f0p$0HdYq1mjgn zqcU#ytbI(erQh)M!$DN=c zob)aNpOrt>_~Jc-Tol-c0s~2SUY7LD?svNDNk)x9cNst4vPu@PS+VaFOyBnbK1YM?0W`%*k-$2d-mZTsWZ}_GVrOLYb{)xfkykLOnSth0t7LBH#Q4w#lnEDYfbqtK!^A6}Be(TLV z()-%kH^MOnA$DAl#6;0q;(8P`@0<463$}+n65fZVc3~e`j;{ez1xa zR7M72GnlI3{adw^n53_0n$*%)F3AZFNit>LsO49xGM6sRglr-`t*zImB_($s9aRZ^ z&0Sep_H>jCoh9EvJK07LYM$|kb zpMKcaMs8yX1!vnOQ3S1L+uNGR(kMc|g8X>h+?Bjl?4OzwFoO6T89rbg>H_FjBFwpG6+EE zN%ri5OinH4dsL@S;9}~71zhj3Y|b1D%iMp!(}3tW*_M4A1FjC`Qt+|9(rrsBs=M=0 z=P+YB005w8wBBy`qm+sncXSHNI0pDs9p%c?uqAMYf5Av zN{`(i^Dk2lRfB2kc(bs=xZ_}>O$S@>*IfzJ9Y@A!D>DLplnl#flvd9#dW19lhV@6I zg7(;x6Y1R@esAAx9~EFH;#6!88D3Eo6OPeuc-V6VT`DY^)HSJcR#NXt4T&eU4}~d; z0Fn1_5&O5cWUe_WJTSb1w%n5aNVNNEyPOYMZwL;2F;NPSZX9Zi3L)HWibmQLj+Q(y z0|vJdKi|%eB4*b1?5dt^k4}gfY_i!2PY>}oezLHK-qSK`o|;jK9wVpcG5}UeqKKKV zwAkJnxh$?{^^vB_=9Cg$q-j=1o0^9CBc7Z^iN${{7~#b*!f)!zH+Xc-o)P}&iG3)h zkER+>l~ovZnn`*;i)yw=^L|J6U>XclESv1t-E7u9rt|iD(?qErP8e0iHuK3F21!Gg zhSkOKt|Ciw5UxfwXHkKcRXrWPvM8IVz|T`6$E^b`eY&tCdfJh>$AhoHLQwt+$EpO! z6qjCaD;a!aRH(InPN0EbSzqnEh(f9~=_5zh?iXvux`#GflkhLWzmsNU2WbVYg=#98 zR!{FzrukEN33nzgVQe%2RGq^S`$UjYG&sDS3At2f#P-eX#vVUSAf)_QU z3AI#$B!`)QqMm}ttH8SbL16sA2YJYMtCP)Da0RI2KJmKf zk?bV%RwA>;1o3;{QWk*Yswqj~UyA@R)(pI><)rc0%1zMg`1JYp+ADycv0%6eUnjOD zpjRCuzug>@^hTa0cD17lC_BcE5mulOf^dB%HKV3>ZL_UJ!y`6fZM~6IY6LTnDTSE6ZpA->X_J`@mnH*#(6w=9ulM zF&}ZjdH}YkBZz5=8aNVG!>SYZx{?&xY{Y5SBIzQ8Jn5Nse8K9>=9DBj9!N*te{{wzHdE1LHD0SFMO8uS$w15z|ff$5W~VZ?dA~#x&f7)3^VDFqjU?~x3iTCKPY)su;gfIrWzWt zmY;zU#mjxz_8jIeVK~BRY(i+UNsTK?pd8>KX~aDYM~A@lfk1sd8INvqz#p1*VP|sv z6{M(1n_N`2!2u@_8gOib+#6(VAGC)e3I=RW zy}poa@S*3i@zaZFH*z?V0 zZB}+k$~M2@GE?x}J0CZ#NKAbymHTw~w|u}b{vJJEAO7WH_=tFXT7iR0Tr8Szk{iak z$#SA6*7Sa6t;@q0y>b)E5d$d|Y@c|VrT;+LRFZB%&-YNz(ku(a|0Mrqk-fk>JIhVR zRW*>HMqwwFSuf@BH?oBqO0u%qq?+xL zi?KbyZN#e+D$Y>mt9S!g^7{m&_nicl1+?{!5skjq?T+645K*SH6W7{CS5^(RRnWnJ zu-*}(r+t`J;^h+-mC7F7R1k1?_bN&-EFkF>ubQO5tN%<1+ttb(nP5_1*QDPi2e`EL zEY70`;CH@E1Q`l?p#JQ!Dizgg)l~aAB63}`WX4QuUXBYWWK@=qOm)-gp!NSrUD>wb z`&5h{PU$g14}q&Z#goj779|-*)vx2-ytB*DAF56zyo3Lg!}3V-x3njX{{-`3ElULW z4KL!Oa3qmcYDl_gZ`0rUNg!6{^QlIS(&$J;khg@ueo|3ere~{xQ{{SF0z)o108z*m z{!O|{d1(bNr;2gj>s)J*%~)y|+NiC2HG*A#Q$rRplR)6YbyXt`LXG3a1fSHnhzB|oJlDT;8qDTuHM*ux? zEq^i5^@aY6=L$#|Y;4LWE}vd!^UJ;K3~TgGOWfauWzGz;@6&Wh^s$OJ^To6|yG`g% zHcEO>7uZsP*xQp7Bi&H%n@8`6cRABq`2ZhDBQbqWlr(m{j%lrB#Lk0=~ac5t)t|Z>Q4jH30;8kH$qkz)OmxP^z%n# zHVReRmb=7d0aq%{>^6wnX?C|2k+Z8VMX6f`ns9SSpP2GrWZN74=-djRJqLuJ zdd_1KD)m+WOq5k&&9B}mo1DH7NnghlGHbT~Hfc>?%q6t4LcQ&Hs zGz;n#p~(4*d4EDN0TT#v2`HsD;(#aLZ@Ao=;K*I~*WgWr2^$b{uxMu7VqfUn7muy= zlPs##Z`6iXy`8JsFe~?!K$A2LjiiX$X<`?&_7kCKLkUNvE4|y;1MwnUBn-lBClYYL`wT;ySc6LAr+HJ0qIS$BW zW+9q1)lG1I;~@K@BKI2vV_^^yK1#fVh>(pEETGjZ0h5 zhbF0{A1<-AOhG4(N%nV*a)MmydUw{{(@K}+fhrB^Tt2tp_UBh5gL}Gh$ zWNynbo30F7KSq=my*=9TWcV*rHl+TnD~T(}88f8Z5<}^xx^e(b=GagHMkWG-mf4F( z;=Zn(lqU&U4zZ#AMOyo~*yZ$30NTH8=#T?=0adgYV$f+GWnEMOcz;bfA=rVQD>qZg yfK{G;*sMIT^q}!}G=q*C=Lfs;R>;VR z$X=mHWZd7o&-e4k@4jC5^*qlx=Q+gJU`;0Wjzk14Kr2 zu=aZFj1G8jZ37D;`U@gDMxbk^E62`y1F$Uo{bPtXm}F7lP9NPfKIWI5ef;dboWRe| zPsZ(%ySIb=6(^a?UM^`1YJw^rtlsTWbmwuD!;DB%b?}U>BPr*-HV6K#5krRGRm-9 z$5_1ZA<|iRr?ly@qMTxS>fxa+^Is{+QLIMv;nu^Cs@f0Sh(FC4!V~br!kfBEZX+*{ zHOSF!r@C}H-*t$0jdvz?rRP)_dEd^zHNn17;yph+{<1yNp8ny&YUrP$!f&m$&faQ- zAc7`7BkRo?vc6c z0<-nnd#{u&*2GL*ml!r6UHJCpQ)_R~1i&ocb>33(%f-Sy%Dq-rJL0 z%@@6di!&emRP?wh&j9{X!HRK`*zEHZLr1WJJNeDrs=LCzEX`f>yHz_vejtT^zF*1RtrrIo!F_j4Ef`}>UWp5OND z%&DkR;*N>KSKkW|;8xV4j<=vLmqxSq{0(^y$l#;1`Hm7N;tw=?Z!xr_Q!{J!c!R9kXI2heg$7akcGqZIWikBl#xb5=fi-~bD7xU63(VmHW@61}0@z^5Y`d16 zBInALloRu$7ARjPII^BJ*y$eRb@E^U_|8YaN@Zm=eRtgK{lFYhgeJ;9oU)FHE!|r} zfYO>$5hL^4^$~qiS3OvjaPV20UZB!)$8~;xeF4w{l$#E|&pJv@J}-PZs(Gw)=s2sA zPR^6@i;?ms6N?dZ8$`)vh9g+G+ASBjX`4Z01G@W`zM9BtUsz|_0Ybmz_%Bg z=-&eGg^sBfIJf^!QW$D$Wdic^O&KzW)3u4U$=<+sR1wX=N`n_e<(GQW*S3YYy(%gE z_{8ElEVLe|+wsvM_3M#lp!_JanT4r&~g{&YS{q zoh-`(%lKCcZ#fcz#0ezBU1vxjW>WtG=haEQjXe%SF$6>DFzU1DzlGE~4( zhMMsykhrj~KbP;wKD5voof-m^e?VNDGSlX+hC}AIfF=sZZcvz0po6dwei8?USd?DF z8bcAea+)fbC7bFc3Mv1O3bfLKYzIy`R?J_naT0r;zKiwfp@Gay9Sf*+V7c* zFzh}-vJ3Hgj)g7lU{lAgk~ItYowK!JeRr&%r26<#O}>VFuri-fzQmQvE5wFxH574& z_*>d({ACwCO>Dlswg`ES&B{&75%ILC6eix!AV*^T+b)+ z+%)H219+3$`~Bphr&tR8ZE4>`@2kE$mP}xA zymwFSeM!4NGH)*S;Auj6N*b6Cac;GnYM3m?&DHM|+AIG8CJIB;#h`qTK@PBJ3#@mS zbLe-Z`A#?5`60`T(P%@U>z1*J(_S# zYgCtd=6Vgx`gd}#HC+PIUhMG?Rbm-7k=V4*%db{Np7C;ilZvDb= zM*NJzAU~sQx8`u2efkcd){^(Mz_C6`9Vol-Sd<|4=T8ob-h$it`P;=HRS`1A7^$Al z0q-azOA(NelC1-k7-k6buuY}g$@RIp zxu&Fo-lin?-o{@dm+s%7HOaQ&pq8ur*DhG$u*@mn#9qlAG0Zt0x;RA_TbFF(smm*N z?C2|+n#7<1LJ{?Ql+e69EO?VvKduclW$g2-nCaW2KbdH=jnDL==|%Z*2U!i}0z^$| zKV45=t@+`N-BW-XzK*!k#X#e0nBYLd9JCFLFP;mZpwEt8Rp@q#ZBoQ+93jL%Y_Ij; z414O{YoM$z0yIhNi%NK=?YWL4sXzLrxP3Qs^3b*YML+iscO1T6bK46bllZKYfyC4> z!GdJ-^v3Jo5f(ZadbUBg=QDO!lV(3nHB9JT+hI;vx=pz!11@IPwGd&rPx;LDuyA+0 zmctth9HnC3uVR*}g-=Yl4TvhfO#_2`$1K5LmUGIBJMkoymH62Lj7#esIe#hBYulnQ z!hy+K{>TRdIj@2+a{hx)C3}DT3J%)(&K50AkWspF(ljjlgQ+IaM6loaAl=Ne7Kp?5 z?3a2%TQaPpFYwI+DcYD}K}y;d#B(zhh_t?$H0jDc5!r8%-STGv(ZmlH zv&h{B-W1yotL@6l5B$Si)165L0hnVSWf)*{&qxjw3FM)oE)P%Ju6R#&N{K~Nb8QZN-qYz|vO-mLsyEF+fxTnYxL*hnRK;E<^ZiEGB zV%WRPATK42hRR7%&K!u3>BOROB7tpUhKm2d@daQS_aAtI8Kz7vQR-Zsk#sE03C-I8 z`?$)V!4*Ot+WAn9;}@i$JL?q+J{Cz~LkFPP;4fSu(zswT#`dVQOq8S>!|>hmoZ)-8 zvpX_dYFAjqMOR;aI$Qi22|9mX>H7gmEB5pNnJ=RU8owsWHtH{zCfF=8U?GIFJUjtO z2RZd==uh>J(~$_yvwV|tBEI8+?$f;^doMdS+(?yZY#^xd&ohAF$7c?=UpO4)WWyDC z1v32x{PQ*d!C05C-wKN*?%%=?hs~PRp?>ZqAbMl}#0PPwUH@xlx9aGVckHaLphgmp zU5tzo7!<&xa|1f9D^sM-66_tD_>qy*9k{+BKW#)5JZ@ zSk%{1(0J3zGu-RrzcVrMH_%!Oq#p7};J-07ff-2%&AsPoK6x+ha*gp3-&yU-Rbfj0 z#&aBSJNce?(_0DU56it^q|8TkW+;>s?WQP4s(Vsjg)TprT+giAdcZ2Ax*Lw&t5`RP ztb~=Lo(%-M?=P^OU$))WZSf;#of$7O0=MJWAH1RG08J~SIJuABO^XxCcuJktTMWVr zx!d3UKmc22YEz8y4^6kqfz2AL%Rt)ZLq4`oH2h{j=|ek`N7E@XzzPyb>F za`=~KcBnb=Ux_QOo9aVtbwmm$^WWN3o|=$yW`Gma&FqRXeX0JRM=VArth_PEv^m0Z zZ%YVE=@%N2Ieu{UeAZnaH5+VJU?%1at9ZRIpiZ;x!3no0%*PKZf#GLi(O?4WHFo5Q zO*X2JP}pEd@!}t#$<%Yt6N;3cB7JOvdW>vB`q($4MIs0*55jDIG8U7icszzp3lM|{ zIpG+0ffsJkjQ=f^o}-@QBVxhlP;0CGuZp_R=}yd0V&;e{a)U~*8{&n5Pior#)?a6q zr8qqtjq|^9Jo zkA?3=_dTW*qx%9Al;Y1BOjY1W9;caR==IuQSK?0pQ771S(E+ohhms)NWyqFe?>Fy3 zD7tW3ey6f~$pw^A{)a~@(kF~zptCz`uoOdS>WG|c{GHu9bcua z{wKB<>n3XrgTl%T;F5SQ)LNsT4J_ius^t=gme@3}KWB%dBV=Z}mvz}LJnVPzZ#sxIxN~XFm=e zHO34vL006>;`3t7bf2`v3-p)ICaDBuPX}KWv1A=`0O?Pm-&(upwy%79F}cH5N8>3a+)j#c~M@;4^; zel2p;mu926lT>dB?fg!N{onTUckx@K|Hm5j4}$C;8E|VoSA5#oXshz=Kh1<3DM+ znhU?B4KnR#LM(ToSY&Q`z=q%Yu|sIAo8ViMiwf&|;{7topfa z4aZU83gzvkeJ7Snoh6?$qiUR)^uve+l>t2a(-k^s8=vm}U1I=?I4iVsm^u^KGs*>_ zI^JfHU{Eu6$0g*Rm;a!VKPpXOCvQs><=8c@opO75p~y#M)0`9_*VIyk;5z=_xN# z0`rF>TNg^_vy9Qe@yV@_{%%meZk5Ymw=|#hb73}Zr`4j>J&nQ7TP3JRhS`vNI@`F@ zEA@`RpIDB&0n|c2vjHP%9}^y!9(0<{PCU)nmcsLBW-b>)Ym>NPWc$3!@-t`Z3(?Uc~|8Bkcn?o$f%jiPGZuY^-4TJSAHUDY;9WdQ0P%W;CJ^udUzzN36 z8*YrqSFs)O#c85>vKm#vy<%txeX3rG5(1yk>=xLtM>|D|AEc5+T0ICh@|QYd+H87 zjlmioUFGWVML!nJ_6-iTR_3ngm-6Eh|tbr$7LQ8W2Lkt z`-_$g-k>v$M-(nx#CvAze%M+ws&KJ=zsG8RTudEgJ;;^}c+R;O6PjnHY^f7%fuZFD zic8B6RuXc?g{APQ=AQ?R^CWG*8!&QHm9qzndFPepCoGd&r?ewaQ5%!xE&Ilh0%q5| z+Q%vw45D@BqOYLNmZZW!B<-)6sGImFezP><92Yn2CI-1)K;ZmjLG8=?Q(29>xdhRO zI89X`A@}bcu88ytKQY0|S0nNUm!;@J3!juEA>>jk+2Ec#qw512AjhgDBskIZoPdqU zP+hTpII$>hE}4s^5U{FLMGDv-Seu>X7ni{jkOeEWJjs6DjOc3*_7h9HbqPLknhlngQJ`( zU5QxCx4p}o3U%`ln(tLWj{kZ+xq^Z$wl>BUPap7J)kS@R>X(CT!*`VYVjv zWIH}tg9jL^D!TOP|+c&KR|ifYSVbz>1=5IeJrRsbi_F;8LNFXCfQvvi+lO~bKPXh?#Q(@ zk0x7rUqBv{e4MNJ_I*hnO5(#z_=ik>T2sxqc6)h>FTXtMRsUY}J9I((E-q>({gFu%7jo0_>I8H0YfRO8S#q;8YL$e+j(RPv7q;Gw3T&=i! zpzd(R`zonJfnR@gi~qS$dw6NQxMI&EmZT@2RGg>O=p0T9x)1t)$FZZ`3K-~~(0QS4 Hcm4kWRQYbi literal 0 HcmV?d00001 diff --git a/themes/matrix/client/src/img/stores/.directory b/themes/matrix/client/src/img/stores/.directory new file mode 100644 index 000000000..7bdc8daf8 --- /dev/null +++ b/themes/matrix/client/src/img/stores/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,57,25 +Version=3 +ViewMode=1 diff --git a/themes/matrix/client/src/img/stores/applestore-badge.svg b/themes/matrix/client/src/img/stores/applestore-badge.svg new file mode 100644 index 000000000..ac111e597 --- /dev/null +++ b/themes/matrix/client/src/img/stores/applestore-badge.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/themes/matrix/client/src/img/stores/googleplay-badge.svg b/themes/matrix/client/src/img/stores/googleplay-badge.svg new file mode 100644 index 000000000..9e33e3aa2 --- /dev/null +++ b/themes/matrix/client/src/img/stores/googleplay-badge.svg @@ -0,0 +1,429 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/themes/matrix/client/src/img/success.png b/themes/matrix/client/src/img/success.png new file mode 100644 index 0000000000000000000000000000000000000000..ee9d6841bbb7208220a831c1c42917eb8facffa5 GIT binary patch literal 3147 zcmV-R47Br!P)@~8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H13&=@CK~#9!?VWj0Ro5NIWpwOJQDRbwNDwr}T5D`fYExS^ZNS!! z({?)3WhP^$({{SdG*eL?n@DR!L!g~W+NQSuv}t7VDXU9R+=7CjxFCufZipa@@1Y=Q z0fk@B56^R+j|cDGckj99-22XPzT=Dw+=`0CLjfNNgnC0Sw=jZrT^)0zv?CqKirDB=}*$K3Qy8 z0m#XXB}uo)ojuDZ2JEA;mJ)zo>nkK+#i=mr-oOa>%SoIPs_I~$EjO(IrB0#^a(-M+lx<9lN`o{IqVIbS~NiSdXS z#6y4#+4;gG!%zX}@U9%;6Klk9xgi43i5)q%FXr4q1XBU%l&+lB6MGhMEK32>WhV=t zbS47Ol|1sgZ;tzc#kvYW*YL?@T;^I{Hcn3gY_d~L7n?2u(7S%QdRK>hyX-yq-N}z3 znAFu7~3~!grS-_H?$ML&Q-^1W1)o)xkf}aUEDS zXKy#WS(dKX@un&fK(6jp0srJ3cJN)TsPU9;tBFSyfR$Ka zd)I0$W;x!Q0MQcg=vhqQ*W9@QJld}-vWEn~JeRu~UMbZiL9{mkHcH5&!#{nO4}5H| z@5p!}0A^CoDwww?#ozPU=uLo133>$hpKXBJyN%r7clAAhXR=m5B>*NQI~L~d_Dh1w zrv#uS*ZU=G@!(^7r^-GIzAF>6VqwmwYy7El9_%i_G6~x|{4VmS0-$Z zh1t83)t=uncL6eabTBT7di&VFV8i+CU~jB~e_YuQf2hfVm_zS#hrdJs`1PG_Fk@S) zPXRE&n^(fD9Z71=G1FauGNU{At)rXZ(!Cb1Cyq;{{(89-7O-72H!A$IoZ(+0@TYHE zs|db(UMx;jdY)zO0<;*}!JEiWI!<)>YUjF|;d_VwY$$wOpI6t}#1;Obs;}|&gAf2c_BYu$vKA@y7)sad{=9{?}TNC zGnpRSw-7%0#NpX45cpABwZnHMY?}>U^|43<1y~_6d{?`1-vQrcBmu%d&l&#tyDhrH zcjfuf=MPq(0BNGbe#g z>Rjl$#TEX!JGYp^cjejK)$o>w3e-M>eC{6(T{j!bOv~xH8b;KQ(l`Gr0`~ zaOf?-%9?CLJN0sRtIw6Q`1keqTDihMBMAH_5|E$(eR>Pt7 zZATNlU{4kXzAH=wz#T1ICQ!)7J6_{G;p1gO!uLLn%;XaA!-^01&7!$UfH#P|;h%Ok zLHHK&;M2##0VX$rU)}ddXnt^q-pY4~L!-c(xknO=02F!J%GekAc5E`5&K^ zF<7?Vfqn_!M<(0jx|j_FVG5d#Lm;+O&PJHJB}r)bbfT!8=}_?353Gao+m`|g{z->9 z;A5ud*{(643_=Mi9DyOh3C9fxGYNdUXr#uF>0qpxbHq_E;R}|Cg7_VVEeKItHOoxx zxC!B7rWGXo#n2^T{#MTtxmDk8fGN3&rh`u}0Y5iMu|$jr`|&+n1%V%$6K@&#bO2~Oh*~5{gss_DhL8E;&g7qh z$YtVC0^VD+%iFG468@x|Rgb)ui37n3MZUptw{Cgs6_{*6_;d|G9hjw5ERT(whg&aO z7Ct6CFaGynree`X?%~6+eJjI%HfL2Yn6X?uO2B$`Z$r1!((vj0e*u{Fg%K#hEk-Mi zdmi>en29HLlnwq26_tDm%>2q2-1*b`zq`7^fAFXuUM@_tG<-Vue;%yx(x~Vm2|9Iw zfB#`$K)}b1ptE-+$ADE{8;3i7+B8al@V{KR-tzDt5B=Jjz-lj!T&h6=gg>X?-GG3P znO>CmHdyi1u}jM`pyffQgg<(Ls5@Zgmq%m4s2e>_Mev=yJ@BOff=_S$;=$^#kAHpt zI{4_~K^PeHz84YqDCS(PhDly(?pryb_PCp%<@NDYfC!Rq(IvoQh>Lb-K+^Fd*wuIz z>bu%ufAa-ccWM{RFMcnez|%#agENW~=V=yzN?f)qcl}Oe#7qFUrvj&xK)k#DsG@}VYI&*_fJ(63EcXL}B0+dw{Ca;_`XvCB zAj3?rfU~j$5e4zP{pF}dfFKgPlz{tCFIIUAGF$++rR4jh1l)=|-Ed#xZMXnbf(axi zqy&b~@jG5AT{}@LKg|MA2|`6%O>AWe$m3r3%ezyi>cLJI0jLC#QUdmQE_d~VIiIeX zr588d1aSK#ke35?+q&-0C{CQICo^3IaJwgvH-`Gx^=+S4kPxOfFWm)jdpeMhi26P1 zogvr!&#_>|L;$yOf($ugsBLEN#p1-RTAuT{3E(koEawi?*YbZjYiH6gSo2{nfZGI; zoJCY#!ljMe>CZHRjUfWKO(mzxMJ1!};33!hJ!+?PO(B5C$|AYEZ20qiPh?@jFOBA5 zxBzZz3~gpvZOl>v@@zjqz<;cC4WA_h@VGa~4@3#@4xc>NpPsiTWjap=Tm|shfm9v{ z65w?{JTJa!ZpoS-h|u>!2;i|HwbZH-gxcd;UfPlL6OsB{C;>d`6BLQ~#GqCJ{HUL@ z&327^+`lDaUkfRKN4c6*(PkNaNr10pCC~ErrWYkQ%Z2PeMs@COMQUL#i2&V5l z$fR#RVfp%-7WzFh>Gupad;ghUfDxn!p*NB%=z9@!=^Iij=)01y)3>Jg(KzUz)zW`c lLjO-1{TleSLaf+V{|9F)WMK{Ekgos$002ovPDHLkV1l3q_6h(1 literal 0 HcmV?d00001 diff --git a/themes/matrix/client/src/img/user.png b/themes/matrix/client/src/img/user.png new file mode 100644 index 0000000000000000000000000000000000000000..00941399d34f0f2d85d323f61daa67c2c00c494d GIT binary patch literal 2933 zcmV-*3ySoKP)>+Y_1buC zheF*1yf*GR)m>@otnGcw%sqGZe)vfU+q>t^|F^R<_ujcTYIJlo%4jV9WAaR39I1IcZ1yCpS zIY0`~LOAsCXaEv{=cNF_2S1lxgn0!F-rv{6d0@U2VDjK!2d)?(^zpb(O#~{z<1L%h zz`>Ihfp_q;fNbDV1ck2z&vq|HoB+`1Xdnp~jL_hfe8nynpNI$nprH$ZHW~ak_zd`^ zVFiH3O##fnhzxsNhFJ&KlZFxi8uS9|F_R$&2KI2yF@ylnkj21V8EVk<1HThm0M?5q z!wen#x-@Ay#-LU3Q-KpQK-IVm#0Vh(6gr)CbjaY;1?MwNCoKRJI2)*zVQI!~;5m{4 zKw;knT4hL@Q-{ZzlmKWYD~#$v`_Iw@bH~becUG-S%q-brIc!hQ%CTE>@`@1rH~t^~ z41WiI7ypKEd+>ucn}h)DJO4JN0|WtfTGFky((<~-_O71$J^cftp@4sjuYs?HuSraT zYBjD2!VN!$U7T<~AWPy*p3UYD9S&zzZK#1y2EI1F2fi1fQN^iT1YmRXE_461$D3Uj zt`?WMJ^jO41wR&iFMLmYZvqmW;3a^GcjNIpyUo>oPp{y|g71wr5Rf26R{^k#lJlU> z;E|J`7V-*yELaPHq3y-G2*4(4_H+EMA1JSBYNZms53C7m<0t@*x4fXK060v^Myhx< z^3f-GCAX-B?*nV&Ji7@;T&HL$KpM}}+_t|Sw^I$@2iC~>tH7*@061HPO<3U4u(#Ll zsCIk$ywnB28nIT+BZv!B6#(a}u_;U3%(j=$=Do z#TDQ{E@c6dl>%I)Smctph;09_P|%>BeJ>}(((xfktJ z7s0;&KLKzvB#H$t|I_wH(ZlzFZ6PYb%qX@a7P;cyPd?-@iU8Oaq5{l|V%wsTEAFGy z*NTiH0Jepw04t){-gxASJA0|%rcngIwottjh+=#5NC6^K0BaQ6B~uCznF16>v8__2 z0Ffy`B??e41&CMy8c~2ADL}*u(1`*(ASuA*0;d!}b9zyLhw=;%u>#yj0luVI+|iR) z3XLLwHTR!XJA#H#02jsLHvDC;VR{11e>-6#D!>C2powB}vGJxJqX>XY&=VEFjRI6q zowRWAT5*}^;a|S)yiN7^;*#@GRF-=mZ=;7B#TC_}hF@9V(it0Xb`w6nHnOvU6tjZv zQCu5%GVtsb8)v#heEXR*D8MP|`{X294%5Wjv`g+KzVLjis&La`#O6b=`CF3!uv2g`gE;3*M` z#;x4QFBQM@Sb;#{W9HL+)51GMDB4+3-$eJ~&*NPZXX+P7fL8JkhK%LUCb~0)Po2vz z6DWM-oDh$Ojp1QkbGw`1CJ`gcS6NyF6W{}-0KXTd@WgEg$(;rE9XVelSop{vlmbkn zIxoQA5N1tN8<~EArgql=;phJ*x&WLxc7cc+qBP*Wtyu+B1lX9gzgWOezti6eA)5ti zfVZT)u7UgBK&6$ng5CG~*y2xspNrIpH&+Myu$D9>mOOMdV9Tq;UhoxC%|Js zpGXZc?K;kFCGqsE9J`?5w@)Hw&vy)6A®tx`|Cc!K{R~B2jc1N|ng#YPY z01+^g*}>)+v;a(kg*ZQ7)0sYS5YJw62)S4!2N7^)@}f|(3`qbcL4r0*!k9bJT`jgp}PPI5fXrZ%isracGF5;IdBtzN$?_Y z46k-0FnAp9&IZ2EGXt&yC`6b8>;=AxfZ)FdvVb2@IQAq2P>3)S*uhSVMm*K;1fHjK z%t;BL5Mc(81b7S(JhovA@HE9^O;P}b2rkn{6#(u#g!>KLW&ICB?B8ANXZSm~9Q{dlI$z1o fINvtfu|oa}IMcn@FFyTa00000NkvXXu0mjfV5=n> literal 0 HcmV?d00001 diff --git a/themes/matrix/client/src/img/warning.png b/themes/matrix/client/src/img/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..c6acd953b142a8e43e293c353a60e6313a8d39eb GIT binary patch literal 4038 zcmZ8kc{CK>7k@Km%nZhmB)g(al4J@g%*Ybi@{R0UV<~HPGGh#)vXiopt-hj2MImcp zY$;2&EGf;GWSK0JWq#AYzjw}k@4R>Jx$oW2UEdvZQv)=Q7!LpdZFo`Fg0oWo5|W!U zw`;lda28mgj^Py~XG9{237kF3|DsJG09+w|2|^mC_HqtIgY>L}{_%4U3Uvu^1EHaz zO4oh80%A^>WZHKCL(h;uQN z$(4fz2^gkydwnSstYvq?z;jH?4dHpjOIsi?F~5mv-=X~B1+IfT=c@TV0XW+s38jp3 zIm`8ol6U1Y3*FZ4G|uCdEr5XCymsumB+=8f9+kg$8eP;^u9f!rnogW5jie4Qx&UZq%;ioEF43zF#7yy~zM-eI zO?nd_=$d}*1Zdj89sj6I?h<22KC>b!%CwF;CBL2iff7f&(SHL%Y3gOm9%rm>z`BQ< zags$B!oM7@imA=`0F#zKMyK{>K{K;0WBK$OE(4l07$MAiJTzu}?T}$Eu6~)JEgc2l zf=SEgU@ShC_l!E7E{fG`pTiY)xdsO#!EcwQcM9VpAIi>eEYTaUlkwl)(ssK%=%={( zyLCh_G^+Jtx;(pcl>ozzx!j?_lkK#SDH7&caoLxQxBIRw{7*%VTL*&Jr`VX~UcGAs z20}q%Os|>1VApPeKz|^ki=-gYS z9Y@17L~Ki*qd&(F9GL7rg&nPk`xaYE{WG~yqtOG|AGd;=yeICQ3?P8Wl(?P zFZ7V8e_`3g%Z~$IWK8`8B+2Uaw1D@FC+7 z-$J8b@&2r&R`X87_vAo=1n`SqVsgn1)n)~(GkKZ^+y9#v%9lDe!PdCl<}d?4l&Z)= zLn&Pk*;lOwuF)@Xq&>Lrv$yIxxEBk9P*f6q;C&Gxctr z>UH@f5#<~SKVIH7{8TvYp}h5)?t8@rj)0$ z=(|UP)`a4gb}Nfh$}-Aot~qRv-2NG&Pw?!YJ_L{-cI0;gx6C17f#<(JU-shN4s}Xm zsr`bk<+y&nAzmdTfd%Fm{E6d?o6#b_8AlL0OZR6X0~$FN0^=Va&iV>~OZ62FfY!fr z+G@7W*EE5ULNUF6+k=dc=OuP(y~w^QFmG}IU~z1DYm-#g5&k$r%25&3Dt&RF(P6Qy z!hED53NQMhpoEN40!T@U!U(PZ!f|Zebkh%KFy7&Ljg0S%@80Y9{`L*_p0Gq(-Xa_n zWwVhZa3tlb{&BvDJc*;YmC03wmleLmlfhCIJloH!f053eR9|}uD3}(DuE;rORKDF~ z8OF`gF;dSjm&vc;j;H-8dLiCxQey{Hz=`iQ)BzERRTZji7F7reYFTV&VRD5#mM;P( zOnzY7S=<_1pZRu#%rT>*GCwrZtj>RI@Q?16O+_|h&X4@_qT z{7RG|7N$Y~RTfv^0XLX@uwvGl4+Up|W7er#Ssc0MwmZo`pUyz>8<(%}N)*8iLT<*$ zO7nn-T=u0({;o{JGM*TzB-T(t)5aqsCE#OXne$tyGP|&oKl$KN3NP@5DkD^g0|1p@ z4(AS`US%VnTz+8Z^%`KGKzv~a`sOjR&m!IZiOqmEvGch4*@fGRIo{5gpM0we7|#}f z^CTPH)gw)SBkBRwS>h6OBTukHNYUs@{TU=wUvp<|w38svkaGfXF17 zWRj^r4BA@-?+x!da`Cm2p9d}nF!q(p6NOeC5{5={Kr7n#qM8v0M{@o_vGlW5W}(rbH_w)a22X{DsMW>q`VKlT zXk-Y086nM7%Po?soy))lmj}z#H&QEp9}4F_KI_@4j-*^)ygEts*RkiKn62eo$G03- zOi`)M-~=((Ir@vJgpNwW2eeFB3y_uX|F=QcfJs>&llMuD- z;!BoDIu2Ioq}!HTPb2l-x0v3yVVB%8h?y?A85*|%wy3iFW1CtV+P>P+YD!=Qp&6nh zpIe}j|SXo%pJ_yAdzPv0Hd-05QBz!d!eVX!e_ z-js2+CCJYh%8^@Wg9~fkF6Qu>bT&Ro^dIN&*5>tgan0OP%w$V%fA-%-^*%>}*H>=U z!jTaxwGN=$~V1I2jb9T`^h|OQK?o+N?D|4 z%z=m!488|@gJ9jym%RV<;yy5CvE=1Zp4VzOXMit87iM+GU@MG@F&tH|m(TSZJC+>> zVV7QsOjvSjJ`@G(vuKq+ovB&m7i#;On+EK^sDZq)O`%>Aot~S$T6tk$6miZ z37nON_ttN@aBChO!q9G=2`91_S^1OMbP-+Qzb~7M7k%ZmY*tzbI>2DcboU%unRGe_ z^g6>eZQIZhx<$yz=BE-TqN+ya*KDr>e5)V=r(T0o09y}kAUswax2W8N6l5)zy{FXT zBCBMbJKg8c8{RJV4?lJfI=NJj3;36BD84{BOqIIaih|ifAHPMW@E@HB`s-w(SAV(o z8BYmb{tLZdgx&WRQ_Flp++DL9J%{!bU5L_O@OV^m-WN_@GfqN+t#6a>ZJ)fIEc54H z_Ahf?jW>B!W)Hfw>LIikDG8h3(g;8IH78vtMT}9pZk9gqaafF_b|zx9h71z{H7U<{ z+UCstV>w|G^N5qtm{_m!4V!Cc_#$GTw0CkB!PSFBD(8Zm(v9+Jmfk6V{CR2+G|9x>_Jus|!Ap5u#`Lp(Fwz*3I*OOGUT(q-M!Afy{tE@U zJ2F=Cu$IgfDXw+ZF2+tPekHA`bg@1(%I2vkB|8wwf zRU;Ls;@*5M8xL*Febm=t`gFhF?0_;=b~>tWNj@wC1yr}RMl6=> zX6-I}2_osksR_lEmhYexY@erD5yZS_@mPt{mq!Rag+PIruP1DNV3J?1b5O|b#ua+1 zKK*Qlxf{Zjnf!_bXYF=)Kg>LK0X;cl;x&6Us;m43gbnAydy`^s!rJ|y34o}Mlo51w#*DkxKu*ZSO^rjzY4`S%iTMPk?%#D9Hq0u zXO7WS9c7da`CWXmSEK4H`Zo6u$TGFji_X0wF zYHis}o|pf`w?RCV<8AhzFDtAF=BVX(@&7)kpvZHP-ER?mQZc1eo%81ghI*#D&vcxr F{{xrIJYfI; literal 0 HcmV?d00001 diff --git a/themes/matrix/client/src/index.ts b/themes/matrix/client/src/index.ts new file mode 100644 index 000000000..802004a8f --- /dev/null +++ b/themes/matrix/client/src/index.ts @@ -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 () { + (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, (global as any).u2f); + 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, (global as any).u2f); + 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); +})(); diff --git a/themes/matrix/client/src/lib/GetPromised.ts b/themes/matrix/client/src/lib/GetPromised.ts new file mode 100644 index 000000000..779139654 --- /dev/null +++ b/themes/matrix/client/src/lib/GetPromised.ts @@ -0,0 +1,14 @@ +import BluebirdPromise = require("bluebird"); + +export default function ($: JQueryStatic, url: string, data: Object, fn: any, + dataType: string): BluebirdPromise { + return new BluebirdPromise((resolve, reject) => { + $.get(url, {}, undefined, dataType) + .done((data: any) => { + resolve(data); + }) + .fail((xhr: JQueryXHR, textStatus: string) => { + reject(textStatus); + }); + }); +} \ No newline at end of file diff --git a/themes/matrix/client/src/lib/INotifier.ts b/themes/matrix/client/src/lib/INotifier.ts new file mode 100644 index 000000000..df947538f --- /dev/null +++ b/themes/matrix/client/src/lib/INotifier.ts @@ -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; +} \ No newline at end of file diff --git a/themes/matrix/client/src/lib/Notifier.ts b/themes/matrix/client/src/lib/Notifier.ts new file mode 100644 index 000000000..c0252b9b9 --- /dev/null +++ b/themes/matrix/client/src/lib/Notifier.ts @@ -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('status %s\ + %s', 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); + } +} \ No newline at end of file diff --git a/themes/matrix/client/src/lib/QueryParametersRetriever.ts b/themes/matrix/client/src/lib/QueryParametersRetriever.ts new file mode 100644 index 000000000..a529adb6e --- /dev/null +++ b/themes/matrix/client/src/lib/QueryParametersRetriever.ts @@ -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, " ")); + } +} \ No newline at end of file diff --git a/themes/matrix/client/src/lib/SafeRedirect.ts b/themes/matrix/client/src/lib/SafeRedirect.ts new file mode 100644 index 000000000..7e7684b8a --- /dev/null +++ b/themes/matrix/client/src/lib/SafeRedirect.ts @@ -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(); +} \ No newline at end of file diff --git a/themes/matrix/client/src/lib/firstfactor/FirstFactorValidator.ts b/themes/matrix/client/src/lib/firstfactor/FirstFactorValidator.ts new file mode 100644 index 000000000..eaa496fdd --- /dev/null +++ b/themes/matrix/client/src/lib/firstfactor/FirstFactorValidator.ts @@ -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 { + return new BluebirdPromise(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)); + }); + }); +} diff --git a/themes/matrix/client/src/lib/firstfactor/UISelectors.ts b/themes/matrix/client/src/lib/firstfactor/UISelectors.ts new file mode 100644 index 000000000..0e971b3c3 --- /dev/null +++ b/themes/matrix/client/src/lib/firstfactor/UISelectors.ts @@ -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"; diff --git a/themes/matrix/client/src/lib/firstfactor/index.ts b/themes/matrix/client/src/lib/firstfactor/index.ts new file mode 100644 index 000000000..24affee2d --- /dev/null +++ b/themes/matrix/client/src/lib/firstfactor/index.ts @@ -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); + }); +} + diff --git a/themes/matrix/client/src/lib/reset-password/constants.ts b/themes/matrix/client/src/lib/reset-password/constants.ts new file mode 100644 index 000000000..d48d4e67d --- /dev/null +++ b/themes/matrix/client/src/lib/reset-password/constants.ts @@ -0,0 +1,2 @@ + +export const FORM_SELECTOR = ".form-signin"; \ No newline at end of file diff --git a/themes/matrix/client/src/lib/reset-password/reset-password-form.ts b/themes/matrix/client/src/lib/reset-password/reset-password-form.ts new file mode 100644 index 000000000..b94279cde --- /dev/null +++ b/themes/matrix/client/src/lib/reset-password/reset-password-form.ts @@ -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); + }); +} diff --git a/themes/matrix/client/src/lib/reset-password/reset-password-request.ts b/themes/matrix/client/src/lib/reset-password/reset-password-request.ts new file mode 100644 index 000000000..846226d75 --- /dev/null +++ b/themes/matrix/client/src/lib/reset-password/reset-password-request.ts @@ -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); + }); +} + diff --git a/themes/matrix/client/src/lib/secondfactor/TOTPValidator.ts b/themes/matrix/client/src/lib/secondfactor/TOTPValidator.ts new file mode 100644 index 000000000..5394139a6 --- /dev/null +++ b/themes/matrix/client/src/lib/secondfactor/TOTPValidator.ts @@ -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 { + return new BluebirdPromise(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)); + }); + }); +} \ No newline at end of file diff --git a/themes/matrix/client/src/lib/secondfactor/U2FValidator.ts b/themes/matrix/client/src/lib/secondfactor/U2FValidator.ts new file mode 100644 index 000000000..5812922f7 --- /dev/null +++ b/themes/matrix/client/src/lib/secondfactor/U2FValidator.ts @@ -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 { + return new BluebirdPromise(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 { + 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, $); + }); +} diff --git a/themes/matrix/client/src/lib/secondfactor/constants.ts b/themes/matrix/client/src/lib/secondfactor/constants.ts new file mode 100644 index 000000000..50bba7571 --- /dev/null +++ b/themes/matrix/client/src/lib/secondfactor/constants.ts @@ -0,0 +1,3 @@ + +export const TOTP_FORM_SELECTOR = ".form-signin.totp"; +export const TOTP_TOKEN_SELECTOR = ".form-signin #token"; diff --git a/themes/matrix/client/src/lib/secondfactor/index.ts b/themes/matrix/client/src/lib/secondfactor/index.ts new file mode 100644 index 000000000..279723dce --- /dev/null +++ b/themes/matrix/client/src/lib/secondfactor/index.ts @@ -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); + }); +} \ No newline at end of file diff --git a/themes/matrix/client/src/lib/totp-register/totp-register.ts b/themes/matrix/client/src/lib/totp-register/totp-register.ts new file mode 100644 index 000000000..6a9aa7ee0 --- /dev/null +++ b/themes/matrix/client/src/lib/totp-register/totp-register.ts @@ -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); +} diff --git a/themes/matrix/client/src/lib/totp-register/ui-selector.ts b/themes/matrix/client/src/lib/totp-register/ui-selector.ts new file mode 100644 index 000000000..9d43fabea --- /dev/null +++ b/themes/matrix/client/src/lib/totp-register/ui-selector.ts @@ -0,0 +1,2 @@ + +export const QRCODE_ID_SELECTOR = "#qrcode"; \ No newline at end of file diff --git a/themes/matrix/client/src/lib/u2f-register/u2f-register.ts b/themes/matrix/client/src/lib/u2f-register/u2f-register.ts new file mode 100644 index 000000000..abf40ee05 --- /dev/null +++ b/themes/matrix/client/src/lib/u2f-register/u2f-register.ts @@ -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 { + return new BluebirdPromise(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 { + 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); + }); + }); +} diff --git a/themes/matrix/client/src/thirdparties/matrix.js b/themes/matrix/client/src/thirdparties/matrix.js new file mode 100644 index 000000000..f9c8d51d4 --- /dev/null +++ b/themes/matrix/client/src/thirdparties/matrix.js @@ -0,0 +1,58 @@ +// Parameters +const fontSize = 12; +const spdMult = 0.5; +const fadeSpd = 0.03; +const headColor = '#FFFFFF'; +const tailColor = '#00FF00'; + +canvas.width = window.innerWidth; +canvas.height = window.innerHeight; +let ctx = canvas.getContext('2d'); +let pos, spd, time, chars; + +function init() { + pos = []; spd = []; time = []; chars = []; + ctx.font = fontSize + 'pt Consolas'; + for (let i = 0; i < canvas.width / fontSize; i++) { + pos[i] = Math.random() * (canvas.height / fontSize); + spd[i] = (Math.random() + 0.2) * spdMult; + time[i] = 0; + chars[i] = ' '; + } +} + +function render() { + requestAnimationFrame(render); + + ctx.fillStyle = tailColor; + for (let i = 0; i < chars.length; ++i) { // Tails + ctx.fillText(chars[i], i * fontSize + 1, pos[i] * fontSize); + } + ctx.fillStyle = `rgba(0, 0, 0, ${fadeSpd})`; + ctx.fillRect(0, 0, canvas.width, canvas.height); // Fading + + ctx.fillStyle = headColor; + for (let x = 0; x < pos.length; ++x){ // Chars + if (time[x] > 1) { + let charCode = (Math.random() < 0.9) ? Math.random() * 93 + 33 + : Math.random() * 15 + 12688; + chars[x] = String.fromCharCode(charCode); + ctx.fillText(chars[x], x * fontSize + 1, pos[x] * fontSize + fontSize); + pos[x]++; + if (pos[x] * fontSize > canvas.height) pos[x] = 0; + time[x] = 0; + } + time[x] += spd[x]; + } +} + +window.onload = function() { + window.onresize = () => { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + ctx.clearRect(0, 0, canvas.width, canvas.height); + init(); + }; + init(); + render(); +}; diff --git a/themes/matrix/client/src/thirdparties/qrcode.min.js b/themes/matrix/client/src/thirdparties/qrcode.min.js new file mode 100644 index 000000000..993e88f39 --- /dev/null +++ b/themes/matrix/client/src/thirdparties/qrcode.min.js @@ -0,0 +1 @@ +var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); \ No newline at end of file diff --git a/themes/matrix/client/src/thirdparties/u2f-api.js b/themes/matrix/client/src/thirdparties/u2f-api.js new file mode 100644 index 000000000..8c7801e3b --- /dev/null +++ b/themes/matrix/client/src/thirdparties/u2f-api.js @@ -0,0 +1,749 @@ +//Copyright 2014-2015 Google Inc. All rights reserved. + +//Use of this source code is governed by a BSD-style +//license that can be found in the LICENSE file or at +//https://developers.google.com/open-source/licenses/bsd + +/** + * @fileoverview The U2F api. + */ +'use strict'; + + +/** + * Namespace for the U2F api. + * @type {Object} + */ +var u2f = u2f || {}; + +/** + * FIDO U2F Javascript API Version + * @number + */ +var js_api_version; + +/** + * The U2F extension id + * @const {string} + */ +// The Chrome packaged app extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the package Chrome app and does not require installing the U2F Chrome extension. + u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; +// The U2F Chrome extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the U2F Chrome extension to authenticate. +// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; + + +/** + * Message types for messsages to/from the extension + * @const + * @enum {string} + */ +u2f.MessageTypes = { + 'U2F_REGISTER_REQUEST': 'u2f_register_request', + 'U2F_REGISTER_RESPONSE': 'u2f_register_response', + 'U2F_SIGN_REQUEST': 'u2f_sign_request', + 'U2F_SIGN_RESPONSE': 'u2f_sign_response', + 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', + 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' +}; + + +/** + * Response status codes + * @const + * @enum {number} + */ +u2f.ErrorCodes = { + 'OK': 0, + 'OTHER_ERROR': 1, + 'BAD_REQUEST': 2, + 'CONFIGURATION_UNSUPPORTED': 3, + 'DEVICE_INELIGIBLE': 4, + 'TIMEOUT': 5 +}; + + +/** + * A message for registration requests + * @typedef {{ + * type: u2f.MessageTypes, + * appId: ?string, + * timeoutSeconds: ?number, + * requestId: ?number + * }} + */ +u2f.U2fRequest; + + +/** + * A message for registration responses + * @typedef {{ + * type: u2f.MessageTypes, + * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), + * requestId: ?number + * }} + */ +u2f.U2fResponse; + + +/** + * An error object for responses + * @typedef {{ + * errorCode: u2f.ErrorCodes, + * errorMessage: ?string + * }} + */ +u2f.Error; + +/** + * Data object for a single sign request. + * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} + */ +u2f.Transport; + + +/** + * Data object for a single sign request. + * @typedef {Array} + */ +u2f.Transports; + +/** + * Data object for a single sign request. + * @typedef {{ + * version: string, + * challenge: string, + * keyHandle: string, + * appId: string + * }} + */ +u2f.SignRequest; + + +/** + * Data object for a sign response. + * @typedef {{ + * keyHandle: string, + * signatureData: string, + * clientData: string + * }} + */ +u2f.SignResponse; + + +/** + * Data object for a registration request. + * @typedef {{ + * version: string, + * challenge: string + * }} + */ +u2f.RegisterRequest; + + +/** + * Data object for a registration response. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: Transports, + * appId: string + * }} + */ +u2f.RegisterResponse; + + +/** + * Data object for a registered key. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: ?Transports, + * appId: ?string + * }} + */ +u2f.RegisteredKey; + + +/** + * Data object for a get API register response. + * @typedef {{ + * js_api_version: number + * }} + */ +u2f.GetJsApiVersionResponse; + + +//Low level MessagePort API support + +/** + * Sets up a MessagePort to the U2F extension using the + * available mechanisms. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + */ +u2f.getMessagePort = function(callback) { + if (typeof chrome != 'undefined' && chrome.runtime) { + // The actual message here does not matter, but we need to get a reply + // for the callback to run. Thus, send an empty signature request + // in order to get a failure response. + var msg = { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: [] + }; + chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { + if (!chrome.runtime.lastError) { + // We are on a whitelisted origin and can talk directly + // with the extension. + u2f.getChromeRuntimePort_(callback); + } else { + // chrome.runtime was available, but we couldn't message + // the extension directly, use iframe + u2f.getIframePort_(callback); + } + }); + } else if (u2f.isAndroidChrome_()) { + u2f.getAuthenticatorPort_(callback); + } else if (u2f.isIosChrome_()) { + u2f.getIosPort_(callback); + } else { + // chrome.runtime was not available at all, which is normal + // when this origin doesn't have access to any extensions. + u2f.getIframePort_(callback); + } +}; + +/** + * Detect chrome running on android based on the browser's useragent. + * @private + */ +u2f.isAndroidChrome_ = function() { + var userAgent = navigator.userAgent; + return userAgent.indexOf('Chrome') != -1 && + userAgent.indexOf('Android') != -1; +}; + +/** + * Detect chrome running on iOS based on the browser's platform. + * @private + */ +u2f.isIosChrome_ = function() { + return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; +}; + +/** + * Connects directly to the extension via chrome.runtime.connect. + * @param {function(u2f.WrappedChromeRuntimePort_)} callback + * @private + */ +u2f.getChromeRuntimePort_ = function(callback) { + var port = chrome.runtime.connect(u2f.EXTENSION_ID, + {'includeTlsChannelId': true}); + setTimeout(function() { + callback(new u2f.WrappedChromeRuntimePort_(port)); + }, 0); +}; + +/** + * Return a 'port' abstraction to the Authenticator app. + * @param {function(u2f.WrappedAuthenticatorPort_)} callback + * @private + */ +u2f.getAuthenticatorPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedAuthenticatorPort_()); + }, 0); +}; + +/** + * Return a 'port' abstraction to the iOS client app. + * @param {function(u2f.WrappedIosPort_)} callback + * @private + */ +u2f.getIosPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedIosPort_()); + }, 0); +}; + +/** + * A wrapper for chrome.runtime.Port that is compatible with MessagePort. + * @param {Port} port + * @constructor + * @private + */ +u2f.WrappedChromeRuntimePort_ = function(port) { + this.port_ = port; +}; + +/** + * Format and return a sign request compliant with the JS API version supported by the extension. + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatSignRequest_ = + function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: challenge, + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: signRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + appId: appId, + challenge: challenge, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; +}; + +/** + * Format and return a register request compliant with the JS API version supported by the extension.. + * @param {Array} signRequests + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatRegisterRequest_ = + function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + for (var i = 0; i < registerRequests.length; i++) { + registerRequests[i].appId = appId; + } + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: registerRequests[0], + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + signRequests: signRequests, + registerRequests: registerRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + appId: appId, + registerRequests: registerRequests, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; +}; + + +/** + * Posts a message on the underlying channel. + * @param {Object} message + */ +u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { + this.port_.postMessage(message); +}; + + +/** + * Emulates the HTML 5 addEventListener interface. Works only for the + * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedChromeRuntimePort_.prototype.addEventListener = + function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message' || name == 'onmessage') { + this.port_.onMessage.addListener(function(message) { + // Emulate a minimal MessageEvent object + handler({'data': message}); + }); + } else { + console.error('WrappedChromeRuntimePort only supports onMessage'); + } +}; + +/** + * Wrap the Authenticator app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedAuthenticatorPort_ = function() { + this.requestId_ = -1; + this.requestObject_ = null; +} + +/** + * Launch the Authenticator intent. + * @param {Object} message + */ +u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { + var intentUrl = + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + + ';S.request=' + encodeURIComponent(JSON.stringify(message)) + + ';end'; + document.location = intentUrl; +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { + return "WrappedAuthenticatorPort_"; +}; + + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message') { + var self = this; + /* Register a callback to that executes when + * chrome injects the response. */ + window.addEventListener( + 'message', self.onRequestUpdate_.bind(self, handler), false); + } else { + console.error('WrappedAuthenticatorPort only supports message'); + } +}; + +/** + * Callback invoked when a response is received from the Authenticator. + * @param function({data: Object}) callback + * @param {Object} message message Object + */ +u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = + function(callback, message) { + var messageObject = JSON.parse(message.data); + var intentUrl = messageObject['intentURL']; + + var errorCode = messageObject['errorCode']; + var responseObject = null; + if (messageObject.hasOwnProperty('data')) { + responseObject = /** @type {Object} */ ( + JSON.parse(messageObject['data'])); + } + + callback({'data': responseObject}); +}; + +/** + * Base URL for intents to Authenticator. + * @const + * @private + */ +u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = + 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; + +/** + * Wrap the iOS client app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedIosPort_ = function() {}; + +/** + * Launch the iOS client app request + * @param {Object} message + */ +u2f.WrappedIosPort_.prototype.postMessage = function(message) { + var str = JSON.stringify(message); + var url = "u2f://auth?" + encodeURI(str); + location.replace(url); +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedIosPort_.prototype.getPortType = function() { + return "WrappedIosPort_"; +}; + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name !== 'message') { + console.error('WrappedIosPort only supports message'); + } +}; + +/** + * Sets up an embedded trampoline iframe, sourced from the extension. + * @param {function(MessagePort)} callback + * @private + */ +u2f.getIframePort_ = function(callback) { + // Create the iframe + var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; + var iframe = document.createElement('iframe'); + iframe.src = iframeOrigin + '/u2f-comms.html'; + iframe.setAttribute('style', 'display:none'); + document.body.appendChild(iframe); + + var channel = new MessageChannel(); + var ready = function(message) { + if (message.data == 'ready') { + channel.port1.removeEventListener('message', ready); + callback(channel.port1); + } else { + console.error('First event on iframe port was not "ready"'); + } + }; + channel.port1.addEventListener('message', ready); + channel.port1.start(); + + iframe.addEventListener('load', function() { + // Deliver the port to the iframe and initialize + iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); + }); +}; + + +//High-level JS API + +/** + * Default extension response timeout in seconds. + * @const + */ +u2f.EXTENSION_TIMEOUT_SEC = 30; + +/** + * A singleton instance for a MessagePort to the extension. + * @type {MessagePort|u2f.WrappedChromeRuntimePort_} + * @private + */ +u2f.port_ = null; + +/** + * Callbacks waiting for a port + * @type {Array} + * @private + */ +u2f.waitingForPort_ = []; + +/** + * A counter for requestIds. + * @type {number} + * @private + */ +u2f.reqCounter_ = 0; + +/** + * A map from requestIds to client callbacks + * @type {Object.} + * @private + */ +u2f.callbackMap_ = {}; + +/** + * Creates or retrieves the MessagePort singleton to use. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + * @private + */ +u2f.getPortSingleton_ = function(callback) { + if (u2f.port_) { + callback(u2f.port_); + } else { + if (u2f.waitingForPort_.length == 0) { + u2f.getMessagePort(function(port) { + u2f.port_ = port; + u2f.port_.addEventListener('message', + /** @type {function(Event)} */ (u2f.responseHandler_)); + + // Careful, here be async callbacks. Maybe. + while (u2f.waitingForPort_.length) + u2f.waitingForPort_.shift()(u2f.port_); + }); + } + u2f.waitingForPort_.push(callback); + } +}; + +/** + * Handles response messages from the extension. + * @param {MessageEvent.} message + * @private + */ +u2f.responseHandler_ = function(message) { + var response = message.data; + var reqId = response['requestId']; + if (!reqId || !u2f.callbackMap_[reqId]) { + console.error('Unknown or missing requestId in response.'); + return; + } + var cb = u2f.callbackMap_[reqId]; + delete u2f.callbackMap_[reqId]; + cb(response['responseData']); +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the sign request. + * @param {string=} appId + * @param {string=} challenge + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual sign request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual sign request in the supported API version. + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * @param {string=} appId + * @param {string=} challenge + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the register request. + * @param {string=} appId + * @param {Array} registerRequests + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual register request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual register request in the supported API version. + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * @param {string=} appId + * @param {Array} registerRequests + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatRegisterRequest_( + appId, registeredKeys, registerRequests, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + + +/** + * Dispatches a message to the extension to find out the supported + * JS API version. + * If the user is on a mobile phone and is thus using Google Authenticator instead + * of the Chrome extension, don't send the request and simply return 0. + * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.getApiVersion = function(callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + // If we are using Android Google Authenticator or iOS client app, + // do not fire an intent to ask which JS API version to use. + if (port.getPortType) { + var apiVersion; + switch (port.getPortType()) { + case 'WrappedIosPort_': + case 'WrappedAuthenticatorPort_': + apiVersion = 1.1; + break; + + default: + apiVersion = 0; + break; + } + callback({ 'js_api_version': apiVersion }); + return; + } + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var req = { + type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, + timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), + requestId: reqId + }; + port.postMessage(req); + }); +}; + diff --git a/themes/matrix/client/test/Notifier.test.ts b/themes/matrix/client/test/Notifier.test.ts new file mode 100644 index 000000000..70bfea146 --- /dev/null +++ b/themes/matrix/client/test/Notifier.test.ts @@ -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"); + }); +}); \ No newline at end of file diff --git a/themes/matrix/client/test/firstfactor/FirstFactorValidator.test.ts b/themes/matrix/client/test/firstfactor/FirstFactorValidator.test.ts new file mode 100644 index 000000000..ac8353278 --- /dev/null +++ b/themes/matrix/client/test/firstfactor/FirstFactorValidator.test.ts @@ -0,0 +1,44 @@ + +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", "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", "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."); + }); + }); +}); \ No newline at end of file diff --git a/themes/matrix/client/test/mocks/NotifierStub.ts b/themes/matrix/client/test/mocks/NotifierStub.ts new file mode 100644 index 000000000..9c268d66d --- /dev/null +++ b/themes/matrix/client/test/mocks/NotifierStub.ts @@ -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(); + } +} \ No newline at end of file diff --git a/themes/matrix/client/test/mocks/jquery.ts b/themes/matrix/client/test/mocks/jquery.ts new file mode 100644 index 000000000..273f90861 --- /dev/null +++ b/themes/matrix/client/test/mocks/jquery.ts @@ -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() + }; +} diff --git a/themes/matrix/client/test/mocks/u2f-api.ts b/themes/matrix/client/test/mocks/u2f-api.ts new file mode 100644 index 000000000..d123f6a95 --- /dev/null +++ b/themes/matrix/client/test/mocks/u2f-api.ts @@ -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() + }; +} \ No newline at end of file diff --git a/themes/matrix/client/test/secondfactor/TOTPValidator.test.ts b/themes/matrix/client/test/secondfactor/TOTPValidator.test.ts new file mode 100644 index 000000000..5dd6f15c3 --- /dev/null +++ b/themes/matrix/client/test/secondfactor/TOTPValidator.test.ts @@ -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(); + }); + }); +}); \ No newline at end of file diff --git a/themes/matrix/client/test/totp-register/totp-register.test.ts b/themes/matrix/client/test/totp-register/totp-register.test.ts new file mode 100644 index 000000000..86fc455a1 --- /dev/null +++ b/themes/matrix/client/test/totp-register/totp-register.test.ts @@ -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); + }); +}); \ No newline at end of file diff --git a/themes/matrix/client/tsconfig.json b/themes/matrix/client/tsconfig.json new file mode 100644 index 000000000..0bb4d62ff --- /dev/null +++ b/themes/matrix/client/tsconfig.json @@ -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/**/*" + ] +} diff --git a/themes/matrix/client/tslint.json b/themes/matrix/client/tslint.json new file mode 100644 index 000000000..c2c1b7501 --- /dev/null +++ b/themes/matrix/client/tslint.json @@ -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 + } +} diff --git a/themes/matrix/server/.directory b/themes/matrix/server/.directory new file mode 100644 index 000000000..b7754766c --- /dev/null +++ b/themes/matrix/server/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,58,20 +Version=3 +ViewMode=1 diff --git a/themes/matrix/server/src/index.ts b/themes/matrix/server/src/index.ts new file mode 100755 index 000000000..fcbf4d022 --- /dev/null +++ b/themes/matrix/server/src/index.ts @@ -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 "); + 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); diff --git a/themes/matrix/server/src/lib/.directory b/themes/matrix/server/src/lib/.directory new file mode 100644 index 000000000..006b379ad --- /dev/null +++ b/themes/matrix/server/src/lib/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,59,13 +Version=3 +ViewMode=1 diff --git a/themes/matrix/server/src/lib/AuthenticationSessionHandler.ts b/themes/matrix/server/src/lib/AuthenticationSessionHandler.ts new file mode 100644 index 000000000..57361bf8a --- /dev/null +++ b/themes/matrix/server/src/lib/AuthenticationSessionHandler.ts @@ -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; + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/ErrorReplies.ts b/themes/matrix/server/src/lib/ErrorReplies.ts new file mode 100644 index 000000000..f1c5f4fd1 --- /dev/null +++ b/themes/matrix/server/src/lib/ErrorReplies.ts @@ -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 }); +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/Exceptions.ts b/themes/matrix/server/src/lib/Exceptions.ts new file mode 100644 index 000000000..83fa4eb6b --- /dev/null +++ b/themes/matrix/server/src/lib/Exceptions.ts @@ -0,0 +1,88 @@ + +export class LdapSearchError extends Error { + constructor(message?: string) { + super(message); + this.name = "LdapSearchError"; + (Object).setPrototypeOf(this, LdapSearchError.prototype); + } +} + +export class LdapBindError extends Error { + constructor(message?: string) { + super(message); + this.name = "LdapBindError"; + (Object).setPrototypeOf(this, LdapBindError.prototype); + } +} + +export class LdapError extends Error { + constructor(message?: string) { + super(message); + this.name = "LdapError"; + (Object).setPrototypeOf(this, LdapError.prototype); + } +} + +export class IdentityError extends Error { + constructor(message?: string) { + super(message); + this.name = "IdentityError"; + (Object).setPrototypeOf(this, IdentityError.prototype); + } +} + +export class AccessDeniedError extends Error { + constructor(message?: string) { + super(message); + this.name = "AccessDeniedError"; + (Object).setPrototypeOf(this, AccessDeniedError.prototype); + } +} + +export class AuthenticationRegulationError extends Error { + constructor(message?: string) { + super(message); + this.name = "AuthenticationRegulationError"; + (Object).setPrototypeOf(this, AuthenticationRegulationError.prototype); + } +} + +export class InvalidTOTPError extends Error { + constructor(message?: string) { + super(message); + this.name = "InvalidTOTPError"; + (Object).setPrototypeOf(this, InvalidTOTPError.prototype); + } +} + +export class NotAuthenticatedError extends Error { + constructor(message?: string) { + super(message); + this.name = "NotAuthenticatedError"; + (Object).setPrototypeOf(this, NotAuthenticatedError.prototype); + } +} + +export class NotAuthorizedError extends Error { + constructor(message?: string) { + super(message); + this.name = "NotAuthanticatedError"; + (Object).setPrototypeOf(this, NotAuthorizedError.prototype); + } +} + +export class FirstFactorValidationError extends Error { + constructor(message?: string) { + super(message); + this.name = "FirstFactorValidationError"; + (Object).setPrototypeOf(this, FirstFactorValidationError.prototype); + } +} + +export class SecondFactorValidationError extends Error { + constructor(message?: string) { + super(message); + this.name = "SecondFactorValidationError"; + (Object).setPrototypeOf(this, FirstFactorValidationError.prototype); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/FirstFactorValidator.ts b/themes/matrix/server/src/lib/FirstFactorValidator.ts new file mode 100644 index 000000000..231060002 --- /dev/null +++ b/themes/matrix/server/src/lib/FirstFactorValidator.ts @@ -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 { + 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(); + }); +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/IdentityCheckMiddleware.spec.ts b/themes/matrix/server/src/lib/IdentityCheckMiddleware.spec.ts new file mode 100644 index 000000000..842ed6bcb --- /dev/null +++ b/themes/matrix/server/src/lib/IdentityCheckMiddleware.spec.ts @@ -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")); + }); + }); + }); +}); diff --git a/themes/matrix/server/src/lib/IdentityCheckMiddleware.ts b/themes/matrix/server/src/lib/IdentityCheckMiddleware.ts new file mode 100644 index 000000000..e72ea4db1 --- /dev/null +++ b/themes/matrix/server/src/lib/IdentityCheckMiddleware.ts @@ -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 { + + 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 { + 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 { + 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 { + + let authSession: AuthenticationSession; + const identityToken = objectPath.get( + 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 { + 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)); + }; +} diff --git a/themes/matrix/server/src/lib/IdentityCheckPreValidationTemplate.ts b/themes/matrix/server/src/lib/IdentityCheckPreValidationTemplate.ts new file mode 100644 index 000000000..0161ce406 --- /dev/null +++ b/themes/matrix/server/src/lib/IdentityCheckPreValidationTemplate.ts @@ -0,0 +1,3 @@ + + +export const PRE_VALIDATION_TEMPLATE = "need-identity-validation"; \ No newline at end of file diff --git a/themes/matrix/server/src/lib/IdentityValidable.ts b/themes/matrix/server/src/lib/IdentityValidable.ts new file mode 100644 index 000000000..075580c9e --- /dev/null +++ b/themes/matrix/server/src/lib/IdentityValidable.ts @@ -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; + postValidationInit(req: Express.Request): Bluebird; + + // 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; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/IdentityValidableStub.spec.ts b/themes/matrix/server/src/lib/IdentityValidableStub.spec.ts new file mode 100644 index 000000000..20a977140 --- /dev/null +++ b/themes/matrix/server/src/lib/IdentityValidableStub.spec.ts @@ -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 { + return this.preValidationInitStub(req); + } + + postValidationInit(req: Express.Request): Bluebird { + 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(); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/Server.spec.ts b/themes/matrix/server/src/lib/Server.spec.ts new file mode 100644 index 000000000..365163254 --- /dev/null +++ b/themes/matrix/server/src/lib/Server.spec.ts @@ -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(); + }); + }); +}); diff --git a/themes/matrix/server/src/lib/Server.ts b/themes/matrix/server/src/lib/Server.ts new file mode 100644 index 000000000..4090f6294 --- /dev/null +++ b/themes/matrix/server/src/lib/Server.ts @@ -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 { + 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((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 { + 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(); + } +} + diff --git a/themes/matrix/server/src/lib/ServerVariables.ts b/themes/matrix/server/src/lib/ServerVariables.ts new file mode 100644 index 000000000..cd3dd6dc8 --- /dev/null +++ b/themes/matrix/server/src/lib/ServerVariables.ts @@ -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; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/ServerVariablesInitializer.ts b/themes/matrix/server/src/lib/ServerVariablesInitializer.ts new file mode 100644 index 000000000..df79238cc --- /dev/null +++ b/themes/matrix/server/src/lib/ServerVariablesInitializer.ts @@ -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 { + 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 { + + 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); + }); + } +} diff --git a/themes/matrix/server/src/lib/ServerVariablesMockBuilder.spec.ts b/themes/matrix/server/src/lib/ServerVariablesMockBuilder.spec.ts new file mode 100644 index 000000000..7874702a0 --- /dev/null +++ b/themes/matrix/server/src/lib/ServerVariablesMockBuilder.spec.ts @@ -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 + }; + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/Level.ts b/themes/matrix/server/src/lib/authentication/Level.ts new file mode 100644 index 000000000..57b6a2346 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/Level.ts @@ -0,0 +1,5 @@ +export enum Level { + NOT_AUTHENTICATED = 0, + ONE_FACTOR = 1, + TWO_FACTOR = 2 +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/GroupsAndEmails.ts b/themes/matrix/server/src/lib/authentication/backends/GroupsAndEmails.ts new file mode 100644 index 000000000..3434ba66f --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/GroupsAndEmails.ts @@ -0,0 +1,5 @@ + +export interface GroupsAndEmails { + groups: string[]; + emails: string[]; +} diff --git a/themes/matrix/server/src/lib/authentication/backends/IUsersDatabase.ts b/themes/matrix/server/src/lib/authentication/backends/IUsersDatabase.ts new file mode 100644 index 000000000..d7fa13b7a --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/IUsersDatabase.ts @@ -0,0 +1,10 @@ +import Bluebird = require("bluebird"); + +import { GroupsAndEmails } from "./GroupsAndEmails"; + +export interface IUsersDatabase { + checkUserPassword(username: string, password: string): Bluebird; + getEmails(username: string): Bluebird; + getGroups(username: string): Bluebird; + updatePassword(username: string, newPassword: string): Bluebird; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts b/themes/matrix/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts new file mode 100644 index 000000000..19341a5dd --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts @@ -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 { + return this.checkUserPasswordStub(username, password); + } + + getEmails(username: string): Bluebird { + return this.getEmailsStub(username); + } + + getGroups(username: string): Bluebird { + return this.getGroupsStub(username); + } + + updatePassword(username: string, newPassword: string): Bluebird { + return this.updatePasswordStub(username, newPassword); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts b/themes/matrix/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts new file mode 100644 index 000000000..a258a78f7 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts @@ -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()); + }); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/file/FileUsersDatabase.ts b/themes/matrix/server/src/lib/authentication/backends/file/FileUsersDatabase.ts new file mode 100644 index 000000000..d34dde21e --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/file/FileUsersDatabase.ts @@ -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 { + return new Bluebird((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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + return this.readDatabase() + .then((database) => { + return this.checkUserExists(database, username) + .then(() => this.retrieveEmails(database, username)); + }); + } + + getGroups(username: string): Bluebird { + return this.readDatabase() + .then((database) => { + return this.checkUserExists(database, username) + .then(() => this.retrieveGroups(database, username)); + }); + } + + updatePassword(username: string, newPassword: string): Bluebird { + return this.readDatabase() + .then((database) => { + return this.checkUserExists(database, username) + .then(() => this.replacePassword(database, username, newPassword)); + }); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/file/ReadWriteQueue.ts b/themes/matrix/server/src/lib/authentication/backends/file/ReadWriteQueue.ts new file mode 100644 index 000000000..957ddaec5 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/file/ReadWriteQueue.ts @@ -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(); + } + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/ISession.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/ISession.ts new file mode 100644 index 000000000..da2c74433 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/ldap/ISession.ts @@ -0,0 +1,12 @@ + +import BluebirdPromise = require("bluebird"); + +export interface ISession { + open(): BluebirdPromise; + close(): BluebirdPromise; + + searchUserDn(username: string): BluebirdPromise; + searchEmails(username: string): BluebirdPromise; + searchGroups(username: string): BluebirdPromise; + modifyPassword(username: string, newPassword: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/ISessionFactory.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/ISessionFactory.ts new file mode 100644 index 000000000..014d1eea5 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/ldap/ISessionFactory.ts @@ -0,0 +1,6 @@ + +import { ISession } from "./ISession"; + +export interface ISessionFactory { + create(userDN: string, password: string): ISession; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts new file mode 100644 index 000000000..f4a6e6306 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts @@ -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); + }) + }); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts new file mode 100644 index 000000000..edda62ec6 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts @@ -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 = (session: ISession) => Bluebird; + +export class LdapUsersDatabase implements IUsersDatabase { + private sessionFactory: ISessionFactory; + private configuration: LdapConfiguration; + + constructor( + sessionFactory: ISessionFactory, + configuration: LdapConfiguration) { + this.sessionFactory = sessionFactory; + this.configuration = configuration; + } + + private withSession( + username: string, + password: string, + cb: SessionCallback): Bluebird { + const session = this.sessionFactory.create(username, password); + return session.open() + .then(() => cb(session)) + .finally(() => session.close()); + } + + checkUserPassword(username: string, password: string): Bluebird { + const that = this; + function verifyUserPassword(userDN: string) { + return that.withSession( + 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 { + 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 { + 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 { + 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)); + }); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts new file mode 100644 index 000000000..9dedfcb7c --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts @@ -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"); + }); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/SafeSession.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/SafeSession.ts new file mode 100644 index 000000000..572209066 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/ldap/SafeSession.ts @@ -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 { + return this.sesion.open(); + } + + close(): BluebirdPromise { + return this.sesion.close(); + } + + searchGroups(username: string): BluebirdPromise { + 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 { + 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 { + 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 { + try { + const sanitizedUsername = Sanitizer.sanitize(username); + return this.sesion.modifyPassword(sanitizedUsername, newPassword); + } + catch (e) { + return BluebirdPromise.reject(new Error(SPECIAL_CHAR_USED_MESSAGE)); + } + } +} diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts new file mode 100644 index 000000000..9dd33fed4 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts @@ -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 { 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); + }); +}); diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/Sanitizer.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/Sanitizer.ts new file mode 100644 index 000000000..be74132ad --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/ldap/Sanitizer.ts @@ -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; + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/Session.spec.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/Session.spec.ts new file mode 100644 index 000000000..d55f6a805 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/ldap/Session.spec.ts @@ -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"]); + }); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/Session.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/Session.ts new file mode 100644 index 000000000..e0284b3c4 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/ldap/Session.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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(); + }); + } +} diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/SessionFactory.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/SessionFactory.ts new file mode 100644 index 000000000..0b6c4bff3 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/ldap/SessionFactory.ts @@ -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 + ) + ); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts new file mode 100644 index 000000000..face3930d --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts @@ -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); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts new file mode 100644 index 000000000..5faf2ba18 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts @@ -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 { + return this.openStub(); + } + + close(): Bluebird { + return this.closeStub(); + } + + searchUserDn(username: string): Bluebird { + return this.searchUserDnStub(username); + } + + searchEmails(username: string): Bluebird { + return this.searchEmailsStub(username); + } + + searchGroups(username: string): Bluebird { + return this.searchGroupsStub(username); + } + + modifyPassword(username: string, newPassword: string): Bluebird { + return this.modifyPasswordStub(username, newPassword); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/connector/Connector.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/connector/Connector.ts new file mode 100644 index 000000000..2542ea7f9 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/ldap/connector/Connector.ts @@ -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; + unbindAsync(): Bluebird; + searchAsync(base: string, query: LdapJs.SearchOptions): Bluebird; + modifyAsync(userdn: string, change: LdapJs.Change): Bluebird; +} + +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 { + return this.client.bindAsync(username, password); + } + + unbindAsync(): Bluebird { + return this.client.unbindAsync(); + } + + searchAsync(base: string, query: any): Bluebird { + const that = this; + return this.client.searchAsync(base, query) + .then(function (res: EventEmitter) { + const doc: SearchEntry[] = []; + return new Bluebird((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 { + return this.client.modifyAsync(dn, changeRequest); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts new file mode 100644 index 000000000..61fef07a4 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts @@ -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); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts new file mode 100644 index 000000000..d11fa6386 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts @@ -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(); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts new file mode 100644 index 000000000..0b78225bf --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts @@ -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 { + return this.bindAsyncStub(username, password); + } + + unbindAsync(): BluebirdPromise { + return this.unbindAsyncStub(); + } + + searchAsync(base: string, query: any): BluebirdPromise { + return this.searchAsyncStub(base, query); + } + + modifyAsync(dn: string, changeRequest: any): BluebirdPromise { + return this.modifyAsyncStub(dn, changeRequest); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/connector/IConnector.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/connector/IConnector.ts new file mode 100644 index 000000000..1e63ab193 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/ldap/connector/IConnector.ts @@ -0,0 +1,9 @@ +import Bluebird = require("bluebird"); +import EventEmitter = require("events"); + +export interface IConnector { + bindAsync(username: string, password: string): Bluebird; + unbindAsync(): Bluebird; + searchAsync(base: string, query: any): Bluebird; + modifyAsync(dn: string, changeRequest: any): Bluebird; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts new file mode 100644 index 000000000..f9ed65eff --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts @@ -0,0 +1,5 @@ +import { IConnector } from "./IConnector"; + +export interface IConnectorFactory { + create(): IConnector; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/totp/ITotpHandler.ts b/themes/matrix/server/src/lib/authentication/totp/ITotpHandler.ts new file mode 100644 index 000000000..d600d31e4 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/totp/ITotpHandler.ts @@ -0,0 +1,6 @@ +import { TOTPSecret } from "../../../../types/TOTPSecret"; + +export interface ITotpHandler { + generate(label: string, issuer: string): TOTPSecret; + validate(token: string, secret: string): boolean; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/totp/TotpHandler.spec.ts b/themes/matrix/server/src/lib/authentication/totp/TotpHandler.spec.ts new file mode 100644 index 000000000..67cffa638 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/totp/TotpHandler.spec.ts @@ -0,0 +1,39 @@ + +import { TotpHandler } from "./TotpHandler"; +import Sinon = require("sinon"); +import Speakeasy = require("speakeasy"); +import Assert = require("assert"); + +describe("authentication/totp/TotpHandler", function() { + let totpValidator: TotpHandler; + let validateStub: Sinon.SinonStub; + + beforeEach(() => { + validateStub = Sinon.stub(Speakeasy.totp, "verify"); + totpValidator = new TotpHandler(Speakeasy); + }); + + afterEach(function() { + validateStub.restore(); + }); + + it("should validate the TOTP token", function() { + const totp_secret = "NBD2ZV64R9UV1O7K"; + const token = "token"; + validateStub.withArgs({ + secret: totp_secret, + token: token, + encoding: "base32", + window: 1 + }).returns(true); + Assert(totpValidator.validate(token, totp_secret)); + }); + + it("should not validate a wrong TOTP token", function() { + const totp_secret = "NBD2ZV64R9UV1O7K"; + const token = "wrong token"; + validateStub.returns(false); + Assert(!totpValidator.validate(token, totp_secret)); + }); +}); + diff --git a/themes/matrix/server/src/lib/authentication/totp/TotpHandler.ts b/themes/matrix/server/src/lib/authentication/totp/TotpHandler.ts new file mode 100644 index 000000000..dfab502a7 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/totp/TotpHandler.ts @@ -0,0 +1,36 @@ +import { ITotpHandler } from "./ITotpHandler"; +import { TOTPSecret } from "../../../../types/TOTPSecret"; +import Speakeasy = require("speakeasy"); + +const TOTP_ENCODING = "base32"; +const WINDOW: number = 1; + +export class TotpHandler implements ITotpHandler { + private speakeasy: typeof Speakeasy; + + constructor(speakeasy: typeof Speakeasy) { + this.speakeasy = speakeasy; + } + + generate(label: string, issuer: string): TOTPSecret { + const secret = this.speakeasy.generateSecret({ + otpauth_url: false + }) as TOTPSecret; + + secret.otpauth_url = this.speakeasy.otpauthURL({ + secret: secret.ascii, + label: label, + issuer: issuer + }); + return secret; + } + + validate(token: string, secret: string): boolean { + return this.speakeasy.totp.verify({ + secret: secret, + encoding: TOTP_ENCODING, + token: token, + window: WINDOW + }); + } +} diff --git a/themes/matrix/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts b/themes/matrix/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts new file mode 100644 index 000000000..ea93330d9 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts @@ -0,0 +1,22 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import { ITotpHandler } from "./ITotpHandler"; +import { TOTPSecret } from "../../../../types/TOTPSecret"; + +export class TotpHandlerStub implements ITotpHandler { + generateStub: Sinon.SinonStub; + validateStub: Sinon.SinonStub; + + constructor() { + this.generateStub = Sinon.stub(); + this.validateStub = Sinon.stub(); + } + + generate(label: string, issuer: string): TOTPSecret { + return this.generateStub(label, issuer); + } + + validate(token: string, secret: string): boolean { + return this.validateStub(token, secret); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/u2f/IU2fHandler.ts b/themes/matrix/server/src/lib/authentication/u2f/IU2fHandler.ts new file mode 100644 index 000000000..b9b7d6f26 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/u2f/IU2fHandler.ts @@ -0,0 +1,9 @@ +import U2f = require("u2f"); + +export interface IU2fHandler { + request(appId: string, keyHandle?: string): U2f.Request; + checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) + : U2f.RegistrationResult | U2f.Error; + checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) + : U2f.SignatureResult | U2f.Error; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/u2f/U2fHandler.ts b/themes/matrix/server/src/lib/authentication/u2f/U2fHandler.ts new file mode 100644 index 000000000..bf3891e5b --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/u2f/U2fHandler.ts @@ -0,0 +1,24 @@ +import { IU2fHandler } from "./IU2fHandler"; +import U2f = require("u2f"); + +export class U2fHandler implements IU2fHandler { + private u2f: typeof U2f; + + constructor(u2f: typeof U2f) { + this.u2f = u2f; + } + + request(appId: string, keyHandle?: string): U2f.Request { + return this.u2f.request(appId, keyHandle); + } + + checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) + : U2f.RegistrationResult | U2f.Error { + return this.u2f.checkRegistration(registrationRequest, registrationResponse); + } + + checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) + : U2f.SignatureResult | U2f.Error { + return this.u2f.checkSignature(signatureRequest, signatureResponse, publicKey); + } +} diff --git a/themes/matrix/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts b/themes/matrix/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts new file mode 100644 index 000000000..135d7eb06 --- /dev/null +++ b/themes/matrix/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts @@ -0,0 +1,31 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import U2f = require("u2f"); +import { IU2fHandler } from "./IU2fHandler"; + + +export class U2fHandlerStub implements IU2fHandler { + requestStub: Sinon.SinonStub; + checkRegistrationStub: Sinon.SinonStub; + checkSignatureStub: Sinon.SinonStub; + + constructor() { + this.requestStub = Sinon.stub(); + this.checkRegistrationStub = Sinon.stub(); + this.checkSignatureStub = Sinon.stub(); + } + + request(appId: string, keyHandle?: string): U2f.Request { + return this.requestStub(appId, keyHandle); + } + + checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) + : U2f.RegistrationResult | U2f.Error { + return this.checkRegistrationStub(registrationRequest, registrationResponse); + } + + checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) + : U2f.SignatureResult | U2f.Error { + return this.checkSignatureStub(signatureRequest, signatureResponse, publicKey); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authorization/Authorizer.spec.ts b/themes/matrix/server/src/lib/authorization/Authorizer.spec.ts new file mode 100644 index 000000000..586814042 --- /dev/null +++ b/themes/matrix/server/src/lib/authorization/Authorizer.spec.ts @@ -0,0 +1,372 @@ + +import Assert = require("assert"); +import winston = require("winston"); +import { Authorizer } from "./Authorizer"; +import { ACLConfiguration, ACLRule } from "../configuration/schema/AclConfiguration"; +import { Level } from "./Level"; + +describe("authorization/Authorizer", function () { + let authorizer: Authorizer; + let configuration: ACLConfiguration; + + describe("configuration is null", function() { + it("should allow access to anything, anywhere for anybody", function() { + configuration = undefined; + authorizer = new Authorizer(configuration, winston); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1", "group2"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "admin.example.com", resource: "/"}, {user: "user3", groups: ["group3"]}), Level.BYPASS); + }); + }); + + describe("configuration is not null", function () { + beforeEach(function () { + configuration = { + default_policy: "deny", + rules: [] + }; + authorizer = new Authorizer(configuration, winston); + }); + + describe("check access control with default policy to deny", function () { + beforeEach(function () { + configuration.default_policy = "deny"; + }); + + it("should deny access when no rule is provided", function () { + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should control access when multiple domain matcher is provided", function () { + configuration.rules = [{ + domain: "*.mail.example.com", + policy: "two_factor", + subject: "user:user1", + resources: [".*"] + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should allow access to all resources when resources is not provided", function () { + configuration.rules = [{ + domain: "*.mail.example.com", + policy: "two_factor", + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + describe("check user rules", function () { + it("should allow access when user has a matching allowing rule", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: [".*"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should deny to other users", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: [".*"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user2", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); + }); + + it("should allow user access only to specific resources", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: ["/private/.*", "^/begin", "/end$"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/middle/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/begin"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/not/begin"}, {user: "user1", groups: ["group1"]}), Level.DENY); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end/x"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should allow access to multiple domains", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: [".*"], + subject: "user:user1" + }, { + domain: "home1.example.com", + policy: "one_factor", + resources: [".*"], + subject: "user:user1" + }, { + domain: "home2.example.com", + policy: "deny", + resources: [".*"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home1.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); + Assert.equal(authorizer.authorization({domain: "home2.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home3.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should apply rules in order", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "one_factor", + resources: ["/my/private/resource"], + subject: "user:user1" + }, { + domain: "home.example.com", + policy: "deny", + resources: ["^/my/private/.*"], + subject: "user:user1" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/my/.*"], + subject: "user:user1" + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/poney"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/duck"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/resource"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); + }); + }); + + describe("check group rules", function () { + it("should allow access when user is in group having a matching allowing rule", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: ["^/$"], + subject: "group:group1" + }, { + domain: "home.example.com", + policy: "one_factor", + resources: ["^/test$"], + subject: "group:group2" + }, { + domain: "home.example.com", + policy: "deny", + resources: ["^/private$"], + subject: "group:group2" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.ONE_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); + }); + }); + }); + + describe("check any rules", function () { + it("should control access when any rules are defined", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "bypass", + resources: ["^/public$"] + }, { + domain: "home.example.com", + policy: "deny", + resources: ["^/private$"] + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, + {user: "user4", groups: ["group5"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, + {user: "user4", groups: ["group5"]}), Level.DENY); + }); + }); + + describe("check access control with default policy to allow", function () { + beforeEach(function () { + configuration.default_policy = "bypass"; + }); + + it("should allow access to anything when no rule is provided", function () { + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + }); + + it("should deny access to one resource when defined", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["/test"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + }); + }); + + describe("check access control with complete use case", function () { + beforeEach(function () { + configuration.default_policy = "deny"; + }); + + it("should control access of multiple user (real use case)", function () { + // Let say we have three users: admin, john, harry. + // admin is in groups ["admins"] + // john is in groups ["dev", "admin-private"] + // harry is in groups ["dev"] + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: ["^/public$", "^/$"] + }, { + domain: "home.example.com", + policy: "two_factor", + resources: [".*"], + subject: "group:admins" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/private/?.*"], + subject: "group:admin-private" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/private/john$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/private/harry"], + subject: "user:harry" + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); + }); + + it("should allow when allowed at group level and denied at user level", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"], + subject: "group:dev" + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); + }); + + it("should allow access when allowed at 'any' level and denied at user level", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"] + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); + }); + + it("should allow access when allowed at 'any' level and denied at group level", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "group:dev" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"] + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); + }); + + it("should respect rules precedence", function () { + // the priority from least to most is 'default_policy', 'all', 'group', 'user' + // and the first rules in each category as a lower priority than the latest. + // You can think of it that way: they override themselves inside each category. + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "group:dev" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"] + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + }); + }); + }); +}); diff --git a/themes/matrix/server/src/lib/authorization/Authorizer.ts b/themes/matrix/server/src/lib/authorization/Authorizer.ts new file mode 100644 index 000000000..889b7ec20 --- /dev/null +++ b/themes/matrix/server/src/lib/authorization/Authorizer.ts @@ -0,0 +1,85 @@ + +import { ACLConfiguration, ACLPolicy, ACLRule } from "../configuration/schema/AclConfiguration"; +import { IAuthorizer } from "./IAuthorizer"; +import { Winston } from "../../../types/Dependencies"; +import { MultipleDomainMatcher } from "./MultipleDomainMatcher"; +import { Level } from "./Level"; +import { Object } from "./Object"; +import { Subject } from "./Subject"; + +function MatchDomain(actualDomain: string) { + return function (rule: ACLRule): boolean { + return MultipleDomainMatcher.match(actualDomain, rule.domain); + }; +} + +function MatchResource(actualResource: string) { + return function (rule: ACLRule): boolean { + // If resources key is not provided, the rule applies to all resources. + if (!rule.resources) return true; + + for (let i = 0; i < rule.resources.length; ++i) { + const regexp = new RegExp(rule.resources[i]); + if (regexp.test(actualResource)) return true; + } + return false; + }; +} + +function MatchSubject(subject: Subject) { + return (rule: ACLRule) => { + // If no subject, matches anybody + if (!rule.subject) return true; + + if (rule.subject.startsWith("user:")) { + const ruleUser = rule.subject.split(":")[1]; + if (subject.user == ruleUser) return true; + } + + if (rule.subject.startsWith("group:")) { + const ruleGroup = rule.subject.split(":")[1]; + if (subject.groups.indexOf(ruleGroup) > -1) return true; + } + return false; + }; +} + +export class Authorizer implements IAuthorizer { + private logger: Winston; + private readonly configuration: ACLConfiguration; + + constructor(configuration: ACLConfiguration, logger_: Winston) { + this.logger = logger_; + this.configuration = configuration; + } + + private getMatchingRules(object: Object, subject: Subject): ACLRule[] { + const rules = this.configuration.rules; + if (!rules) return []; + return rules + .filter(MatchDomain(object.domain)) + .filter(MatchResource(object.resource)) + .filter(MatchSubject(subject)); + } + + private ruleToLevel(policy: string): Level { + if (policy == "bypass") { + return Level.BYPASS; + } else if (policy == "one_factor") { + return Level.ONE_FACTOR; + } else if (policy == "two_factor") { + return Level.TWO_FACTOR; + } + return Level.DENY; + } + + authorization(object: Object, subject: Subject): Level { + if (!this.configuration) return Level.BYPASS; + + const rules = this.getMatchingRules(object, subject); + + return (rules.length > 0) + ? this.ruleToLevel(rules[0].policy) // extract the policy of the first matching rule + : this.ruleToLevel(this.configuration.default_policy); // otherwise use the default policy + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authorization/AuthorizerStub.spec.ts b/themes/matrix/server/src/lib/authorization/AuthorizerStub.spec.ts new file mode 100644 index 000000000..9bd6f4a85 --- /dev/null +++ b/themes/matrix/server/src/lib/authorization/AuthorizerStub.spec.ts @@ -0,0 +1,17 @@ +import Sinon = require("sinon"); +import { IAuthorizer } from "./IAuthorizer"; +import { Level } from "./Level"; +import { Object } from "./Object"; +import { Subject } from "./Subject"; + +export class AuthorizerStub implements IAuthorizer { + authorizationMock: Sinon.SinonStub; + + constructor() { + this.authorizationMock = Sinon.stub(); + } + + authorization(object: Object, subject: Subject): Level { + return this.authorizationMock(object, subject); + } +} diff --git a/themes/matrix/server/src/lib/authorization/IAuthorizer.ts b/themes/matrix/server/src/lib/authorization/IAuthorizer.ts new file mode 100644 index 000000000..fe7ba367a --- /dev/null +++ b/themes/matrix/server/src/lib/authorization/IAuthorizer.ts @@ -0,0 +1,7 @@ +import { Level } from "./Level"; +import { Subject } from "./Subject"; +import { Object } from "./Object"; + +export interface IAuthorizer { + authorization(object: Object, subject: Subject): Level; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authorization/Level.ts b/themes/matrix/server/src/lib/authorization/Level.ts new file mode 100644 index 000000000..d12802610 --- /dev/null +++ b/themes/matrix/server/src/lib/authorization/Level.ts @@ -0,0 +1,6 @@ +export enum Level { + BYPASS = 0, + ONE_FACTOR = 1, + TWO_FACTOR = 2, + DENY = 3 +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authorization/MultipleDomainMatcher.ts b/themes/matrix/server/src/lib/authorization/MultipleDomainMatcher.ts new file mode 100644 index 000000000..64c647a4a --- /dev/null +++ b/themes/matrix/server/src/lib/authorization/MultipleDomainMatcher.ts @@ -0,0 +1,12 @@ + +export class MultipleDomainMatcher { + static match(domain: string, pattern: string): boolean { + if (pattern.startsWith("*") && + domain.endsWith(pattern.substr(1))) { + return true; + } + else if (domain == pattern) { + return true; + } + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authorization/Object.ts b/themes/matrix/server/src/lib/authorization/Object.ts new file mode 100644 index 000000000..5411b0d24 --- /dev/null +++ b/themes/matrix/server/src/lib/authorization/Object.ts @@ -0,0 +1,5 @@ + +export interface Object { + domain: string; + resource: string; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authorization/Subject.ts b/themes/matrix/server/src/lib/authorization/Subject.ts new file mode 100644 index 000000000..310d6b4c0 --- /dev/null +++ b/themes/matrix/server/src/lib/authorization/Subject.ts @@ -0,0 +1,5 @@ + +export interface Subject { + user: string; + groups: string[]; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/ConfigurationParser.spec.ts b/themes/matrix/server/src/lib/configuration/ConfigurationParser.spec.ts new file mode 100644 index 000000000..60c0f6182 --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/ConfigurationParser.spec.ts @@ -0,0 +1,171 @@ +import * as Assert from "assert"; +import { Configuration } from "./schema/Configuration"; +import { ACLConfiguration } from "./schema/AclConfiguration"; +import { ConfigurationParser } from "./ConfigurationParser"; + +describe("configuration/ConfigurationParser", function () { + function buildYamlConfig(): Configuration { + const yaml_config: Configuration = { + port: 8080, + authentication_backend: { + ldap: { + url: "http://ldap", + base_dn: "dc=example,dc=com", + additional_users_dn: "ou=users", + additional_groups_dn: "ou=groups", + user: "user", + password: "pass" + }, + }, + session: { + domain: "example.com", + secret: "secret", + expiration: 40000 + }, + storage: { + local: { + path: "/mydirectory" + } + }, + regulation: { + max_retries: 3, + find_time: 5 * 60, + ban_time: 5 * 60 + }, + logs_level: "debug", + notifier: { + email: { + username: "user", + password: "password", + sender: "admin@example.com", + service: "gmail" + } + } + }; + return yaml_config; + } + + describe("port", function () { + it("should read the port from the yaml file", function () { + const yaml_config = buildYamlConfig(); + yaml_config.port = 7070; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.port, 7070); + }); + + it("should default the port to 8080 if not provided", function () { + const yaml_config = buildYamlConfig(); + delete yaml_config.port; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.port, 8080); + }); + }); + + describe("test session configuration", function() { + it("should get the session attributes", function () { + const yaml_config = buildYamlConfig(); + yaml_config.session = { + domain: "example.com", + secret: "secret", + expiration: 3600, + inactivity: 4000 + }; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.session.domain, "example.com"); + Assert.equal(config.session.secret, "secret"); + Assert.equal(config.session.expiration, 3600); + Assert.equal(config.session.inactivity, 4000); + }); + + it("should be ok not specifying inactivity", function () { + const yaml_config = buildYamlConfig(); + yaml_config.session = { + domain: "example.com", + secret: "secret", + expiration: 3600 + }; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.session.domain, "example.com"); + Assert.equal(config.session.secret, "secret"); + Assert.equal(config.session.expiration, 3600); + Assert.equal(config.session.inactivity, undefined); + }); + }); + + it("should get the log level", function () { + const yaml_config = buildYamlConfig(); + yaml_config.logs_level = "debug"; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.logs_level, "debug"); + }); + + it("should get the notifier config", function () { + const userConfig = buildYamlConfig(); + userConfig.notifier = { + email: { + username: "user", + password: "pass", + sender: "admin@example.com", + service: "gmail" + } + }; + const config = ConfigurationParser.parse(userConfig); + Assert.deepEqual(config.notifier, { + email: { + username: "user", + password: "pass", + sender: "admin@example.com", + service: "gmail" + } + }); + }); + + describe("access_control", function() { + it("should adapt access_control when it is already ok", function () { + const userConfig = buildYamlConfig(); + userConfig.access_control = { + default_policy: "deny", + rules: [{ + domain: "www.example.com", + policy: "two_factor", + subject: "user:user" + }, { + domain: "public.example.com", + policy: "two_factor" + }] + }; + const config = ConfigurationParser.parse(userConfig); + Assert.deepEqual(config.access_control, { + default_policy: "deny", + rules: [{ + domain: "www.example.com", + policy: "two_factor", + subject: "user:user" + }, { + domain: "public.example.com", + policy: "two_factor" + }] + } as ACLConfiguration); + }); + + + it("should adapt access_control when it is empty", function () { + const userConfig = buildYamlConfig(); + userConfig.access_control = {} as any; + const config = ConfigurationParser.parse(userConfig); + Assert.deepEqual(config.access_control, { + default_policy: "bypass", + rules: [] + }); + }); + }); + + describe("default_redirection_url", function() { + it("should parse default_redirection_url", function() { + const userConfig = buildYamlConfig(); + userConfig.default_redirection_url = "dummy_url"; + const config = ConfigurationParser.parse(userConfig); + Assert.deepEqual(config.default_redirection_url, "dummy_url"); + }); + }); +}); diff --git a/themes/matrix/server/src/lib/configuration/ConfigurationParser.ts b/themes/matrix/server/src/lib/configuration/ConfigurationParser.ts new file mode 100644 index 000000000..d92d163c4 --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/ConfigurationParser.ts @@ -0,0 +1,39 @@ + +import * as ObjectPath from "object-path"; +import { Configuration, complete } from "./schema/Configuration"; +import Ajv = require("ajv"); +import Path = require("path"); +import Util = require("util"); + +export class ConfigurationParser { + private static parseTypes(configuration: Configuration): string[] { + const schema = require(Path.resolve(__dirname, "./Configuration.schema.json")); + const ajv = new Ajv({ + allErrors: true, + missingRefs: "fail" + }); + ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json")); + const valid = ajv.validate(schema, configuration); + if (!valid) + return ajv.errors.map( + (e: Ajv.ErrorObject) => { return ajv.errorsText([e]); }); + return []; + } + + static parse(configuration: Configuration): Configuration { + const validationErrors = this.parseTypes(configuration); + if (validationErrors.length > 0) { + validationErrors.forEach((e: string) => { console.log(e); }); + throw new Error("Malformed configuration (schema). Please double-check your configuration file."); + } + + const [newConfiguration, completionErrors] = complete(configuration); + + if (completionErrors.length > 0) { + completionErrors.forEach((e: string) => { console.log(e); }); + throw new Error("Malformed configuration (validator). Please double-check your configuration file."); + } + return newConfiguration; + } +} + diff --git a/themes/matrix/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts b/themes/matrix/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts new file mode 100644 index 000000000..d4a3093ee --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts @@ -0,0 +1,149 @@ +import { SessionConfigurationBuilder } from "./SessionConfigurationBuilder"; +import { Configuration } from "./schema/Configuration"; +import { GlobalDependencies } from "../../../types/Dependencies"; + +import ExpressSession = require("express-session"); +import ConnectRedis = require("connect-redis"); +import Sinon = require("sinon"); +import Assert = require("assert"); + +describe("configuration/SessionConfigurationBuilder", function () { + const configuration: Configuration = { + access_control: { + default_policy: "deny", + rules: [] + }, + totp: { + issuer: "authelia.com" + }, + authentication_backend: { + ldap: { + url: "ldap://ldap", + user: "user", + base_dn: "dc=example,dc=com", + password: "password", + additional_groups_dn: "ou=groups", + additional_users_dn: "ou=users", + group_name_attribute: "", + groups_filter: "", + mail_attribute: "", + users_filter: "" + }, + }, + logs_level: "debug", + notifier: { + filesystem: { + filename: "/test" + } + }, + port: 8080, + session: { + name: "authelia_session", + domain: "example.com", + expiration: 3600, + secret: "secret" + }, + regulation: { + max_retries: 3, + ban_time: 5 * 60, + find_time: 5 * 60 + }, + storage: { + local: { + in_memory: true + } + } + }; + + const deps: GlobalDependencies = { + ConnectRedis: Sinon.spy() as any, + ldapjs: Sinon.spy() as any, + nedb: Sinon.spy() as any, + session: Sinon.spy() as any, + speakeasy: Sinon.spy() as any, + u2f: Sinon.spy() as any, + winston: Sinon.spy() as any, + Redis: Sinon.spy() as any + }; + + it("should return session options without redis options", function () { + const options = SessionConfigurationBuilder.build(configuration, deps); + const expectedOptions = { + name: "authelia_session", + secret: "secret", + resave: false, + saveUninitialized: true, + cookie: { + secure: true, + httpOnly: true, + maxAge: 3600, + domain: "example.com" + } + }; + + Assert.deepEqual(expectedOptions, options); + }); + + it("should return session options with redis options", function () { + configuration.session["redis"] = { + host: "redis.example.com", + port: 6379 + }; + const RedisStoreMock = Sinon.spy(); + const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); + + deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; + deps.Redis = { + createClient: Sinon.mock().returns(redisClient) + } as any; + + const options = SessionConfigurationBuilder.build(configuration, deps); + + const expectedOptions: ExpressSession.SessionOptions = { + secret: "secret", + resave: false, + saveUninitialized: true, + name: "authelia_session", + cookie: { + secure: true, + httpOnly: true, + maxAge: 3600, + domain: "example.com" + }, + store: Sinon.match.object as any + }; + + Assert((deps.ConnectRedis as Sinon.SinonStub).calledWith(deps.session)); + Assert.equal(options.secret, expectedOptions.secret); + Assert.equal(options.resave, expectedOptions.resave); + Assert.equal(options.saveUninitialized, expectedOptions.saveUninitialized); + Assert.deepEqual(options.cookie, expectedOptions.cookie); + Assert(options.store != undefined); + }); + + it("should return session options with redis password", function () { + configuration.session["redis"] = { + host: "redis.example.com", + port: 6379, + password: "authelia_pass" + }; + const RedisStoreMock = Sinon.spy(); + const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); + const createClientStub = Sinon.stub(); + + deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; + deps.Redis = { + createClient: createClientStub + } as any; + + createClientStub.returns(redisClient); + + const options = SessionConfigurationBuilder.build(configuration, deps); + + Assert(createClientStub.calledWith({ + host: "redis.example.com", + port: 6379, + password: "authelia_pass" + })); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/SessionConfigurationBuilder.ts b/themes/matrix/server/src/lib/configuration/SessionConfigurationBuilder.ts new file mode 100644 index 000000000..6ce643d9d --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/SessionConfigurationBuilder.ts @@ -0,0 +1,52 @@ +import ExpressSession = require("express-session"); +import Redis = require("redis"); + +import { Configuration } from "./schema/Configuration"; +import { GlobalDependencies } from "../../../types/Dependencies"; +import { RedisStoreOptions } from "connect-redis"; + +export class SessionConfigurationBuilder { + + static build(configuration: Configuration, deps: GlobalDependencies): ExpressSession.SessionOptions { + const sessionOptions: ExpressSession.SessionOptions = { + name: configuration.session.name, + secret: configuration.session.secret, + resave: false, + saveUninitialized: true, + cookie: { + secure: true, + httpOnly: true, + maxAge: configuration.session.expiration, + domain: configuration.session.domain + }, + }; + + if (configuration.session.redis) { + let redisOptions; + const options: Redis.ClientOpts = { + host: configuration.session.redis.host, + port: configuration.session.redis.port + }; + + if (configuration.session.redis.password) { + options["password"] = configuration.session.redis.password; + } + const client = deps.Redis.createClient(options); + + client.on("error", function (err: Error) { + console.error("Redis error:", err); + }); + + redisOptions = { + client: client, + logErrors: true + }; + + if (redisOptions) { + const RedisStore = deps.ConnectRedis(deps.session); + sessionOptions.store = new RedisStore(redisOptions); + } + } + return sessionOptions; + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/AclConfiguration.spec.ts b/themes/matrix/server/src/lib/configuration/schema/AclConfiguration.spec.ts new file mode 100644 index 000000000..d1e2a03a2 --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/schema/AclConfiguration.spec.ts @@ -0,0 +1,34 @@ +import { ACLConfiguration, complete } from "./AclConfiguration"; +import Assert = require("assert"); + +describe("configuration/schema/AclConfiguration", function() { + it("should complete ACLConfiguration", function() { + const configuration: ACLConfiguration = {}; + const [newConfiguration, errors] = complete(configuration); + + Assert.deepEqual(newConfiguration.default_policy, "bypass"); + Assert.deepEqual(newConfiguration.rules, []); + }); + + it("should return errors when subject is not good", function() { + const configuration: ACLConfiguration = { + default_policy: "deny", + rules: [{ + domain: "dev.example.com", + subject: "user:abc", + policy: "bypass" + }, { + domain: "dev.example.com", + subject: "user:def", + policy: "bypass" + }, { + domain: "dev.example.com", + subject: "badkey:abc", + policy: "bypass" + }] + }; + const [newConfiguration, errors] = complete(configuration); + + Assert.deepEqual(errors, ["Rule 2 has wrong subject. It should be starting with user: or group:."]); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/AclConfiguration.ts b/themes/matrix/server/src/lib/configuration/schema/AclConfiguration.ts new file mode 100644 index 000000000..40401dd64 --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/schema/AclConfiguration.ts @@ -0,0 +1,41 @@ + +export type ACLPolicy = "deny" | "bypass" | "one_factor" | "two_factor"; + +export type ACLRule = { + domain: string; + resources?: string[]; + subject?: string; + policy: ACLPolicy; +}; + +export interface ACLConfiguration { + default_policy?: ACLPolicy; + rules?: ACLRule[]; +} + +export function complete(configuration: ACLConfiguration): [ACLConfiguration, string[]] { + const newConfiguration: ACLConfiguration = (configuration) + ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.default_policy) { + newConfiguration.default_policy = "bypass"; + } + + if (!newConfiguration.rules) { + newConfiguration.rules = []; + } + + if (newConfiguration.rules.length > 0) { + const errors: string[] = []; + newConfiguration.rules.forEach((r, idx) => { + if (r.subject && !r.subject.match(/^(user|group):[a-zA-Z0-9]+$/)) { + errors.push(`Rule ${idx} has wrong subject. It should be starting with user: or group:.`); + } + }); + if (errors.length > 0) { + return [newConfiguration, errors]; + } + } + + return [newConfiguration, []]; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts b/themes/matrix/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts new file mode 100644 index 000000000..3ca86381c --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts @@ -0,0 +1,11 @@ +import { AuthenticationBackendConfiguration, complete } from "./AuthenticationBackendConfiguration"; +import Assert = require("assert"); + +describe("configuration/schema/AuthenticationBackendConfiguration", function() { + it("should ensure there is at least one key", function() { + const configuration: AuthenticationBackendConfiguration = {} as any; + const [newConfiguration, error] = complete(configuration); + + Assert.equal(error, "Authentication backend must have one of the following keys:`ldap` or `file`"); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts b/themes/matrix/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts new file mode 100644 index 000000000..7f77f894f --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts @@ -0,0 +1,25 @@ +import { LdapConfiguration } from "./LdapConfiguration"; +import { FileUsersDatabaseConfiguration } from "./FileUsersDatabaseConfiguration"; + +export interface AuthenticationBackendConfiguration { + ldap?: LdapConfiguration; + file?: FileUsersDatabaseConfiguration; +} + +export function complete( + configuration: AuthenticationBackendConfiguration) + : [AuthenticationBackendConfiguration, string] { + + const newConfiguration: AuthenticationBackendConfiguration = (configuration) + ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (Object.keys(newConfiguration).length != 1) { + return [ + newConfiguration, + "Authentication backend must have one of the following keys:" + + "`ldap` or `file`" + ]; + } + + return [newConfiguration, undefined]; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/Configuration.ts b/themes/matrix/server/src/lib/configuration/schema/Configuration.ts new file mode 100644 index 000000000..8d16a5fb2 --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/schema/Configuration.ts @@ -0,0 +1,68 @@ +import { ACLConfiguration, complete as AclConfigurationComplete } from "./AclConfiguration"; +import { AuthenticationBackendConfiguration, complete as AuthenticationBackendComplete } from "./AuthenticationBackendConfiguration"; +import { NotifierConfiguration, complete as NotifierConfigurationComplete } from "./NotifierConfiguration"; +import { RegulationConfiguration, complete as RegulationConfigurationComplete } from "./RegulationConfiguration"; +import { SessionConfiguration, complete as SessionConfigurationComplete } from "./SessionConfiguration"; +import { StorageConfiguration, complete as StorageConfigurationComplete } from "./StorageConfiguration"; +import { TotpConfiguration, complete as TotpConfigurationComplete } from "./TotpConfiguration"; + +export interface Configuration { + access_control?: ACLConfiguration; + authentication_backend: AuthenticationBackendConfiguration; + default_redirection_url?: string; + logs_level?: string; + notifier?: NotifierConfiguration; + port?: number; + regulation?: RegulationConfiguration; + session?: SessionConfiguration; + storage?: StorageConfiguration; + totp?: TotpConfiguration; +} + +export function complete( + configuration: Configuration): + [Configuration, string[]] { + + const newConfiguration: Configuration = JSON.parse( + JSON.stringify(configuration)); + const errors: string[] = []; + + const [acls, aclsErrors] = AclConfigurationComplete( + newConfiguration.access_control); + + newConfiguration.access_control = acls; + if (aclsErrors.length > 0) { + errors.concat(aclsErrors); + } + + const [backend, error] = + AuthenticationBackendComplete( + newConfiguration.authentication_backend); + + if (error) errors.push(error); + newConfiguration.authentication_backend = backend; + + if (!newConfiguration.logs_level) { + newConfiguration.logs_level = "info"; + } + + const [notifier, notifierError] = NotifierConfigurationComplete( + newConfiguration.notifier); + newConfiguration.notifier = notifier; + if (notifierError) errors.push(notifierError); + + if (!newConfiguration.port) { + newConfiguration.port = 8080; + } + + newConfiguration.regulation = RegulationConfigurationComplete( + newConfiguration.regulation); + newConfiguration.session = SessionConfigurationComplete( + newConfiguration.session); + newConfiguration.storage = StorageConfigurationComplete( + newConfiguration.storage); + newConfiguration.totp = TotpConfigurationComplete( + newConfiguration.totp); + + return [newConfiguration, errors]; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts b/themes/matrix/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts new file mode 100644 index 000000000..d19002ba9 --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts @@ -0,0 +1,4 @@ + +export interface FileUsersDatabaseConfiguration { + path: string; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/LdapConfiguration.spec.ts b/themes/matrix/server/src/lib/configuration/schema/LdapConfiguration.spec.ts new file mode 100644 index 000000000..cc73d1085 --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/schema/LdapConfiguration.spec.ts @@ -0,0 +1,25 @@ +import Assert = require("assert"); +import { LdapConfiguration, complete } from "./LdapConfiguration"; + +describe("configuration/schema/AuthenticationMethodsConfiguration", function() { + it("should ensure at least one key is provided", function() { + const configuration: LdapConfiguration = { + url: "ldap.example.com", + base_dn: "dc=example,dc=com", + user: "admin", + password: "password" + }; + const newConfiguration = complete(configuration); + + Assert.deepEqual(newConfiguration, { + url: "ldap.example.com", + base_dn: "dc=example,dc=com", + user: "admin", + password: "password", + users_filter: "cn={0}", + group_name_attribute: "cn", + groups_filter: "member={dn}", + mail_attribute: "mail" + }); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/LdapConfiguration.ts b/themes/matrix/server/src/lib/configuration/schema/LdapConfiguration.ts new file mode 100644 index 000000000..5dacb9390 --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/schema/LdapConfiguration.ts @@ -0,0 +1,40 @@ +import Util = require("util"); + +export interface LdapConfiguration { + url: string; + base_dn: string; + + additional_users_dn?: string; + users_filter?: string; + + additional_groups_dn?: string; + groups_filter?: string; + + group_name_attribute?: string; + mail_attribute?: string; + + user: string; // admin username + password: string; // admin password +} + +export function complete(configuration: LdapConfiguration): LdapConfiguration { + const newConfiguration: LdapConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.users_filter) { + newConfiguration.users_filter = "cn={0}"; + } + + if (!newConfiguration.groups_filter) { + newConfiguration.groups_filter = "member={dn}"; + } + + if (!newConfiguration.group_name_attribute) { + newConfiguration.group_name_attribute = "cn"; + } + + if (!newConfiguration.mail_attribute) { + newConfiguration.mail_attribute = "mail"; + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts b/themes/matrix/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts new file mode 100644 index 000000000..6c576e8e0 --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts @@ -0,0 +1,40 @@ +import Assert = require("assert"); +import { NotifierConfiguration, complete } from "./NotifierConfiguration"; + +describe("configuration/schema/NotifierConfiguration", function() { + it("should use a default notifier when none is provided", function() { + const configuration: NotifierConfiguration = {}; + const [newConfiguration, error] = complete(configuration); + + Assert.deepEqual(newConfiguration.filesystem, {filename: "/tmp/authelia/notification.txt"}); + }); + + it("should ensure correct key is provided", function() { + const configuration = { + abc: "badvalue" + }; + const [newConfiguration, error] = complete(configuration as any); + + Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"); + }); + + it("should ensure there is no more than one key", function() { + const configuration: NotifierConfiguration = { + smtp: { + host: "smtp.example.com", + port: 25, + secure: false, + sender: "test@example.com" + }, + email: { + username: "test", + password: "test", + sender: "test@example.com", + service: "gmail" + } + }; + const [newConfiguration, error] = complete(configuration); + + Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/NotifierConfiguration.ts b/themes/matrix/server/src/lib/configuration/schema/NotifierConfiguration.ts new file mode 100644 index 000000000..7bcce15c2 --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/schema/NotifierConfiguration.ts @@ -0,0 +1,45 @@ + +export interface EmailNotifierConfiguration { + username: string; + password: string; + sender: string; + service: string; +} + +export interface SmtpNotifierConfiguration { + username?: string; + password?: string; + host: string; + port: number; + secure: boolean; + sender: string; +} + +export interface FileSystemNotifierConfiguration { + filename: string; +} + +export interface NotifierConfiguration { + email?: EmailNotifierConfiguration; + smtp?: SmtpNotifierConfiguration; + filesystem?: FileSystemNotifierConfiguration; +} + +export function complete(configuration: NotifierConfiguration): [NotifierConfiguration, string] { + const newConfiguration: NotifierConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (Object.keys(newConfiguration).length == 0) + newConfiguration.filesystem = { filename: "/tmp/authelia/notification.txt" }; + + const ERROR = "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"; + + if (Object.keys(newConfiguration).length != 1) + return [newConfiguration, ERROR]; + + const key = Object.keys(newConfiguration)[0]; + + if (key != "filesystem" && key != "smtp" && key != "email") + return [newConfiguration, ERROR]; + + return [newConfiguration, undefined]; +} diff --git a/themes/matrix/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts b/themes/matrix/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts new file mode 100644 index 000000000..dce2caf4e --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts @@ -0,0 +1,13 @@ +import Assert = require("assert"); +import { RegulationConfiguration, complete } from "./RegulationConfiguration"; + +describe("configuration/schema/RegulationConfiguration", function() { + it("should return default regulation configuration", function() { + const configuration: RegulationConfiguration = {}; + const newConfiguration = complete(configuration); + + Assert.equal(newConfiguration.ban_time, 300); + Assert.equal(newConfiguration.find_time, 120); + Assert.equal(newConfiguration.max_retries, 3); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/RegulationConfiguration.ts b/themes/matrix/server/src/lib/configuration/schema/RegulationConfiguration.ts new file mode 100644 index 000000000..117463f43 --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/schema/RegulationConfiguration.ts @@ -0,0 +1,23 @@ +export interface RegulationConfiguration { + max_retries?: number; + find_time?: number; + ban_time?: number; +} + +export function complete(configuration: RegulationConfiguration): RegulationConfiguration { + const newConfiguration: RegulationConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.max_retries) { + newConfiguration.max_retries = 3; + } + + if (!newConfiguration.find_time) { + newConfiguration.find_time = 120; // seconds + } + + if (!newConfiguration.ban_time) { + newConfiguration.ban_time = 300; // seconds + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/SessionConfiguration.spec.ts b/themes/matrix/server/src/lib/configuration/schema/SessionConfiguration.spec.ts new file mode 100644 index 000000000..e54010837 --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/schema/SessionConfiguration.spec.ts @@ -0,0 +1,16 @@ +import Assert = require("assert"); +import { SessionConfiguration, complete } from "./SessionConfiguration"; + +describe("configuration/schema/SessionConfiguration", function() { + it("should return default regulation configuration", function() { + const configuration: SessionConfiguration = { + domain: "example.com", + secret: "unsecure_secret" + }; + const newConfiguration = complete(configuration); + + Assert.equal(newConfiguration.name, 'authelia_session'); + Assert.equal(newConfiguration.expiration, 3600000); + Assert.equal(newConfiguration.inactivity, undefined); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/SessionConfiguration.ts b/themes/matrix/server/src/lib/configuration/schema/SessionConfiguration.ts new file mode 100644 index 000000000..2c88bb215 --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/schema/SessionConfiguration.ts @@ -0,0 +1,32 @@ +export interface SessionRedisOptions { + host: string; + port: number; + password?: string; +} + +export interface SessionConfiguration { + name?: string; + domain: string; + secret: string; + expiration?: number; + inactivity?: number; + redis?: SessionRedisOptions; +} + +export function complete(configuration: SessionConfiguration): SessionConfiguration { + const newConfiguration: SessionConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.name) { + newConfiguration.name = "authelia_session"; + } + + if (!newConfiguration.expiration) { + newConfiguration.expiration = 3600000; // 1 hour + } + + if (!newConfiguration.inactivity) { + newConfiguration.inactivity = undefined; // disabled + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/StorageConfiguration.spec.ts b/themes/matrix/server/src/lib/configuration/schema/StorageConfiguration.spec.ts new file mode 100644 index 000000000..9d02a11b6 --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/schema/StorageConfiguration.spec.ts @@ -0,0 +1,15 @@ +import Assert = require("assert"); +import { StorageConfiguration, complete } from "./StorageConfiguration"; + +describe("configuration/schema/StorageConfiguration", function() { + it("should return default regulation configuration", function() { + const configuration: StorageConfiguration = {}; + const newConfiguration = complete(configuration); + + Assert.deepEqual(newConfiguration, { + local: { + in_memory: true + } + }); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/StorageConfiguration.ts b/themes/matrix/server/src/lib/configuration/schema/StorageConfiguration.ts new file mode 100644 index 000000000..47e356ef4 --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/schema/StorageConfiguration.ts @@ -0,0 +1,30 @@ +export interface MongoStorageConfiguration { + url: string; + database: string; + auth?: { + username: string; + password: string; + }; +} + +export interface LocalStorageConfiguration { + path?: string; + in_memory?: boolean; +} + +export interface StorageConfiguration { + local?: LocalStorageConfiguration; + mongo?: MongoStorageConfiguration; +} + +export function complete(configuration: StorageConfiguration): StorageConfiguration { + const newConfiguration: StorageConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.local && !newConfiguration.mongo) { + newConfiguration.local = { + in_memory: true + }; + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/TotpConfiguration.ts b/themes/matrix/server/src/lib/configuration/schema/TotpConfiguration.ts new file mode 100644 index 000000000..683135639 --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/schema/TotpConfiguration.ts @@ -0,0 +1,13 @@ +export interface TotpConfiguration { + issuer: string; +} + +export function complete(configuration: TotpConfiguration): TotpConfiguration { + const newConfiguration: TotpConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.issuer) { + newConfiguration.issuer = "authelia.com"; + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts b/themes/matrix/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts new file mode 100644 index 000000000..8008b4833 --- /dev/null +++ b/themes/matrix/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts @@ -0,0 +1,9 @@ + +export interface UserInfo { + username: string; + password_hash: string; + email: string; + groups?: string[]; +} + +export type UserDatabaseConfiguration = UserInfo[]; \ No newline at end of file diff --git a/themes/matrix/server/src/lib/connectors/mongo/IMongoClient.d.ts b/themes/matrix/server/src/lib/connectors/mongo/IMongoClient.d.ts new file mode 100644 index 000000000..36cb4b8bf --- /dev/null +++ b/themes/matrix/server/src/lib/connectors/mongo/IMongoClient.d.ts @@ -0,0 +1,6 @@ +import MongoDB = require("mongodb"); +import Bluebird = require("bluebird"); + +export interface IMongoClient { + collection(name: string): Bluebird +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/connectors/mongo/MongoClient.spec.ts b/themes/matrix/server/src/lib/connectors/mongo/MongoClient.spec.ts new file mode 100644 index 000000000..ca0c68593 --- /dev/null +++ b/themes/matrix/server/src/lib/connectors/mongo/MongoClient.spec.ts @@ -0,0 +1,119 @@ +import Assert = require("assert"); +import Bluebird = require("bluebird"); +import MongoDB = require("mongodb"); +import Sinon = require("sinon"); + +import { MongoClient } from "./MongoClient"; +import { GlobalLoggerStub } from "../../logging/GlobalLoggerStub.spec"; +import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration"; + +describe("connectors/mongo/MongoClient", function () { + let MongoClientStub: any; + let mongoClientStub: any; + let mongoDatabaseStub: any; + let logger: GlobalLoggerStub = new GlobalLoggerStub(); + + const configuration: MongoStorageConfiguration = { + url: "mongo://url", + database: "databasename" + }; + + describe("connection", () => { + before(() => { + mongoClientStub = { + db: Sinon.stub() + }; + mongoDatabaseStub = { + on: Sinon.stub(), + collection: Sinon.stub() + } + MongoClientStub = Sinon.stub( + MongoDB.MongoClient, "connect"); + MongoClientStub.yields( + undefined, mongoClientStub); + mongoClientStub.db.returns( + mongoDatabaseStub); + }); + + after(() => { + MongoClientStub.restore(); + }); + + it("should use credentials from configuration", () => { + configuration.auth = { + username: "authelia", + password: "authelia_pass" + }; + + const client = new MongoClient(configuration, logger); + return client.collection("test") + .then(() => { + Assert(MongoClientStub.calledWith("mongo://url", { + auth: { + user: "authelia", + password: "authelia_pass" + } + })) + }); + }); + }); + + describe("collection", () => { + before(function() { + mongoClientStub = { + db: Sinon.stub() + }; + mongoDatabaseStub = { + on: Sinon.stub(), + collection: Sinon.stub() + } + }); + + describe("Connection to mongo is ok", function() { + before(function () { + MongoClientStub = Sinon.stub( + MongoDB.MongoClient, "connect"); + MongoClientStub.yields( + undefined, mongoClientStub); + mongoClientStub.db.returns( + mongoDatabaseStub); + }); + + after(function () { + MongoClientStub.restore(); + }); + + it("should create a collection", function () { + const COLLECTION_NAME = "mycollection"; + const client = new MongoClient(configuration, logger); + + mongoDatabaseStub.collection.returns("COL"); + return client.collection(COLLECTION_NAME) + .then((collection) => mongoDatabaseStub.collection.calledWith(COLLECTION_NAME)); + }); + }); + + describe("Connection to mongo is broken", function() { + before(function () { + MongoClientStub = Sinon.stub( + MongoDB.MongoClient, "connect"); + MongoClientStub.yields( + new Error("Failed connection"), undefined); + }); + + after(function () { + MongoClientStub.restore(); + }); + + it("should fail creating the collection", function() { + const COLLECTION_NAME = "mycollection"; + const client = new MongoClient(configuration, logger); + + mongoDatabaseStub.collection.returns("COL"); + return client.collection(COLLECTION_NAME) + .then((collection) => Bluebird.reject(new Error("should not be here."))) + .catch((err) => Bluebird.resolve()); + }); + }) + }); +}); diff --git a/themes/matrix/server/src/lib/connectors/mongo/MongoClient.ts b/themes/matrix/server/src/lib/connectors/mongo/MongoClient.ts new file mode 100644 index 000000000..d15731e97 --- /dev/null +++ b/themes/matrix/server/src/lib/connectors/mongo/MongoClient.ts @@ -0,0 +1,76 @@ + +import MongoDB = require("mongodb"); +import { IMongoClient } from "./IMongoClient"; +import Bluebird = require("bluebird"); +import { AUTHENTICATION_FAILED } from "../../../../../shared/UserMessages"; +import { IGlobalLogger } from "../../logging/IGlobalLogger"; +import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration"; + +export class MongoClient implements IMongoClient { + private configuration: MongoStorageConfiguration; + + private database: MongoDB.Db; + private client: MongoDB.MongoClient; + private logger: IGlobalLogger; + + constructor( + configuration: MongoStorageConfiguration, + logger: IGlobalLogger) { + + this.configuration = configuration; + this.logger = logger; + } + + connect(): Bluebird { + const that = this; + const options: MongoDB.MongoClientOptions = {}; + if (that.configuration.auth) { + options["auth"] = { + user: that.configuration.auth.username, + password: that.configuration.auth.password + }; + } + + return new Bluebird((resolve, reject) => { + MongoDB.MongoClient.connect( + this.configuration.url, + options, + function(err, client) { + if (err) { + reject(err); + return; + } + resolve(client); + }); + }) + .then(function (client: MongoDB.MongoClient) { + that.database = client.db(that.configuration.database); + that.database.on("close", () => { + that.logger.info("[MongoClient] Lost connection."); + }); + that.database.on("reconnect", () => { + that.logger.info("[MongoClient] Reconnected."); + }); + that.client = client; + }); + } + + close(): Bluebird { + if (this.client) { + this.client.close(); + this.database = undefined; + this.client = undefined; + } + return Bluebird.resolve(); + } + + collection(name: string): Bluebird { + if (!this.client) { + const that = this; + return this.connect() + .then(() => Bluebird.resolve(that.database.collection(name))); + } + + return Bluebird.resolve(this.database.collection(name)); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/connectors/mongo/MongoClientStub.spec.ts b/themes/matrix/server/src/lib/connectors/mongo/MongoClientStub.spec.ts new file mode 100644 index 000000000..1cfd48e32 --- /dev/null +++ b/themes/matrix/server/src/lib/connectors/mongo/MongoClientStub.spec.ts @@ -0,0 +1,16 @@ +import Sinon = require("sinon"); +import MongoDB = require("mongodb"); +import Bluebird = require("bluebird"); +import { IMongoClient } from "../../../../src/lib/connectors/mongo/IMongoClient"; + +export class MongoClientStub implements IMongoClient { + public collectionStub: Sinon.SinonStub; + + constructor() { + this.collectionStub = Sinon.stub(); + } + + collection(name: string): Bluebird { + return this.collectionStub(name); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/logging/GlobalLogger.ts b/themes/matrix/server/src/lib/logging/GlobalLogger.ts new file mode 100644 index 000000000..4da7acf49 --- /dev/null +++ b/themes/matrix/server/src/lib/logging/GlobalLogger.ts @@ -0,0 +1,34 @@ +import { IGlobalLogger } from "./IGlobalLogger"; +import Util = require("util"); +import Express = require("express"); +import Winston = require("winston"); + +declare module "express" { + interface Request { + id: string; + } +} + +export class GlobalLogger implements IGlobalLogger { + private winston: typeof Winston; + constructor(winston: typeof Winston) { + this.winston = winston; + } + + private buildMessage(message: string, ...args: any[]): string { + return Util.format("date='%s' message='%s'", new Date(), + Util.format(message, ...args)); + } + + info(message: string, ...args: any[]): void { + this.winston.info(this.buildMessage(message, ...args)); + } + + debug(message: string, ...args: any[]): void { + this.winston.debug(this.buildMessage(message, ...args)); + } + + error(message: string, ...args: any[]): void { + this.winston.debug(this.buildMessage(message, ...args)); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/logging/GlobalLoggerStub.spec.ts b/themes/matrix/server/src/lib/logging/GlobalLoggerStub.spec.ts new file mode 100644 index 000000000..d4bb13710 --- /dev/null +++ b/themes/matrix/server/src/lib/logging/GlobalLoggerStub.spec.ts @@ -0,0 +1,38 @@ +import Sinon = require("sinon"); +import { GlobalLogger } from "./GlobalLogger"; +import Winston = require("winston"); +import Express = require("express"); +import { IGlobalLogger } from "./IGlobalLogger"; + +export class GlobalLoggerStub implements IGlobalLogger { + infoStub: Sinon.SinonStub; + debugStub: Sinon.SinonStub; + errorStub: Sinon.SinonStub; + private globalLogger: IGlobalLogger; + + constructor(enableLogging?: boolean) { + this.infoStub = Sinon.stub(); + this.debugStub = Sinon.stub(); + this.errorStub = Sinon.stub(); + if (enableLogging) + this.globalLogger = new GlobalLogger(Winston); + } + + info(message: string, ...args: any[]): void { + if (this.globalLogger) + this.globalLogger.info(message, ...args); + this.infoStub(message, ...args); + } + + debug(message: string, ...args: any[]): void { + if (this.globalLogger) + this.globalLogger.info(message, ...args); + this.debugStub(message, ...args); + } + + error(message: string, ...args: any[]): void { + if (this.globalLogger) + this.globalLogger.info(message, ...args); + this.errorStub(message, ...args); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/logging/IGlobalLogger.ts b/themes/matrix/server/src/lib/logging/IGlobalLogger.ts new file mode 100644 index 000000000..548515ec7 --- /dev/null +++ b/themes/matrix/server/src/lib/logging/IGlobalLogger.ts @@ -0,0 +1,5 @@ +export interface IGlobalLogger { + info(message: string, ...args: any[]): void; + debug(message: string, ...args: any[]): void; + error(message: string, ...args: any[]): void; +} diff --git a/themes/matrix/server/src/lib/logging/IRequestLogger.ts b/themes/matrix/server/src/lib/logging/IRequestLogger.ts new file mode 100644 index 000000000..126a601fe --- /dev/null +++ b/themes/matrix/server/src/lib/logging/IRequestLogger.ts @@ -0,0 +1,7 @@ +import Express = require("express"); + +export interface IRequestLogger { + info(req: Express.Request, message: string, ...args: any[]): void; + debug(req: Express.Request, message: string, ...args: any[]): void; + error(req: Express.Request, message: string, ...args: any[]): void; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/logging/RequestLogger.ts b/themes/matrix/server/src/lib/logging/RequestLogger.ts new file mode 100644 index 000000000..c45c66018 --- /dev/null +++ b/themes/matrix/server/src/lib/logging/RequestLogger.ts @@ -0,0 +1,45 @@ +import { IRequestLogger } from "./IRequestLogger"; +import Util = require("util"); +import Express = require("express"); +import Winston = require("winston"); + +declare module "express" { + interface Request { + id: string; + } +} + +export class RequestLogger implements IRequestLogger { + private winston: typeof Winston; + + constructor(winston: typeof Winston) { + this.winston = winston; + } + + private formatHeader(req: Express.Request) { + const clientIP = req.ip; // The IP of the original client going through the proxy chain. + return Util.format("date='%s' method='%s', path='%s' requestId='%s' sessionId='%s' ip='%s'", + new Date(), req.method, req.path, req.id, req.sessionID, clientIP); + } + + private formatBody(message: string) { + return Util.format("message='%s'", message); + } + + private formatMessage(req: Express.Request, message: string) { + return Util.format("%s %s", this.formatHeader(req), + this.formatBody(message)); + } + + info(req: Express.Request, message: string, ...args: any[]): void { + this.winston.info(this.formatMessage(req, message), ...args); + } + + debug(req: Express.Request, message: string, ...args: any[]): void { + this.winston.debug(this.formatMessage(req, message), ...args); + } + + error(req: Express.Request, message: string, ...args: any[]): void { + this.winston.error(this.formatMessage(req, message), ...args); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/logging/RequestLoggerStub.spec.ts b/themes/matrix/server/src/lib/logging/RequestLoggerStub.spec.ts new file mode 100644 index 000000000..b0e375210 --- /dev/null +++ b/themes/matrix/server/src/lib/logging/RequestLoggerStub.spec.ts @@ -0,0 +1,38 @@ +import { IRequestLogger } from "./IRequestLogger"; +import Sinon = require("sinon"); +import { RequestLogger } from "./RequestLogger"; +import Winston = require("winston"); +import Express = require("express"); + +export class RequestLoggerStub implements IRequestLogger { + infoStub: Sinon.SinonStub; + debugStub: Sinon.SinonStub; + errorStub: Sinon.SinonStub; + private requestLogger: RequestLogger; + + constructor(enableLogging?: boolean) { + this.infoStub = Sinon.stub(); + this.debugStub = Sinon.stub(); + this.errorStub = Sinon.stub(); + if (enableLogging) + this.requestLogger = new RequestLogger(Winston); + } + + info(req: Express.Request, message: string, ...args: any[]): void { + if (this.requestLogger) + this.requestLogger.info(req, message, ...args); + this.infoStub(req, message, ...args); + } + + debug(req: Express.Request, message: string, ...args: any[]): void { + if (this.requestLogger) + this.requestLogger.info(req, message, ...args); + this.debugStub(req, message, ...args); + } + + error(req: Express.Request, message: string, ...args: any[]): void { + if (this.requestLogger) + this.requestLogger.info(req, message, ...args); + this.errorStub(req, message, ...args); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/AbstractEmailNotifier.ts b/themes/matrix/server/src/lib/notifiers/AbstractEmailNotifier.ts new file mode 100644 index 000000000..198e4e5db --- /dev/null +++ b/themes/matrix/server/src/lib/notifiers/AbstractEmailNotifier.ts @@ -0,0 +1,23 @@ + +import { INotifier } from "../notifiers/INotifier"; +import { Identity } from "../../../types/Identity"; + +import Fs = require("fs"); +import Path = require("path"); +import Ejs = require("ejs"); +import BluebirdPromise = require("bluebird"); + +const email_template = Fs.readFileSync(Path.join(__dirname, "../../resources/email-template.ejs"), "UTF-8"); + +export abstract class AbstractEmailNotifier implements INotifier { + notify(to: string, subject: string, link: string): BluebirdPromise { + const d = { + url: link, + button_title: "Continue", + title: subject + }; + return this.sendEmail(to, subject, Ejs.render(email_template, d)); + } + + abstract sendEmail(to: string, subject: string, content: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/EmailNotifier.spec.ts b/themes/matrix/server/src/lib/notifiers/EmailNotifier.spec.ts new file mode 100644 index 000000000..8211bbc02 --- /dev/null +++ b/themes/matrix/server/src/lib/notifiers/EmailNotifier.spec.ts @@ -0,0 +1,54 @@ +import * as sinon from "sinon"; +import * as Assert from "assert"; +import BluebirdPromise = require("bluebird"); + +import { MailSenderStub } from "./MailSenderStub.spec"; +import EmailNotifier = require("./EmailNotifier"); + + +describe("notifiers/EmailNotifier", function () { + it("should send an email to given user", function () { + const mailSender = new MailSenderStub(); + const options = { + username: "user_gmail", + password: "pass_gmail", + sender: "admin@example.com", + service: "gmail" + }; + + mailSender.sendStub.returns(BluebirdPromise.resolve()); + const sender = new EmailNotifier.EmailNotifier(options, mailSender); + const subject = "subject"; + const url = "http://test.com"; + + return sender.notify("user@example.com", subject, url) + .then(function () { + Assert.equal(mailSender.sendStub.getCall(0).args[0].to, "user@example.com"); + Assert.equal(mailSender.sendStub.getCall(0).args[0].subject, "subject"); + return BluebirdPromise.resolve(); + }); + }); + + it("should fail while sending an email", function () { + const mailSender = new MailSenderStub(); + const options = { + username: "user_gmail", + password: "pass_gmail", + sender: "admin@example.com", + service: "gmail" + }; + + mailSender.sendStub.returns(BluebirdPromise.reject(new Error("Failed to send mail"))); + const sender = new EmailNotifier.EmailNotifier(options, mailSender); + const subject = "subject"; + const url = "http://test.com"; + + return sender.notify("user@example.com", subject, url) + .then(function () { + return BluebirdPromise.reject(new Error()); + }, function() { + Assert.equal(mailSender.sendStub.getCall(0).args[0].from, "admin@example.com"); + return BluebirdPromise.resolve(); + }); + }); +}); diff --git a/themes/matrix/server/src/lib/notifiers/EmailNotifier.ts b/themes/matrix/server/src/lib/notifiers/EmailNotifier.ts new file mode 100644 index 000000000..4df7c861e --- /dev/null +++ b/themes/matrix/server/src/lib/notifiers/EmailNotifier.ts @@ -0,0 +1,27 @@ + +import * as BluebirdPromise from "bluebird"; + +import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; +import { EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; +import { IMailSender } from "./IMailSender"; + +export class EmailNotifier extends AbstractEmailNotifier { + private mailSender: IMailSender; + private sender: string; + + constructor(options: EmailNotifierConfiguration, mailSender: IMailSender) { + super(); + this.mailSender = mailSender; + this.sender = options.sender; + } + + sendEmail(to: string, subject: string, content: string) { + const mailOptions = { + from: this.sender, + to: to, + subject: subject, + html: content + }; + return this.mailSender.send(mailOptions); + } +} diff --git a/themes/matrix/server/src/lib/notifiers/FileSystemNotifier.ts b/themes/matrix/server/src/lib/notifiers/FileSystemNotifier.ts new file mode 100644 index 000000000..23f6242c4 --- /dev/null +++ b/themes/matrix/server/src/lib/notifiers/FileSystemNotifier.ts @@ -0,0 +1,22 @@ +import * as BluebirdPromise from "bluebird"; +import * as util from "util"; +import * as Fs from "fs"; +import { INotifier } from "./INotifier"; +import { Identity } from "../../../types/Identity"; + +import { FileSystemNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; + +export class FileSystemNotifier implements INotifier { + private filename: string; + + constructor(options: FileSystemNotifierConfiguration) { + this.filename = options.filename; + } + + notify(to: string, subject: string, link: string): BluebirdPromise { + const content = util.format("Date: %s\nEmail: %s\nSubject: %s\nLink: %s", + new Date().toString(), to, subject, link); + const writeFilePromised: any = BluebirdPromise.promisify(Fs.writeFile); + return writeFilePromised(this.filename, content); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/IMailSender.ts b/themes/matrix/server/src/lib/notifiers/IMailSender.ts new file mode 100644 index 000000000..34ac464a8 --- /dev/null +++ b/themes/matrix/server/src/lib/notifiers/IMailSender.ts @@ -0,0 +1,6 @@ +import BluebirdPromise = require("bluebird"); +import Nodemailer = require("nodemailer"); + +export interface IMailSender { + send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/IMailSenderBuilder.ts b/themes/matrix/server/src/lib/notifiers/IMailSenderBuilder.ts new file mode 100644 index 000000000..36d4dcdf7 --- /dev/null +++ b/themes/matrix/server/src/lib/notifiers/IMailSenderBuilder.ts @@ -0,0 +1,7 @@ +import { IMailSender } from "./IMailSender"; +import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; + +export interface IMailSenderBuilder { + buildEmail(options: EmailNotifierConfiguration): IMailSender; + buildSmtp(options: SmtpNotifierConfiguration): IMailSender; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/INotifier.ts b/themes/matrix/server/src/lib/notifiers/INotifier.ts new file mode 100644 index 000000000..b9a6b138c --- /dev/null +++ b/themes/matrix/server/src/lib/notifiers/INotifier.ts @@ -0,0 +1,5 @@ +import * as BluebirdPromise from "bluebird"; + +export interface INotifier { + notify(to: string, subject: string, link: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/MailSender.ts b/themes/matrix/server/src/lib/notifiers/MailSender.ts new file mode 100644 index 000000000..536a88e63 --- /dev/null +++ b/themes/matrix/server/src/lib/notifiers/MailSender.ts @@ -0,0 +1,42 @@ +import { IMailSender } from "./IMailSender"; +import Nodemailer = require("nodemailer"); +import NodemailerDirectTransport = require("nodemailer-direct-transport"); +import NodemailerSmtpTransport = require("nodemailer-smtp-transport"); +import BluebirdPromise = require("bluebird"); + +export class MailSender implements IMailSender { + private transporter: Nodemailer.Transporter; + + constructor(options: NodemailerDirectTransport.DirectOptions | + NodemailerSmtpTransport.SmtpOptions, nodemailer: typeof Nodemailer) { + this.transporter = nodemailer.createTransport(options); + } + + verify(): BluebirdPromise { + const that = this; + return new BluebirdPromise(function (resolve, reject) { + that.transporter.verify(function (error: Error, success: any) { + if (error) { + reject(new Error("Unable to connect to SMTP server. \ + Please check the service is running and your credentials are correct.")); + return; + } + resolve(); + }); + }); + } + + send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise { + const that = this; + return new BluebirdPromise(function (resolve, reject) { + that.transporter.sendMail(mailOptions, (error: Error, + data: Nodemailer.SentMessageInfo) => { + if (error) { + reject(new Error("Error while sending email: " + error.message)); + return; + } + resolve(); + }); + }); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/MailSenderBuilder.spec.ts b/themes/matrix/server/src/lib/notifiers/MailSenderBuilder.spec.ts new file mode 100644 index 000000000..41e0db426 --- /dev/null +++ b/themes/matrix/server/src/lib/notifiers/MailSenderBuilder.spec.ts @@ -0,0 +1,67 @@ + +import { MailSenderBuilder } from ".//MailSenderBuilder"; +import Nodemailer = require("nodemailer"); +import Sinon = require("sinon"); +import Assert = require("assert"); + +describe("notifiers/MailSenderBuilder", function() { + let createTransportStub: Sinon.SinonStub; + beforeEach(function() { + createTransportStub = Sinon.stub(Nodemailer, "createTransport"); + }); + + afterEach(function() { + createTransportStub.restore(); + }); + + it("should create a email mail sender", function() { + const mailSenderBuilder = new MailSenderBuilder(Nodemailer); + mailSenderBuilder.buildEmail({ + username: "user_gmail", + password: "pass_gmail", + sender: "admin@example.com", + service: "gmail" + }); + Assert.equal(createTransportStub.getCall(0).args[0].auth.user, "user_gmail"); + Assert.equal(createTransportStub.getCall(0).args[0].auth.pass, "pass_gmail"); + Assert.equal(createTransportStub.getCall(0).args[0].service, "gmail"); + }); + + describe("build smtp mail sender", function() { + it("should create a smtp mail sender with authenticated user", function() { + const mailSenderBuilder = new MailSenderBuilder(Nodemailer); + mailSenderBuilder.buildSmtp({ + host: "mail.example.com", + password: "password", + port: 25, + secure: true, + username: "user", + sender: "admin@example.com" + }); + Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { + host: "mail.example.com", + auth: { + pass: "password", + user: "user" + }, + port: 25, + secure: true, + }); + }); + + it("should create a smtp mail sender with anonymous user", function() { + const mailSenderBuilder = new MailSenderBuilder(Nodemailer); + mailSenderBuilder.buildSmtp({ + host: "mail.example.com", + port: 25, + secure: true, + sender: "admin@example.com" + }); + Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { + host: "mail.example.com", + port: 25, + secure: true, + }); + }); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/MailSenderBuilder.ts b/themes/matrix/server/src/lib/notifiers/MailSenderBuilder.ts new file mode 100644 index 000000000..1d06be52f --- /dev/null +++ b/themes/matrix/server/src/lib/notifiers/MailSenderBuilder.ts @@ -0,0 +1,42 @@ +import { IMailSender } from "./IMailSender"; +import { IMailSenderBuilder } from "./IMailSenderBuilder"; +import { MailSender } from "./MailSender"; +import Nodemailer = require("nodemailer"); +import NodemailerSmtpTransport = require("nodemailer-smtp-transport"); +import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; + +export class MailSenderBuilder implements IMailSenderBuilder { + private nodemailer: typeof Nodemailer; + + constructor(nodemailer: typeof Nodemailer) { + this.nodemailer = nodemailer; + } + + buildEmail(options: EmailNotifierConfiguration): IMailSender { + const emailOptions = { + service: options.service, + auth: { + user: options.username, + pass: options.password + } + }; + return new MailSender(emailOptions, this.nodemailer); + } + + buildSmtp(options: SmtpNotifierConfiguration): IMailSender { + const smtpOptions: NodemailerSmtpTransport.SmtpOptions = { + host: options.host, + port: options.port, + secure: options.secure, // upgrade later with STARTTLS + }; + + if (options.username && options.password) { + smtpOptions.auth = { + user: options.username, + pass: options.password + }; + } + + return new MailSender(smtpOptions, this.nodemailer); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts b/themes/matrix/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts new file mode 100644 index 000000000..5b76f6e56 --- /dev/null +++ b/themes/matrix/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts @@ -0,0 +1,25 @@ +import { IMailSenderBuilder } from "../../../src/lib/notifiers/IMailSenderBuilder"; +import BluebirdPromise = require("bluebird"); +import Nodemailer = require("nodemailer"); +import Sinon = require("sinon"); +import { IMailSender } from "../../../src/lib/notifiers/IMailSender"; +import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../../../src/lib/configuration/schema/NotifierConfiguration"; + +export class MailSenderBuilderStub implements IMailSenderBuilder { + buildEmailStub: Sinon.SinonStub; + buildSmtpStub: Sinon.SinonStub; + + constructor() { + this.buildEmailStub = Sinon.stub(); + this.buildSmtpStub = Sinon.stub(); + } + + buildEmail(options: EmailNotifierConfiguration): IMailSender { + return this.buildEmailStub(options); + } + + buildSmtp(options: SmtpNotifierConfiguration): IMailSender { + return this.buildSmtpStub(options); + } + +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/MailSenderStub.spec.ts b/themes/matrix/server/src/lib/notifiers/MailSenderStub.spec.ts new file mode 100644 index 000000000..d57c458fc --- /dev/null +++ b/themes/matrix/server/src/lib/notifiers/MailSenderStub.spec.ts @@ -0,0 +1,16 @@ +import { IMailSender } from "../../../src/lib/notifiers/IMailSender"; +import BluebirdPromise = require("bluebird"); +import Nodemailer = require("nodemailer"); +import Sinon = require("sinon"); + +export class MailSenderStub implements IMailSender { + sendStub: Sinon.SinonStub; + + constructor() { + this.sendStub = Sinon.stub(); + } + + send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise { + return this.sendStub(mailOptions); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/NotifierFactory.spec.ts b/themes/matrix/server/src/lib/notifiers/NotifierFactory.spec.ts new file mode 100644 index 000000000..f15e7667f --- /dev/null +++ b/themes/matrix/server/src/lib/notifiers/NotifierFactory.spec.ts @@ -0,0 +1,42 @@ + +import * as sinon from "sinon"; +import * as BluebirdPromise from "bluebird"; +import * as assert from "assert"; + +import { NotifierFactory } from "./NotifierFactory"; +import { EmailNotifier } from "./EmailNotifier"; +import { SmtpNotifier } from "./SmtpNotifier"; +import { MailSenderBuilderStub } from "./MailSenderBuilderStub.spec"; + + +describe("notifiers/NotifierFactory", function () { + let mailSenderBuilderStub: MailSenderBuilderStub; + it("should build a Email Notifier", function () { + const options = { + email: { + username: "abc", + password: "password", + sender: "admin@example.com", + service: "gmail" + } + }; + mailSenderBuilderStub = new MailSenderBuilderStub(); + assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof EmailNotifier); + }); + + it("should build a SMTP Notifier", function () { + const options = { + smtp: { + username: "user", + password: "pass", + secure: true, + host: "localhost", + port: 25, + sender: "admin@example.com" + } + }; + + mailSenderBuilderStub = new MailSenderBuilderStub(); + assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof SmtpNotifier); + }); +}); diff --git a/themes/matrix/server/src/lib/notifiers/NotifierFactory.ts b/themes/matrix/server/src/lib/notifiers/NotifierFactory.ts new file mode 100644 index 000000000..a89155feb --- /dev/null +++ b/themes/matrix/server/src/lib/notifiers/NotifierFactory.ts @@ -0,0 +1,33 @@ + +import { NotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; +import Nodemailer = require("nodemailer"); +import { INotifier } from "./INotifier"; + +import { FileSystemNotifier } from "./FileSystemNotifier"; +import { EmailNotifier } from "./EmailNotifier"; +import { SmtpNotifier } from "./SmtpNotifier"; +import { IMailSender } from "./IMailSender"; +import { IMailSenderBuilder } from "./IMailSenderBuilder"; + +export class NotifierFactory { + static build(options: NotifierConfiguration, mailSenderBuilder: IMailSenderBuilder): INotifier { + if ("email" in options) { + const mailSender = mailSenderBuilder.buildEmail(options.email); + return new EmailNotifier(options.email, mailSender); + } + else if ("smtp" in options) { + const mailSender = mailSenderBuilder.buildSmtp(options.smtp); + return new SmtpNotifier(options.smtp, mailSender); + } + else if ("filesystem" in options) { + return new FileSystemNotifier(options.filesystem); + } + else { + throw new Error("No available notifier option detected."); + } + } +} + + + + diff --git a/themes/matrix/server/src/lib/notifiers/NotifierStub.spec.ts b/themes/matrix/server/src/lib/notifiers/NotifierStub.spec.ts new file mode 100644 index 000000000..f99231b50 --- /dev/null +++ b/themes/matrix/server/src/lib/notifiers/NotifierStub.spec.ts @@ -0,0 +1,16 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); + +import { INotifier } from "./INotifier"; + +export class NotifierStub implements INotifier { + notifyStub: Sinon.SinonStub; + + constructor() { + this.notifyStub = Sinon.stub(); + } + + notify(to: string, subject: string, link: string): BluebirdPromise { + return this.notifyStub(to, subject, link); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/SmtpNotifier.ts b/themes/matrix/server/src/lib/notifiers/SmtpNotifier.ts new file mode 100644 index 000000000..f93a6d4a0 --- /dev/null +++ b/themes/matrix/server/src/lib/notifiers/SmtpNotifier.ts @@ -0,0 +1,30 @@ + + +import * as BluebirdPromise from "bluebird"; + +import { IMailSender } from "./IMailSender"; +import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; +import { SmtpNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; + +export class SmtpNotifier extends AbstractEmailNotifier { + private mailSender: IMailSender; + private sender: string; + + constructor(options: SmtpNotifierConfiguration, + mailSender: IMailSender) { + super(); + this.mailSender = mailSender; + this.sender = options.sender; + } + + sendEmail(to: string, subject: string, content: string) { + const mailOptions = { + from: this.sender, + to: to, + subject: subject, + html: content + }; + const that = this; + return this.mailSender.send(mailOptions); + } +} diff --git a/themes/matrix/server/src/lib/regulation/IRegulator.ts b/themes/matrix/server/src/lib/regulation/IRegulator.ts new file mode 100644 index 000000000..c49425b24 --- /dev/null +++ b/themes/matrix/server/src/lib/regulation/IRegulator.ts @@ -0,0 +1,6 @@ +import BluebirdPromise = require("bluebird"); + +export interface IRegulator { + mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; + regulate(userId: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/regulation/Regulator.spec.ts b/themes/matrix/server/src/lib/regulation/Regulator.spec.ts new file mode 100644 index 000000000..f9c6e6086 --- /dev/null +++ b/themes/matrix/server/src/lib/regulation/Regulator.spec.ts @@ -0,0 +1,186 @@ + +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import Assert = require("assert"); + +import { Regulator } from "./Regulator"; +import MockDate = require("mockdate"); +import exceptions = require("../Exceptions"); +import { UserDataStoreStub } from "../storage/UserDataStoreStub.spec"; + +describe("regulation/Regulator", function () { + const USER1 = "USER1"; + const USER2 = "USER2"; + let userDataStoreStub: UserDataStoreStub; + + beforeEach(function () { + userDataStoreStub = new UserDataStoreStub(); + const dataStore: { [userId: string]: { userId: string, date: Date, isAuthenticationSuccessful: boolean }[] } = { + [USER1]: [], + [USER2]: [] + }; + + userDataStoreStub.saveAuthenticationTraceStub.callsFake(function (userId, isAuthenticationSuccessful) { + dataStore[userId].unshift({ + userId: userId, + date: new Date(), + isAuthenticationSuccessful: isAuthenticationSuccessful, + }); + return BluebirdPromise.resolve(); + }); + + userDataStoreStub.retrieveLatestAuthenticationTracesStub.callsFake(function (userId, count) { + const ret = (dataStore[userId].length <= count) ? dataStore[userId] : dataStore[userId].slice(0, 3); + return BluebirdPromise.resolve(ret); + }); + }); + + afterEach(function () { + MockDate.reset(); + }); + + function markAuthenticationAt(regulator: Regulator, user: string, time: string, success: boolean) { + MockDate.set(time); + return regulator.mark(user, success); + } + + it("should mark 2 authentication and regulate (accept)", function () { + const regulator = new Regulator(userDataStoreStub, 3, 10, 10); + + return regulator.mark(USER1, false) + .then(function () { + return regulator.mark(USER1, true); + }) + .then(function () { + return regulator.regulate(USER1); + }); + }); + + it("should mark 3 authentications and regulate (reject)", function () { + const regulator = new Regulator(userDataStoreStub, 3, 10, 10); + + return regulator.mark(USER1, false) + .then(function () { + return regulator.mark(USER1, false); + }) + .then(function () { + return regulator.mark(USER1, false); + }) + .then(function () { + return regulator.regulate(USER1); + }) + .then(function () { return BluebirdPromise.reject(new Error("should not be here!")); }) + .catch(exceptions.AuthenticationRegulationError, function () { + return BluebirdPromise.resolve(); + }); + }); + + it("should mark 1 failed, 1 successful and 1 failed authentications within minimum time and regulate (accept)", function () { + const regulator = new Regulator(userDataStoreStub, 3, 60, 30); + + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", true); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:20", false); + }) + .then(function () { + return regulator.regulate(USER1); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:30", false); + }) + .then(function () { + return regulator.regulate(USER1); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:39", false); + }) + .then(function () { + return regulator.regulate(USER1); + }) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here!")); + }, + function () { + return BluebirdPromise.resolve(); + }); + }); + + it("should regulate user if number of failures is greater than 3 in allowed time lapse", function () { + function markAuthentications(regulator: Regulator, user: string) { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:45", false); + }) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:01:05", false); + }) + .then(function () { + return regulator.regulate(user); + }); + } + + const regulator1 = new Regulator(userDataStoreStub, 3, 60, 60); + const regulator2 = new Regulator(userDataStoreStub, 3, 2 * 60, 60); + + const p1 = markAuthentications(regulator1, USER1); + const p2 = markAuthentications(regulator2, USER2); + + return BluebirdPromise.join(p1, p2) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here...")); + }, function () { + Assert(p1.isFulfilled()); + Assert(p2.isRejected()); + }); + }); + + it("should user wait after regulation to authenticate again", function () { + function markAuthentications(regulator: Regulator, user: string) { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:10", false); + }) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:15", false); + }) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:25", false); + }) + .then(function () { + MockDate.set("1/2/2000 00:00:54"); + return regulator.regulate(user); + }) + .then(function () { + return BluebirdPromise.reject(new Error("should fail at this time")); + }, function () { + MockDate.set("1/2/2000 00:00:56"); + return regulator.regulate(user); + }); + } + + const regulator = new Regulator(userDataStoreStub, 4, 30, 30); + return markAuthentications(regulator, USER1); + }); + + it("should disable regulation when max_retries is set to 0", function () { + const maxRetries = 0; + const regulator = new Regulator(userDataStoreStub, maxRetries, 60, 30); + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", false); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:15", false); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:25", false); + }) + .then(function () { + MockDate.set("1/2/2000 00:00:26"); + return regulator.regulate(USER1); + }); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/regulation/Regulator.ts b/themes/matrix/server/src/lib/regulation/Regulator.ts new file mode 100644 index 000000000..1037a6a17 --- /dev/null +++ b/themes/matrix/server/src/lib/regulation/Regulator.ts @@ -0,0 +1,55 @@ + +import * as BluebirdPromise from "bluebird"; +import exceptions = require("../Exceptions"); +import { IUserDataStore } from "../storage/IUserDataStore"; +import { AuthenticationTraceDocument } from "../storage/AuthenticationTraceDocument"; +import { IRegulator } from "./IRegulator"; + +export class Regulator implements IRegulator { + private userDataStore: IUserDataStore; + private banTime: number; + private findTime: number; + private maxRetries: number; + + constructor(userDataStore: any, maxRetries: number, findTime: number, banTime: number) { + this.userDataStore = userDataStore; + this.banTime = banTime; + this.findTime = findTime; + this.maxRetries = maxRetries; + } + + // Mark authentication + mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { + return this.userDataStore.saveAuthenticationTrace(userId, isAuthenticationSuccessful); + } + + regulate(userId: string): BluebirdPromise { + const that = this; + + if (that.maxRetries <= 0) return BluebirdPromise.resolve(); + + return this.userDataStore.retrieveLatestAuthenticationTraces(userId, that.maxRetries) + .then((docs: AuthenticationTraceDocument[]) => { + // less than the max authorized number of authentication in time range, thus authorizing access + if (docs.length < that.maxRetries) return BluebirdPromise.resolve(); + + const numberOfFailedAuth = docs + .map(function (d: AuthenticationTraceDocument) { return d.isAuthenticationSuccessful == false ? 1 : 0; }) + .reduce(function (acc, v) { return acc + v; }, 0); + + if (numberOfFailedAuth < this.maxRetries) return BluebirdPromise.resolve(); + + const newestDocument = docs[0]; + const oldestDocument = docs[that.maxRetries - 1]; + + const authenticationsTimeRangeInSeconds = (newestDocument.date.getTime() - oldestDocument.date.getTime()) / 1000; + const tooManyAuthInTimelapse = (authenticationsTimeRangeInSeconds < this.findTime); + const stillInBannedTimeRange = (new Date(new Date().getTime() - this.banTime * 1000) < newestDocument.date); + + if (tooManyAuthInTimelapse && stillInBannedTimeRange) + throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes."); + + return BluebirdPromise.resolve(); + }); + } +} diff --git a/themes/matrix/server/src/lib/regulation/RegulatorStub.spec.ts b/themes/matrix/server/src/lib/regulation/RegulatorStub.spec.ts new file mode 100644 index 000000000..ca8a00fb1 --- /dev/null +++ b/themes/matrix/server/src/lib/regulation/RegulatorStub.spec.ts @@ -0,0 +1,22 @@ +import Bluebird = require("bluebird"); +import Sinon = require("sinon"); +import { IRegulator } from "./IRegulator"; + + +export class RegulatorStub implements IRegulator { + markStub: Sinon.SinonStub; + regulateStub: Sinon.SinonStub; + + constructor() { + this.markStub = Sinon.stub(); + this.regulateStub = Sinon.stub(); + } + + mark(userId: string, isAuthenticationSuccessful: boolean): Bluebird { + return this.markStub(userId, isAuthenticationSuccessful); + } + + regulate(userId: string): Bluebird { + return this.regulateStub(userId); + } +} diff --git a/themes/matrix/server/src/lib/routes/error/401/get.spec.ts b/themes/matrix/server/src/lib/routes/error/401/get.spec.ts new file mode 100644 index 000000000..9fdac9c3c --- /dev/null +++ b/themes/matrix/server/src/lib/routes/error/401/get.spec.ts @@ -0,0 +1,61 @@ +import Sinon = require("sinon"); +import Express = require("express"); +import Assert = require("assert"); +import Get401 from "./get"; +import { ServerVariables } from "../../../ServerVariables"; +import { ServerVariablesMockBuilder, ServerVariablesMock } + from "../../../ServerVariablesMockBuilder.spec"; + +describe("routes/error/401/get", function () { + let vars: ServerVariables; + let mocks: ServerVariablesMock; + let req: any; + let res: any; + let renderSpy: Sinon.SinonSpy; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + vars = s.variables; + mocks = s.mocks; + + renderSpy = Sinon.spy(); + req = { + headers: {} + }; + res = { + render: renderSpy + }; + }); + + it("should set redirection url to the default redirection url", function () { + vars.config.default_redirection_url = "http://default-redirection"; + return Get401(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/401", { + redirection_url: "http://default-redirection" + })); + }); + }); + + it("should set redirection url to the referer", function () { + req.headers["referer"] = "http://redirection"; + return Get401(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/401", { + redirection_url: "http://redirection" + })); + }); + }); + + it("should render without redirecting the user", function () { + return Get401(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/401", { + redirection_url: undefined + })); + }); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/error/401/get.ts b/themes/matrix/server/src/lib/routes/error/401/get.ts new file mode 100644 index 000000000..ca4a3963d --- /dev/null +++ b/themes/matrix/server/src/lib/routes/error/401/get.ts @@ -0,0 +1,15 @@ + +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import redirector from "../redirector"; +import { ServerVariables } from "../../../ServerVariables"; + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response): BluebirdPromise { + const redirectionUrl = redirector(req, vars); + res.render("errors/401", { + redirection_url: redirectionUrl + }); + return BluebirdPromise.resolve(); + }; +} diff --git a/themes/matrix/server/src/lib/routes/error/403/get.spec.ts b/themes/matrix/server/src/lib/routes/error/403/get.spec.ts new file mode 100644 index 000000000..22eb84853 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/error/403/get.spec.ts @@ -0,0 +1,61 @@ +import Sinon = require("sinon"); +import Express = require("express"); +import Assert = require("assert"); +import Get403 from "./get"; +import { ServerVariables } from "../../../ServerVariables"; +import { ServerVariablesMockBuilder, ServerVariablesMock } + from "../../../ServerVariablesMockBuilder.spec"; + +describe("routes/error/403/get", function () { + let vars: ServerVariables; + let mocks: ServerVariablesMock; + let req: any; + let res: any; + let renderSpy: Sinon.SinonSpy; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + vars = s.variables; + mocks = s.mocks; + + renderSpy = Sinon.spy(); + req = { + headers: {} + }; + res = { + render: renderSpy + }; + }); + + it("should set redirection url to the default redirection url", function () { + vars.config.default_redirection_url = "http://default-redirection"; + return Get403(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/403", { + redirection_url: "http://default-redirection" + })); + }); + }); + + it("should set redirection url to the referer", function () { + req.headers["referer"] = "http://redirection"; + return Get403(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/403", { + redirection_url: "http://redirection" + })); + }); + }); + + it("should render without redirecting the user", function () { + return Get403(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/403", { + redirection_url: undefined + })); + }); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/error/403/get.ts b/themes/matrix/server/src/lib/routes/error/403/get.ts new file mode 100644 index 000000000..3ab0319e5 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/error/403/get.ts @@ -0,0 +1,15 @@ + +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import redirector from "../redirector"; +import { ServerVariables } from "../../../ServerVariables"; + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response): BluebirdPromise { + const redirectionUrl = redirector(req, vars); + res.render("errors/403", { + redirection_url: redirectionUrl + }); + return BluebirdPromise.resolve(); + }; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/error/404/get.spec.ts b/themes/matrix/server/src/lib/routes/error/404/get.spec.ts new file mode 100644 index 000000000..73e4e6cef --- /dev/null +++ b/themes/matrix/server/src/lib/routes/error/404/get.spec.ts @@ -0,0 +1,19 @@ +import Sinon = require("sinon"); +import Express = require("express"); +import Assert = require("assert"); +import Get404 from "./get"; + +describe("routes/error/404/get", function () { + it("should render the page", function () { + const req = {} as Express.Request; + const res = { + render: Sinon.stub() + }; + + return Get404(req, res as any) + .then(function () { + Assert(res.render.calledOnce); + Assert(res.render.calledWith("errors/404")); + }); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/error/404/get.ts b/themes/matrix/server/src/lib/routes/error/404/get.ts new file mode 100644 index 000000000..6693b6fc8 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/error/404/get.ts @@ -0,0 +1,8 @@ + +import BluebirdPromise = require("bluebird"); +import express = require("express"); + +export default function (req: express.Request, res: express.Response): BluebirdPromise { + res.render("errors/404"); + return BluebirdPromise.resolve(); +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/error/redirector.ts b/themes/matrix/server/src/lib/routes/error/redirector.ts new file mode 100644 index 000000000..b1a3ccc11 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/error/redirector.ts @@ -0,0 +1,13 @@ +import Express = require("express"); +import { ServerVariables } from "../../ServerVariables"; + +export default function (req: Express.Request, vars: ServerVariables): string { + let redirectionUrl: string; + + if (req.headers && req.headers["referer"]) + redirectionUrl = "" + req.headers["referer"]; + else if (vars.config.default_redirection_url) + redirectionUrl = vars.config.default_redirection_url; + + return redirectionUrl; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/firstfactor/get.ts b/themes/matrix/server/src/lib/routes/firstfactor/get.ts new file mode 100644 index 000000000..d94f656c8 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/firstfactor/get.ts @@ -0,0 +1,72 @@ + +import express = require("express"); +import Endpoints = require("../../../../../shared/api"); +import BluebirdPromise = require("bluebird"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Constants = require("../../../../../shared/constants"); +import Util = require("util"); +import { ServerVariables } from "../../ServerVariables"; +import { SafeRedirector } from "../../utils/SafeRedirection"; +import { Level } from "../../authentication/Level"; + +function getRedirectParam( + req: express.Request) { + return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; +} + +function redirectToSecondFactorPage( + req: express.Request, + res: express.Response) { + + const redirectUrl = getRedirectParam(req); + if (!redirectUrl) + res.redirect(Endpoints.SECOND_FACTOR_GET); + else + res.redirect( + Util.format("%s?%s=%s", + Endpoints.SECOND_FACTOR_GET, + Constants.REDIRECT_QUERY_PARAM, + redirectUrl)); +} + +function redirectToService( + req: express.Request, + res: express.Response, + redirector: SafeRedirector) { + const redirectUrl = getRedirectParam(req); + if (!redirectUrl) { + res.redirect(Endpoints.LOGGED_IN); + } else { + redirector.redirectOrElse(res, redirectUrl, Endpoints.LOGGED_IN); + } +} + +function renderFirstFactor( + res: express.Response) { + + res.render("firstfactor", { + first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST, + reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET + }); +} + +export default function ( + vars: ServerVariables) { + + const redirector = new SafeRedirector(vars.config.session.domain); + return function (req: express.Request, res: express.Response): BluebirdPromise { + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (authSession.authentication_level == Level.ONE_FACTOR) { + redirectToSecondFactorPage(req, res); + } else if (authSession.authentication_level == Level.TWO_FACTOR) { + redirectToService(req, res, redirector); + } else { + renderFirstFactor(res); + } + resolve(); + }); + }; +} diff --git a/themes/matrix/server/src/lib/routes/firstfactor/post.spec.ts b/themes/matrix/server/src/lib/routes/firstfactor/post.spec.ts new file mode 100644 index 000000000..e1d078cdd --- /dev/null +++ b/themes/matrix/server/src/lib/routes/firstfactor/post.spec.ts @@ -0,0 +1,136 @@ + +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import Assert = require("assert"); +import FirstFactorPost = require("./post"); +import exceptions = require("../../Exceptions"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../types/AuthenticationSession"; +import Endpoints = require("../../../../../shared/api"); +import AuthenticationRegulatorMock = require("../../regulation/RegulatorStub.spec"); +import ExpressMock = require("../../stubs/express.spec"); +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../ServerVariables"; + +describe("routes/firstfactor/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let emails: string[]; + let groups: string[]; + let vars: ServerVariables; + let mocks: ServerVariablesMock; + let authSession: AuthenticationSession; + + beforeEach(function () { + emails = ["test_ok@example.com"]; + groups = ["group1", "group2" ]; + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + mocks.authorizer.authorizationMock.returns(true); + mocks.regulator.regulateStub.returns(BluebirdPromise.resolve()); + mocks.regulator.markStub.returns(BluebirdPromise.resolve()); + + req = { + originalUrl: "/api/firstfactor", + body: { + username: "username", + password: "password" + }, + query: { + redirect: "http://redirect.url" + }, + session: { + cookie: {} + }, + headers: { + host: "home.example.com" + } + }; + + res = ExpressMock.ResponseMock(); + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + }); + + it("should reply with 204 if success", function () { + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.resolve({ + emails: emails, + groups: groups + })); + return FirstFactorPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal("username", authSession.userid); + Assert(res.send.calledOnce); + }); + }); + + describe("keep me logged in", () => { + beforeEach(() => { + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.resolve({ + emails: emails, + groups: groups + })); + req.body.keepMeLoggedIn = "true"; + return FirstFactorPost.default(vars)(req as any, res as any); + }); + + it("should set keep_me_logged_in session variable to true", function () { + Assert.equal(authSession.keep_me_logged_in, true); + }); + + it("should set cookie maxAge to one year", function () { + Assert.equal(req.session.cookie.maxAge, 365 * 24 * 60 * 60 * 1000); + }); + }); + + it("should retrieve email from LDAP", function () { + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.resolve([{ mail: ["test@example.com"] }])); + return FirstFactorPost.default(vars)(req as any, res as any); + }); + + it("should set first email address as user session variable", function () { + const emails = ["test_ok@example.com"]; + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.resolve({ + emails: emails, + groups: groups + })); + + return FirstFactorPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal("test_ok@example.com", authSession.email); + }); + }); + + it("should return error message when LDAP authenticator throws", function () { + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.reject(new exceptions.LdapBindError("Bad credentials"))); + + return FirstFactorPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.equal(mocks.regulator.markStub.getCall(0).args[0], "username"); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + }); + }); + + it("should return error message when regulator rejects authentication", function () { + const err = new exceptions.AuthenticationRegulationError("Authentication regulation..."); + mocks.regulator.regulateStub.returns(BluebirdPromise.reject(err)); + return FirstFactorPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + }); + }); +}); + + diff --git a/themes/matrix/server/src/lib/routes/firstfactor/post.ts b/themes/matrix/server/src/lib/routes/firstfactor/post.ts new file mode 100644 index 000000000..565681d6a --- /dev/null +++ b/themes/matrix/server/src/lib/routes/firstfactor/post.ts @@ -0,0 +1,101 @@ + +import Exceptions = require("../../Exceptions"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import Endpoint = require("../../../../../shared/api"); +import ErrorReplies = require("../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Constants = require("../../../../../shared/constants"); +import UserMessages = require("../../../../../shared/UserMessages"); +import { ServerVariables } from "../../ServerVariables"; +import { AuthenticationSession } from "../../../../types/AuthenticationSession"; +import { GroupsAndEmails } from "../../authentication/backends/GroupsAndEmails"; +import { Level as AuthenticationLevel } from "../../authentication/Level"; +import { Level as AuthorizationLevel } from "../../authorization/Level"; +import { URLDecomposer } from "../../utils/URLDecomposer"; + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response) + : BluebirdPromise { + const username: string = req.body.username; + const password: string = req.body.password; + const keepMeLoggedIn: boolean = req.body.keepMeLoggedIn && + req.body.keepMeLoggedIn === "true"; + let authSession: AuthenticationSession; + + if (keepMeLoggedIn) { + // Stay connected for 1 year. + vars.logger.debug(req, "User requested to stay logged in for one year."); + req.session.cookie.maxAge = 365 * 24 * 60 * 60 * 1000; + } + + return BluebirdPromise.resolve() + .then(function () { + if (!username || !password) { + return BluebirdPromise.reject(new Error("No username or password.")); + } + vars.logger.info(req, "Starting authentication of user \"%s\"", username); + authSession = AuthenticationSessionHandler.get(req, vars.logger); + return vars.regulator.regulate(username); + }) + .then(function () { + vars.logger.info(req, "No regulation applied."); + return vars.usersDatabase.checkUserPassword(username, password); + }) + .then(function (groupsAndEmails: GroupsAndEmails) { + vars.logger.info(req, + "LDAP binding successful. Retrieved information about user are %s", + JSON.stringify(groupsAndEmails)); + authSession.userid = username; + authSession.keep_me_logged_in = keepMeLoggedIn; + authSession.authentication_level = AuthenticationLevel.ONE_FACTOR; + const redirectUrl: string = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined" + // Fuck, don't know why it is a string! + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : ""; + + const emails: string[] = groupsAndEmails.emails; + const groups: string[] = groupsAndEmails.groups; + const decomposition = URLDecomposer.fromUrl(redirectUrl); + const authorizationLevel = (decomposition) + ? vars.authorizer.authorization( + {domain: decomposition.domain, resource: decomposition.path}, + {user: username, groups: groups}) + : AuthorizationLevel.TWO_FACTOR; + + if (emails.length > 0) + authSession.email = emails[0]; + authSession.groups = groups; + + vars.logger.debug(req, "Mark successful authentication to regulator."); + vars.regulator.mark(username, true); + + if (authorizationLevel <= AuthorizationLevel.ONE_FACTOR) { + let newRedirectionUrl: string = redirectUrl; + if (!newRedirectionUrl) + newRedirectionUrl = Endpoint.LOGGED_IN; + res.send({ + redirect: newRedirectionUrl + }); + vars.logger.debug(req, "Redirect to '%s'", redirectUrl); + } + else { + let newRedirectUrl = Endpoint.SECOND_FACTOR_GET; + if (redirectUrl) { + newRedirectUrl += "?" + Constants.REDIRECT_QUERY_PARAM + "=" + + redirectUrl; + } + vars.logger.debug(req, "Redirect to '%s'", newRedirectUrl); + res.send({ + redirect: newRedirectUrl + }); + } + return BluebirdPromise.resolve(); + }) + .catch(Exceptions.LdapBindError, function (err: Error) { + vars.regulator.mark(username, false); + return ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)(err); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)); + }; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/loggedin/get.ts b/themes/matrix/server/src/lib/routes/loggedin/get.ts new file mode 100644 index 000000000..283a041b1 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/loggedin/get.ts @@ -0,0 +1,23 @@ +import Express = require("express"); +import Endpoints = require("../../../../../shared/api"); +import BluebirdPromise = require("bluebird"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { ServerVariables } from "../../ServerVariables"; +import ErrorReplies = require("../../ErrorReplies"); + +export default function (vars: ServerVariables) { + function handler(req: Express.Request, res: Express.Response): BluebirdPromise { + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + res.render("already-logged-in", { + logout_endpoint: Endpoints.LOGOUT_GET, + username: authSession.userid, + redirection_url: vars.config.default_redirection_url + }); + resolve(); + }) + .catch(ErrorReplies.replyWithError401(req, res, vars.logger)); + } + + return handler; +} diff --git a/themes/matrix/server/src/lib/routes/logout/get.ts b/themes/matrix/server/src/lib/routes/logout/get.ts new file mode 100644 index 000000000..4d5112146 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/logout/get.ts @@ -0,0 +1,20 @@ + +import express = require("express"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Constants = require("../../../../../shared/constants"); +import { ServerVariables } from "../../ServerVariables"; + +function getRedirectParam(req: express.Request) { + return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; +} + +export default function (vars: ServerVariables) { + return function(req: express.Request, res: express.Response) { + const redirect_param = getRedirectParam(req); + const redirect_url = redirect_param || "/"; + AuthenticationSessionHandler.reset(req); + res.redirect(redirect_url); + }; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/password-reset/constants.ts b/themes/matrix/server/src/lib/routes/password-reset/constants.ts new file mode 100644 index 000000000..5c639e92a --- /dev/null +++ b/themes/matrix/server/src/lib/routes/password-reset/constants.ts @@ -0,0 +1,2 @@ + +export const CHALLENGE = "reset-password"; \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/password-reset/form/post.spec.ts b/themes/matrix/server/src/lib/routes/password-reset/form/post.spec.ts new file mode 100644 index 000000000..ed029c906 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/password-reset/form/post.spec.ts @@ -0,0 +1,122 @@ + +import PasswordResetFormPost = require("./post"); +import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; +import { UserDataStore } from "../../../storage/UserDataStore"; +import Sinon = require("sinon"); +import Assert = require("assert"); +import BluebirdPromise = require("bluebird"); +import ExpressMock = require("../../../stubs/express.spec"); +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../ServerVariables"; +import { Level } from "../../../authentication/Level"; + +describe("routes/password-reset/form/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let vars: ServerVariables; + let mocks: ServerVariablesMock; + let authSession: AuthenticationSession; + + beforeEach(function () { + req = { + originalUrl: "/api/password-reset", + body: { + userid: "user" + }, + session: {}, + headers: { + host: "localhost" + } + }; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + + mocks.config.authentication_backend.ldap = { + url: "ldap://ldapjs", + mail_attribute: "mail", + user: "user", + password: "password", + additional_users_dn: "ou=users", + additional_groups_dn: "ou=groups", + base_dn: "dc=example,dc=com", + users_filter: "user", + group_name_attribute: "cn", + groups_filter: "groups" + }; + + res = ExpressMock.ResponseMock(); + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + authSession.userid = "user"; + authSession.email = "user@example.com"; + authSession.authentication_level = Level.ONE_FACTOR; + }); + + describe("test reset password post", () => { + it("should update the password and reset auth_session for reauthentication", function () { + req.body = {}; + req.body.password = "new-password"; + + mocks.usersDatabase.updatePasswordStub.returns(BluebirdPromise.resolve()); + + authSession.identity_check = { + userid: "user", + challenge: "reset-password" + }; + return PasswordResetFormPost.default(vars)(req as any, res as any) + .then(function () { + return AuthenticationSessionHandler.get(req as any, vars.logger); + }).then(function (_authSession) { + Assert.equal(res.status.getCall(0).args[0], 204); + Assert.equal(_authSession.authentication_level, Level.NOT_AUTHENTICATED); + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if identity_challenge does not exist", function () { + authSession.identity_check = { + userid: "user", + challenge: undefined + }; + return PasswordResetFormPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "An error occurred during password reset. Your password has not been changed." + }); + }); + }); + + it("should fail when ldap fails", function () { + req.body = {}; + req.body.password = "new-password"; + + mocks.usersDatabase.updatePasswordStub + .returns(BluebirdPromise.reject("Internal error with LDAP")); + + authSession.identity_check = { + challenge: "reset-password", + userid: "user" + }; + return PasswordResetFormPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "An error occurred during password reset. Your password has not been changed." + }); + return BluebirdPromise.resolve(); + }); + }); + }); +}); diff --git a/themes/matrix/server/src/lib/routes/password-reset/form/post.ts b/themes/matrix/server/src/lib/routes/password-reset/form/post.ts new file mode 100644 index 000000000..fccd7471b --- /dev/null +++ b/themes/matrix/server/src/lib/routes/password-reset/form/post.ts @@ -0,0 +1,50 @@ + +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); +import exceptions = require("../../../Exceptions"); +import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; +import ErrorReplies = require("../../../ErrorReplies"); +import UserMessages = require("../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../ServerVariables"; + +import Constants = require("./../constants"); + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const newPassword = objectPath.get(req, "body.password"); + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (!authSession.identity_check) { + reject(new Error("No identity check initiated")); + return; + } + + vars.logger.info(req, "User %s wants to reset his/her password.", + authSession.identity_check.userid); + vars.logger.debug(req, "Challenge %s", authSession.identity_check.challenge); + + if (authSession.identity_check.challenge != Constants.CHALLENGE) { + reject(new Error("Bad challenge.")); + return; + } + resolve(); + }) + .then(function () { + return vars.usersDatabase.updatePassword(authSession.identity_check.userid, newPassword); + }) + .then(function () { + vars.logger.info(req, "Password reset for user '%s'", + authSession.identity_check.userid); + AuthenticationSessionHandler.reset(req); + res.status(204); + res.send(); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.RESET_PASSWORD_FAILED)); + }; +} diff --git a/themes/matrix/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts b/themes/matrix/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts new file mode 100644 index 000000000..ac6a41754 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts @@ -0,0 +1,92 @@ + +import PasswordResetHandler + from "./PasswordResetHandler"; +import { UserDataStore } from "../../../storage/UserDataStore"; +import Sinon = require("sinon"); +import winston = require("winston"); +import assert = require("assert"); +import BluebirdPromise = require("bluebird"); +import ExpressMock = require("../../../stubs/express.spec"); +import { ServerVariablesMock, ServerVariablesMockBuilder } + from "../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../ServerVariables"; + +describe("routes/password-reset/identity/PasswordResetHandler", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + req = { + originalUrl: "/non-api/xxx", + query: { + userid: "user" + }, + session: { + auth: { + userid: "user", + email: "user@example.com", + first_factor: true, + second_factor: false + } + }, + headers: { + host: "localhost" + } + }; + + const options = { + inMemoryOnly: true + }; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + mocks.userDataStore.saveU2FRegistrationStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub + .returns(BluebirdPromise.resolve({})); + res = ExpressMock.ResponseMock(); + }); + + describe("test reset password identity pre check", () => { + it("should fail when no userid is provided", function () { + req.query.userid = undefined; + const handler = new PasswordResetHandler(vars.logger, + vars.usersDatabase); + return handler.preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject("It should fail"); + }) + .catch(function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if ldap fail", function () { + mocks.usersDatabase.getEmailsStub + .returns(BluebirdPromise.reject("Internal error")); + new PasswordResetHandler(vars.logger, vars.usersDatabase) + .preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here")); + }, + function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should returns identity when ldap replies", function () { + mocks.usersDatabase.getEmailsStub + .returns(BluebirdPromise.resolve(["test@example.com"])); + return new PasswordResetHandler(vars.logger, vars.usersDatabase) + .preValidationInit(req as any); + }); + }); +}); diff --git a/themes/matrix/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts b/themes/matrix/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts new file mode 100644 index 000000000..42ae92cda --- /dev/null +++ b/themes/matrix/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts @@ -0,0 +1,69 @@ +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); + +import exceptions = require("../../../Exceptions"); +import { Identity } from "../../../../../types/Identity"; +import { IdentityValidable } from "../../../IdentityValidable"; +import { PRE_VALIDATION_TEMPLATE } from "../../../IdentityCheckPreValidationTemplate"; +import Constants = require("../constants"); +import { IRequestLogger } from "../../../logging/IRequestLogger"; +import { IUsersDatabase } from "../../../authentication/backends/IUsersDatabase"; + +export const TEMPLATE_NAME = "password-reset-form"; + +export default class PasswordResetHandler implements IdentityValidable { + private logger: IRequestLogger; + private usersDatabase: IUsersDatabase; + + constructor(logger: IRequestLogger, usersDatabase: IUsersDatabase) { + this.logger = logger; + this.usersDatabase = usersDatabase; + } + + challenge(): string { + return Constants.CHALLENGE; + } + + preValidationInit(req: express.Request): BluebirdPromise { + const that = this; + const userid: string = + objectPath.get(req, "query.userid"); + return BluebirdPromise.resolve() + .then(function () { + that.logger.debug(req, "User '%s' requested a password reset", userid); + if (!userid) + return BluebirdPromise.reject( + new exceptions.AccessDeniedError("No user id provided")); + + return that.usersDatabase.getEmails(userid); + }) + .then(function (emails: string[]) { + if (!emails && emails.length <= 0) throw new Error("No email found"); + const identity = { + email: emails[0], + userid: userid + }; + return BluebirdPromise.resolve(identity); + }) + .catch(function (err: Error) { + return BluebirdPromise.reject(new exceptions.IdentityError(err.message)); + }); + } + + preValidationResponse(req: express.Request, res: express.Response) { + res.render(PRE_VALIDATION_TEMPLATE); + } + + postValidationInit(req: express.Request) { + return BluebirdPromise.resolve(); + } + + postValidationResponse(req: express.Request, res: express.Response) { + res.render(TEMPLATE_NAME); + } + + mailSubject(): string { + return "Reset your password"; + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/password-reset/request/get.ts b/themes/matrix/server/src/lib/routes/password-reset/request/get.ts new file mode 100644 index 000000000..8f3ae2b4b --- /dev/null +++ b/themes/matrix/server/src/lib/routes/password-reset/request/get.ts @@ -0,0 +1,13 @@ + +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); +import exceptions = require("../../../Exceptions"); + +import Constants = require("./../constants"); + +const TEMPLATE_NAME = "password-reset-request"; + +export default function (req: express.Request, res: express.Response) { + res.render(TEMPLATE_NAME); +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/secondfactor/get.spec.ts b/themes/matrix/server/src/lib/routes/secondfactor/get.spec.ts new file mode 100644 index 000000000..6c77e1f69 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/secondfactor/get.spec.ts @@ -0,0 +1,44 @@ +import SecondFactorGet from "./get"; +import { ServerVariablesMockBuilder, ServerVariablesMock } + from "../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../ServerVariables"; +import Sinon = require("sinon"); +import ExpressMock = require("../../stubs/express.spec"); +import Assert = require("assert"); +import Endpoints = require("../../../../../shared/api"); +import BluebirdPromise = require("bluebird"); + +describe("routes/secondfactor/get", function () { + let mocks: ServerVariablesMock; + let vars: ServerVariables; + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false + } + }; + }); + + describe("test rendering", function () { + it("should render second factor page", function () { + req.session.auth.second_factor = false; + return SecondFactorGet(vars)(req as any, res as any) + .then(function () { + Assert(res.render.calledWith("secondfactor")); + return BluebirdPromise.resolve(); + }); + }); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/secondfactor/get.ts b/themes/matrix/server/src/lib/routes/secondfactor/get.ts new file mode 100644 index 000000000..9f6deb4c6 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/secondfactor/get.ts @@ -0,0 +1,28 @@ + +import Express = require("express"); +import Endpoints = require("../../../../../shared/api"); +import BluebirdPromise = require("bluebird"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { ServerVariables } from "../../ServerVariables"; + +const TEMPLATE_NAME = "secondfactor"; + +export default function (vars: ServerVariables) { + function handler(req: Express.Request, res: Express.Response) + : BluebirdPromise { + + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + + res.render(TEMPLATE_NAME, { + username: authSession.userid, + totp_identity_start_endpoint: + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, + u2f_identity_start_endpoint: + Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET + }); + resolve(); + }); + } + return handler; +} diff --git a/themes/matrix/server/src/lib/routes/secondfactor/redirect.spec.ts b/themes/matrix/server/src/lib/routes/secondfactor/redirect.spec.ts new file mode 100644 index 000000000..ea66e6dca --- /dev/null +++ b/themes/matrix/server/src/lib/routes/secondfactor/redirect.spec.ts @@ -0,0 +1,41 @@ +import Redirect from "./redirect"; +import ExpressMock = require("../../stubs/express.spec"); +import { ServerVariablesMockBuilder, ServerVariablesMock } +from "../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../ServerVariables"; +import Assert = require("assert"); + +describe("routes/secondfactor/redirect", function() { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + }); + + it("should redirect to default_redirection_url", function() { + vars.config.default_redirection_url = "http://default_redirection_url"; + Redirect(vars)(req as any, res as any) + .then(function() { + Assert(res.json.calledWith({ + redirect: "http://default_redirection_url" + })); + }); + }); + + it("should redirect to /", function() { + Redirect(vars)(req as any, res as any) + .then(function() { + Assert(res.json.calledWith({ + redirect: "/" + })); + }); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/secondfactor/redirect.ts b/themes/matrix/server/src/lib/routes/secondfactor/redirect.ts new file mode 100644 index 000000000..5d84d9ebf --- /dev/null +++ b/themes/matrix/server/src/lib/routes/secondfactor/redirect.ts @@ -0,0 +1,30 @@ + +import express = require("express"); +import objectPath = require("object-path"); +import Endpoints = require("../../../../../shared/api"); +import { ServerVariables } from "../../ServerVariables"; +import BluebirdPromise = require("bluebird"); +import ErrorReplies = require("../../ErrorReplies"); +import UserMessages = require("../../../../../shared/UserMessages"); +import { RedirectionMessage } from "../../../../../shared/RedirectionMessage"; +import Constants = require("../../../../../shared/constants"); + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response) + : BluebirdPromise { + + return new BluebirdPromise(function (resolve, reject) { + let redirectUrl: string = "/"; + if (vars.config.default_redirection_url) { + redirectUrl = vars.config.default_redirection_url; + } + vars.logger.debug(req, "Request redirection to \"%s\".", redirectUrl); + res.json({ + redirect: redirectUrl + } as RedirectionMessage); + return resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + }; +} diff --git a/themes/matrix/server/src/lib/routes/secondfactor/totp/constants.ts b/themes/matrix/server/src/lib/routes/secondfactor/totp/constants.ts new file mode 100644 index 000000000..7b5a1dcfe --- /dev/null +++ b/themes/matrix/server/src/lib/routes/secondfactor/totp/constants.ts @@ -0,0 +1,4 @@ + +export const CHALLENGE = "totp-register"; +export const TEMPLATE_NAME = "totp-register"; + diff --git a/themes/matrix/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts b/themes/matrix/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts new file mode 100644 index 000000000..78b8ea3ea --- /dev/null +++ b/themes/matrix/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts @@ -0,0 +1,116 @@ +import Sinon = require("sinon"); +import RegistrationHandler from "./RegistrationHandler"; +import { Identity } from "../../../../../../types/Identity"; +import { UserDataStore } from "../../../../storage/UserDataStore"; +import BluebirdPromise = require("bluebird"); +import ExpressMock = require("../../../../stubs/express.spec"); +import { ServerVariablesMock, ServerVariablesMockBuilder } + from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; +import Assert = require("assert"); + +describe("routes/secondfactor/totp/identity/RegistrationHandler", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + req.session = { + auth: { + userid: "user", + email: "user@example.com", + first_factor: true, + second_factor: false, + identity_check: { + userid: "user", + challenge: "totp-register" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.saveTOTPSecretStub + .returns(BluebirdPromise.resolve({})); + + res = ExpressMock.ResponseMock(); + }); + + describe("test totp registration pre validation", function () { + it("should fail if first_factor has not been passed", function () { + req.session.auth.first_factor = false; + return new RegistrationHandler(vars.logger, vars.userDataStore, + vars.totpHandler, vars.config.totp) + .preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("It should fail")); + }) + .catch(function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if userid is missing", function (done) { + req.session.auth.first_factor = false; + req.session.auth.userid = undefined; + + new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler, + vars.config.totp) + .preValidationInit(req as any) + .catch(function (err: Error) { + done(); + }); + }); + + it("should fail if email is missing", function (done) { + req.session.auth.first_factor = false; + req.session.auth.email = undefined; + + new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler, + vars.config.totp) + .preValidationInit(req as any) + .catch(function (err: Error) { + done(); + }); + }); + + it("should succeed if first factor passed, userid and email are provided", + function () { + return new RegistrationHandler(vars.logger, vars.userDataStore, + vars.totpHandler, vars.config.totp) + .preValidationInit(req as any); + }); + }); + + describe("test totp registration post validation", function () { + it("should generate a secret using userId as label and issuer defined in config", function () { + vars.config.totp = { + issuer: "issuer" + }; + return new RegistrationHandler(vars.logger, vars.userDataStore, + vars.totpHandler, vars.config.totp) + .postValidationResponse(req as any, res as any) + .then(function() { + Assert(mocks.totpHandler.generateStub.calledWithExactly("user", "issuer")); + }); + }); + }); +}); diff --git a/themes/matrix/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts b/themes/matrix/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts new file mode 100644 index 000000000..b39b6d045 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts @@ -0,0 +1,112 @@ + +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); + +import { Identity } from "../../../../../../types/Identity"; +import { IdentityValidable } from "../../../../IdentityValidable"; +import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; +import Constants = require("../constants"); +import Endpoints = require("../../../../../../../shared/api"); +import ErrorReplies = require("../../../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import FirstFactorValidator = require("../../../../FirstFactorValidator"); +import { IRequestLogger } from "../../../../logging/IRequestLogger"; +import { IUserDataStore } from "../../../../storage/IUserDataStore"; +import { ITotpHandler } from "../../../../authentication/totp/ITotpHandler"; +import { TOTPSecret } from "../../../../../../types/TOTPSecret"; +import { TotpConfiguration } from "../../../../configuration/schema/TotpConfiguration"; + + +export default class RegistrationHandler implements IdentityValidable { + private logger: IRequestLogger; + private userDataStore: IUserDataStore; + private totp: ITotpHandler; + private configuration: TotpConfiguration; + + constructor(logger: IRequestLogger, + userDataStore: IUserDataStore, + totp: ITotpHandler, configuration: TotpConfiguration) { + this.logger = logger; + this.userDataStore = userDataStore; + this.totp = totp; + this.configuration = configuration; + } + + challenge(): string { + return Constants.CHALLENGE; + } + + private retrieveIdentity(req: express.Request): BluebirdPromise { + const that = this; + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, that.logger); + const userid = authSession.userid; + const email = authSession.email; + + if (!(userid && email)) { + return reject(new Error("User ID or email is missing")); + } + + const identity = { + email: email, + userid: userid + }; + return resolve(identity); + }); + } + + preValidationInit(req: express.Request): BluebirdPromise { + const that = this; + return FirstFactorValidator.validate(req, this.logger) + .then(function () { + return that.retrieveIdentity(req); + }); + } + + preValidationResponse(req: express.Request, res: express.Response) { + res.render(PRE_VALIDATION_TEMPLATE); + } + + postValidationInit(req: express.Request) { + return FirstFactorValidator.validate(req, this.logger); + } + + postValidationResponse(req: express.Request, res: express.Response) + : BluebirdPromise { + const that = this; + let secret: TOTPSecret; + let userId: string; + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, that.logger); + userId = authSession.userid; + + if (authSession.identity_check.challenge != Constants.CHALLENGE + || !userId) + return reject(new Error("Bad challenge.")); + + resolve(); + }) + .then(function () { + secret = that.totp.generate(userId, + that.configuration.issuer); + that.logger.debug(req, "Save the TOTP secret in DB"); + return that.userDataStore.saveTOTPSecret(userId, secret); + }) + .then(function () { + AuthenticationSessionHandler.reset(req); + + res.render(Constants.TEMPLATE_NAME, { + base32_secret: secret.base32, + otpauth_url: secret.otpauth_url, + login_endpoint: Endpoints.FIRST_FACTOR_GET + }); + }) + .catch(ErrorReplies.replyWithError200(req, res, that.logger, UserMessages.OPERATION_FAILED)); + } + + mailSubject(): string { + return "Set up Authelia's one-time password"; + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts b/themes/matrix/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts new file mode 100644 index 000000000..70a20d39d --- /dev/null +++ b/themes/matrix/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts @@ -0,0 +1,76 @@ + +import BluebirdPromise = require("bluebird"); +import Sinon = require("sinon"); +import Assert = require("assert"); +import Exceptions = require("../../../../Exceptions"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import SignPost = require("./post"); +import { ServerVariables } from "../../../../ServerVariables"; + +import ExpressMock = require("../../../../stubs/express.spec"); +import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; +import { Level } from "../../../../authentication/Level"; + +describe("routes/secondfactor/totp/sign/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let authSession: AuthenticationSession; + let vars: ServerVariables; + let mocks: ServerVariablesMock; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + vars = s.variables; + mocks = s.mocks; + const app_get = Sinon.stub(); + req = { + originalUrl: "/api/totp-register", + app: {}, + body: { + token: "abc" + }, + session: {}, + query: { + redirect: "http://redirect" + } + }; + res = ExpressMock.ResponseMock(); + + const doc = { + userid: "user", + secret: { + base32: "ABCDEF" + } + }; + mocks.userDataStore.retrieveTOTPSecretStub.returns(BluebirdPromise.resolve(doc)); + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + authSession.userid = "user"; + authSession.authentication_level = Level.ONE_FACTOR; + }); + + + it("should send status code 200 when totp is valid", function () { + mocks.totpHandler.validateStub.returns(true); + return SignPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(authSession.authentication_level, Level.TWO_FACTOR); + return BluebirdPromise.resolve(); + }); + }); + + it("should send error message when totp is not valid", function () { + mocks.totpHandler.validateStub.returns(false); + return SignPost.default(vars)(req as any, res as any) + .then(function () { + Assert.notEqual(authSession.authentication_level, Level.TWO_FACTOR); + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); +}); + diff --git a/themes/matrix/server/src/lib/routes/secondfactor/totp/sign/post.ts b/themes/matrix/server/src/lib/routes/secondfactor/totp/sign/post.ts new file mode 100644 index 000000000..34a276d12 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/secondfactor/totp/sign/post.ts @@ -0,0 +1,42 @@ +import Bluebird = require("bluebird"); +import Express = require("express"); + +import { TOTPSecretDocument } from "../../../../storage/TOTPSecretDocument"; +import Endpoints = require("../../../../../../../shared/api"); +import Redirect from "../../redirect"; +import ErrorReplies = require("../../../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../../ServerVariables"; +import { Level } from "../../../../authentication/Level"; + +const UNAUTHORIZED_MESSAGE = "Unauthorized access"; + +export default function (vars: ServerVariables) { + function handler(req: Express.Request, res: Express.Response): Bluebird { + let authSession: AuthenticationSession; + const token = req.body.token; + + return new Bluebird(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + vars.logger.info(req, "Initiate TOTP validation for user \"%s\".", authSession.userid); + resolve(); + }) + .then(function () { + return vars.userDataStore.retrieveTOTPSecret(authSession.userid); + }) + .then(function (doc: TOTPSecretDocument) { + if (!vars.totpHandler.validate(token, doc.secret.base32)) + return Bluebird.reject(new Error("Invalid TOTP token.")); + + vars.logger.debug(req, "TOTP validation succeeded."); + authSession.authentication_level = Level.TWO_FACTOR; + Redirect(vars)(req, res); + return Bluebird.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + return handler; +} diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts new file mode 100644 index 000000000..7f16c0ee8 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts @@ -0,0 +1,11 @@ + +import util = require("util"); +import express = require("express"); + +function extract_app_id(req: express.Request): string { + return util.format("https://%s", req.headers.host); +} + +export = { + extract_app_id: extract_app_id +}; \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts new file mode 100644 index 000000000..a54bfbfe6 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts @@ -0,0 +1,96 @@ +import Sinon = require("sinon"); +import Assert = require("assert"); +import BluebirdPromise = require("bluebird"); + +import { Identity } from "../../../../../../types/Identity"; +import RegistrationHandler from "./RegistrationHandler"; +import ExpressMock = require("../../../../stubs/express.spec"); +import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; + +describe("routes/secondfactor/u2f/identity/RegistrationHandler", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + req.app = {}; + req.session = { + auth: { + userid: "user", + email: "user@example.com", + first_factor: true, + second_factor: false + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + + res = ExpressMock.ResponseMock(); + res.send = Sinon.spy(); + res.json = Sinon.spy(); + res.status = Sinon.spy(); + }); + + describe("test u2f registration check", test_registration_check); + + function test_registration_check() { + it("should fail if first_factor has not been passed", function () { + req.session.auth.first_factor = false; + return new RegistrationHandler(vars.logger).preValidationInit(req as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if userid is missing", function () { + req.session.auth.first_factor = false; + req.session.auth.userid = undefined; + + return new RegistrationHandler(vars.logger).preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here")); + }, + function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if email is missing", function () { + req.session.auth.first_factor = false; + req.session.auth.email = undefined; + + return new RegistrationHandler(vars.logger).preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here")); + }, + function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should succeed if first factor passed, userid and email are provided", function () { + req.session.auth.first_factor = true; + req.session.auth.email = "admin@example.com"; + req.session.auth.userid = "user"; + return new RegistrationHandler(vars.logger).preValidationInit(req as any); + }); + } +}); diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts new file mode 100644 index 000000000..bc4713c77 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts @@ -0,0 +1,73 @@ + +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import objectPath = require("object-path"); + +import { IdentityValidable } from "../../../../IdentityValidable"; +import { Identity } from "../../../../../../types/Identity"; +import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; +import FirstFactorValidator = require("../../../../FirstFactorValidator"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { IRequestLogger } from "../../../../logging/IRequestLogger"; + +const CHALLENGE = "u2f-register"; +const MAIL_SUBJECT = "Register your security key with Authelia"; + +const POST_VALIDATION_TEMPLATE_NAME = "u2f-register"; + + +export default class RegistrationHandler implements IdentityValidable { + private logger: IRequestLogger; + + constructor(logger: IRequestLogger) { + this.logger = logger; + } + + challenge(): string { + return CHALLENGE; + } + + private retrieveIdentity(req: express.Request): BluebirdPromise { + const that = this; + return new BluebirdPromise(function(resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, that.logger); + const userid = authSession.userid; + const email = authSession.email; + + if (!(userid && email)) { + return reject(new Error("User ID or email is missing")); + } + + const identity = { + email: email, + userid: userid + }; + return resolve(identity); + }); + } + + preValidationInit(req: express.Request): BluebirdPromise { + const that = this; + return FirstFactorValidator.validate(req, this.logger) + .then(function () { + return that.retrieveIdentity(req); + }); + } + + preValidationResponse(req: express.Request, res: express.Response) { + res.render(PRE_VALIDATION_TEMPLATE); + } + + postValidationInit(req: express.Request) { + return FirstFactorValidator.validate(req, this.logger); + } + + postValidationResponse(req: express.Request, res: express.Response) { + res.render(POST_VALIDATION_TEMPLATE_NAME); + } + + mailSubject(): string { + return MAIL_SUBJECT; + } +} + diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts new file mode 100644 index 000000000..de3347a21 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts @@ -0,0 +1,146 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import assert = require("assert"); +import U2FRegisterPost = require("./post"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import ExpressMock = require("../../../../stubs/express.spec"); +import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; + + +describe("routes/secondfactor/u2f/register/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + let authSession: AuthenticationSession; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.originalUrl = "/api/xxxx"; + req.app = {}; + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false, + identity_check: { + challenge: "u2f-register", + userid: "user" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + }); + + describe("test registration", test_registration); + + + function test_registration() { + it("should save u2f meta and return status code 200", function () { + const expectedStatus = { + keyHandle: "keyHandle", + publicKey: "pbk", + certificate: "cert" + }; + mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve(expectedStatus)); + + authSession.register_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { + assert.equal("user", mocks.userDataStore.saveU2FRegistrationStub.getCall(0).args[0]); + assert.equal(authSession.identity_check, undefined); + }); + }); + + it("should return error message on finishRegistration error", function () { + mocks.u2f.checkRegistrationStub.returns({ errorCode: 500 }); + + authSession.register_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function () { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); + + it("should return error message when register_request is not provided", function () { + mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); + authSession.register_request = undefined; + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function () { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); + + it("should return error message when no auth request has been initiated", function () { + mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); + authSession.register_request = undefined; + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function () { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); + + it("should return error message when identity has not been verified", function () { + authSession.identity_check = undefined; + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function () { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); + } +}); + diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/register/post.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/register/post.ts new file mode 100644 index 000000000..7296ccbe5 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/secondfactor/u2f/register/post.ts @@ -0,0 +1,64 @@ + +import { UserDataStore } from "../../../../storage/UserDataStore"; +import objectPath = require("object-path"); +import u2f_common = require("../U2FCommon"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import U2f = require("u2f"); +import { U2FRegistration } from "../../../../../../types/U2FRegistration"; +import redirect from "../../redirect"; +import ErrorReplies = require("../../../../ErrorReplies"); +import { ServerVariables } from "../../../../ServerVariables"; +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; + + +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const appid = u2f_common.extract_app_id(req); + const registrationResponse: U2f.RegistrationData = req.body; + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + const registrationRequest = authSession.register_request; + + if (!registrationRequest) { + return reject(new Error("No registration request")); + } + + if (!authSession.identity_check + || authSession.identity_check.challenge != "u2f-register") { + return reject(new Error("Bad challenge for registration request")); + } + + vars.logger.info(req, "Finishing registration"); + vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); + vars.logger.debug(req, "RegistrationResponse = %s", JSON.stringify(registrationResponse)); + + return resolve(vars.u2f.checkRegistration(registrationRequest, registrationResponse)); + }) + .then(function (u2fResult: U2f.RegistrationResult | U2f.Error): BluebirdPromise { + if (objectPath.has(u2fResult, "errorCode")) + return BluebirdPromise.reject(new Error("Error while registering.")); + + const registrationResult: U2f.RegistrationResult = u2fResult as U2f.RegistrationResult; + vars.logger.info(req, "Store registration and reply"); + vars.logger.debug(req, "RegistrationResult = %s", JSON.stringify(registrationResult)); + const registration: U2FRegistration = { + keyHandle: registrationResult.keyHandle, + publicKey: registrationResult.publicKey + }; + return vars.userDataStore.saveU2FRegistration(authSession.userid, appid, registration); + }) + .then(function () { + authSession.identity_check = undefined; + redirect(vars)(req, res); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + return handler; +} diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts new file mode 100644 index 000000000..a207c9109 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts @@ -0,0 +1,86 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import Assert = require("assert"); +import U2FRegisterRequestGet = require("./get"); +import ExpressMock = require("../../../../stubs/express.spec"); +import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; + +describe("routes/secondfactor/u2f/register_request/get", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.originalUrl = "/api/xxxx"; + req.app = {}; + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false, + identity_check: { + challenge: "u2f-register", + userid: "user" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + }); + + describe("test registration request", () => { + it("should send back the registration request and save it in the session", function () { + const expectedRequest = { + test: "abc" + }; + mocks.u2f.requestStub.returns(BluebirdPromise.resolve(expectedRequest)); + return U2FRegisterRequestGet.default(vars)(req as any, res as any) + .then(function () { + Assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); + }); + }); + + it("should return internal error on registration request", function () { + res.send = sinon.spy(); + const user_key_container = {}; + mocks.u2f.requestStub.returns(BluebirdPromise.reject("Internal error")); + return U2FRegisterRequestGet.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + }); + }); + + it("should return forbidden if identity has not been verified", function () { + req.session.auth.identity_check = undefined; + return U2FRegisterRequestGet.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(403, res.status.getCall(0).args[0]); + }); + }); + }); +}); + diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/register_request/get.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/register_request/get.ts new file mode 100644 index 000000000..f611af933 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/secondfactor/u2f/register_request/get.ts @@ -0,0 +1,43 @@ + +import { UserDataStore } from "../../../../storage/UserDataStore"; + +import objectPath = require("object-path"); +import u2f_common = require("../U2FCommon"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import U2f = require("u2f"); +import ErrorReplies = require("../../../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../../ServerVariables"; + +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const appid: string = u2f_common.extract_app_id(req); + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (!authSession.identity_check + || authSession.identity_check.challenge != "u2f-register") { + res.status(403); + res.send(); + return reject(new Error("Bad challenge.")); + } + + vars.logger.info(req, "Starting registration for appId '%s'", appid); + return resolve(vars.u2f.request(appid)); + }) + .then(function (registrationRequest: U2f.Request) { + vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); + authSession.register_request = registrationRequest; + res.json(registrationRequest); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + + return handler; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts new file mode 100644 index 000000000..9b137e66d --- /dev/null +++ b/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts @@ -0,0 +1,101 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import Assert = require("assert"); +import U2FSignPost = require("./post"); +import { ServerVariables } from "../../../../ServerVariables"; +import winston = require("winston"); + +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; +import ExpressMock = require("../../../../stubs/express.spec"); +import U2FMock = require("../../../../stubs/u2f.spec"); +import U2f = require("u2f"); +import { Level } from "../../../../authentication/Level"; + +describe("routes/secondfactor/u2f/sign/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.app = {}; + req.originalUrl = "/api/xxxx"; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req.session = { + auth: { + userid: "user", + authentication_level: Level.ONE_FACTOR, + identity_check: { + challenge: "u2f-register", + userid: "user" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const options = { + inMemoryOnly: true + }; + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + }); + + it("should return status code 204", function () { + const expectedStatus = { + keyHandle: "keyHandle", + publicKey: "pbk", + certificate: "cert" + }; + mocks.u2f.checkSignatureStub.returns(expectedStatus); + + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ + registration: { + publicKey: "PUBKEY" + } + })); + + req.session.auth.sign_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + return U2FSignPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(req.session.auth.authentication_level, Level.TWO_FACTOR); + }); + }); + + it("should return unauthorized error on registration request internal error", function () { + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ + registration: { + publicKey: "PUBKEY" + } + })); + mocks.u2f.checkSignatureStub.returns({ errorCode: 500 }); + + req.session.auth.sign_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + return U2FSignPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], + { error: "Operation failed." }); + }); + }); +}); + diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign/post.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign/post.ts new file mode 100644 index 000000000..7ee711c2c --- /dev/null +++ b/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign/post.ts @@ -0,0 +1,57 @@ + +import objectPath = require("object-path"); +import u2f_common = require("../U2FCommon"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import { UserDataStore } from "../../../../storage/UserDataStore"; +import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; +import { Winston } from "../../../../../../types/Dependencies"; +import U2f = require("u2f"); +import exceptions = require("../../../../Exceptions"); +import redirect from "../../redirect"; +import ErrorReplies = require("../../../../ErrorReplies"); +import { ServerVariables } from "../../../../ServerVariables"; +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import { Level } from "../../../../authentication/Level"; + +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const appId = u2f_common.extract_app_id(req); + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (!authSession.sign_request) { + const err = new Error("No sign request"); + ErrorReplies.replyWithError401(req, res, vars.logger)(err); + return reject(err); + } + resolve(); + }) + .then(function () { + const userid = authSession.userid; + return vars.userDataStore.retrieveU2FRegistration(userid, appId); + }) + .then(function (doc: U2FRegistrationDocument): BluebirdPromise { + const signRequest = authSession.sign_request; + const signData: U2f.SignatureData = req.body; + vars.logger.info(req, "Finish authentication"); + return BluebirdPromise.resolve(vars.u2f.checkSignature(signRequest, signData, doc.registration.publicKey)); + }) + .then(function (result: U2f.SignatureResult | U2f.Error): BluebirdPromise { + if (objectPath.has(result, "errorCode")) + return BluebirdPromise.reject(new Error("Error while signing")); + vars.logger.info(req, "Successful authentication"); + authSession.authentication_level = Level.TWO_FACTOR; + redirect(vars)(req, res); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + + return handler; +} + diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts new file mode 100644 index 000000000..dd52b27e0 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts @@ -0,0 +1,68 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import assert = require("assert"); +import U2FSignRequestGet = require("./get"); +import ExpressMock = require("../../../../stubs/express.spec"); +import { Request } from "u2f"; +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; + +import { SignMessage } from "../../../../../../../shared/SignMessage"; + +describe("routes/secondfactor/u2f/sign_request/get", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.originalUrl = "/api/xxxx"; + req.app = {}; + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false, + identity_check: { + challenge: "u2f-register", + userid: "user" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + }); + + it("should send back the sign request and save it in the session", function () { + const expectedRequest: Request = { + version: "U2F_V2", + appId: 'app', + challenge: 'challenge!' + }; + mocks.u2f.requestStub.returns(expectedRequest); + mocks.userDataStore.retrieveU2FRegistrationStub + .returns(BluebirdPromise.resolve({ + registration: { + keyHandle: "KeyHandle" + } + })); + + return U2FSignRequestGet.default(vars)(req as any, res as any) + .then(() => { + assert.deepEqual(expectedRequest, req.session.auth.sign_request); + assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); + }); + }); +}); + diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts new file mode 100644 index 000000000..9e93dde06 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts @@ -0,0 +1,42 @@ + +import u2f_common = require("../../../secondfactor/u2f/U2FCommon"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; +import exceptions = require("../../../../Exceptions"); +import ErrorReplies = require("../../../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../../ServerVariables"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; + +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const appId = u2f_common.extract_app_id(req); + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + resolve(); + }) + .then(function () { + return vars.userDataStore.retrieveU2FRegistration(authSession.userid, appId); + }) + .then(function (doc: U2FRegistrationDocument): BluebirdPromise { + if (!doc) + return BluebirdPromise.reject(new exceptions.AccessDeniedError("No U2F registration document found.")); + + const appId: string = u2f_common.extract_app_id(req); + vars.logger.info(req, "Start authentication of app '%s'", appId); + vars.logger.debug(req, "AppId = %s, keyHandle = %s", appId, JSON.stringify(doc.registration.keyHandle)); + + const request = vars.u2f.request(appId, doc.registration.keyHandle); + authSession.sign_request = request; + res.json(request); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + return handler; +} diff --git a/themes/matrix/server/src/lib/routes/verify/access_control.ts b/themes/matrix/server/src/lib/routes/verify/access_control.ts new file mode 100644 index 000000000..136239aeb --- /dev/null +++ b/themes/matrix/server/src/lib/routes/verify/access_control.ts @@ -0,0 +1,51 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import Util = require("util"); + +import Exceptions = require("../../Exceptions"); + +import { Level as AuthorizationLevel } from "../../authorization/Level"; +import { Level as AuthenticationLevel } from "../../authentication/Level"; +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { ServerVariables } from "../../ServerVariables"; + +function isAuthorized( + authorization: AuthorizationLevel, + authentication: AuthenticationLevel): boolean { + + if (authorization == AuthorizationLevel.BYPASS) { + return true; + } else if (authorization == AuthorizationLevel.ONE_FACTOR && + authentication >= AuthenticationLevel.ONE_FACTOR) { + return true; + } else if (authorization == AuthorizationLevel.TWO_FACTOR && + authentication >= AuthenticationLevel.TWO_FACTOR) { + return true; + } + return false; +} + +export default function ( + req: Express.Request, + vars: ServerVariables, + domain: string, resource: string, + user: string, groups: string[], + authenticationLevel: AuthenticationLevel) { + + return new BluebirdPromise(function (resolve, reject) { + const authorizationLevel = vars.authorizer + .authorization({domain, resource}, {user, groups}); + + if (!isAuthorized(authorizationLevel, authenticationLevel)) { + if (authorizationLevel == AuthorizationLevel.DENY) { + reject(new Exceptions.NotAuthorizedError( + Util.format("User %s is not authorized to access %s%s", user, domain, resource))); + return; + } + reject(new Exceptions.NotAuthenticatedError(Util.format( + "User '%s' is not sufficiently authorized to access %s%s.", user, domain, resource))); + return; + } + resolve(); + }); +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/verify/get.spec.ts b/themes/matrix/server/src/lib/routes/verify/get.spec.ts new file mode 100644 index 000000000..67cf19fb6 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/verify/get.spec.ts @@ -0,0 +1,320 @@ + +import Assert = require("assert"); +import BluebirdPromise = require("bluebird"); +import Express = require("express"); +import Sinon = require("sinon"); +import winston = require("winston"); + +import VerifyGet = require("./get"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../types/AuthenticationSession"; +import ExpressMock = require("../../stubs/express.spec"); +import { ServerVariables } from "../../ServerVariables"; +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../ServerVariablesMockBuilder.spec"; +import { Level } from "../../authentication/Level"; +import { Level as AuthorizationLevel } from "../../authorization/Level"; + +describe("routes/verify/get", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + let authSession: AuthenticationSession; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + req.originalUrl = "/api/xxxx"; + req.query = { + redirect: "undefined" + }; + AuthenticationSessionHandler.reset(req as any); + req.headers["x-original-url"] = "https://secret.example.com/"; + const s = ServerVariablesMockBuilder.build(false); + mocks = s.mocks; + vars = s.variables; + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + }); + + describe("with session cookie", function () { + it("should be already authenticated", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + authSession.authentication_level = Level.TWO_FACTOR; + authSession.userid = "myuser"; + authSession.groups = ["mygroup", "othergroup"]; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser"); + Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); + Assert.equal(204, res.status.getCall(0).args[0]); + }); + }); + + function test_session(_authSession: AuthenticationSession, status_code: number) { + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert.equal(status_code, res.status.getCall(0).args[0]); + }); + } + + function test_non_authenticated_401(authSession: AuthenticationSession) { + return test_session(authSession, 401); + } + + function test_unauthorized_403(authSession: AuthenticationSession) { + return test_session(authSession, 403); + } + + function test_authorized(authSession: AuthenticationSession) { + return test_session(authSession, 204); + } + + describe("given user tries to access a 2-factor endpoint", function () { + before(function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + }); + + describe("given different cases of session", function () { + it("should not be authenticated when second factor is missing", function () { + return test_non_authenticated_401({ + keep_me_logged_in: false, + userid: "user", + authentication_level: Level.ONE_FACTOR, + email: undefined, + groups: [], + last_activity_datetime: new Date().getTime() + }); + }); + + it("should not be authenticated when userid is missing", function () { + return test_non_authenticated_401({ + keep_me_logged_in: false, + userid: undefined, + authentication_level: Level.TWO_FACTOR, + email: undefined, + groups: [], + last_activity_datetime: new Date().getTime() + }); + }); + + it("should not be authenticated when level is insufficient", function () { + return test_non_authenticated_401({ + keep_me_logged_in: false, + userid: "user", + authentication_level: Level.NOT_AUTHENTICATED, + email: undefined, + groups: [], + last_activity_datetime: new Date().getTime() + }); + }); + + it("should not be authenticated when session has not be initiated", function () { + return test_non_authenticated_401(undefined); + }); + + it("should not be authenticated when domain is not allowed for user", function () { + authSession.authentication_level = Level.TWO_FACTOR; + authSession.userid = "myuser"; + req.headers["x-original-url"] = "https://test.example.com/"; + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.DENY); + + return test_unauthorized_403({ + keep_me_logged_in: false, + authentication_level: Level.TWO_FACTOR, + userid: "user", + groups: ["group1", "group2"], + email: undefined, + last_activity_datetime: new Date().getTime() + }); + }); + }); + }); + + describe("given user tries to access a single factor endpoint", function () { + beforeEach(function () { + req.headers["x-original-url"] = "https://redirect.url/"; + }); + + it("should be authenticated when first factor is validated", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); + authSession.authentication_level = Level.ONE_FACTOR; + authSession.userid = "user1"; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWith(204)); + Assert(res.send.calledOnce); + }); + }); + + it("should be rejected with 401 when not authenticated", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); + authSession.authentication_level = Level.NOT_AUTHENTICATED; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWith(401)); + }); + }); + }); + + describe("inactivity period", function () { + it("should update last inactivity period on requests on /api/verify", function () { + mocks.config.session.inactivity = 200000; + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + const currentTime = new Date().getTime() - 1000; + AuthenticationSessionHandler.reset(req as any); + authSession.authentication_level = Level.TWO_FACTOR; + authSession.userid = "myuser"; + authSession.groups = ["mygroup", "othergroup"]; + authSession.last_activity_datetime = currentTime; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + return AuthenticationSessionHandler.get(req as any, vars.logger); + }) + .then(function (authSession) { + Assert(authSession.last_activity_datetime > currentTime); + }); + }); + + it("should reset session when max inactivity period has been reached", function () { + mocks.config.session.inactivity = 1; + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + const currentTime = new Date().getTime() - 1000; + AuthenticationSessionHandler.reset(req as any); + authSession.authentication_level = Level.TWO_FACTOR; + authSession.userid = "myuser"; + authSession.groups = ["mygroup", "othergroup"]; + authSession.last_activity_datetime = currentTime; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + return AuthenticationSessionHandler.get(req as any, vars.logger); + }) + .then(function (authSession) { + Assert.equal(authSession.authentication_level, Level.NOT_AUTHENTICATED); + Assert.equal(authSession.userid, undefined); + }); + }); + }); + }); + + describe("response type 401 | 302", function() { + it("should return error code 401", function() { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( + "Invalid credentials")); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should redirect to provided redirection url", function() { + const REDIRECT_URL = "http://redirection_url.com"; + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( + "Invalid credentials")); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + req.query["rd"] = REDIRECT_URL; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.redirect.calledWithExactly(REDIRECT_URL)); + }); + }); + }); + + describe("with basic auth", function () { + it("should authenticate correctly", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.returns({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "john"); + Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); + Assert.equal(204, res.status.getCall(0).args[0]); + }); + }); + + it("should fail when endpoint is protected by two factors", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.config.access_control.rules = [{ + domain: "secret.example.com", + policy: "two_factor" + }]; + mocks.usersDatabase.checkUserPasswordStub.resolves({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should fail when base64 token is not valid", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.resolves({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic i_m*not_a_base64*token"; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should fail when base64 token has not format user:psswd", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.resolves({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzOmJhZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should fail when bad user password is provided", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( + "Invalid credentials")); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should fail when resource is restricted", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.resolves({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + }); +}); + diff --git a/themes/matrix/server/src/lib/routes/verify/get.ts b/themes/matrix/server/src/lib/routes/verify/get.ts new file mode 100644 index 000000000..f73861696 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/verify/get.ts @@ -0,0 +1,91 @@ +import BluebirdPromise = require("bluebird"); +import Express = require("express"); +import Exceptions = require("../../Exceptions"); +import ErrorReplies = require("../../ErrorReplies"); +import { ServerVariables } from "../../ServerVariables"; +import GetWithSessionCookieMethod from "./get_session_cookie"; +import GetWithBasicAuthMethod from "./get_basic_auth"; +import Constants = require("../../../../../shared/constants"); +import ObjectPath = require("object-path"); + +import { AuthenticationSessionHandler } + from "../../AuthenticationSessionHandler"; +import { AuthenticationSession } + from "../../../../types/AuthenticationSession"; + +const REMOTE_USER = "Remote-User"; +const REMOTE_GROUPS = "Remote-Groups"; + + +function verifyWithSelectedMethod(req: Express.Request, res: Express.Response, + vars: ServerVariables, authSession: AuthenticationSession) + : () => BluebirdPromise<{ username: string, groups: string[] }> { + return function () { + const authorization: string = "" + req.headers["proxy-authorization"]; + if (authorization && authorization.startsWith("Basic ")) + return GetWithBasicAuthMethod(req, res, vars, authorization); + + return GetWithSessionCookieMethod(req, res, vars, authSession); + }; +} + +function setRedirectHeader(req: Express.Request, res: Express.Response) { + return function () { + const originalUrl = ObjectPath.get( + req, "headers.x-original-url"); + res.set("Redirect", originalUrl); + return BluebirdPromise.resolve(); + }; +} + +function setUserAndGroupsHeaders(res: Express.Response) { + return function (u: { username: string, groups: string[] }) { + res.setHeader(REMOTE_USER, u.username); + res.setHeader(REMOTE_GROUPS, u.groups.join(",")); + return BluebirdPromise.resolve(); + }; +} + +function replyWith200(res: Express.Response) { + return function () { + res.status(204); + res.send(); + }; +} + +function getRedirectParam(req: Express.Request) { + return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; +} + +export default function (vars: ServerVariables) { + return function (req: Express.Request, res: Express.Response) + : BluebirdPromise { + let authSession: AuthenticationSession; + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + resolve(); + }) + .then(setRedirectHeader(req, res)) + .then(verifyWithSelectedMethod(req, res, vars, authSession)) + .then(setUserAndGroupsHeaders(res)) + .then(replyWith200(res)) + // The user is authenticated but has restricted access -> 403 + .catch(Exceptions.NotAuthorizedError, + ErrorReplies.replyWithError403(req, res, vars.logger)) + .catch(Exceptions.NotAuthenticatedError, + ErrorReplies.replyWithError401(req, res, vars.logger)) + // The user is not yet authenticated -> 401 + .catch((err) => { + const redirectUrl = getRedirectParam(req); + if (redirectUrl) { + ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err); + } + else { + ErrorReplies.replyWithError401(req, res, vars.logger)(err); + } + }); + }; +} + diff --git a/themes/matrix/server/src/lib/routes/verify/get_basic_auth.ts b/themes/matrix/server/src/lib/routes/verify/get_basic_auth.ts new file mode 100644 index 000000000..af23c76c9 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/verify/get_basic_auth.ts @@ -0,0 +1,55 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import ObjectPath = require("object-path"); +import { ServerVariables } from "../../ServerVariables"; +import { AuthenticationSession } + from "../../../../types/AuthenticationSession"; +import AccessControl from "./access_control"; +import { URLDecomposer } from "../../utils/URLDecomposer"; +import { Level } from "../../authentication/Level"; + +export default function (req: Express.Request, res: Express.Response, + vars: ServerVariables, authorizationHeader: string) + : BluebirdPromise<{ username: string, groups: string[] }> { + let username: string; + const uri = ObjectPath.get(req, "headers.x-original-url"); + const urlDecomposition = URLDecomposer.fromUrl(uri); + + return BluebirdPromise.resolve() + .then(() => { + const base64Re = new RegExp("^Basic ((?:[A-Za-z0-9+/]{4})*" + + "(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)$"); + const isTokenValidBase64 = base64Re.test(authorizationHeader); + + if (!isTokenValidBase64) { + return BluebirdPromise.reject(new Error("No valid base64 token found in the header")); + } + + const tokenMatches = authorizationHeader.match(base64Re); + const base64Token = tokenMatches[1]; + const decodedToken = Buffer.from(base64Token, "base64").toString(); + const splittedToken = decodedToken.split(":"); + + if (splittedToken.length != 2) { + return BluebirdPromise.reject(new Error( + "The authorization token is invalid. Expecting 'userid:password'")); + } + + username = splittedToken[0]; + const password = splittedToken[1]; + return vars.usersDatabase.checkUserPassword(username, password); + }) + .then(function (groupsAndEmails) { + return AccessControl(req, vars, urlDecomposition.domain, urlDecomposition.path, + username, groupsAndEmails.groups, Level.ONE_FACTOR) + .then(() => BluebirdPromise.resolve({ + username: username, + groups: groupsAndEmails.groups + })); + }) + .catch(function (err: Error) { + return BluebirdPromise.reject( + new Error("Unable to authenticate the user with basic auth. Cause: " + + err.message)); + }); +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/verify/get_session_cookie.ts b/themes/matrix/server/src/lib/routes/verify/get_session_cookie.ts new file mode 100644 index 000000000..070344812 --- /dev/null +++ b/themes/matrix/server/src/lib/routes/verify/get_session_cookie.ts @@ -0,0 +1,78 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import Util = require("util"); +import ObjectPath = require("object-path"); + +import Exceptions = require("../../Exceptions"); +import { Configuration } from "../../configuration/schema/Configuration"; +import { ServerVariables } from "../../ServerVariables"; +import { IRequestLogger } from "../../logging/IRequestLogger"; +import { AuthenticationSession } + from "../../../../types/AuthenticationSession"; +import { AuthenticationSessionHandler } + from "../../AuthenticationSessionHandler"; +import AccessControl from "./access_control"; +import { URLDecomposer } from "../../utils/URLDecomposer"; + +function verify_inactivity(req: Express.Request, + authSession: AuthenticationSession, + configuration: Configuration, logger: IRequestLogger) + : BluebirdPromise { + + // If inactivity is not specified, then inactivity timeout does not apply + if (!configuration.session.inactivity || authSession.keep_me_logged_in) { + return BluebirdPromise.resolve(); + } + + const lastActivityTime = authSession.last_activity_datetime; + const currentTime = new Date().getTime(); + authSession.last_activity_datetime = currentTime; + + const inactivityPeriodMs = currentTime - lastActivityTime; + logger.debug(req, "Inactivity period was %s s and max period was %s.", + inactivityPeriodMs / 1000, configuration.session.inactivity / 1000); + if (inactivityPeriodMs < configuration.session.inactivity) { + return BluebirdPromise.resolve(); + } + + logger.debug(req, "Session has been reset after too long inactivity period."); + AuthenticationSessionHandler.reset(req); + return BluebirdPromise.reject(new Error("Inactivity period exceeded.")); +} + +export default function (req: Express.Request, res: Express.Response, + vars: ServerVariables, authSession: AuthenticationSession) + : BluebirdPromise<{ username: string, groups: string[] }> { + + return BluebirdPromise.resolve() + .then(() => { + const username = authSession.userid; + const groups = authSession.groups; + + if (!authSession.userid) { + return BluebirdPromise.reject(new Exceptions.AccessDeniedError( + "userid is missing")); + } + + const originalUrl = ObjectPath.get( + req, "headers.x-original-url"); + const originalUri = + ObjectPath.get(req, "headers.x-original-uri"); + + const d = URLDecomposer.fromUrl(originalUrl); + vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", d.domain, + d.path, username, groups.join(",")); + return AccessControl(req, vars, d.domain, d.path, username, groups, + authSession.authentication_level); + }) + .then(() => { + return verify_inactivity(req, authSession, + vars.config, vars.logger); + }) + .then(() => { + return BluebirdPromise.resolve({ + username: authSession.userid, + groups: authSession.groups + }); + }); +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/AuthenticationTraceDocument.d.ts b/themes/matrix/server/src/lib/storage/AuthenticationTraceDocument.d.ts new file mode 100644 index 000000000..69818c055 --- /dev/null +++ b/themes/matrix/server/src/lib/storage/AuthenticationTraceDocument.d.ts @@ -0,0 +1,6 @@ + +export interface AuthenticationTraceDocument { + userId: string; + date: Date; + isAuthenticationSuccessful: boolean; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/CollectionFactoryFactory.ts b/themes/matrix/server/src/lib/storage/CollectionFactoryFactory.ts new file mode 100644 index 000000000..92b29abfa --- /dev/null +++ b/themes/matrix/server/src/lib/storage/CollectionFactoryFactory.ts @@ -0,0 +1,15 @@ +import { ICollectionFactory } from "./ICollectionFactory"; +import { NedbCollectionFactory } from "./nedb/NedbCollectionFactory"; +import { MongoCollectionFactory } from "./mongo/MongoCollectionFactory"; +import { IMongoClient } from "../connectors/mongo/IMongoClient"; + + +export class CollectionFactoryFactory { + static createNedb(options: Nedb.DataStoreOptions): ICollectionFactory { + return new NedbCollectionFactory(options); + } + + static createMongo(client: IMongoClient): ICollectionFactory { + return new MongoCollectionFactory(client); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/CollectionFactoryStub.spec.ts b/themes/matrix/server/src/lib/storage/CollectionFactoryStub.spec.ts new file mode 100644 index 000000000..17f8bb021 --- /dev/null +++ b/themes/matrix/server/src/lib/storage/CollectionFactoryStub.spec.ts @@ -0,0 +1,16 @@ +import BluebirdPromise = require("bluebird"); +import Sinon = require("sinon"); +import { ICollection } from "./ICollection"; +import { ICollectionFactory } from "./ICollectionFactory"; + +export class CollectionFactoryStub implements ICollectionFactory { + buildStub: Sinon.SinonStub; + + constructor() { + this.buildStub = Sinon.stub(); + } + + build(collectionName: string): ICollection { + return this.buildStub(collectionName); + } +} diff --git a/themes/matrix/server/src/lib/storage/CollectionStub.spec.ts b/themes/matrix/server/src/lib/storage/CollectionStub.spec.ts new file mode 100644 index 000000000..42895d672 --- /dev/null +++ b/themes/matrix/server/src/lib/storage/CollectionStub.spec.ts @@ -0,0 +1,39 @@ +import BluebirdPromise = require("bluebird"); +import Sinon = require("sinon"); +import { ICollection } from "./ICollection"; + +export class CollectionStub implements ICollection { + findStub: Sinon.SinonStub; + findOneStub: Sinon.SinonStub; + updateStub: Sinon.SinonStub; + removeStub: Sinon.SinonStub; + insertStub: Sinon.SinonStub; + + constructor() { + this.findStub = Sinon.stub(); + this.findOneStub = Sinon.stub(); + this.updateStub = Sinon.stub(); + this.removeStub = Sinon.stub(); + this.insertStub = Sinon.stub(); + } + + find(filter: any, sortKeys: any, count: number): BluebirdPromise { + return this.findStub(filter, sortKeys, count); + } + + findOne(filter: any): BluebirdPromise { + return this.findOneStub(filter); + } + + update(filter: any, document: any, options: any): BluebirdPromise { + return this.updateStub(filter, document, options); + } + + remove(filter: any): BluebirdPromise { + return this.removeStub(filter); + } + + insert(document: any): BluebirdPromise { + return this.insertStub(document); + } +} diff --git a/themes/matrix/server/src/lib/storage/ICollection.d.ts b/themes/matrix/server/src/lib/storage/ICollection.d.ts new file mode 100644 index 000000000..caa6c2a87 --- /dev/null +++ b/themes/matrix/server/src/lib/storage/ICollection.d.ts @@ -0,0 +1,11 @@ +/* istanbul ignore next */ +import BluebirdPromise = require("bluebird"); + +/* istanbul ignore next */ +export interface ICollection { + find(query: any, sortKeys: any, count: number): BluebirdPromise; + findOne(query: any): BluebirdPromise; + update(query: any, updateQuery: any, options?: any): BluebirdPromise; + remove(query: any): BluebirdPromise; + insert(document: any): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/ICollectionFactory.d.ts b/themes/matrix/server/src/lib/storage/ICollectionFactory.d.ts new file mode 100644 index 000000000..39eb42c77 --- /dev/null +++ b/themes/matrix/server/src/lib/storage/ICollectionFactory.d.ts @@ -0,0 +1,6 @@ + +import { ICollection } from "./ICollection"; + +export interface ICollectionFactory { + build(collectionName: string): ICollection; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/IUserDataStore.d.ts b/themes/matrix/server/src/lib/storage/IUserDataStore.d.ts new file mode 100644 index 000000000..81df482aa --- /dev/null +++ b/themes/matrix/server/src/lib/storage/IUserDataStore.d.ts @@ -0,0 +1,21 @@ +import BluebirdPromise = require("bluebird"); +import { TOTPSecretDocument } from "./TOTPSecretDocument"; +import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; +import { U2FRegistration } from "../../../types/U2FRegistration"; +import { TOTPSecret } from "../../../types/TOTPSecret"; +import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; +import { IdentityValidationDocument } from "./IdentityValidationDocument"; + +export interface IUserDataStore { + saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise; + retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise; + + saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; + retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise; + + produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise; + consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise; + + saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise; + retrieveTOTPSecret(userId: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/IdentityValidationDocument.d.ts b/themes/matrix/server/src/lib/storage/IdentityValidationDocument.d.ts new file mode 100644 index 000000000..e7fd7b3f4 --- /dev/null +++ b/themes/matrix/server/src/lib/storage/IdentityValidationDocument.d.ts @@ -0,0 +1,7 @@ + +export interface IdentityValidationDocument { + userId: string; + token: string; + challenge: string; + maxDate: Date; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/TOTPSecretDocument.d.ts b/themes/matrix/server/src/lib/storage/TOTPSecretDocument.d.ts new file mode 100644 index 000000000..a6c0bf9e6 --- /dev/null +++ b/themes/matrix/server/src/lib/storage/TOTPSecretDocument.d.ts @@ -0,0 +1,6 @@ +import { TOTPSecret } from "../../../types/TOTPSecret"; + +export interface TOTPSecretDocument { + userid: string; + secret: TOTPSecret; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/U2FRegistrationDocument.d.ts b/themes/matrix/server/src/lib/storage/U2FRegistrationDocument.d.ts new file mode 100644 index 000000000..efec6cb1d --- /dev/null +++ b/themes/matrix/server/src/lib/storage/U2FRegistrationDocument.d.ts @@ -0,0 +1,8 @@ + +import { U2FRegistration } from "../../../types/U2FRegistration"; + +export interface U2FRegistrationDocument { + userId: string; + appId: string; + registration: U2FRegistration; +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/UserDataStore.spec.ts b/themes/matrix/server/src/lib/storage/UserDataStore.spec.ts new file mode 100644 index 000000000..66fb85461 --- /dev/null +++ b/themes/matrix/server/src/lib/storage/UserDataStore.spec.ts @@ -0,0 +1,264 @@ + +import * as Assert from "assert"; +import * as Sinon from "sinon"; +import * as MockDate from "mockdate"; +import BluebirdPromise = require("bluebird"); + +import { UserDataStore } from "./UserDataStore"; +import { TOTPSecret } from "../../../types/TOTPSecret"; +import { U2FRegistration } from "../../../types/U2FRegistration"; +import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; +import { CollectionStub } from "./CollectionStub.spec"; +import { CollectionFactoryStub } from "./CollectionFactoryStub.spec"; + +describe("storage/UserDataStore", function () { + let factory: CollectionFactoryStub; + let collection: CollectionStub; + let userId: string; + let appId: string; + let totpSecret: TOTPSecret; + let u2fRegistration: U2FRegistration; + + beforeEach(function () { + factory = new CollectionFactoryStub(); + collection = new CollectionStub(); + + userId = "user"; + appId = "https://myappId"; + + totpSecret = { + ascii: "abc", + base32: "ABCDKZLEFZGREJK", + otpauth_url: "totp://test", + google_auth_qr: "dummy", + hex: "dummy", + qr_code_ascii: "dummy", + qr_code_base32: "dummy", + qr_code_hex: "dummy" + }; + + u2fRegistration = { + keyHandle: "KEY_HANDLE", + publicKey: "publickey" + }; + }); + + it("should correctly creates collections", function () { + new UserDataStore(factory); + + Assert.equal(4, factory.buildStub.callCount); + Assert(factory.buildStub.calledWith("authentication_traces")); + Assert(factory.buildStub.calledWith("identity_validation_tokens")); + Assert(factory.buildStub.calledWith("u2f_registrations")); + Assert(factory.buildStub.calledWith("totp_secrets")); + }); + + describe("TOTP secrets collection", function () { + it("should save a totp secret", function () { + factory.buildStub.returns(collection); + collection.updateStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.saveTOTPSecret(userId, totpSecret) + .then(function (doc) { + Assert(collection.updateStub.calledOnce); + Assert(collection.updateStub.calledWith({ userId: userId }, { + userId: userId, + secret: totpSecret + }, { upsert: true })); + return BluebirdPromise.resolve(); + }); + }); + + it("should retrieve a totp secret", function () { + factory.buildStub.returns(collection); + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.retrieveTOTPSecret(userId) + .then(function (doc) { + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ userId: userId })); + return BluebirdPromise.resolve(); + }); + }); + }); + + describe("U2F secrets collection", function () { + it("should save a U2F secret", function () { + factory.buildStub.returns(collection); + collection.updateStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.saveU2FRegistration(userId, appId, u2fRegistration) + .then(function (doc) { + Assert(collection.updateStub.calledOnce); + Assert(collection.updateStub.calledWith({ + userId: userId, + appId: appId + }, { + userId: userId, + appId: appId, + registration: u2fRegistration + }, { upsert: true })); + return BluebirdPromise.resolve(); + }); + }); + + it("should retrieve a U2F secret", function () { + factory.buildStub.returns(collection); + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.retrieveU2FRegistration(userId, appId) + .then(function (doc) { + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ + userId: userId, + appId: appId + })); + return BluebirdPromise.resolve(); + }); + }); + }); + + + describe("Regulator traces collection", function () { + it("should save a trace", function () { + factory.buildStub.returns(collection); + collection.insertStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.saveAuthenticationTrace(userId, true) + .then(function (doc) { + Assert(collection.insertStub.calledOnce); + Assert(collection.insertStub.calledWith({ + userId: userId, + date: Sinon.match.date, + isAuthenticationSuccessful: true + })); + return BluebirdPromise.resolve(); + }); + }); + + function should_retrieve_latest_authentication_traces(count: number) { + factory.buildStub.returns(collection); + collection.findStub.withArgs().returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.retrieveLatestAuthenticationTraces(userId, count) + .then(function (doc: AuthenticationTraceDocument[]) { + Assert(collection.findStub.calledOnce); + Assert(collection.findStub.calledWith({ + userId: userId, + }, { date: -1 }, count)); + return BluebirdPromise.resolve(); + }); + } + + it("should retrieve 3 latest failed authentication traces", function () { + should_retrieve_latest_authentication_traces(3); + }); + }); + + + describe("Identity validation collection", function () { + it("should save a identity validation token", function () { + factory.buildStub.returns(collection); + collection.insertStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + const maxAge = 400; + const token = "TOKEN"; + const challenge = "CHALLENGE"; + + return dataStore.produceIdentityValidationToken(userId, token, challenge, maxAge) + .then(function (doc) { + Assert(collection.insertStub.calledOnce); + Assert(collection.insertStub.calledWith({ + userId: userId, + token: token, + challenge: challenge, + maxDate: Sinon.match.date + })); + return BluebirdPromise.resolve(); + }); + }); + + it("should consume an identity token successfully", function () { + factory.buildStub.returns(collection); + + MockDate.set(100); + + const token = "TOKEN"; + const challenge = "CHALLENGE"; + + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ + userId: "USER", + token: token, + challenge: challenge, + maxDate: new Date() + })); + collection.removeStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + MockDate.set(80); + + return dataStore.consumeIdentityValidationToken(token, challenge) + .then(function (doc) { + MockDate.reset(); + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ + token: token, + challenge: challenge + })); + + Assert(collection.removeStub.calledOnce); + Assert(collection.removeStub.calledWith({ + token: token, + challenge: challenge + })); + return BluebirdPromise.resolve(); + }); + }); + + it("should consume an expired identity token", function () { + factory.buildStub.returns(collection); + + MockDate.set(0); + + const token = "TOKEN"; + const challenge = "CHALLENGE"; + + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ + userId: "USER", + token: token, + challenge: challenge, + maxDate: new Date() + })); + + const dataStore = new UserDataStore(factory); + + MockDate.set(80000); + + return dataStore.consumeIdentityValidationToken(token, challenge) + .then(function () { return BluebirdPromise.reject(new Error("should not be here")); }) + .catch(function () { + MockDate.reset(); + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ + token: token, + challenge: challenge + })); + return BluebirdPromise.resolve(); + }); + }); + }); +}); diff --git a/themes/matrix/server/src/lib/storage/UserDataStore.ts b/themes/matrix/server/src/lib/storage/UserDataStore.ts new file mode 100644 index 000000000..27b0cddbd --- /dev/null +++ b/themes/matrix/server/src/lib/storage/UserDataStore.ts @@ -0,0 +1,143 @@ +import * as BluebirdPromise from "bluebird"; +import * as path from "path"; +import { IUserDataStore } from "./IUserDataStore"; +import { ICollection } from "./ICollection"; +import { ICollectionFactory } from "./ICollectionFactory"; +import { TOTPSecretDocument } from "./TOTPSecretDocument"; +import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; +import { U2FRegistration } from "../../../types/U2FRegistration"; +import { TOTPSecret } from "../../../types/TOTPSecret"; +import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; +import { IdentityValidationDocument } from "./IdentityValidationDocument"; + +// Constants + +const IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME = "identity_validation_tokens"; +const AUTHENTICATION_TRACES_COLLECTION_NAME = "authentication_traces"; + +const U2F_REGISTRATIONS_COLLECTION_NAME = "u2f_registrations"; +const TOTP_SECRETS_COLLECTION_NAME = "totp_secrets"; + + +export interface U2FRegistrationKey { + userId: string; + appId: string; +} + +// Source + +export class UserDataStore implements IUserDataStore { + private u2fSecretCollection: ICollection; + private identityCheckTokensCollection: ICollection; + private authenticationTracesCollection: ICollection; + private totpSecretCollection: ICollection; + + private collectionFactory: ICollectionFactory; + + constructor(collectionFactory: ICollectionFactory) { + this.collectionFactory = collectionFactory; + + this.u2fSecretCollection = this.collectionFactory.build(U2F_REGISTRATIONS_COLLECTION_NAME); + this.identityCheckTokensCollection = this.collectionFactory.build(IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME); + this.authenticationTracesCollection = this.collectionFactory.build(AUTHENTICATION_TRACES_COLLECTION_NAME); + this.totpSecretCollection = this.collectionFactory.build(TOTP_SECRETS_COLLECTION_NAME); + } + + saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { + const newDocument: U2FRegistrationDocument = { + userId: userId, + appId: appId, + registration: registration + }; + + const filter: U2FRegistrationKey = { + userId: userId, + appId: appId + }; + + return this.u2fSecretCollection.update(filter, newDocument, { upsert: true }); + } + + retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { + const filter: U2FRegistrationKey = { + userId: userId, + appId: appId + }; + return this.u2fSecretCollection.findOne(filter); + } + + saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { + const newDocument: AuthenticationTraceDocument = { + userId: userId, + date: new Date(), + isAuthenticationSuccessful: isAuthenticationSuccessful, + }; + + return this.authenticationTracesCollection.insert(newDocument); + } + + retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { + const q = { + userId: userId + }; + + return this.authenticationTracesCollection.find(q, { date: -1 }, count); + } + + produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { + const newDocument: IdentityValidationDocument = { + userId: userId, + token: token, + challenge: challenge, + maxDate: new Date(new Date().getTime() + maxAge) + }; + + return this.identityCheckTokensCollection.insert(newDocument); + } + + consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { + const that = this; + const filter = { + token: token, + challenge: challenge + }; + + let identityValidationDocument: IdentityValidationDocument; + + return this.identityCheckTokensCollection.findOne(filter) + .then(function (doc: IdentityValidationDocument) { + if (!doc) { + return BluebirdPromise.reject(new Error("Registration token does not exist")); + } + + identityValidationDocument = doc; + const current_date = new Date(); + if (current_date > doc.maxDate) + return BluebirdPromise.reject(new Error("Registration token is not valid anymore")); + + return that.identityCheckTokensCollection.remove(filter); + }) + .then(() => { + return BluebirdPromise.resolve(identityValidationDocument); + }); + } + + saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { + const doc = { + userId: userId, + secret: secret + }; + + const filter = { + userId: userId + }; + return this.totpSecretCollection.update(filter, doc, { upsert: true }); + } + + retrieveTOTPSecret(userId: string): BluebirdPromise { + const filter = { + userId: userId + }; + return this.totpSecretCollection.findOne(filter); + } +} diff --git a/themes/matrix/server/src/lib/storage/UserDataStoreStub.spec.ts b/themes/matrix/server/src/lib/storage/UserDataStoreStub.spec.ts new file mode 100644 index 000000000..5ea27a2de --- /dev/null +++ b/themes/matrix/server/src/lib/storage/UserDataStoreStub.spec.ts @@ -0,0 +1,64 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); + +import { TOTPSecretDocument } from "./TOTPSecretDocument"; +import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; +import { U2FRegistration } from "../../../types/U2FRegistration"; +import { TOTPSecret } from "../../../types/TOTPSecret"; +import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; +import { IdentityValidationDocument } from "./IdentityValidationDocument"; +import { IUserDataStore } from "./IUserDataStore"; + +export class UserDataStoreStub implements IUserDataStore { + saveU2FRegistrationStub: Sinon.SinonStub; + retrieveU2FRegistrationStub: Sinon.SinonStub; + saveAuthenticationTraceStub: Sinon.SinonStub; + retrieveLatestAuthenticationTracesStub: Sinon.SinonStub; + produceIdentityValidationTokenStub: Sinon.SinonStub; + consumeIdentityValidationTokenStub: Sinon.SinonStub; + saveTOTPSecretStub: Sinon.SinonStub; + retrieveTOTPSecretStub: Sinon.SinonStub; + + constructor() { + this.saveU2FRegistrationStub = Sinon.stub(); + this.retrieveU2FRegistrationStub = Sinon.stub(); + this.saveAuthenticationTraceStub = Sinon.stub(); + this.retrieveLatestAuthenticationTracesStub = Sinon.stub(); + this.produceIdentityValidationTokenStub = Sinon.stub(); + this.consumeIdentityValidationTokenStub = Sinon.stub(); + this.saveTOTPSecretStub = Sinon.stub(); + this.retrieveTOTPSecretStub = Sinon.stub(); + } + + saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { + return this.saveU2FRegistrationStub(userId, appId, registration); + } + + retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { + return this.retrieveU2FRegistrationStub(userId, appId); + } + + saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { + return this.saveAuthenticationTraceStub(userId, isAuthenticationSuccessful); + } + + retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { + return this.retrieveLatestAuthenticationTracesStub(userId, count); + } + + produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { + return this.produceIdentityValidationTokenStub(userId, token, challenge, maxAge); + } + + consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { + return this.consumeIdentityValidationTokenStub(token, challenge); + } + + saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { + return this.saveTOTPSecretStub(userId, secret); + } + + retrieveTOTPSecret(userId: string): BluebirdPromise { + return this.retrieveTOTPSecretStub(userId); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/mongo/MongoCollection.spec.ts b/themes/matrix/server/src/lib/storage/mongo/MongoCollection.spec.ts new file mode 100644 index 000000000..74a773a1e --- /dev/null +++ b/themes/matrix/server/src/lib/storage/mongo/MongoCollection.spec.ts @@ -0,0 +1,110 @@ +import Assert = require("assert"); +import Sinon = require("sinon"); +import MongoDB = require("mongodb"); +import BluebirdPromise = require("bluebird"); +import { MongoClientStub } from "../../connectors/mongo/MongoClientStub.spec"; +import { MongoCollection } from "./MongoCollection"; + +describe("storage/mongo/MongoCollection", function () { + let mongoCollectionStub: any; + let mongoClientStub: MongoClientStub; + let findStub: Sinon.SinonStub; + let findOneStub: Sinon.SinonStub; + let insertOneStub: Sinon.SinonStub; + let updateStub: Sinon.SinonStub; + let removeStub: Sinon.SinonStub; + let countStub: Sinon.SinonStub; + const COLLECTION_NAME = "collection"; + + before(function () { + mongoClientStub = new MongoClientStub(); + mongoCollectionStub = Sinon.createStubInstance(require("mongodb").Collection as any); + findStub = mongoCollectionStub.find as Sinon.SinonStub; + findOneStub = mongoCollectionStub.findOne as Sinon.SinonStub; + insertOneStub = mongoCollectionStub.insertOne as Sinon.SinonStub; + updateStub = mongoCollectionStub.update as Sinon.SinonStub; + removeStub = mongoCollectionStub.remove as Sinon.SinonStub; + countStub = mongoCollectionStub.count as Sinon.SinonStub; + mongoClientStub.collectionStub.returns( + BluebirdPromise.resolve(mongoCollectionStub) + ); + }); + + describe("find", function () { + it("should find a document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + findStub.returns({ + sort: Sinon.stub().returns({ + limit: Sinon.stub().returns({ + toArray: Sinon.stub().returns(BluebirdPromise.resolve([])) + }) + }) + }); + + return collection.find({ key: "KEY" }) + .then(function () { + Assert(findStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("findOne", function () { + it("should find one document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + findOneStub.returns(BluebirdPromise.resolve({})); + + return collection.findOne({ key: "KEY" }) + .then(function () { + Assert(findOneStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("insert", function () { + it("should insert a document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + insertOneStub.returns(BluebirdPromise.resolve({})); + + return collection.insert({ key: "KEY" }) + .then(function () { + Assert(insertOneStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("update", function () { + it("should update a document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + updateStub.returns(BluebirdPromise.resolve({})); + + return collection.update({ key: "KEY" }, { key: "KEY", value: 1 }) + .then(function () { + Assert(updateStub.calledWith({ key: "KEY" }, { key: "KEY", value: 1 })); + }); + }); + }); + + describe("remove", function () { + it("should remove a document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + removeStub.returns(BluebirdPromise.resolve({})); + + return collection.remove({ key: "KEY" }) + .then(function () { + Assert(removeStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("count", function () { + it("should count documents in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + countStub.returns(BluebirdPromise.resolve({})); + + return collection.count({ key: "KEY" }) + .then(function () { + Assert(countStub.calledWith({ key: "KEY" })); + }); + }); + }); +}); diff --git a/themes/matrix/server/src/lib/storage/mongo/MongoCollection.ts b/themes/matrix/server/src/lib/storage/mongo/MongoCollection.ts new file mode 100644 index 000000000..9771389f1 --- /dev/null +++ b/themes/matrix/server/src/lib/storage/mongo/MongoCollection.ts @@ -0,0 +1,50 @@ +import Bluebird = require("bluebird"); +import { ICollection } from "../ICollection"; +import MongoDB = require("mongodb"); +import { IMongoClient } from "../../connectors/mongo/IMongoClient"; + + +export class MongoCollection implements ICollection { + private mongoClient: IMongoClient; + private collectionName: string; + + constructor(collectionName: string, mongoClient: IMongoClient) { + this.collectionName = collectionName; + this.mongoClient = mongoClient; + } + + private collection(): Bluebird { + return this.mongoClient.collection(this.collectionName); + } + + find(query: any, sortKeys?: any, count?: number): Bluebird { + return this.collection() + .then((collection) => collection.find(query).sort(sortKeys).limit(count)) + .then((query) => query.toArray()); + } + + findOne(query: any): Bluebird { + return this.collection() + .then((collection) => collection.findOne(query)); + } + + update(query: any, updateQuery: any, options?: any): Bluebird { + return this.collection() + .then((collection) => collection.update(query, updateQuery, options)); + } + + remove(query: any): Bluebird { + return this.collection() + .then((collection) => collection.remove(query)); + } + + insert(document: any): Bluebird { + return this.collection() + .then((collection) => collection.insertOne(document)); + } + + count(query: any): Bluebird { + return this.collection() + .then((collection) => collection.count(query)); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts b/themes/matrix/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts new file mode 100644 index 000000000..bd959cacb --- /dev/null +++ b/themes/matrix/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts @@ -0,0 +1,21 @@ +import Assert = require("assert"); +import Sinon = require("sinon"); +import { MongoClientStub } from "../../connectors/mongo/MongoClientStub.spec"; +import { MongoCollectionFactory } from "./MongoCollectionFactory"; + +describe("storage/mongo/MongoCollectionFactory", function () { + let mongoClient: MongoClientStub; + + before(function() { + mongoClient = new MongoClientStub(); + }); + + describe("create", function () { + it("should create a collection", function () { + const COLLECTION_NAME = "COLLECTION_NAME"; + + const factory = new MongoCollectionFactory(mongoClient); + Assert(factory.build(COLLECTION_NAME)); + }); + }); +}); diff --git a/themes/matrix/server/src/lib/storage/mongo/MongoCollectionFactory.ts b/themes/matrix/server/src/lib/storage/mongo/MongoCollectionFactory.ts new file mode 100644 index 000000000..14a8262c9 --- /dev/null +++ b/themes/matrix/server/src/lib/storage/mongo/MongoCollectionFactory.ts @@ -0,0 +1,19 @@ +import BluebirdPromise = require("bluebird"); +import { ICollection } from "../ICollection"; +import { ICollectionFactory } from "../ICollectionFactory"; +import { MongoCollection } from "./MongoCollection"; +import path = require("path"); +import MongoDB = require("mongodb"); +import { IMongoClient } from "../../connectors/mongo/IMongoClient"; + +export class MongoCollectionFactory implements ICollectionFactory { + private mongoClient: IMongoClient; + + constructor(mongoClient: IMongoClient) { + this.mongoClient = mongoClient; + } + + build(collectionName: string): ICollection { + return new MongoCollection(collectionName, this.mongoClient); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/nedb/NedbCollection.spec.ts b/themes/matrix/server/src/lib/storage/nedb/NedbCollection.spec.ts new file mode 100644 index 000000000..a69962b67 --- /dev/null +++ b/themes/matrix/server/src/lib/storage/nedb/NedbCollection.spec.ts @@ -0,0 +1,136 @@ +import Sinon = require("sinon"); +import Assert = require("assert"); + +import { NedbCollection } from "./NedbCollection"; + +describe("storage/nedb/NedbCollection", function () { + describe("insert", function () { + it("should insert one entry", function () { + const nedbOptions = { + inMemoryOnly: true + }; + const collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou" }); + + return collection.count({}).then(function (count: number) { + Assert.equal(1, count); + }); + }); + + it("should insert three entries", function () { + const nedbOptions = { + inMemoryOnly: true + }; + const collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou" }); + collection.insert({ key: "hello" }); + collection.insert({ key: "hey" }); + + return collection.count({}).then(function (count: number) { + Assert.equal(3, count); + }); + }); + }); + + describe("find", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "hello" }); + collection.insert({ key: "hey" }); + collection.insert({ key: "coucou", value: 2 }); + }); + + it("should find one hello", function () { + return collection.find({ key: "hello" }, { key: 1 }) + .then(function (docs: { key: string }[]) { + Assert.equal(1, docs.length); + Assert(docs[0].key == "hello"); + }); + }); + + it("should find two coucou", function () { + return collection.find({ key: "coucou" }, { value: 1 }) + .then(function (docs: { value: number }[]) { + Assert.equal(2, docs.length); + }); + }); + }); + + describe("findOne", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "coucou", value: 1 }); + }); + + it("should find two coucou", function () { + const doc = { key: "coucou", value: 1 }; + return collection.count(doc) + .then(function (count: number) { + Assert.equal(4, count); + return collection.findOne(doc); + }); + }); + }); + + describe("update", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou", value: 1 }); + }); + + it("should update the value", function () { + return collection.update({ key: "coucou" }, { key: "coucou", value: 2 }, { multi: true }) + .then(function () { + return collection.find({ key: "coucou" }); + }) + .then(function (docs: { key: string, value: number }[]) { + Assert.equal(1, docs.length); + Assert.equal(2, docs[0].value); + }); + }); + }); + + describe("update", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou" }); + collection.insert({ key: "hello" }); + }); + + it("should update the value", function () { + return collection.remove({ key: "coucou" }) + .then(function () { + return collection.count({}); + }) + .then(function (count: number) { + Assert.equal(1, count); + }); + }); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/nedb/NedbCollection.ts b/themes/matrix/server/src/lib/storage/nedb/NedbCollection.ts new file mode 100644 index 000000000..88a93ad05 --- /dev/null +++ b/themes/matrix/server/src/lib/storage/nedb/NedbCollection.ts @@ -0,0 +1,47 @@ +import BluebirdPromise = require("bluebird"); +import { ICollection } from "../ICollection"; +import Nedb = require("nedb"); + +declare module "nedb" { + export class NedbAsync extends Nedb { + constructor(pathOrOptions?: string | Nedb.DataStoreOptions); + updateAsync(query: any, updateQuery: any, options?: Nedb.UpdateOptions): BluebirdPromise; + findOneAsync(query: any): BluebirdPromise; + insertAsync(newDoc: T): BluebirdPromise; + removeAsync(query: any): BluebirdPromise; + countAsync(query: any): BluebirdPromise; + } +} + +export class NedbCollection implements ICollection { + private collection: Nedb.NedbAsync; + + constructor(options: Nedb.DataStoreOptions) { + this.collection = BluebirdPromise.promisifyAll(new Nedb(options)) as Nedb.NedbAsync; + } + + find(query: any, sortKeys?: any, count?: number): BluebirdPromise { + const q = this.collection.find(query).sort(sortKeys).limit(count); + return BluebirdPromise.promisify(q.exec, { context: q })(); + } + + findOne(query: any): BluebirdPromise { + return this.collection.findOneAsync(query); + } + + update(query: any, updateQuery: any, options?: any): BluebirdPromise { + return this.collection.updateAsync(query, updateQuery, options); + } + + remove(query: any): BluebirdPromise { + return this.collection.removeAsync(query); + } + + insert(document: any): BluebirdPromise { + return this.collection.insertAsync(document); + } + + count(query: any): BluebirdPromise { + return this.collection.countAsync(query); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts b/themes/matrix/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts new file mode 100644 index 000000000..da90c661f --- /dev/null +++ b/themes/matrix/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts @@ -0,0 +1,16 @@ +import Sinon = require("sinon"); +import Assert = require("assert"); + +import { NedbCollectionFactory } from "./NedbCollectionFactory"; + +describe("storage/nedb/NedbCollectionFactory", function() { + it("should create a nedb collection", function() { + const nedbOptions = { + inMemoryOnly: true + }; + const factory = new NedbCollectionFactory(nedbOptions); + + const collection = factory.build("mycollection"); + Assert(collection); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/nedb/NedbCollectionFactory.ts b/themes/matrix/server/src/lib/storage/nedb/NedbCollectionFactory.ts new file mode 100644 index 000000000..49c4dc853 --- /dev/null +++ b/themes/matrix/server/src/lib/storage/nedb/NedbCollectionFactory.ts @@ -0,0 +1,28 @@ +import { ICollection } from "../ICollection"; +import { ICollectionFactory } from "../ICollectionFactory"; +import { NedbCollection } from "./NedbCollection"; +import path = require("path"); +import Nedb = require("nedb"); + +export interface NedbOptions { + inMemoryOnly?: boolean; + directory?: string; +} + +export class NedbCollectionFactory implements ICollectionFactory { + private options: Nedb.DataStoreOptions; + + constructor(options: Nedb.DataStoreOptions) { + this.options = options; + } + + build(collectionName: string): ICollection { + const datastoreOptions: Nedb.DataStoreOptions = { + inMemoryOnly: this.options.inMemoryOnly || false, + autoload: true, + filename: (this.options.filename) ? path.resolve(this.options.filename, collectionName) : undefined + }; + + return new NedbCollection(datastoreOptions); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/stubs/express.spec.ts b/themes/matrix/server/src/lib/stubs/express.spec.ts new file mode 100644 index 000000000..48f15d7e1 --- /dev/null +++ b/themes/matrix/server/src/lib/stubs/express.spec.ts @@ -0,0 +1,103 @@ + +import sinon = require("sinon"); +import express = require("express"); + +export interface RequestMock { + app?: any; + body?: any; + session?: any; + headers?: any; + get?: any; + query?: any; + originalUrl: string; +} + +export interface ResponseMock { + send: sinon.SinonStub | sinon.SinonSpy; + sendStatus: sinon.SinonStub; + sendFile: sinon.SinonStub; + sendfile: sinon.SinonStub; + status: sinon.SinonStub | sinon.SinonSpy; + json: sinon.SinonStub | sinon.SinonSpy; + links: sinon.SinonStub; + jsonp: sinon.SinonStub; + download: sinon.SinonStub; + contentType: sinon.SinonStub; + type: sinon.SinonStub; + format: sinon.SinonStub; + attachment: sinon.SinonStub; + set: sinon.SinonStub; + header: sinon.SinonStub; + headersSent: boolean; + get: sinon.SinonStub; + clearCookie: sinon.SinonStub; + cookie: sinon.SinonStub; + location: sinon.SinonStub; + redirect: sinon.SinonStub | sinon.SinonSpy; + render: sinon.SinonStub | sinon.SinonSpy; + locals: sinon.SinonStub; + charset: string; + vary: sinon.SinonStub; + app: any; + write: sinon.SinonStub; + writeContinue: sinon.SinonStub; + writeHead: sinon.SinonStub; + statusCode: number; + statusMessage: string; + setHeader: sinon.SinonStub; + setTimeout: sinon.SinonStub; + sendDate: boolean; + getHeader: sinon.SinonStub; +} + +export function RequestMock(): RequestMock { + return { + originalUrl: "/non-api/xxx", + app: { + get: sinon.stub() + }, + headers: { + "x-forwarded-for": "127.0.0.1" + }, + session: {} + }; +} +export function ResponseMock(): ResponseMock { + return { + send: sinon.stub(), + status: sinon.stub(), + json: sinon.stub(), + sendStatus: sinon.stub(), + links: sinon.stub(), + jsonp: sinon.stub(), + sendFile: sinon.stub(), + sendfile: sinon.stub(), + download: sinon.stub(), + contentType: sinon.stub(), + type: sinon.stub(), + format: sinon.stub(), + attachment: sinon.stub(), + set: sinon.stub(), + header: sinon.stub(), + headersSent: true, + get: sinon.stub(), + clearCookie: sinon.stub(), + cookie: sinon.stub(), + location: sinon.stub(), + redirect: sinon.stub(), + render: sinon.stub(), + locals: sinon.stub(), + charset: "utf-8", + vary: sinon.stub(), + app: sinon.stub(), + write: sinon.stub(), + writeContinue: sinon.stub(), + writeHead: sinon.stub(), + statusCode: 200, + statusMessage: "message", + setHeader: sinon.stub(), + setTimeout: sinon.stub(), + sendDate: true, + getHeader: sinon.stub() + }; +} diff --git a/themes/matrix/server/src/lib/stubs/ldapjs.spec.ts b/themes/matrix/server/src/lib/stubs/ldapjs.spec.ts new file mode 100644 index 000000000..045c0e11b --- /dev/null +++ b/themes/matrix/server/src/lib/stubs/ldapjs.spec.ts @@ -0,0 +1,50 @@ + +import Sinon = require("sinon"); + +export class LdapjsMock { + createClientStub: sinon.SinonStub; + + constructor() { + this.createClientStub = Sinon.stub(); + } + + createClient(params: any) { + return this.createClientStub(params); + } +} + +export class LdapjsClientMock { + bindStub: sinon.SinonStub; + unbindStub: sinon.SinonStub; + searchStub: sinon.SinonStub; + modifyStub: sinon.SinonStub; + onStub: sinon.SinonStub; + + constructor() { + this.bindStub = Sinon.stub(); + this.unbindStub = Sinon.stub(); + this.searchStub = Sinon.stub(); + this.modifyStub = Sinon.stub(); + this.onStub = Sinon.stub(); + } + + bind() { + return this.bindStub(); + } + + unbind() { + return this.unbindStub(); + } + + search() { + return this.searchStub(); + } + + modify() { + return this.modifyStub(); + } + + on() { + return this.onStub(); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/stubs/speakeasy.spec.ts b/themes/matrix/server/src/lib/stubs/speakeasy.spec.ts new file mode 100644 index 000000000..023614dcd --- /dev/null +++ b/themes/matrix/server/src/lib/stubs/speakeasy.spec.ts @@ -0,0 +1,7 @@ + +import sinon = require("sinon"); + +export = { + totp: sinon.stub(), + generateSecret: sinon.stub() +}; diff --git a/themes/matrix/server/src/lib/stubs/u2f.spec.ts b/themes/matrix/server/src/lib/stubs/u2f.spec.ts new file mode 100644 index 000000000..234b28c11 --- /dev/null +++ b/themes/matrix/server/src/lib/stubs/u2f.spec.ts @@ -0,0 +1,16 @@ + +import sinon = require("sinon"); + +export interface U2FMock { + request: sinon.SinonStub; + checkSignature: sinon.SinonStub; + checkRegistration: sinon.SinonStub; +} + +export function U2FMock(): U2FMock { + return { + request: sinon.stub(), + checkSignature: sinon.stub(), + checkRegistration: sinon.stub() + }; +} diff --git a/themes/matrix/server/src/lib/utils/HashGenerator.spec.ts b/themes/matrix/server/src/lib/utils/HashGenerator.spec.ts new file mode 100644 index 000000000..f19619a65 --- /dev/null +++ b/themes/matrix/server/src/lib/utils/HashGenerator.spec.ts @@ -0,0 +1,18 @@ +import Assert = require("assert"); +import { HashGenerator } from "./HashGenerator"; + +describe("utils/HashGenerator", function () { + it("should compute correct ssha512 (password)", function () { + return HashGenerator.ssha512("password", 500000, "jgiCMRyGXzoqpxS3") + .then(function (hash: string) { + Assert.equal(hash, "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"); + }); + }); + + it("should compute correct ssha512 (test)", function () { + return HashGenerator.ssha512("test", 500000, "abcdefghijklmnop") + .then(function (hash: string) { + Assert.equal(hash, "{CRYPT}$6$rounds=500000$abcdefghijklmnop$sTlNGf0VO/HTQIOXemmaBbV28HUch/qhWOA1/4dsDj6CDQYhUgXbYSPL6gccAsWMr2zD5fFWwhKmPdG.yxphs."); + }); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/utils/HashGenerator.ts b/themes/matrix/server/src/lib/utils/HashGenerator.ts new file mode 100644 index 000000000..e67de32b7 --- /dev/null +++ b/themes/matrix/server/src/lib/utils/HashGenerator.ts @@ -0,0 +1,23 @@ +import BluebirdPromise = require("bluebird"); +import RandomString = require("randomstring"); +import Util = require("util"); +const crypt = require("crypt3"); + +export class HashGenerator { + static ssha512( + password: string, + rounds: number = 500000, + salt?: string): BluebirdPromise { + const saltSize = 16; + // $6 means SHA512 + const _salt = Util.format("$6$rounds=%d$%s", rounds, + (salt) ? salt : RandomString.generate(16)); + + const cryptAsync = BluebirdPromise.promisify(crypt); + + return cryptAsync(password, _salt) + .then(function (hash: string) { + return BluebirdPromise.resolve(Util.format("{CRYPT}%s", hash)); + }); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/utils/ObjectCloner.ts b/themes/matrix/server/src/lib/utils/ObjectCloner.ts new file mode 100644 index 000000000..3e125d749 --- /dev/null +++ b/themes/matrix/server/src/lib/utils/ObjectCloner.ts @@ -0,0 +1,6 @@ + +export class ObjectCloner { + static clone(obj: any): any { + return JSON.parse(JSON.stringify(obj)); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/utils/SafeRedirection.spec.ts b/themes/matrix/server/src/lib/utils/SafeRedirection.spec.ts new file mode 100644 index 000000000..4126949fd --- /dev/null +++ b/themes/matrix/server/src/lib/utils/SafeRedirection.spec.ts @@ -0,0 +1,33 @@ +import Assert = require("assert"); +import Sinon = require("sinon"); +import { SafeRedirector } from "./SafeRedirection"; + +describe("web_server/middlewares/SafeRedirection", () => { + describe("Url is in protected domain", () => { + before(() => { + this.redirector = new SafeRedirector("example.com"); + this.res = {redirect: Sinon.stub()}; + }); + + it("should redirect to provided url", () => { + this.redirector.redirectOrElse(this.res, + "https://mysubdomain.example.com:8080/abc", + "https://authelia.example.com"); + Assert(this.res.redirect.calledWith("https://mysubdomain.example.com:8080/abc")); + }); + + it("should redirect to default url when wrong domain", () => { + this.redirector.redirectOrElse(this.res, + "https://mysubdomain.domain.rtf:8080/abc", + "https://authelia.example.com"); + Assert(this.res.redirect.calledWith("https://authelia.example.com")); + }); + + it("should redirect to default url when not terminating by domain", () => { + this.redirector.redirectOrElse(this.res, + "https://mysubdomain.example.com.rtf:8080/abc", + "https://authelia.example.com"); + Assert(this.res.redirect.calledWith("https://authelia.example.com")); + }); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/utils/SafeRedirection.ts b/themes/matrix/server/src/lib/utils/SafeRedirection.ts new file mode 100644 index 000000000..9e6a32e0c --- /dev/null +++ b/themes/matrix/server/src/lib/utils/SafeRedirection.ts @@ -0,0 +1,22 @@ +import Express = require("express"); +import { DomainExtractor } from "../../../../shared/DomainExtractor"; +import { BelongToDomain } from "../../../../shared/BelongToDomain"; + + +export class SafeRedirector { + private domain: string; + + constructor(domain: string) { + this.domain = domain; + } + + redirectOrElse( + res: Express.Response, + url: string, + defaultUrl: string): void { + if (BelongToDomain(url, this.domain)) { + res.redirect(url); + } + res.redirect(defaultUrl); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/utils/URLDecomposer.spec.ts b/themes/matrix/server/src/lib/utils/URLDecomposer.spec.ts new file mode 100644 index 000000000..cbb038738 --- /dev/null +++ b/themes/matrix/server/src/lib/utils/URLDecomposer.spec.ts @@ -0,0 +1,46 @@ +import { URLDecomposer } from "./URLDecomposer"; +import Assert = require("assert"); + +describe("utils/URLDecomposer", function () { + describe("test fromUrl", function () { + it("should return domain from https url", function () { + const d = URLDecomposer.fromUrl("https://www.example.com/test/abc"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/test/abc"); + }); + + it("should return domain from http url", function () { + const d = URLDecomposer.fromUrl("http://www.example.com/test/abc"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/test/abc"); + }); + + it("should return domain when url contains port", function () { + const d = URLDecomposer.fromUrl("https://www.example.com:8080/test/abc"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/test/abc"); + }); + + it("should return default path when no path provided", function () { + const d = URLDecomposer.fromUrl("https://www.example.com:8080"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/"); + }); + + it("should return default path when provided", function () { + const d = URLDecomposer.fromUrl("https://www.example.com:8080/"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/"); + }); + + it("should return undefined when does not match", function () { + const d = URLDecomposer.fromUrl("https:///abc/test"); + Assert.equal(d, undefined); + }); + + it("should return undefined when does not match", function () { + const d = URLDecomposer.fromUrl("https:///abc/test"); + Assert.equal(d, undefined); + }); + }); +}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/utils/URLDecomposer.ts b/themes/matrix/server/src/lib/utils/URLDecomposer.ts new file mode 100644 index 000000000..9bdf2e9d2 --- /dev/null +++ b/themes/matrix/server/src/lib/utils/URLDecomposer.ts @@ -0,0 +1,15 @@ +export class URLDecomposer { + static fromUrl(url: string): {domain: string, path: string} { + if (!url) return; + const match = url.match(/https?:\/\/([a-z0-9_.-]+)(:[0-9]+)?(.*)/); + + if (!match) return; + + if (match[1] && !match[3]) { + return {domain: match[1], path: "/"}; + } else if (match[1] && match[3]) { + return {domain: match[1], path: match[3]}; + } + return; + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/web_server/Configurator.ts b/themes/matrix/server/src/lib/web_server/Configurator.ts new file mode 100644 index 000000000..6e404874a --- /dev/null +++ b/themes/matrix/server/src/lib/web_server/Configurator.ts @@ -0,0 +1,47 @@ +import { Configuration } from "../configuration/schema/Configuration"; +import { GlobalDependencies } from "../../../types/Dependencies"; +import { SessionConfigurationBuilder } from + "../configuration/SessionConfigurationBuilder"; +import Path = require("path"); +import Express = require("express"); +import * as BodyParser from "body-parser"; +import { RestApi } from "./RestApi"; +import { WithHeadersLogged } from "./middlewares/WithHeadersLogged"; +import { ServerVariables } from "../ServerVariables"; +import Helmet = require("helmet"); + +const addRequestId = require("express-request-id")(); + +// Constants +const TRUST_PROXY = "trust proxy"; +const X_POWERED_BY = "x-powered-by"; +const VIEWS = "views"; +const VIEW_ENGINE = "view engine"; +const PUG = "pug"; + +export class Configurator { + static configure(config: Configuration, + app: Express.Application, + vars: ServerVariables, + deps: GlobalDependencies): void { + const viewsDirectory = Path.resolve(__dirname, "../../views"); + const publicHtmlDirectory = Path.resolve(__dirname, "../../public_html"); + + const expressSessionOptions = SessionConfigurationBuilder.build(config, deps); + + app.use(Express.static(publicHtmlDirectory)); + app.use(BodyParser.urlencoded({ extended: false })); + app.use(BodyParser.json()); + app.use(deps.session(expressSessionOptions)); + app.use(addRequestId); + app.use(WithHeadersLogged.middleware(vars.logger)); + app.disable(X_POWERED_BY); + app.enable(TRUST_PROXY); + app.use(Helmet()); + + app.set(VIEWS, viewsDirectory); + app.set(VIEW_ENGINE, PUG); + + RestApi.setup(app, vars); + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/web_server/RestApi.ts b/themes/matrix/server/src/lib/web_server/RestApi.ts new file mode 100644 index 000000000..9144a15b9 --- /dev/null +++ b/themes/matrix/server/src/lib/web_server/RestApi.ts @@ -0,0 +1,125 @@ +import Express = require("express"); + +import FirstFactorGet = require("../routes/firstfactor/get"); +import SecondFactorGet = require("../routes/secondfactor/get"); + +import FirstFactorPost = require("../routes/firstfactor/post"); +import LogoutGet = require("../routes/logout/get"); +import VerifyGet = require("../routes/verify/get"); +import TOTPSignGet = require("../routes/secondfactor/totp/sign/post"); + +import IdentityCheckMiddleware = require("../IdentityCheckMiddleware"); + +import TOTPRegistrationIdentityHandler from "../routes/secondfactor/totp/identity/RegistrationHandler"; +import U2FRegistrationIdentityHandler from "../routes/secondfactor/u2f/identity/RegistrationHandler"; +import ResetPasswordIdentityHandler from "../routes/password-reset/identity/PasswordResetHandler"; + +import U2FSignPost = require("../routes/secondfactor/u2f/sign/post"); +import U2FSignRequestGet = require("../routes/secondfactor/u2f/sign_request/get"); + +import U2FRegisterPost = require("../routes/secondfactor/u2f/register/post"); +import U2FRegisterRequestGet = require("../routes/secondfactor/u2f/register_request/get"); + +import ResetPasswordFormPost = require("../routes/password-reset/form/post"); +import ResetPasswordRequestPost = require("../routes/password-reset/request/get"); + +import Error401Get = require("../routes/error/401/get"); +import Error403Get = require("../routes/error/403/get"); +import Error404Get = require("../routes/error/404/get"); + +import LoggedIn = require("../routes/loggedin/get"); + +import { ServerVariables } from "../ServerVariables"; +import Endpoints = require("../../../../shared/api"); +import { RequireValidatedFirstFactor } from "./middlewares/RequireValidatedFirstFactor"; + +function setupTotp(app: Express.Application, vars: ServerVariables) { + app.post(Endpoints.SECOND_FACTOR_TOTP_POST, + RequireValidatedFirstFactor.middleware(vars.logger), + TOTPSignGet.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, + RequireValidatedFirstFactor.middleware(vars.logger)); + + app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, + RequireValidatedFirstFactor.middleware(vars.logger)); + + IdentityCheckMiddleware.register(app, + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, + new TOTPRegistrationIdentityHandler(vars.logger, + vars.userDataStore, vars.totpHandler, vars.config.totp), + vars); +} + +function setupU2f(app: Express.Application, vars: ServerVariables) { + app.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, + RequireValidatedFirstFactor.middleware(vars.logger), + U2FSignRequestGet.default(vars)); + + app.post(Endpoints.SECOND_FACTOR_U2F_SIGN_POST, + RequireValidatedFirstFactor.middleware(vars.logger), + U2FSignPost.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, + RequireValidatedFirstFactor.middleware(vars.logger), + U2FRegisterRequestGet.default(vars)); + + app.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, + RequireValidatedFirstFactor.middleware(vars.logger), + U2FRegisterPost.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, + RequireValidatedFirstFactor.middleware(vars.logger)); + + app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, + RequireValidatedFirstFactor.middleware(vars.logger)); + + IdentityCheckMiddleware.register(app, + Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, + Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, + new U2FRegistrationIdentityHandler(vars.logger), vars); +} + +function setupResetPassword(app: Express.Application, vars: ServerVariables) { + IdentityCheckMiddleware.register(app, + Endpoints.RESET_PASSWORD_IDENTITY_START_GET, + Endpoints.RESET_PASSWORD_IDENTITY_FINISH_GET, + new ResetPasswordIdentityHandler(vars.logger, vars.usersDatabase), + vars); + + app.get(Endpoints.RESET_PASSWORD_REQUEST_GET, + ResetPasswordRequestPost.default); + app.post(Endpoints.RESET_PASSWORD_FORM_POST, + ResetPasswordFormPost.default(vars)); +} + +function setupErrors(app: Express.Application, vars: ServerVariables) { + app.get(Endpoints.ERROR_401_GET, Error401Get.default(vars)); + app.get(Endpoints.ERROR_403_GET, Error403Get.default(vars)); + app.get(Endpoints.ERROR_404_GET, Error404Get.default); +} + +export class RestApi { + static setup(app: Express.Application, vars: ServerVariables): void { + app.get(Endpoints.FIRST_FACTOR_GET, FirstFactorGet.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_GET, + RequireValidatedFirstFactor.middleware(vars.logger), + SecondFactorGet.default(vars)); + + app.get(Endpoints.LOGOUT_GET, LogoutGet.default(vars)); + + app.get(Endpoints.VERIFY_GET, VerifyGet.default(vars)); + app.post(Endpoints.FIRST_FACTOR_POST, FirstFactorPost.default(vars)); + + setupTotp(app, vars); + setupU2f(app, vars); + setupResetPassword(app, vars); + setupErrors(app, vars); + + app.get(Endpoints.LOGGED_IN, + RequireValidatedFirstFactor.middleware(vars.logger), + LoggedIn.default(vars)); + } +} diff --git a/themes/matrix/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts b/themes/matrix/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts new file mode 100644 index 000000000..ecfd75765 --- /dev/null +++ b/themes/matrix/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts @@ -0,0 +1,27 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import ErrorReplies = require("../../ErrorReplies"); +import { IRequestLogger } from "../../logging/IRequestLogger"; +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Exceptions = require("../../Exceptions"); +import { Level } from "../../authentication/Level"; + +export class RequireValidatedFirstFactor { + static middleware(logger: IRequestLogger) { + return function (req: Express.Request, res: Express.Response, + next: Express.NextFunction): BluebirdPromise { + + 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.")); + + next(); + resolve(); + }) + .catch(ErrorReplies.replyWithError401(req, res, logger)); + }; + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/web_server/middlewares/WithHeadersLogged.ts b/themes/matrix/server/src/lib/web_server/middlewares/WithHeadersLogged.ts new file mode 100644 index 000000000..139db1141 --- /dev/null +++ b/themes/matrix/server/src/lib/web_server/middlewares/WithHeadersLogged.ts @@ -0,0 +1,12 @@ +import Express = require("express"); +import { IRequestLogger } from "../../logging/IRequestLogger"; + +export class WithHeadersLogged { + static middleware(logger: IRequestLogger) { + return function (req: Express.Request, res: Express.Response, + next: Express.NextFunction): void { + logger.debug(req, "Headers = %s", JSON.stringify(req.headers)); + next(); + }; + } +} \ No newline at end of file diff --git a/themes/matrix/server/src/resources/email-template.ejs b/themes/matrix/server/src/resources/email-template.ejs new file mode 100644 index 000000000..f59c2f944 --- /dev/null +++ b/themes/matrix/server/src/resources/email-template.ejs @@ -0,0 +1,254 @@ + + + + + + Simples-Minimalistic Responsive Template + + + + + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + +
 
+ + + + + + + +
+

<%= title %>

+
+ +
 
+
+
+ + + + + + + + +
+ + + + + + +
 
+
+ + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + +
 
+ + + + + + + + + + + + + + + + + + +
+ This email has been sent to you in order to validate your identity. Please ignore it if you do not know why you received it. +
 
+ <%= button_title %> +
+
 
+
+
+ + + + + + + + +
+ + + + + + + + + + + + +
 
 
 
+
+ + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + +
+ Please ignore this email if you did not initiate the process. +
+
+
+ + + + + diff --git a/themes/matrix/server/src/views/already-logged-in.pug b/themes/matrix/server/src/views/already-logged-in.pug new file mode 100644 index 000000000..137bbea30 --- /dev/null +++ b/themes/matrix/server/src/views/already-logged-in.pug @@ -0,0 +1,14 @@ +extends layout/layout.pug + +block form-header + h1 Sign in + +block content + img(class="header-img" src="/img/success.png" alt="success") + if redirection_url + p You are already logged in as #{ username }.

+ | If you are not redirected in few seconds, click here.

+ | Otherwise, click here to log off. + else + p You are already logged in as #{ username }.

+ | Click here to log off. diff --git a/themes/matrix/server/src/views/errors/.directory b/themes/matrix/server/src/views/errors/.directory new file mode 100644 index 000000000..33f71bea4 --- /dev/null +++ b/themes/matrix/server/src/views/errors/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,59,57 +Version=3 +ViewMode=1 diff --git a/themes/matrix/server/src/views/errors/401.pug b/themes/matrix/server/src/views/errors/401.pug new file mode 100644 index 000000000..b7a222ad0 --- /dev/null +++ b/themes/matrix/server/src/views/errors/401.pug @@ -0,0 +1,16 @@ +extends ../layout/layout.pug + +block variables + - page_classname = "error-401"; + +block form-header + h1 Error 401 + +block content + img(class="header-img" src="/img/warning.png" alt="warning") + if redirection_url + p You are not authorized to access this resource.

+ | Please click here if you are not + | redirected in few seconds. + else + p You are not authorized to access this resource. \ No newline at end of file diff --git a/themes/matrix/server/src/views/errors/403.pug b/themes/matrix/server/src/views/errors/403.pug new file mode 100644 index 000000000..f4b5ca8a2 --- /dev/null +++ b/themes/matrix/server/src/views/errors/403.pug @@ -0,0 +1,16 @@ +extends ../layout/layout.pug + +block variables + - page_classname = "error-403"; + +block form-header + h1 Error 403 + +block content + img(class="header-img" src="/img/warning.png" alt="warning") + if redirection_url + p You don't have enough privileges to access this resource.

+ | Please click here if you are not + | redirected in few seconds. + else + p You don't have enough privileges to access this resource. diff --git a/themes/matrix/server/src/views/errors/404.pug b/themes/matrix/server/src/views/errors/404.pug new file mode 100644 index 000000000..06d6375fc --- /dev/null +++ b/themes/matrix/server/src/views/errors/404.pug @@ -0,0 +1,11 @@ +extends ../layout/layout.pug + +block variables + - page_classname = "error-404"; + +block form-header +

Error 404

+ +block content + img(class="header-img" src="/img/warning.png" alt="warning") + p Page not found. diff --git a/themes/matrix/server/src/views/firstfactor.pug b/themes/matrix/server/src/views/firstfactor.pug new file mode 100644 index 000000000..5e85e570c --- /dev/null +++ b/themes/matrix/server/src/views/firstfactor.pug @@ -0,0 +1,23 @@ +extends layout/layout.pug + +block variables + - page_classname = "firstfactor"; + +block form-header + h1 Sign in + +block content + div(class="notification") + img(class="header-img" src="/img/matrix_circle_128x128.png" alt="user profile") + p Enter your credentials to sign in + form(class="form-signin") + div(class="form-inputs") + input(type="text" class="form-control" id="username" placeholder="Username" required autofocus) + input(type="password" class="form-control" id="password" placeholder="Password" required) + button(id="signin" class="btn btn-lg btn-primary btn-block" type="submit") Sign in + div(class="keep-me-logged-in pull-left") + input(type="checkbox" id="keep_me_logged_in" name="keep_me_logged_in" value="true") + label(for="keep_me_logged_in") Keep me logged in + div(class="bottom-right-links pull-right") + a(href=reset_password_request_endpoint, class="link forgot-password") Forgot password? + span(class="clearfix") diff --git a/themes/matrix/server/src/views/layout/layout.pug b/themes/matrix/server/src/views/layout/layout.pug new file mode 100644 index 000000000..1d845be49 --- /dev/null +++ b/themes/matrix/server/src/views/layout/layout.pug @@ -0,0 +1,30 @@ +block variables + +doctype html +html + head + title Authelia - 2FA + meta(name="viewport", content="width=device-width, initial-scale=1.0") + meta(name="robots", content="noindex, nofollow, nosnippet, noarchive") + meta(http-equiv="Content-Security-Policy", content="default-src 'self'; img-src 'self' data:;") + link(rel="icon", href="/img/icon.png" type="image/png" sizes="32x32") + link(rel="stylesheet", type="text/css", href="/css/authelia.css") + if redirection_url + meta(http-equiv="refresh" content="4;url=" + redirection_url) + body + canvas#canvas(width='400', height='300') + script(src='/js/matrix.js') + div(class="container") + div(class="row") + div(class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-6 col-lg-offset-3") + div(class="account-wall " + page_classname) + div(class="row header") + block form-header + div(class="row body") + div(class="form col-xs-10 col-xs-offset-1 col-sm-8 col-sm-offset-2 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2") + block content + div(class="row footer poweredby-block") + div(class="poweredby col-xs-6 col-xs-offset-4 col-sm-6 col-sm-offset-4 col-md-6 col-md-offset-4") + | Powered by Authelia + block entrypoint + script(src="/js/authelia.js", type="text/javascript") diff --git a/themes/matrix/server/src/views/need-identity-validation.pug b/themes/matrix/server/src/views/need-identity-validation.pug new file mode 100644 index 000000000..4cfd62717 --- /dev/null +++ b/themes/matrix/server/src/views/need-identity-validation.pug @@ -0,0 +1,12 @@ +extends layout/layout.pug + +block variables + - page_classname = "identity-validation"; + +block form-header + h1 Registration + +block content + img(class="header-img" src="/img/mail.png" alt="mail") + p A confirmation email has been sent to your mailbox. + | Please open it and click on the link within 15 minutes to confirm the registration. diff --git a/themes/matrix/server/src/views/password-reset-form.pug b/themes/matrix/server/src/views/password-reset-form.pug new file mode 100644 index 000000000..fd9311892 --- /dev/null +++ b/themes/matrix/server/src/views/password-reset-form.pug @@ -0,0 +1,18 @@ +extends layout/layout.pug + +block variables + - page_classname = "password-reset-form"; + +block form-header + h1 Reset password + +block content + div(class="notification") + img(class="header-img" src="/img/password_white.png" alt="password") + p Set your new password and confirm it. + form(class="form-signin") + div(class="form-inputs") + input(class="form-control" type="password" name="password1" id="password1" placeholder="New password" required="required") + input(class="form-control" type="password" name="password2" id="password2" placeholder="Password confirmation" required="required") + button(id="reset-password-button" class="btn btn-lg btn-primary btn-block" type="submit") Reset Password + span(class="clearfix") diff --git a/themes/matrix/server/src/views/password-reset-request.pug b/themes/matrix/server/src/views/password-reset-request.pug new file mode 100644 index 000000000..855b5998b --- /dev/null +++ b/themes/matrix/server/src/views/password-reset-request.pug @@ -0,0 +1,18 @@ +extends layout/layout.pug + +block variables + - page_classname = "password-reset-request"; + +block form-header + h1 Reset password + +block content + div(class="notification") + div + img(class="header-img" src="/img/password_white.png" alt="password") + p After giving your username, you will receive an email to change your password. + form(class="form-signin") + div(class="form-inputs") + input(type="text" class="form-control" name="username" id="username" placeholder="Your username" required="required") + button(id="reset-password-button" class="btn btn-lg btn-primary btn-block" type="submit") Reset Password + span(class="clearfix") diff --git a/themes/matrix/server/src/views/secondfactor.pug b/themes/matrix/server/src/views/secondfactor.pug new file mode 100644 index 000000000..87b578185 --- /dev/null +++ b/themes/matrix/server/src/views/secondfactor.pug @@ -0,0 +1,31 @@ +extends layout/layout.pug + +block variables + - page_classname = "secondfactor"; + +block form-header + h1 Sign in + +block content + div + div(class="notification notification-totp") + h3 Hi #{username} + div(class="row") + div(class="u2f-token") + img(src="/img/pendrive.png", alt="security key") + p + | Please, touch your security key
+ b Or
+ | Get a one-time password + form(class="form-signin totp") + div(class="form-inputs") + input(type="text" autocomplete="off" class="form-control" id="token" placeholder="Token" required autofocus) + button(class="btn btn-lg btn-primary btn-block totp-button" type="submit") Sign in + div(class="pull-right bottom-right-links") + div Need to register? + div + a(href=u2f_identity_start_endpoint, class="link register-u2f", data-toggle="tooltip", title="A security key is required to register.") Security key + | | + a(href=totp_identity_start_endpoint, class="link register-totp") Google Authenticator + span(class="clearfix") + script(src="/js/u2f-api.js", type="text/javascript") diff --git a/themes/matrix/server/src/views/totp-register.pug b/themes/matrix/server/src/views/totp-register.pug new file mode 100644 index 000000000..1b4d98354 --- /dev/null +++ b/themes/matrix/server/src/views/totp-register.pug @@ -0,0 +1,25 @@ +extends layout/layout.pug + +block variables + - page_classname = "totp-register"; + +block form-header + h1 One-time passwords + +block content + p Open Google Authenticator and add this entry + p(id="secret") #{ base32_secret } + p or scan this barcode + div(id="qrcode") #{ otpauth_url } + p + a(href=login_endpoint, id="login-button") Login + div(class="need-google-authenticator") + | Need Google Authenticator? + div(class="store-badges") + a(href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1', target="_blank") + img(alt='Get it on Google Play', src='/img/stores/googleplay-badge.svg', class="store-badge") + a(href='https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8', target="_blank") + img(alt='Get it on Apple Store' src='/img/stores/applestore-badge.svg' class="store-badge") + +block entrypoint + script(src="/js/qrcode.min.js", type="text/javascript" ) diff --git a/themes/matrix/server/src/views/u2f-register.pug b/themes/matrix/server/src/views/u2f-register.pug new file mode 100644 index 000000000..d52eba6ca --- /dev/null +++ b/themes/matrix/server/src/views/u2f-register.pug @@ -0,0 +1,12 @@ +extends layout/layout.pug + +block variables + - page_classname = "u2f-register"; + +block form-header + h1 Register your security key + +block content + p Touch the token to register your security key. + img(src="/img/pendrive.png" alt="pendrive") + script(src="/js/u2f-api.js", type="text/javascript") \ No newline at end of file diff --git a/themes/matrix/server/test/requests.ts b/themes/matrix/server/test/requests.ts new file mode 100644 index 000000000..93fa0de47 --- /dev/null +++ b/themes/matrix/server/test/requests.ts @@ -0,0 +1,94 @@ + +import BluebirdPromise = require("bluebird"); +import request = require("request"); +import assert = require("assert"); +import express = require("express"); +import nodemailer = require("nodemailer"); +import Endpoints = require("../../shared/api"); + +declare module "request" { + export interface RequestAPI { + getAsync(uri: string, options?: RequiredUriUrl): BluebirdPromise; + getAsync(uri: string): BluebirdPromise; + getAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; + + postAsync(uri: string, options?: CoreOptions): BluebirdPromise; + postAsync(uri: string): BluebirdPromise; + postAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; + } +} + +const requestAsync: typeof request = BluebirdPromise.promisifyAll(request) as typeof request; + +export = function (port: number) { + const PORT = port; + const BASE_URL = "http://localhost:" + PORT; + + function execute_totp(jar: request.CookieJar, token: string) { + return requestAsync.postAsync({ + url: BASE_URL + Endpoints.SECOND_FACTOR_TOTP_POST, + jar: jar, + form: { + token: token + } + }); + } + + function execute_u2f_authentication(jar: request.CookieJar) { + return requestAsync.getAsync({ + url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, + jar: jar + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200); + return requestAsync.postAsync({ + url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_POST, + jar: jar, + form: { + } + }); + }); + } + + function execute_verification(jar: request.CookieJar) { + return requestAsync.getAsync({ url: BASE_URL + Endpoints.VERIFY_GET, jar: jar }); + } + + function execute_login(jar: request.CookieJar) { + return requestAsync.getAsync({ url: BASE_URL + Endpoints.FIRST_FACTOR_GET, jar: jar }); + } + + function execute_first_factor(jar: request.CookieJar) { + return requestAsync.postAsync({ + url: BASE_URL + Endpoints.FIRST_FACTOR_POST, + jar: jar, + form: { + username: "test_ok", + password: "password" + } + }); + } + + function execute_failing_first_factor(jar: request.CookieJar) { + return requestAsync.postAsync({ + url: BASE_URL + Endpoints.FIRST_FACTOR_POST, + jar: jar, + form: { + username: "test_nok", + password: "password" + } + }); + } + + return { + login: execute_login, + verify: execute_verification, + u2f_authentication: execute_u2f_authentication, + first_factor: execute_first_factor, + failing_first_factor: execute_failing_first_factor, + totp: execute_totp, + }; +}; + diff --git a/themes/matrix/server/tsconfig.json b/themes/matrix/server/tsconfig.json new file mode 100644 index 000000000..ebe98c5ed --- /dev/null +++ b/themes/matrix/server/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "moduleResolution": "node", + "noImplicitAny": true, + "sourceMap": true, + "removeComments": true, + "outDir": "../dist", + "baseUrl": ".", + "paths": { + "*": [ + "./types/*", + "../shared/types/*" + ] + } + }, + "exclude": [ + "src/**/*.spec.ts" + ] +} diff --git a/themes/matrix/server/tslint.json b/themes/matrix/server/tslint.json new file mode 100644 index 000000000..c2c1b7501 --- /dev/null +++ b/themes/matrix/server/tslint.json @@ -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 + } +} diff --git a/themes/matrix/server/types/.directory b/themes/matrix/server/types/.directory new file mode 100644 index 000000000..1e65000ea --- /dev/null +++ b/themes/matrix/server/types/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,58,27 +Version=3 +ViewMode=1 diff --git a/themes/matrix/server/types/AuthenticationSession.ts b/themes/matrix/server/types/AuthenticationSession.ts new file mode 100644 index 000000000..bbed0e715 --- /dev/null +++ b/themes/matrix/server/types/AuthenticationSession.ts @@ -0,0 +1,18 @@ +import U2f = require("u2f"); +import { Level } from "../src/lib/authentication/Level"; + +export interface AuthenticationSession { + userid: string; + authentication_level: Level; + keep_me_logged_in: boolean; + last_activity_datetime: number; + identity_check?: { + challenge: string; + userid: string; + }; + register_request?: U2f.Request; + sign_request?: U2f.Request; + email: string; + groups: string[]; + redirect?: string; +} \ No newline at end of file diff --git a/themes/matrix/server/types/Dependencies.ts b/themes/matrix/server/types/Dependencies.ts new file mode 100644 index 000000000..f20404dbc --- /dev/null +++ b/themes/matrix/server/types/Dependencies.ts @@ -0,0 +1,29 @@ +import winston = require("winston"); +import speakeasy = require("speakeasy"); +import nodemailer = require("nodemailer"); +import session = require("express-session"); +import nedb = require("nedb"); +import ldapjs = require("ldapjs"); +import u2f = require("u2f"); +import RedisSession = require("connect-redis"); +import Redis = require("redis"); + +export type Speakeasy = typeof speakeasy; +export type Winston = typeof winston; +export type Session = typeof session; +export type Nedb = typeof nedb; +export type Ldapjs = typeof ldapjs; +export type U2f = typeof u2f; +export type ConnectRedis = typeof RedisSession; +export type Redis = typeof Redis; + +export interface GlobalDependencies { + u2f: U2f; + ldapjs: Ldapjs; + session: Session; + Redis: Redis; + ConnectRedis: ConnectRedis; + winston: Winston; + speakeasy: Speakeasy; + nedb: Nedb; +} \ No newline at end of file diff --git a/themes/matrix/server/types/Identity.ts b/themes/matrix/server/types/Identity.ts new file mode 100644 index 000000000..e985984e2 --- /dev/null +++ b/themes/matrix/server/types/Identity.ts @@ -0,0 +1,6 @@ + + +export interface Identity { + userid: string; + email: string; +} \ No newline at end of file diff --git a/themes/matrix/server/types/TOTPSecret.ts b/themes/matrix/server/types/TOTPSecret.ts new file mode 100644 index 000000000..d6775f2f0 --- /dev/null +++ b/themes/matrix/server/types/TOTPSecret.ts @@ -0,0 +1,11 @@ + +export interface TOTPSecret { + ascii: string; + hex: string; + base32: string; + qr_code_ascii: string; + qr_code_hex: string; + qr_code_base32: string; + google_auth_qr: string; + otpauth_url: string; + } \ No newline at end of file diff --git a/themes/matrix/server/types/U2FRegistration.ts b/themes/matrix/server/types/U2FRegistration.ts new file mode 100644 index 000000000..b6080af07 --- /dev/null +++ b/themes/matrix/server/types/U2FRegistration.ts @@ -0,0 +1,5 @@ + +export interface U2FRegistration { + keyHandle: string; + publicKey: string; +} \ No newline at end of file diff --git a/themes/matrix/server/types/dovehash.d.ts b/themes/matrix/server/types/dovehash.d.ts new file mode 100644 index 000000000..c354609c0 --- /dev/null +++ b/themes/matrix/server/types/dovehash.d.ts @@ -0,0 +1,4 @@ + +declare module "dovehash" { + function encode(algo: string, text: string): string; +} \ No newline at end of file diff --git a/themes/matrix/server/types/speakeasy.d.ts b/themes/matrix/server/types/speakeasy.d.ts new file mode 100644 index 000000000..6ea06948b --- /dev/null +++ b/themes/matrix/server/types/speakeasy.d.ts @@ -0,0 +1,96 @@ +declare module "speakeasy" { + export = speakeasy + + interface SharedOptions { + encoding?: string + algorithm?: string + } + + interface DigestOptions extends SharedOptions { + secret: string + counter: number + } + + interface HOTPOptions extends SharedOptions { + secret: string + counter: number + digest?: Buffer + digits?: number + } + + interface HOTPVerifyOptions extends SharedOptions { + secret: string + token: string + counter: number + digits?: number + window?: number + } + + interface TOTPOptions extends SharedOptions { + secret: string + time?: number + step?: number + epoch?: number + counter?: number + digits?: number + } + + interface TOTPVerifyOptions extends SharedOptions { + secret: string + token: string + time?: number + step?: number + epoch?: number + counter?: number + digits?: number + window?: number + } + + interface GenerateSecretOptions { + length?: number + symbols?: boolean + otpauth_url?: boolean + name?: string + issuer?: string + } + + interface GeneratedSecret { + ascii: string + hex: string + base32: string + qr_code_ascii: string + qr_code_hex: string + qr_code_base32: string + google_auth_qr: string + otpauth_url: string + } + + interface OTPAuthURLOptions extends SharedOptions { + secret: string + label: string + type?: string + counter?: number + issuer?: string + digits?: number + period?: number + } + + interface Speakeasy { + digest: (options: DigestOptions) => Buffer + hotp: { + (options: HOTPOptions): string, + verifyDelta: (options: HOTPVerifyOptions) => boolean, + verify: (options: HOTPVerifyOptions) => boolean, + } + totp: { + (options: TOTPOptions): string + verifyDelta: (options: TOTPVerifyOptions) => boolean, + verify: (options: TOTPVerifyOptions) => boolean, + } + generateSecret: (options?: GenerateSecretOptions) => GeneratedSecret + generateSecretASCII: (length?: number, symbols?: boolean) => string + otpauthURL: (options: OTPAuthURLOptions) => string + } + + const speakeasy: Speakeasy +} \ No newline at end of file From 6bd9d04eb9576aa7103de85360788d59d15de2a1 Mon Sep 17 00:00:00 2001 From: BankaiNoJutsu Date: Mon, 17 Dec 2018 23:27:58 +0100 Subject: [PATCH 02/11] Added cleaning of dist folder before build, by adding grunt-clean, fixed css concat --- Gruntfile.js | 8 +++--- package-lock.json | 72 +++++++++++++++++++++++++++++++++-------------- package.json | 1 + 3 files changed, 56 insertions(+), 25 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 5fdbb88d1..e5ae19198 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -14,6 +14,7 @@ module.exports = function (grunt) { TS_NODE_PROJECT: "server/tsconfig.json" } }, + clean: ['dist'], run: { "compile-server": { cmd: "./node_modules/.bin/tsc", @@ -201,8 +202,6 @@ module.exports = function (grunt) { 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` @@ -224,6 +223,7 @@ module.exports = function (grunt) { grunt.loadNpmTasks('grunt-contrib-copy'); grunt.loadNpmTasks('grunt-contrib-cssmin'); grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-run'); grunt.loadNpmTasks('grunt-env'); @@ -248,8 +248,8 @@ module.exports = function (grunt) { 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', ['clean', 'build', 'run:minify', 'cssmin', 'run:include-minified-script']); + grunt.registerTask('schema', ['run:generate-config-schema']) grunt.registerTask('docker-build', ['run:docker-build']); diff --git a/package-lock.json b/package-lock.json index 358ff9980..6e9837892 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2971,14 +2971,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2993,20 +2991,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -3123,8 +3118,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -3136,7 +3130,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3151,7 +3144,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3159,14 +3151,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -3185,7 +3175,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -3266,8 +3255,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -3279,7 +3267,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -3401,7 +3388,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3745,6 +3731,50 @@ } } }, + "grunt-contrib-clean": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-2.0.0.tgz", + "integrity": "sha512-g5ZD3ORk6gMa5ugZosLDQl3dZO7cI3R14U75hTM+dVLVxdMNJCPVmwf9OUt4v4eWgpKKWWoVK9DZc1amJp4nQw==", + "dev": true, + "requires": { + "async": "^2.6.1", + "rimraf": "^2.6.2" + }, + "dependencies": { + "async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "dev": true, + "requires": { + "lodash": "^4.17.10" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "^7.0.5" + } + } + } + }, "grunt-contrib-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/grunt-contrib-concat/-/grunt-contrib-concat-1.0.1.tgz", diff --git a/package.json b/package.json index ce7bbf9a5..db74a773b 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "cucumber": "^4.0.0", "grunt": "^1.0.3", "grunt-browserify": "^5.0.0", + "grunt-contrib-clean": "^2.0.0", "grunt-contrib-concat": "^1.0.1", "grunt-contrib-copy": "^1.0.0", "grunt-contrib-cssmin": "^2.2.0", From dedd712039620c096d17798a9dfabaffc679c8f1 Mon Sep 17 00:00:00 2001 From: BankaiNoJutsu Date: Tue, 18 Dec 2018 07:47:07 +0100 Subject: [PATCH 03/11] added black theme and fixed main css matrix.js (not needed) --- Gruntfile.js | 33 +- themes/black/client/src/css/.directory | 4 + .../black/client/src/css/00-bootstrap.min.css | 5768 +++++++++++++++++ themes/black/client/src/css/01-main.css | 77 + themes/black/client/src/css/02-login.css | 136 + themes/black/client/src/css/03-errors.css | 12 + .../client/src/css/03-password-reset-form.css | 4 + .../src/css/03-password-reset-request.css | 4 + .../black/client/src/css/03-totp-register.css | 22 + .../black/client/src/css/03-u2f-register.css | 5 + .../client/src/img/RandomizedPattern.svg | 1 + themes/black/client/src/img/background.jpg | Bin 0 -> 587 bytes themes/black/client/src/img/icon.png | Bin 0 -> 1461 bytes themes/black/client/src/img/mail.png | Bin 0 -> 3545 bytes .../client/src/img/notifications/.directory | 4 + .../client/src/img/notifications/error.png | Bin 0 -> 863 bytes .../client/src/img/notifications/info.png | Bin 0 -> 732 bytes .../client/src/img/notifications/success.png | Bin 0 -> 931 bytes .../client/src/img/notifications/warning.png | Bin 0 -> 580 bytes themes/black/client/src/img/padlock.png | Bin 0 -> 3265 bytes .../black/client/src/img/password_white.png | Bin 0 -> 3858 bytes themes/black/client/src/img/pendrive.png | Bin 0 -> 6721 bytes themes/black/client/src/img/sharingan.png | Bin 0 -> 9213 bytes themes/black/client/src/img/stores/.directory | 4 + .../src/img/stores/applestore-badge.svg | 129 + .../src/img/stores/googleplay-badge.svg | 429 ++ themes/black/client/src/img/success.png | Bin 0 -> 3147 bytes themes/black/client/src/img/user.png | Bin 0 -> 2933 bytes themes/black/client/src/img/warning.png | Bin 0 -> 4038 bytes themes/black/client/src/index.ts | 34 + themes/black/client/src/lib/GetPromised.ts | 14 + themes/black/client/src/lib/INotifier.ts | 14 + themes/black/client/src/lib/Notifier.ts | 83 + .../src/lib/QueryParametersRetriever.ts | 12 + themes/black/client/src/lib/SafeRedirect.ts | 10 + .../lib/firstfactor/FirstFactorValidator.ts | 46 + .../client/src/lib/firstfactor/UISelectors.ts | 5 + .../black/client/src/lib/firstfactor/index.ts | 49 + .../src/lib/reset-password/constants.ts | 2 + .../lib/reset-password/reset-password-form.ts | 57 + .../reset-password/reset-password-request.ts | 56 + .../src/lib/secondfactor/TOTPValidator.ts | 28 + .../src/lib/secondfactor/U2FValidator.ts | 42 + .../client/src/lib/secondfactor/constants.ts | 3 + .../client/src/lib/secondfactor/index.ts | 59 + .../src/lib/totp-register/totp-register.ts | 11 + .../src/lib/totp-register/ui-selector.ts | 2 + .../src/lib/u2f-register/u2f-register.ts | 56 + .../client/src/thirdparties/qrcode.min.js | 1 + .../black/client/src/thirdparties/u2f-api.js | 749 +++ themes/black/client/test/Notifier.test.ts | 71 + .../firstfactor/FirstFactorValidator.test.ts | 44 + .../black/client/test/mocks/NotifierStub.ts | 33 + themes/black/client/test/mocks/jquery.ts | 59 + themes/black/client/test/mocks/u2f-api.ts | 14 + .../test/secondfactor/TOTPValidator.test.ts | 37 + .../test/totp-register/totp-register.test.ts | 31 + themes/black/client/tsconfig.json | 24 + themes/black/client/tslint.json | 60 + themes/black/server/.directory | 4 + themes/black/server/src/index.ts | 28 + themes/black/server/src/lib/.directory | 4 + .../src/lib/AuthenticationSessionHandler.ts | 45 + themes/black/server/src/lib/ErrorReplies.ts | 49 + themes/black/server/src/lib/Exceptions.ts | 88 + .../server/src/lib/FirstFactorValidator.ts | 20 + .../src/lib/IdentityCheckMiddleware.spec.ts | 176 + .../server/src/lib/IdentityCheckMiddleware.ts | 138 + .../lib/IdentityCheckPreValidationTemplate.ts | 3 + .../black/server/src/lib/IdentityValidable.ts | 19 + .../src/lib/IdentityValidableStub.spec.ts | 52 + themes/black/server/src/lib/Server.spec.ts | 81 + themes/black/server/src/lib/Server.ts | 93 + .../black/server/src/lib/ServerVariables.ts | 21 + .../src/lib/ServerVariablesInitializer.ts | 116 + .../lib/ServerVariablesMockBuilder.spec.ts | 87 + .../server/src/lib/authentication/Level.ts | 5 + .../backends/GroupsAndEmails.ts | 5 + .../authentication/backends/IUsersDatabase.ts | 10 + .../backends/IUsersDatabaseStub.spec.ts | 35 + .../backends/file/FileUsersDatabase.spec.ts | 224 + .../backends/file/FileUsersDatabase.ts | 182 + .../backends/file/ReadWriteQueue.ts | 60 + .../authentication/backends/ldap/ISession.ts | 12 + .../backends/ldap/ISessionFactory.ts | 6 + .../backends/ldap/LdapUsersDatabase.spec.ts | 386 ++ .../backends/ldap/LdapUsersDatabase.ts | 107 + .../backends/ldap/SafeSession.spec.ts | 76 + .../backends/ldap/SafeSession.ts | 62 + .../backends/ldap/Sanitizer.spec.ts | 25 + .../authentication/backends/ldap/Sanitizer.ts | 25 + .../backends/ldap/Session.spec.ts | 127 + .../authentication/backends/ldap/Session.ts | 156 + .../backends/ldap/SessionFactory.ts | 37 + .../backends/ldap/SessionFactoryStub.spec.ts | 16 + .../backends/ldap/SessionStub.spec.ts | 46 + .../backends/ldap/connector/Connector.ts | 69 + .../ldap/connector/ConnectorFactory.ts | 18 + .../connector/ConnectorFactoryStub.spec.ts | 17 + .../ldap/connector/ConnectorStub.spec.ts | 34 + .../backends/ldap/connector/IConnector.ts | 9 + .../ldap/connector/IConnectorFactory.ts | 5 + .../lib/authentication/totp/ITotpHandler.ts | 6 + .../authentication/totp/TotpHandler.spec.ts | 39 + .../lib/authentication/totp/TotpHandler.ts | 36 + .../totp/TotpHandlerStub.spec.ts | 22 + .../src/lib/authentication/u2f/IU2fHandler.ts | 9 + .../src/lib/authentication/u2f/U2fHandler.ts | 24 + .../authentication/u2f/U2fHandlerStub.spec.ts | 31 + .../src/lib/authorization/Authorizer.spec.ts | 372 ++ .../src/lib/authorization/Authorizer.ts | 85 + .../lib/authorization/AuthorizerStub.spec.ts | 17 + .../src/lib/authorization/IAuthorizer.ts | 7 + .../server/src/lib/authorization/Level.ts | 6 + .../authorization/MultipleDomainMatcher.ts | 12 + .../server/src/lib/authorization/Object.ts | 5 + .../server/src/lib/authorization/Subject.ts | 5 + .../configuration/ConfigurationParser.spec.ts | 171 + .../lib/configuration/ConfigurationParser.ts | 39 + .../SessionConfigurationBuilder.spec.ts | 149 + .../SessionConfigurationBuilder.ts | 52 + .../schema/AclConfiguration.spec.ts | 34 + .../configuration/schema/AclConfiguration.ts | 41 + ...AuthenticationBackendConfiguration.spec.ts | 11 + .../AuthenticationBackendConfiguration.ts | 25 + .../lib/configuration/schema/Configuration.ts | 68 + .../schema/FileUsersDatabaseConfiguration.ts | 4 + .../schema/LdapConfiguration.spec.ts | 25 + .../configuration/schema/LdapConfiguration.ts | 40 + .../schema/NotifierConfiguration.spec.ts | 40 + .../schema/NotifierConfiguration.ts | 45 + .../schema/RegulationConfiguration.spec.ts | 13 + .../schema/RegulationConfiguration.ts | 23 + .../schema/SessionConfiguration.spec.ts | 16 + .../schema/SessionConfiguration.ts | 32 + .../schema/StorageConfiguration.spec.ts | 15 + .../schema/StorageConfiguration.ts | 30 + .../configuration/schema/TotpConfiguration.ts | 13 + .../schema/UserDatabaseConfiguration.ts | 9 + .../lib/connectors/mongo/IMongoClient.d.ts | 6 + .../lib/connectors/mongo/MongoClient.spec.ts | 119 + .../src/lib/connectors/mongo/MongoClient.ts | 76 + .../connectors/mongo/MongoClientStub.spec.ts | 16 + .../server/src/lib/logging/GlobalLogger.ts | 34 + .../src/lib/logging/GlobalLoggerStub.spec.ts | 38 + .../server/src/lib/logging/IGlobalLogger.ts | 5 + .../server/src/lib/logging/IRequestLogger.ts | 7 + .../server/src/lib/logging/RequestLogger.ts | 45 + .../src/lib/logging/RequestLoggerStub.spec.ts | 38 + .../lib/notifiers/AbstractEmailNotifier.ts | 23 + .../src/lib/notifiers/EmailNotifier.spec.ts | 54 + .../server/src/lib/notifiers/EmailNotifier.ts | 27 + .../src/lib/notifiers/FileSystemNotifier.ts | 22 + .../server/src/lib/notifiers/IMailSender.ts | 6 + .../src/lib/notifiers/IMailSenderBuilder.ts | 7 + .../server/src/lib/notifiers/INotifier.ts | 5 + .../server/src/lib/notifiers/MailSender.ts | 42 + .../lib/notifiers/MailSenderBuilder.spec.ts | 67 + .../src/lib/notifiers/MailSenderBuilder.ts | 42 + .../notifiers/MailSenderBuilderStub.spec.ts | 25 + .../src/lib/notifiers/MailSenderStub.spec.ts | 16 + .../src/lib/notifiers/NotifierFactory.spec.ts | 42 + .../src/lib/notifiers/NotifierFactory.ts | 33 + .../src/lib/notifiers/NotifierStub.spec.ts | 16 + .../server/src/lib/notifiers/SmtpNotifier.ts | 30 + .../server/src/lib/regulation/IRegulator.ts | 6 + .../src/lib/regulation/Regulator.spec.ts | 186 + .../server/src/lib/regulation/Regulator.ts | 55 + .../src/lib/regulation/RegulatorStub.spec.ts | 22 + .../src/lib/routes/error/401/get.spec.ts | 61 + .../server/src/lib/routes/error/401/get.ts | 15 + .../src/lib/routes/error/403/get.spec.ts | 61 + .../server/src/lib/routes/error/403/get.ts | 15 + .../src/lib/routes/error/404/get.spec.ts | 19 + .../server/src/lib/routes/error/404/get.ts | 8 + .../server/src/lib/routes/error/redirector.ts | 13 + .../server/src/lib/routes/firstfactor/get.ts | 72 + .../src/lib/routes/firstfactor/post.spec.ts | 136 + .../server/src/lib/routes/firstfactor/post.ts | 101 + .../server/src/lib/routes/loggedin/get.ts | 23 + .../black/server/src/lib/routes/logout/get.ts | 20 + .../lib/routes/password-reset/constants.ts | 2 + .../routes/password-reset/form/post.spec.ts | 122 + .../lib/routes/password-reset/form/post.ts | 50 + .../identity/PasswordResetHandler.spec.ts | 92 + .../identity/PasswordResetHandler.ts | 69 + .../lib/routes/password-reset/request/get.ts | 13 + .../src/lib/routes/secondfactor/get.spec.ts | 44 + .../server/src/lib/routes/secondfactor/get.ts | 28 + .../lib/routes/secondfactor/redirect.spec.ts | 41 + .../src/lib/routes/secondfactor/redirect.ts | 30 + .../lib/routes/secondfactor/totp/constants.ts | 4 + .../totp/identity/RegistrationHandler.spec.ts | 116 + .../totp/identity/RegistrationHandler.ts | 112 + .../secondfactor/totp/sign/post.spec.ts | 76 + .../lib/routes/secondfactor/totp/sign/post.ts | 42 + .../lib/routes/secondfactor/u2f/U2FCommon.ts | 11 + .../u2f/identity/RegistrationHandler.spec.ts | 96 + .../u2f/identity/RegistrationHandler.ts | 73 + .../secondfactor/u2f/register/post.spec.ts | 146 + .../routes/secondfactor/u2f/register/post.ts | 64 + .../u2f/register_request/get.spec.ts | 86 + .../secondfactor/u2f/register_request/get.ts | 43 + .../routes/secondfactor/u2f/sign/post.spec.ts | 101 + .../lib/routes/secondfactor/u2f/sign/post.ts | 57 + .../secondfactor/u2f/sign_request/get.spec.ts | 68 + .../secondfactor/u2f/sign_request/get.ts | 42 + .../src/lib/routes/verify/access_control.ts | 51 + .../server/src/lib/routes/verify/get.spec.ts | 320 + .../black/server/src/lib/routes/verify/get.ts | 91 + .../src/lib/routes/verify/get_basic_auth.ts | 55 + .../lib/routes/verify/get_session_cookie.ts | 78 + .../storage/AuthenticationTraceDocument.d.ts | 6 + .../lib/storage/CollectionFactoryFactory.ts | 15 + .../lib/storage/CollectionFactoryStub.spec.ts | 16 + .../src/lib/storage/CollectionStub.spec.ts | 39 + .../server/src/lib/storage/ICollection.d.ts | 11 + .../src/lib/storage/ICollectionFactory.d.ts | 6 + .../src/lib/storage/IUserDataStore.d.ts | 21 + .../storage/IdentityValidationDocument.d.ts | 7 + .../src/lib/storage/TOTPSecretDocument.d.ts | 6 + .../lib/storage/U2FRegistrationDocument.d.ts | 8 + .../src/lib/storage/UserDataStore.spec.ts | 264 + .../server/src/lib/storage/UserDataStore.ts | 143 + .../src/lib/storage/UserDataStoreStub.spec.ts | 64 + .../lib/storage/mongo/MongoCollection.spec.ts | 110 + .../src/lib/storage/mongo/MongoCollection.ts | 50 + .../mongo/MongoCollectionFactory.spec.ts | 21 + .../storage/mongo/MongoCollectionFactory.ts | 19 + .../lib/storage/nedb/NedbCollection.spec.ts | 136 + .../src/lib/storage/nedb/NedbCollection.ts | 47 + .../nedb/NedbCollectionFactory.spec.ts | 16 + .../lib/storage/nedb/NedbCollectionFactory.ts | 28 + .../server/src/lib/stubs/express.spec.ts | 103 + .../black/server/src/lib/stubs/ldapjs.spec.ts | 50 + .../server/src/lib/stubs/speakeasy.spec.ts | 7 + themes/black/server/src/lib/stubs/u2f.spec.ts | 16 + .../src/lib/utils/HashGenerator.spec.ts | 18 + .../server/src/lib/utils/HashGenerator.ts | 23 + .../server/src/lib/utils/ObjectCloner.ts | 6 + .../src/lib/utils/SafeRedirection.spec.ts | 33 + .../server/src/lib/utils/SafeRedirection.ts | 22 + .../src/lib/utils/URLDecomposer.spec.ts | 46 + .../server/src/lib/utils/URLDecomposer.ts | 15 + .../server/src/lib/web_server/Configurator.ts | 47 + .../server/src/lib/web_server/RestApi.ts | 125 + .../RequireValidatedFirstFactor.ts | 27 + .../middlewares/WithHeadersLogged.ts | 12 + .../server/src/resources/email-template.ejs | 254 + .../server/src/views/already-logged-in.pug | 14 + .../black/server/src/views/errors/.directory | 4 + themes/black/server/src/views/errors/401.pug | 16 + themes/black/server/src/views/errors/403.pug | 16 + themes/black/server/src/views/errors/404.pug | 11 + themes/black/server/src/views/firstfactor.pug | 23 + .../black/server/src/views/layout/layout.pug | 28 + .../src/views/need-identity-validation.pug | 12 + .../server/src/views/password-reset-form.pug | 18 + .../src/views/password-reset-request.pug | 18 + .../black/server/src/views/secondfactor.pug | 31 + .../black/server/src/views/totp-register.pug | 25 + .../black/server/src/views/u2f-register.pug | 12 + themes/black/server/test/requests.ts | 94 + themes/black/server/tsconfig.json | 21 + themes/black/server/tslint.json | 60 + themes/black/server/types/.directory | 4 + .../server/types/AuthenticationSession.ts | 18 + themes/black/server/types/Dependencies.ts | 29 + themes/black/server/types/Identity.ts | 6 + themes/black/server/types/TOTPSecret.ts | 11 + themes/black/server/types/U2FRegistration.ts | 5 + themes/black/server/types/dovehash.d.ts | 4 + themes/black/server/types/speakeasy.d.ts | 96 + .../main/server/src/views/layout/layout.pug | 1 - 274 files changed, 18976 insertions(+), 2 deletions(-) create mode 100644 themes/black/client/src/css/.directory create mode 100644 themes/black/client/src/css/00-bootstrap.min.css create mode 100644 themes/black/client/src/css/01-main.css create mode 100644 themes/black/client/src/css/02-login.css create mode 100644 themes/black/client/src/css/03-errors.css create mode 100644 themes/black/client/src/css/03-password-reset-form.css create mode 100644 themes/black/client/src/css/03-password-reset-request.css create mode 100644 themes/black/client/src/css/03-totp-register.css create mode 100644 themes/black/client/src/css/03-u2f-register.css create mode 100644 themes/black/client/src/img/RandomizedPattern.svg create mode 100644 themes/black/client/src/img/background.jpg create mode 100644 themes/black/client/src/img/icon.png create mode 100644 themes/black/client/src/img/mail.png create mode 100644 themes/black/client/src/img/notifications/.directory create mode 100644 themes/black/client/src/img/notifications/error.png create mode 100644 themes/black/client/src/img/notifications/info.png create mode 100644 themes/black/client/src/img/notifications/success.png create mode 100644 themes/black/client/src/img/notifications/warning.png create mode 100644 themes/black/client/src/img/padlock.png create mode 100644 themes/black/client/src/img/password_white.png create mode 100644 themes/black/client/src/img/pendrive.png create mode 100644 themes/black/client/src/img/sharingan.png create mode 100644 themes/black/client/src/img/stores/.directory create mode 100644 themes/black/client/src/img/stores/applestore-badge.svg create mode 100644 themes/black/client/src/img/stores/googleplay-badge.svg create mode 100644 themes/black/client/src/img/success.png create mode 100644 themes/black/client/src/img/user.png create mode 100644 themes/black/client/src/img/warning.png create mode 100644 themes/black/client/src/index.ts create mode 100644 themes/black/client/src/lib/GetPromised.ts create mode 100644 themes/black/client/src/lib/INotifier.ts create mode 100644 themes/black/client/src/lib/Notifier.ts create mode 100644 themes/black/client/src/lib/QueryParametersRetriever.ts create mode 100644 themes/black/client/src/lib/SafeRedirect.ts create mode 100644 themes/black/client/src/lib/firstfactor/FirstFactorValidator.ts create mode 100644 themes/black/client/src/lib/firstfactor/UISelectors.ts create mode 100644 themes/black/client/src/lib/firstfactor/index.ts create mode 100644 themes/black/client/src/lib/reset-password/constants.ts create mode 100644 themes/black/client/src/lib/reset-password/reset-password-form.ts create mode 100644 themes/black/client/src/lib/reset-password/reset-password-request.ts create mode 100644 themes/black/client/src/lib/secondfactor/TOTPValidator.ts create mode 100644 themes/black/client/src/lib/secondfactor/U2FValidator.ts create mode 100644 themes/black/client/src/lib/secondfactor/constants.ts create mode 100644 themes/black/client/src/lib/secondfactor/index.ts create mode 100644 themes/black/client/src/lib/totp-register/totp-register.ts create mode 100644 themes/black/client/src/lib/totp-register/ui-selector.ts create mode 100644 themes/black/client/src/lib/u2f-register/u2f-register.ts create mode 100644 themes/black/client/src/thirdparties/qrcode.min.js create mode 100644 themes/black/client/src/thirdparties/u2f-api.js create mode 100644 themes/black/client/test/Notifier.test.ts create mode 100644 themes/black/client/test/firstfactor/FirstFactorValidator.test.ts create mode 100644 themes/black/client/test/mocks/NotifierStub.ts create mode 100644 themes/black/client/test/mocks/jquery.ts create mode 100644 themes/black/client/test/mocks/u2f-api.ts create mode 100644 themes/black/client/test/secondfactor/TOTPValidator.test.ts create mode 100644 themes/black/client/test/totp-register/totp-register.test.ts create mode 100644 themes/black/client/tsconfig.json create mode 100644 themes/black/client/tslint.json create mode 100644 themes/black/server/.directory create mode 100755 themes/black/server/src/index.ts create mode 100644 themes/black/server/src/lib/.directory create mode 100644 themes/black/server/src/lib/AuthenticationSessionHandler.ts create mode 100644 themes/black/server/src/lib/ErrorReplies.ts create mode 100644 themes/black/server/src/lib/Exceptions.ts create mode 100644 themes/black/server/src/lib/FirstFactorValidator.ts create mode 100644 themes/black/server/src/lib/IdentityCheckMiddleware.spec.ts create mode 100644 themes/black/server/src/lib/IdentityCheckMiddleware.ts create mode 100644 themes/black/server/src/lib/IdentityCheckPreValidationTemplate.ts create mode 100644 themes/black/server/src/lib/IdentityValidable.ts create mode 100644 themes/black/server/src/lib/IdentityValidableStub.spec.ts create mode 100644 themes/black/server/src/lib/Server.spec.ts create mode 100644 themes/black/server/src/lib/Server.ts create mode 100644 themes/black/server/src/lib/ServerVariables.ts create mode 100644 themes/black/server/src/lib/ServerVariablesInitializer.ts create mode 100644 themes/black/server/src/lib/ServerVariablesMockBuilder.spec.ts create mode 100644 themes/black/server/src/lib/authentication/Level.ts create mode 100644 themes/black/server/src/lib/authentication/backends/GroupsAndEmails.ts create mode 100644 themes/black/server/src/lib/authentication/backends/IUsersDatabase.ts create mode 100644 themes/black/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts create mode 100644 themes/black/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts create mode 100644 themes/black/server/src/lib/authentication/backends/file/FileUsersDatabase.ts create mode 100644 themes/black/server/src/lib/authentication/backends/file/ReadWriteQueue.ts create mode 100644 themes/black/server/src/lib/authentication/backends/ldap/ISession.ts create mode 100644 themes/black/server/src/lib/authentication/backends/ldap/ISessionFactory.ts create mode 100644 themes/black/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts create mode 100644 themes/black/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts create mode 100644 themes/black/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts create mode 100644 themes/black/server/src/lib/authentication/backends/ldap/SafeSession.ts create mode 100644 themes/black/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts create mode 100644 themes/black/server/src/lib/authentication/backends/ldap/Sanitizer.ts create mode 100644 themes/black/server/src/lib/authentication/backends/ldap/Session.spec.ts create mode 100644 themes/black/server/src/lib/authentication/backends/ldap/Session.ts create mode 100644 themes/black/server/src/lib/authentication/backends/ldap/SessionFactory.ts create mode 100644 themes/black/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts create mode 100644 themes/black/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts create mode 100644 themes/black/server/src/lib/authentication/backends/ldap/connector/Connector.ts create mode 100644 themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts create mode 100644 themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts create mode 100644 themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts create mode 100644 themes/black/server/src/lib/authentication/backends/ldap/connector/IConnector.ts create mode 100644 themes/black/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts create mode 100644 themes/black/server/src/lib/authentication/totp/ITotpHandler.ts create mode 100644 themes/black/server/src/lib/authentication/totp/TotpHandler.spec.ts create mode 100644 themes/black/server/src/lib/authentication/totp/TotpHandler.ts create mode 100644 themes/black/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts create mode 100644 themes/black/server/src/lib/authentication/u2f/IU2fHandler.ts create mode 100644 themes/black/server/src/lib/authentication/u2f/U2fHandler.ts create mode 100644 themes/black/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts create mode 100644 themes/black/server/src/lib/authorization/Authorizer.spec.ts create mode 100644 themes/black/server/src/lib/authorization/Authorizer.ts create mode 100644 themes/black/server/src/lib/authorization/AuthorizerStub.spec.ts create mode 100644 themes/black/server/src/lib/authorization/IAuthorizer.ts create mode 100644 themes/black/server/src/lib/authorization/Level.ts create mode 100644 themes/black/server/src/lib/authorization/MultipleDomainMatcher.ts create mode 100644 themes/black/server/src/lib/authorization/Object.ts create mode 100644 themes/black/server/src/lib/authorization/Subject.ts create mode 100644 themes/black/server/src/lib/configuration/ConfigurationParser.spec.ts create mode 100644 themes/black/server/src/lib/configuration/ConfigurationParser.ts create mode 100644 themes/black/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts create mode 100644 themes/black/server/src/lib/configuration/SessionConfigurationBuilder.ts create mode 100644 themes/black/server/src/lib/configuration/schema/AclConfiguration.spec.ts create mode 100644 themes/black/server/src/lib/configuration/schema/AclConfiguration.ts create mode 100644 themes/black/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts create mode 100644 themes/black/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts create mode 100644 themes/black/server/src/lib/configuration/schema/Configuration.ts create mode 100644 themes/black/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts create mode 100644 themes/black/server/src/lib/configuration/schema/LdapConfiguration.spec.ts create mode 100644 themes/black/server/src/lib/configuration/schema/LdapConfiguration.ts create mode 100644 themes/black/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts create mode 100644 themes/black/server/src/lib/configuration/schema/NotifierConfiguration.ts create mode 100644 themes/black/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts create mode 100644 themes/black/server/src/lib/configuration/schema/RegulationConfiguration.ts create mode 100644 themes/black/server/src/lib/configuration/schema/SessionConfiguration.spec.ts create mode 100644 themes/black/server/src/lib/configuration/schema/SessionConfiguration.ts create mode 100644 themes/black/server/src/lib/configuration/schema/StorageConfiguration.spec.ts create mode 100644 themes/black/server/src/lib/configuration/schema/StorageConfiguration.ts create mode 100644 themes/black/server/src/lib/configuration/schema/TotpConfiguration.ts create mode 100644 themes/black/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts create mode 100644 themes/black/server/src/lib/connectors/mongo/IMongoClient.d.ts create mode 100644 themes/black/server/src/lib/connectors/mongo/MongoClient.spec.ts create mode 100644 themes/black/server/src/lib/connectors/mongo/MongoClient.ts create mode 100644 themes/black/server/src/lib/connectors/mongo/MongoClientStub.spec.ts create mode 100644 themes/black/server/src/lib/logging/GlobalLogger.ts create mode 100644 themes/black/server/src/lib/logging/GlobalLoggerStub.spec.ts create mode 100644 themes/black/server/src/lib/logging/IGlobalLogger.ts create mode 100644 themes/black/server/src/lib/logging/IRequestLogger.ts create mode 100644 themes/black/server/src/lib/logging/RequestLogger.ts create mode 100644 themes/black/server/src/lib/logging/RequestLoggerStub.spec.ts create mode 100644 themes/black/server/src/lib/notifiers/AbstractEmailNotifier.ts create mode 100644 themes/black/server/src/lib/notifiers/EmailNotifier.spec.ts create mode 100644 themes/black/server/src/lib/notifiers/EmailNotifier.ts create mode 100644 themes/black/server/src/lib/notifiers/FileSystemNotifier.ts create mode 100644 themes/black/server/src/lib/notifiers/IMailSender.ts create mode 100644 themes/black/server/src/lib/notifiers/IMailSenderBuilder.ts create mode 100644 themes/black/server/src/lib/notifiers/INotifier.ts create mode 100644 themes/black/server/src/lib/notifiers/MailSender.ts create mode 100644 themes/black/server/src/lib/notifiers/MailSenderBuilder.spec.ts create mode 100644 themes/black/server/src/lib/notifiers/MailSenderBuilder.ts create mode 100644 themes/black/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts create mode 100644 themes/black/server/src/lib/notifiers/MailSenderStub.spec.ts create mode 100644 themes/black/server/src/lib/notifiers/NotifierFactory.spec.ts create mode 100644 themes/black/server/src/lib/notifiers/NotifierFactory.ts create mode 100644 themes/black/server/src/lib/notifiers/NotifierStub.spec.ts create mode 100644 themes/black/server/src/lib/notifiers/SmtpNotifier.ts create mode 100644 themes/black/server/src/lib/regulation/IRegulator.ts create mode 100644 themes/black/server/src/lib/regulation/Regulator.spec.ts create mode 100644 themes/black/server/src/lib/regulation/Regulator.ts create mode 100644 themes/black/server/src/lib/regulation/RegulatorStub.spec.ts create mode 100644 themes/black/server/src/lib/routes/error/401/get.spec.ts create mode 100644 themes/black/server/src/lib/routes/error/401/get.ts create mode 100644 themes/black/server/src/lib/routes/error/403/get.spec.ts create mode 100644 themes/black/server/src/lib/routes/error/403/get.ts create mode 100644 themes/black/server/src/lib/routes/error/404/get.spec.ts create mode 100644 themes/black/server/src/lib/routes/error/404/get.ts create mode 100644 themes/black/server/src/lib/routes/error/redirector.ts create mode 100644 themes/black/server/src/lib/routes/firstfactor/get.ts create mode 100644 themes/black/server/src/lib/routes/firstfactor/post.spec.ts create mode 100644 themes/black/server/src/lib/routes/firstfactor/post.ts create mode 100644 themes/black/server/src/lib/routes/loggedin/get.ts create mode 100644 themes/black/server/src/lib/routes/logout/get.ts create mode 100644 themes/black/server/src/lib/routes/password-reset/constants.ts create mode 100644 themes/black/server/src/lib/routes/password-reset/form/post.spec.ts create mode 100644 themes/black/server/src/lib/routes/password-reset/form/post.ts create mode 100644 themes/black/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts create mode 100644 themes/black/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts create mode 100644 themes/black/server/src/lib/routes/password-reset/request/get.ts create mode 100644 themes/black/server/src/lib/routes/secondfactor/get.spec.ts create mode 100644 themes/black/server/src/lib/routes/secondfactor/get.ts create mode 100644 themes/black/server/src/lib/routes/secondfactor/redirect.spec.ts create mode 100644 themes/black/server/src/lib/routes/secondfactor/redirect.ts create mode 100644 themes/black/server/src/lib/routes/secondfactor/totp/constants.ts create mode 100644 themes/black/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts create mode 100644 themes/black/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts create mode 100644 themes/black/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts create mode 100644 themes/black/server/src/lib/routes/secondfactor/totp/sign/post.ts create mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts create mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts create mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts create mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts create mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/register/post.ts create mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts create mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/register_request/get.ts create mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts create mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/sign/post.ts create mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts create mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts create mode 100644 themes/black/server/src/lib/routes/verify/access_control.ts create mode 100644 themes/black/server/src/lib/routes/verify/get.spec.ts create mode 100644 themes/black/server/src/lib/routes/verify/get.ts create mode 100644 themes/black/server/src/lib/routes/verify/get_basic_auth.ts create mode 100644 themes/black/server/src/lib/routes/verify/get_session_cookie.ts create mode 100644 themes/black/server/src/lib/storage/AuthenticationTraceDocument.d.ts create mode 100644 themes/black/server/src/lib/storage/CollectionFactoryFactory.ts create mode 100644 themes/black/server/src/lib/storage/CollectionFactoryStub.spec.ts create mode 100644 themes/black/server/src/lib/storage/CollectionStub.spec.ts create mode 100644 themes/black/server/src/lib/storage/ICollection.d.ts create mode 100644 themes/black/server/src/lib/storage/ICollectionFactory.d.ts create mode 100644 themes/black/server/src/lib/storage/IUserDataStore.d.ts create mode 100644 themes/black/server/src/lib/storage/IdentityValidationDocument.d.ts create mode 100644 themes/black/server/src/lib/storage/TOTPSecretDocument.d.ts create mode 100644 themes/black/server/src/lib/storage/U2FRegistrationDocument.d.ts create mode 100644 themes/black/server/src/lib/storage/UserDataStore.spec.ts create mode 100644 themes/black/server/src/lib/storage/UserDataStore.ts create mode 100644 themes/black/server/src/lib/storage/UserDataStoreStub.spec.ts create mode 100644 themes/black/server/src/lib/storage/mongo/MongoCollection.spec.ts create mode 100644 themes/black/server/src/lib/storage/mongo/MongoCollection.ts create mode 100644 themes/black/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts create mode 100644 themes/black/server/src/lib/storage/mongo/MongoCollectionFactory.ts create mode 100644 themes/black/server/src/lib/storage/nedb/NedbCollection.spec.ts create mode 100644 themes/black/server/src/lib/storage/nedb/NedbCollection.ts create mode 100644 themes/black/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts create mode 100644 themes/black/server/src/lib/storage/nedb/NedbCollectionFactory.ts create mode 100644 themes/black/server/src/lib/stubs/express.spec.ts create mode 100644 themes/black/server/src/lib/stubs/ldapjs.spec.ts create mode 100644 themes/black/server/src/lib/stubs/speakeasy.spec.ts create mode 100644 themes/black/server/src/lib/stubs/u2f.spec.ts create mode 100644 themes/black/server/src/lib/utils/HashGenerator.spec.ts create mode 100644 themes/black/server/src/lib/utils/HashGenerator.ts create mode 100644 themes/black/server/src/lib/utils/ObjectCloner.ts create mode 100644 themes/black/server/src/lib/utils/SafeRedirection.spec.ts create mode 100644 themes/black/server/src/lib/utils/SafeRedirection.ts create mode 100644 themes/black/server/src/lib/utils/URLDecomposer.spec.ts create mode 100644 themes/black/server/src/lib/utils/URLDecomposer.ts create mode 100644 themes/black/server/src/lib/web_server/Configurator.ts create mode 100644 themes/black/server/src/lib/web_server/RestApi.ts create mode 100644 themes/black/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts create mode 100644 themes/black/server/src/lib/web_server/middlewares/WithHeadersLogged.ts create mode 100644 themes/black/server/src/resources/email-template.ejs create mode 100644 themes/black/server/src/views/already-logged-in.pug create mode 100644 themes/black/server/src/views/errors/.directory create mode 100644 themes/black/server/src/views/errors/401.pug create mode 100644 themes/black/server/src/views/errors/403.pug create mode 100644 themes/black/server/src/views/errors/404.pug create mode 100644 themes/black/server/src/views/firstfactor.pug create mode 100644 themes/black/server/src/views/layout/layout.pug create mode 100644 themes/black/server/src/views/need-identity-validation.pug create mode 100644 themes/black/server/src/views/password-reset-form.pug create mode 100644 themes/black/server/src/views/password-reset-request.pug create mode 100644 themes/black/server/src/views/secondfactor.pug create mode 100644 themes/black/server/src/views/totp-register.pug create mode 100644 themes/black/server/src/views/u2f-register.pug create mode 100644 themes/black/server/test/requests.ts create mode 100644 themes/black/server/tsconfig.json create mode 100644 themes/black/server/tslint.json create mode 100644 themes/black/server/types/.directory create mode 100644 themes/black/server/types/AuthenticationSession.ts create mode 100644 themes/black/server/types/Dependencies.ts create mode 100644 themes/black/server/types/Identity.ts create mode 100644 themes/black/server/types/TOTPSecret.ts create mode 100644 themes/black/server/types/U2FRegistration.ts create mode 100644 themes/black/server/types/dovehash.d.ts create mode 100644 themes/black/server/types/speakeasy.d.ts diff --git a/Gruntfile.js b/Gruntfile.js index e5ae19198..1509cc90c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -131,6 +131,30 @@ module.exports = function (grunt) { src: '**', dest: `${buildDir}/server/src/public_html/js/` }, + black_resources: { + expand: true, + cwd: 'themes/black/server/src/resources', + src: '**', + dest: `${buildDir}/server/src/resources/` + }, + black_views: { + expand: true, + cwd: 'themes/black/server/src/views', + src: '**', + dest: `${buildDir}/server/src/views/` + }, + black_images: { + expand: true, + cwd: 'themes/black/client/src/img', + src: '**', + dest: `${buildDir}/server/src/public_html/img/` + }, + black_thirdparties: { + expand: true, + cwd: 'themes/black/client/src/thirdparties', + src: '**', + dest: `${buildDir}/server/src/public_html/js/` + }, schema: { src: schemaDir, dest: `${buildDir}/${schemaDir}` @@ -206,6 +230,10 @@ module.exports = function (grunt) { src: ['themes/matrix/client/src/css/*.css'], dest: `${buildDir}/server/src/public_html/css/authelia.css` }, + black_css: { + src: ['themes/black/client/src/css/*.css'], + dest: `${buildDir}/server/src/public_html/css/authelia.css` + }, }, cssmin: { target: { @@ -243,10 +271,13 @@ module.exports = function (grunt) { grunt.registerTask('copy-resources-matrix', ['copy:matrix_resources', 'copy:matrix_views', 'copy:matrix_images', 'copy:matrix_thirdparties', 'concat:matrix_css']); + grunt.registerTask('copy-resources-black', ['copy:black_resources', 'copy:black_views', 'copy:black_images', 'copy:black_thirdparties', 'concat:black_css']); + 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-server-black', ['compile-server', 'copy-resources-black', 'generate-config-schema']); + grunt.registerTask('build', ['build-client', 'build-server-'+target]); grunt.registerTask('build-dist', ['clean', 'build', 'run:minify', 'cssmin', 'run:include-minified-script']); diff --git a/themes/black/client/src/css/.directory b/themes/black/client/src/css/.directory new file mode 100644 index 000000000..6e4b3f639 --- /dev/null +++ b/themes/black/client/src/css/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,56,41 +Version=3 +ViewMode=1 diff --git a/themes/black/client/src/css/00-bootstrap.min.css b/themes/black/client/src/css/00-bootstrap.min.css new file mode 100644 index 000000000..dfeacbb88 --- /dev/null +++ b/themes/black/client/src/css/00-bootstrap.min.css @@ -0,0 +1,5768 @@ +/*! * Bootstrap v3.3.7 (http://getbootstrap.com) * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ +html{ + font-family:sans-serif; + -webkit-text-size-adjust:100%; + -ms-text-size-adjust:100% +} +body{ + margin:0 +} +article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{ + display:block +} +audio,canvas,progress,video{ + display:inline-block; + vertical-align:baseline +} +audio:not([controls]){ + display:none; + height:0 +} +[hidden],template{ + display:none +} +a{ + background-color:transparent +} +a:active,a:hover{ + outline:0 +} +abbr[title]{ + border-bottom:1px dotted +} +b,strong{ + font-weight:700 +} +dfn{ + font-style:italic +} +h1{ + margin:.67em 0; + font-size:2em +} +mark{ + color:#000; + background:#ff0 +} +small{ + font-size:80% +} +sub,sup{ + position:relative; + font-size:75%; + line-height:0; + vertical-align:baseline +} +sup{ + top:-.5em +} +sub{ + bottom:-.25em +} +img{ + border:0 +} +svg:not(:root){ + overflow:hidden +} +figure{ + margin:1em 40px +} +hr{ + height:0; + -webkit-box-sizing:content-box; + -moz-box-sizing:content-box; + box-sizing:content-box +} +pre{ + overflow:auto +} +code,kbd,pre,samp{ + font-family:monospace,monospace; + font-size:1em +} +button,input,optgroup,select,textarea{ + margin:0; + font:inherit; + color:inherit +} +button{ + overflow:visible +} +button,select{ + text-transform:none +} +button,html input[type=button],input[type=reset],input[type=submit]{ + -webkit-appearance:button; + cursor:pointer +} +button[disabled],html input[disabled]{ + cursor:default +} +button::-moz-focus-inner,input::-moz-focus-inner{ + padding:0; + border:0 +} +input{ + line-height:normal +} +input[type=checkbox],input[type=radio]{ + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + padding:0 +} +input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{ + height:auto +} +input[type=search]{ + -webkit-box-sizing:content-box; + -moz-box-sizing:content-box; + box-sizing:content-box; + -webkit-appearance:textfield +} +input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{ + -webkit-appearance:none +} +fieldset{ + padding:.35em .625em .75em; + margin:0 2px; + border:1px solid silver +} +legend{ + padding:0; + border:0 +} +textarea{ + overflow:auto +} +optgroup{ + font-weight:700 +} +table{ + border-spacing:0; + border-collapse:collapse +} +td,th{ + padding:0 +} +/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ +@media print{ + *,:after,:before{ + color:#000!important; + text-shadow:none!important; + background:0 0!important; + -webkit-box-shadow:none!important; + box-shadow:none!important + } + a,a:visited{ + text-decoration:underline + } + a[href]:after{ + content:" (" attr(href) ")" + } + abbr[title]:after{ + content:" (" attr(title) ")" + } + a[href^="javascript:"]:after,a[href^="#"]:after{ + content:"" + } + blockquote,pre{ + border:1px solid #999; + page-break-inside:avoid + } + thead{ + display:table-header-group + } + img,tr{ + page-break-inside:avoid + } + img{ + max-width:100%!important + } + h2,h3,p{ + orphans:3; + widows:3 + } + h2,h3{ + page-break-after:avoid + } + .navbar{ + display:none + } + .btn>.caret,.dropup>.btn>.caret{ + border-top-color:#000!important + } + .label{ + border:1px solid #000 + } + .table{ + border-collapse:collapse!important + } + .table td,.table th{ + background-color:#fff!important + } + .table-bordered td,.table-bordered th{ + border:1px solid #ddd!important + } +} +@font-face{ + font-family:'Glyphicons Halflings'; + src:url(../fonts/glyphicons-halflings-regular.eot); + src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg') +} +.glyphicon{ + position:relative; + top:1px; + display:inline-block; + font-family:'Glyphicons Halflings'; + font-style:normal; + font-weight:400; + line-height:1; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale +} +.glyphicon-asterisk:before{ + content:"\002a" +} +.glyphicon-plus:before{ + content:"\002b" +} +.glyphicon-eur:before,.glyphicon-euro:before{ + content:"\20ac" +} +.glyphicon-minus:before{ + content:"\2212" +} +.glyphicon-cloud:before{ + content:"\2601" +} +.glyphicon-envelope:before{ + content:"\2709" +} +.glyphicon-pencil:before{ + content:"\270f" +} +.glyphicon-glass:before{ + content:"\e001" +} +.glyphicon-music:before{ + content:"\e002" +} +.glyphicon-search:before{ + content:"\e003" +} +.glyphicon-heart:before{ + content:"\e005" +} +.glyphicon-star:before{ + content:"\e006" +} +.glyphicon-star-empty:before{ + content:"\e007" +} +.glyphicon-user:before{ + content:"\e008" +} +.glyphicon-film:before{ + content:"\e009" +} +.glyphicon-th-large:before{ + content:"\e010" +} +.glyphicon-th:before{ + content:"\e011" +} +.glyphicon-th-list:before{ + content:"\e012" +} +.glyphicon-ok:before{ + content:"\e013" +} +.glyphicon-remove:before{ + content:"\e014" +} +.glyphicon-zoom-in:before{ + content:"\e015" +} +.glyphicon-zoom-out:before{ + content:"\e016" +} +.glyphicon-off:before{ + content:"\e017" +} +.glyphicon-signal:before{ + content:"\e018" +} +.glyphicon-cog:before{ + content:"\e019" +} +.glyphicon-trash:before{ + content:"\e020" +} +.glyphicon-home:before{ + content:"\e021" +} +.glyphicon-file:before{ + content:"\e022" +} +.glyphicon-time:before{ + content:"\e023" +} +.glyphicon-road:before{ + content:"\e024" +} +.glyphicon-download-alt:before{ + content:"\e025" +} +.glyphicon-download:before{ + content:"\e026" +} +.glyphicon-upload:before{ + content:"\e027" +} +.glyphicon-inbox:before{ + content:"\e028" +} +.glyphicon-play-circle:before{ + content:"\e029" +} +.glyphicon-repeat:before{ + content:"\e030" +} +.glyphicon-refresh:before{ + content:"\e031" +} +.glyphicon-list-alt:before{ + content:"\e032" +} +.glyphicon-lock:before{ + content:"\e033" +} +.glyphicon-flag:before{ + content:"\e034" +} +.glyphicon-headphones:before{ + content:"\e035" +} +.glyphicon-volume-off:before{ + content:"\e036" +} +.glyphicon-volume-down:before{ + content:"\e037" +} +.glyphicon-volume-up:before{ + content:"\e038" +} +.glyphicon-qrcode:before{ + content:"\e039" +} +.glyphicon-barcode:before{ + content:"\e040" +} +.glyphicon-tag:before{ + content:"\e041" +} +.glyphicon-tags:before{ + content:"\e042" +} +.glyphicon-book:before{ + content:"\e043" +} +.glyphicon-bookmark:before{ + content:"\e044" +} +.glyphicon-print:before{ + content:"\e045" +} +.glyphicon-camera:before{ + content:"\e046" +} +.glyphicon-font:before{ + content:"\e047" +} +.glyphicon-bold:before{ + content:"\e048" +} +.glyphicon-italic:before{ + content:"\e049" +} +.glyphicon-text-height:before{ + content:"\e050" +} +.glyphicon-text-width:before{ + content:"\e051" +} +.glyphicon-align-left:before{ + content:"\e052" +} +.glyphicon-align-center:before{ + content:"\e053" +} +.glyphicon-align-right:before{ + content:"\e054" +} +.glyphicon-align-justify:before{ + content:"\e055" +} +.glyphicon-list:before{ + content:"\e056" +} +.glyphicon-indent-left:before{ + content:"\e057" +} +.glyphicon-indent-right:before{ + content:"\e058" +} +.glyphicon-facetime-video:before{ + content:"\e059" +} +.glyphicon-picture:before{ + content:"\e060" +} +.glyphicon-map-marker:before{ + content:"\e062" +} +.glyphicon-adjust:before{ + content:"\e063" +} +.glyphicon-tint:before{ + content:"\e064" +} +.glyphicon-edit:before{ + content:"\e065" +} +.glyphicon-share:before{ + content:"\e066" +} +.glyphicon-check:before{ + content:"\e067" +} +.glyphicon-move:before{ + content:"\e068" +} +.glyphicon-step-backward:before{ + content:"\e069" +} +.glyphicon-fast-backward:before{ + content:"\e070" +} +.glyphicon-backward:before{ + content:"\e071" +} +.glyphicon-play:before{ + content:"\e072" +} +.glyphicon-pause:before{ + content:"\e073" +} +.glyphicon-stop:before{ + content:"\e074" +} +.glyphicon-forward:before{ + content:"\e075" +} +.glyphicon-fast-forward:before{ + content:"\e076" +} +.glyphicon-step-forward:before{ + content:"\e077" +} +.glyphicon-eject:before{ + content:"\e078" +} +.glyphicon-chevron-left:before{ + content:"\e079" +} +.glyphicon-chevron-right:before{ + content:"\e080" +} +.glyphicon-plus-sign:before{ + content:"\e081" +} +.glyphicon-minus-sign:before{ + content:"\e082" +} +.glyphicon-remove-sign:before{ + content:"\e083" +} +.glyphicon-ok-sign:before{ + content:"\e084" +} +.glyphicon-question-sign:before{ + content:"\e085" +} +.glyphicon-info-sign:before{ + content:"\e086" +} +.glyphicon-screenshot:before{ + content:"\e087" +} +.glyphicon-remove-circle:before{ + content:"\e088" +} +.glyphicon-ok-circle:before{ + content:"\e089" +} +.glyphicon-ban-circle:before{ + content:"\e090" +} +.glyphicon-arrow-left:before{ + content:"\e091" +} +.glyphicon-arrow-right:before{ + content:"\e092" +} +.glyphicon-arrow-up:before{ + content:"\e093" +} +.glyphicon-arrow-down:before{ + content:"\e094" +} +.glyphicon-share-alt:before{ + content:"\e095" +} +.glyphicon-resize-full:before{ + content:"\e096" +} +.glyphicon-resize-small:before{ + content:"\e097" +} +.glyphicon-exclamation-sign:before{ + content:"\e101" +} +.glyphicon-gift:before{ + content:"\e102" +} +.glyphicon-leaf:before{ + content:"\e103" +} +.glyphicon-fire:before{ + content:"\e104" +} +.glyphicon-eye-open:before{ + content:"\e105" +} +.glyphicon-eye-close:before{ + content:"\e106" +} +.glyphicon-warning-sign:before{ + content:"\e107" +} +.glyphicon-plane:before{ + content:"\e108" +} +.glyphicon-calendar:before{ + content:"\e109" +} +.glyphicon-random:before{ + content:"\e110" +} +.glyphicon-comment:before{ + content:"\e111" +} +.glyphicon-magnet:before{ + content:"\e112" +} +.glyphicon-chevron-up:before{ + content:"\e113" +} +.glyphicon-chevron-down:before{ + content:"\e114" +} +.glyphicon-retweet:before{ + content:"\e115" +} +.glyphicon-shopping-cart:before{ + content:"\e116" +} +.glyphicon-folder-close:before{ + content:"\e117" +} +.glyphicon-folder-open:before{ + content:"\e118" +} +.glyphicon-resize-vertical:before{ + content:"\e119" +} +.glyphicon-resize-horizontal:before{ + content:"\e120" +} +.glyphicon-hdd:before{ + content:"\e121" +} +.glyphicon-bullhorn:before{ + content:"\e122" +} +.glyphicon-bell:before{ + content:"\e123" +} +.glyphicon-certificate:before{ + content:"\e124" +} +.glyphicon-thumbs-up:before{ + content:"\e125" +} +.glyphicon-thumbs-down:before{ + content:"\e126" +} +.glyphicon-hand-right:before{ + content:"\e127" +} +.glyphicon-hand-left:before{ + content:"\e128" +} +.glyphicon-hand-up:before{ + content:"\e129" +} +.glyphicon-hand-down:before{ + content:"\e130" +} +.glyphicon-circle-arrow-right:before{ + content:"\e131" +} +.glyphicon-circle-arrow-left:before{ + content:"\e132" +} +.glyphicon-circle-arrow-up:before{ + content:"\e133" +} +.glyphicon-circle-arrow-down:before{ + content:"\e134" +} +.glyphicon-globe:before{ + content:"\e135" +} +.glyphicon-wrench:before{ + content:"\e136" +} +.glyphicon-tasks:before{ + content:"\e137" +} +.glyphicon-filter:before{ + content:"\e138" +} +.glyphicon-briefcase:before{ + content:"\e139" +} +.glyphicon-fullscreen:before{ + content:"\e140" +} +.glyphicon-dashboard:before{ + content:"\e141" +} +.glyphicon-paperclip:before{ + content:"\e142" +} +.glyphicon-heart-empty:before{ + content:"\e143" +} +.glyphicon-link:before{ + content:"\e144" +} +.glyphicon-phone:before{ + content:"\e145" +} +.glyphicon-pushpin:before{ + content:"\e146" +} +.glyphicon-usd:before{ + content:"\e148" +} +.glyphicon-gbp:before{ + content:"\e149" +} +.glyphicon-sort:before{ + content:"\e150" +} +.glyphicon-sort-by-alphabet:before{ + content:"\e151" +} +.glyphicon-sort-by-alphabet-alt:before{ + content:"\e152" +} +.glyphicon-sort-by-order:before{ + content:"\e153" +} +.glyphicon-sort-by-order-alt:before{ + content:"\e154" +} +.glyphicon-sort-by-attributes:before{ + content:"\e155" +} +.glyphicon-sort-by-attributes-alt:before{ + content:"\e156" +} +.glyphicon-unchecked:before{ + content:"\e157" +} +.glyphicon-expand:before{ + content:"\e158" +} +.glyphicon-collapse-down:before{ + content:"\e159" +} +.glyphicon-collapse-up:before{ + content:"\e160" +} +.glyphicon-log-in:before{ + content:"\e161" +} +.glyphicon-flash:before{ + content:"\e162" +} +.glyphicon-log-out:before{ + content:"\e163" +} +.glyphicon-new-window:before{ + content:"\e164" +} +.glyphicon-record:before{ + content:"\e165" +} +.glyphicon-save:before{ + content:"\e166" +} +.glyphicon-open:before{ + content:"\e167" +} +.glyphicon-saved:before{ + content:"\e168" +} +.glyphicon-import:before{ + content:"\e169" +} +.glyphicon-export:before{ + content:"\e170" +} +.glyphicon-send:before{ + content:"\e171" +} +.glyphicon-floppy-disk:before{ + content:"\e172" +} +.glyphicon-floppy-saved:before{ + content:"\e173" +} +.glyphicon-floppy-remove:before{ + content:"\e174" +} +.glyphicon-floppy-save:before{ + content:"\e175" +} +.glyphicon-floppy-open:before{ + content:"\e176" +} +.glyphicon-credit-card:before{ + content:"\e177" +} +.glyphicon-transfer:before{ + content:"\e178" +} +.glyphicon-cutlery:before{ + content:"\e179" +} +.glyphicon-header:before{ + content:"\e180" +} +.glyphicon-compressed:before{ + content:"\e181" +} +.glyphicon-earphone:before{ + content:"\e182" +} +.glyphicon-phone-alt:before{ + content:"\e183" +} +.glyphicon-tower:before{ + content:"\e184" +} +.glyphicon-stats:before{ + content:"\e185" +} +.glyphicon-sd-video:before{ + content:"\e186" +} +.glyphicon-hd-video:before{ + content:"\e187" +} +.glyphicon-subtitles:before{ + content:"\e188" +} +.glyphicon-sound-stereo:before{ + content:"\e189" +} +.glyphicon-sound-dolby:before{ + content:"\e190" +} +.glyphicon-sound-5-1:before{ + content:"\e191" +} +.glyphicon-sound-6-1:before{ + content:"\e192" +} +.glyphicon-sound-7-1:before{ + content:"\e193" +} +.glyphicon-copyright-mark:before{ + content:"\e194" +} +.glyphicon-registration-mark:before{ + content:"\e195" +} +.glyphicon-cloud-download:before{ + content:"\e197" +} +.glyphicon-cloud-upload:before{ + content:"\e198" +} +.glyphicon-tree-conifer:before{ + content:"\e199" +} +.glyphicon-tree-deciduous:before{ + content:"\e200" +} +.glyphicon-cd:before{ + content:"\e201" +} +.glyphicon-save-file:before{ + content:"\e202" +} +.glyphicon-open-file:before{ + content:"\e203" +} +.glyphicon-level-up:before{ + content:"\e204" +} +.glyphicon-copy:before{ + content:"\e205" +} +.glyphicon-paste:before{ + content:"\e206" +} +.glyphicon-alert:before{ + content:"\e209" +} +.glyphicon-equalizer:before{ + content:"\e210" +} +.glyphicon-king:before{ + content:"\e211" +} +.glyphicon-queen:before{ + content:"\e212" +} +.glyphicon-pawn:before{ + content:"\e213" +} +.glyphicon-bishop:before{ + content:"\e214" +} +.glyphicon-knight:before{ + content:"\e215" +} +.glyphicon-baby-formula:before{ + content:"\e216" +} +.glyphicon-tent:before{ + content:"\26fa" +} +.glyphicon-blackboard:before{ + content:"\e218" +} +.glyphicon-bed:before{ + content:"\e219" +} +.glyphicon-apple:before{ + content:"\f8ff" +} +.glyphicon-erase:before{ + content:"\e221" +} +.glyphicon-hourglass:before{ + content:"\231b" +} +.glyphicon-lamp:before{ + content:"\e223" +} +.glyphicon-duplicate:before{ + content:"\e224" +} +.glyphicon-piggy-bank:before{ + content:"\e225" +} +.glyphicon-scissors:before{ + content:"\e226" +} +.glyphicon-bitcoin:before{ + content:"\e227" +} +.glyphicon-btc:before{ + content:"\e227" +} +.glyphicon-xbt:before{ + content:"\e227" +} +.glyphicon-yen:before{ + content:"\00a5" +} +.glyphicon-jpy:before{ + content:"\00a5" +} +.glyphicon-ruble:before{ + content:"\20bd" +} +.glyphicon-rub:before{ + content:"\20bd" +} +.glyphicon-scale:before{ + content:"\e230" +} +.glyphicon-ice-lolly:before{ + content:"\e231" +} +.glyphicon-ice-lolly-tasted:before{ + content:"\e232" +} +.glyphicon-education:before{ + content:"\e233" +} +.glyphicon-option-horizontal:before{ + content:"\e234" +} +.glyphicon-option-vertical:before{ + content:"\e235" +} +.glyphicon-menu-hamburger:before{ + content:"\e236" +} +.glyphicon-modal-window:before{ + content:"\e237" +} +.glyphicon-oil:before{ + content:"\e238" +} +.glyphicon-grain:before{ + content:"\e239" +} +.glyphicon-sunglasses:before{ + content:"\e240" +} +.glyphicon-text-size:before{ + content:"\e241" +} +.glyphicon-text-color:before{ + content:"\e242" +} +.glyphicon-text-background:before{ + content:"\e243" +} +.glyphicon-object-align-top:before{ + content:"\e244" +} +.glyphicon-object-align-bottom:before{ + content:"\e245" +} +.glyphicon-object-align-horizontal:before{ + content:"\e246" +} +.glyphicon-object-align-left:before{ + content:"\e247" +} +.glyphicon-object-align-vertical:before{ + content:"\e248" +} +.glyphicon-object-align-right:before{ + content:"\e249" +} +.glyphicon-triangle-right:before{ + content:"\e250" +} +.glyphicon-triangle-left:before{ + content:"\e251" +} +.glyphicon-triangle-bottom:before{ + content:"\e252" +} +.glyphicon-triangle-top:before{ + content:"\e253" +} +.glyphicon-console:before{ + content:"\e254" +} +.glyphicon-superscript:before{ + content:"\e255" +} +.glyphicon-subscript:before{ + content:"\e256" +} +.glyphicon-menu-left:before{ + content:"\e257" +} +.glyphicon-menu-right:before{ + content:"\e258" +} +.glyphicon-menu-down:before{ + content:"\e259" +} +.glyphicon-menu-up:before{ + content:"\e260" +} +*{ + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box +} +:after,:before{ + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box +} +html{ + font-size:10px; + -webkit-tap-highlight-color:rgba(0,0,0,0) +} +body{ + font-family:"Helvetica Neue",Helvetica,Arial,sans-serif; + font-size:14px; + line-height:1.42857143; + color:#333; + background-color:#fff +} +button,input,select,textarea{ + font-family:inherit; + font-size:inherit; + line-height:inherit +} +a{ + color:#337ab7; + text-decoration:none +} +a:focus,a:hover{ + color:#23527c; + text-decoration:underline +} +a:focus{ + outline:5px auto -webkit-focus-ring-color; + outline-offset:-2px +} +figure{ + margin:0 +} +img{ + vertical-align:middle +} +.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{ + display:block; + max-width:100%; + height:auto +} +.img-rounded{ + border-radius:6px +} +.img-thumbnail{ + display:inline-block; + max-width:100%; + height:auto; + padding:4px; + line-height:1.42857143; + background-color:#fff; + border:1px solid #ddd; + border-radius:4px; + -webkit-transition:all .2s ease-in-out; + -o-transition:all .2s ease-in-out; + transition:all .2s ease-in-out +} +.img-circle{ + border-radius:50% +} +hr{ + margin-top:20px; + margin-bottom:20px; + border:0; + border-top:1px solid #eee +} +.sr-only{ + position:absolute; + width:1px; + height:1px; + padding:0; + margin:-1px; + overflow:hidden; + clip:rect(0,0,0,0); + border:0 +} +.sr-only-focusable:active,.sr-only-focusable:focus{ + position:static; + width:auto; + height:auto; + margin:0; + overflow:visible; + clip:auto +} +[role=button]{ + cursor:pointer +} +.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{ + font-family:inherit; + font-weight:500; + line-height:1.1; + color:inherit +} +.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{ + font-weight:400; + line-height:1; + color:#777 +} +.h1,.h2,.h3,h1,h2,h3{ + margin-top:20px; + margin-bottom:10px +} +.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{ + font-size:65% +} +.h4,.h5,.h6,h4,h5,h6{ + margin-top:10px; + margin-bottom:10px +} +.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{ + font-size:75% +} +.h1,h1{ + font-size:36px +} +.h2,h2{ + font-size:30px +} +.h3,h3{ + font-size:24px +} +.h4,h4{ + font-size:18px +} +.h5,h5{ + font-size:14px +} +.h6,h6{ + font-size:12px +} +p{ + margin:0 0 10px +} +.lead{ + margin-bottom:20px; + font-size:16px; + font-weight:300; + line-height:1.4 +} +@media (min-width:768px){ + .lead{ + font-size:21px + } +} +.small,small{ + font-size:85% +} +.mark,mark{ + padding:.2em; + background-color:#fcf8e3 +} +.text-left{ + text-align:left +} +.text-right{ + text-align:right +} +.text-center{ + text-align:center +} +.text-justify{ + text-align:justify +} +.text-nowrap{ + white-space:nowrap +} +.text-lowercase{ + text-transform:lowercase +} +.text-uppercase{ + text-transform:uppercase +} +.text-capitalize{ + text-transform:capitalize +} +.text-muted{ + color:#777 +} +.text-primary{ + color:#337ab7 +} +a.text-primary:focus,a.text-primary:hover{ + color:#286090 +} +.text-success{ + color:#3c763d +} +a.text-success:focus,a.text-success:hover{ + color:#2b542c +} +.text-info{ + color:#31708f +} +a.text-info:focus,a.text-info:hover{ + color:#245269 +} +.text-warning{ + color:#8a6d3b +} +a.text-warning:focus,a.text-warning:hover{ + color:#66512c +} +.text-danger{ + color:#a94442 +} +a.text-danger:focus,a.text-danger:hover{ + color:#843534 +} +.bg-primary{ + color:#fff; + background-color:#337ab7 +} +a.bg-primary:focus,a.bg-primary:hover{ + background-color:#286090 +} +.bg-success{ + background-color:#dff0d8 +} +a.bg-success:focus,a.bg-success:hover{ + background-color:#c1e2b3 +} +.bg-info{ + background-color:#d9edf7 +} +a.bg-info:focus,a.bg-info:hover{ + background-color:#afd9ee +} +.bg-warning{ + background-color:#fcf8e3 +} +a.bg-warning:focus,a.bg-warning:hover{ + background-color:#f7ecb5 +} +.bg-danger{ + background-color:#f2dede +} +a.bg-danger:focus,a.bg-danger:hover{ + background-color:#e4b9b9 +} +.page-header{ + padding-bottom:9px; + margin:40px 0 20px; + border-bottom:1px solid #eee +} +ol,ul{ + margin-top:0; + margin-bottom:10px +} +ol ol,ol ul,ul ol,ul ul{ + margin-bottom:0 +} +.list-unstyled{ + padding-left:0; + list-style:none +} +.list-inline{ + padding-left:0; + margin-left:-5px; + list-style:none +} +.list-inline>li{ + display:inline-block; + padding-right:5px; + padding-left:5px +} +dl{ + margin-top:0; + margin-bottom:20px +} +dd,dt{ + line-height:1.42857143 +} +dt{ + font-weight:700 +} +dd{ + margin-left:0 +} +@media (min-width:768px){ + .dl-horizontal dt{ + float:left; + width:160px; + overflow:hidden; + clear:left; + text-align:right; + text-overflow:ellipsis; + white-space:nowrap + } + .dl-horizontal dd{ + margin-left:180px + } +} +abbr[data-original-title],abbr[title]{ + cursor:help; + border-bottom:1px dotted #777 +} +.initialism{ + font-size:90%; + text-transform:uppercase +} +blockquote{ + padding:10px 20px; + margin:0 0 20px; + font-size:17.5px; + border-left:5px solid #eee +} +blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{ + margin-bottom:0 +} +blockquote .small,blockquote footer,blockquote small{ + display:block; + font-size:80%; + line-height:1.42857143; + color:#777 +} +blockquote .small:before,blockquote footer:before,blockquote small:before{ + content:'\2014 \00A0' +} +.blockquote-reverse,blockquote.pull-right{ + padding-right:15px; + padding-left:0; + text-align:right; + border-right:5px solid #eee; + border-left:0 +} +.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{ + content:'' +} +.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{ + content:'\00A0 \2014' +} +address{ + margin-bottom:20px; + font-style:normal; + line-height:1.42857143 +} +code,kbd,pre,samp{ + font-family:Menlo,Monaco,Consolas,"Courier New",monospace +} +code{ + padding:2px 4px; + font-size:90%; + color:#c7254e; + background-color:#f9f2f4; + border-radius:4px +} +kbd{ + padding:2px 4px; + font-size:90%; + color:#fff; + background-color:#333; + border-radius:3px; + -webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25); + box-shadow:inset 0 -1px 0 rgba(0,0,0,.25) +} +kbd kbd{ + padding:0; + font-size:100%; + font-weight:700; + -webkit-box-shadow:none; + box-shadow:none +} +pre{ + display:block; + padding:9.5px; + margin:0 0 10px; + font-size:13px; + line-height:1.42857143; + color:#333; + word-break:break-all; + word-wrap:break-word; + background-color:#f5f5f5; + border:1px solid #ccc; + border-radius:4px +} +pre code{ + padding:0; + font-size:inherit; + color:inherit; + white-space:pre-wrap; + background-color:transparent; + border-radius:0 +} +.pre-scrollable{ + max-height:340px; + overflow-y:scroll +} +.container{ + padding-right:15px; + padding-left:15px; + margin-right:auto; + margin-left:auto +} +@media (min-width:768px){ + .container{ + width:750px + } +} +@media (min-width:992px){ + .container{ + width:970px + } +} +@media (min-width:1200px){ + .container{ + width:1170px + } +} +.container-fluid{ + padding-right:15px; + padding-left:15px; + margin-right:auto; + margin-left:auto +} +.row{ + margin-right:-15px; + margin-left:-15px +} +.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{ + position:relative; + min-height:1px; + padding-right:15px; + padding-left:15px +} +.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{ + float:left +} +.col-xs-12{ + width:100% +} +.col-xs-11{ + width:91.66666667% +} +.col-xs-10{ + width:83.33333333% +} +.col-xs-9{ + width:75% +} +.col-xs-8{ + width:66.66666667% +} +.col-xs-7{ + width:58.33333333% +} +.col-xs-6{ + width:50% +} +.col-xs-5{ + width:41.66666667% +} +.col-xs-4{ + width:33.33333333% +} +.col-xs-3{ + width:25% +} +.col-xs-2{ + width:16.66666667% +} +.col-xs-1{ + width:8.33333333% +} +.col-xs-pull-12{ + right:100% +} +.col-xs-pull-11{ + right:91.66666667% +} +.col-xs-pull-10{ + right:83.33333333% +} +.col-xs-pull-9{ + right:75% +} +.col-xs-pull-8{ + right:66.66666667% +} +.col-xs-pull-7{ + right:58.33333333% +} +.col-xs-pull-6{ + right:50% +} +.col-xs-pull-5{ + right:41.66666667% +} +.col-xs-pull-4{ + right:33.33333333% +} +.col-xs-pull-3{ + right:25% +} +.col-xs-pull-2{ + right:16.66666667% +} +.col-xs-pull-1{ + right:8.33333333% +} +.col-xs-pull-0{ + right:auto +} +.col-xs-push-12{ + left:100% +} +.col-xs-push-11{ + left:91.66666667% +} +.col-xs-push-10{ + left:83.33333333% +} +.col-xs-push-9{ + left:75% +} +.col-xs-push-8{ + left:66.66666667% +} +.col-xs-push-7{ + left:58.33333333% +} +.col-xs-push-6{ + left:50% +} +.col-xs-push-5{ + left:41.66666667% +} +.col-xs-push-4{ + left:33.33333333% +} +.col-xs-push-3{ + left:25% +} +.col-xs-push-2{ + left:16.66666667% +} +.col-xs-push-1{ + left:8.33333333% +} +.col-xs-push-0{ + left:auto +} +.col-xs-offset-12{ + margin-left:100% +} +.col-xs-offset-11{ + margin-left:91.66666667% +} +.col-xs-offset-10{ + margin-left:83.33333333% +} +.col-xs-offset-9{ + margin-left:75% +} +.col-xs-offset-8{ + margin-left:66.66666667% +} +.col-xs-offset-7{ + margin-left:58.33333333% +} +.col-xs-offset-6{ + margin-left:50% +} +.col-xs-offset-5{ + margin-left:41.66666667% +} +.col-xs-offset-4{ + margin-left:33.33333333% +} +.col-xs-offset-3{ + margin-left:25% +} +.col-xs-offset-2{ + margin-left:16.66666667% +} +.col-xs-offset-1{ + margin-left:8.33333333% +} +.col-xs-offset-0{ + margin-left:0 +} +@media (min-width:768px){ + .col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{ + float:left + } + .col-sm-12{ + width:100% + } + .col-sm-11{ + width:91.66666667% + } + .col-sm-10{ + width:83.33333333% + } + .col-sm-9{ + width:75% + } + .col-sm-8{ + width:66.66666667% + } + .col-sm-7{ + width:58.33333333% + } + .col-sm-6{ + width:50% + } + .col-sm-5{ + width:41.66666667% + } + .col-sm-4{ + width:33.33333333% + } + .col-sm-3{ + width:25% + } + .col-sm-2{ + width:16.66666667% + } + .col-sm-1{ + width:8.33333333% + } + .col-sm-pull-12{ + right:100% + } + .col-sm-pull-11{ + right:91.66666667% + } + .col-sm-pull-10{ + right:83.33333333% + } + .col-sm-pull-9{ + right:75% + } + .col-sm-pull-8{ + right:66.66666667% + } + .col-sm-pull-7{ + right:58.33333333% + } + .col-sm-pull-6{ + right:50% + } + .col-sm-pull-5{ + right:41.66666667% + } + .col-sm-pull-4{ + right:33.33333333% + } + .col-sm-pull-3{ + right:25% + } + .col-sm-pull-2{ + right:16.66666667% + } + .col-sm-pull-1{ + right:8.33333333% + } + .col-sm-pull-0{ + right:auto + } + .col-sm-push-12{ + left:100% + } + .col-sm-push-11{ + left:91.66666667% + } + .col-sm-push-10{ + left:83.33333333% + } + .col-sm-push-9{ + left:75% + } + .col-sm-push-8{ + left:66.66666667% + } + .col-sm-push-7{ + left:58.33333333% + } + .col-sm-push-6{ + left:50% + } + .col-sm-push-5{ + left:41.66666667% + } + .col-sm-push-4{ + left:33.33333333% + } + .col-sm-push-3{ + left:25% + } + .col-sm-push-2{ + left:16.66666667% + } + .col-sm-push-1{ + left:8.33333333% + } + .col-sm-push-0{ + left:auto + } + .col-sm-offset-12{ + margin-left:100% + } + .col-sm-offset-11{ + margin-left:91.66666667% + } + .col-sm-offset-10{ + margin-left:83.33333333% + } + .col-sm-offset-9{ + margin-left:75% + } + .col-sm-offset-8{ + margin-left:66.66666667% + } + .col-sm-offset-7{ + margin-left:58.33333333% + } + .col-sm-offset-6{ + margin-left:50% + } + .col-sm-offset-5{ + margin-left:41.66666667% + } + .col-sm-offset-4{ + margin-left:33.33333333% + } + .col-sm-offset-3{ + margin-left:25% + } + .col-sm-offset-2{ + margin-left:16.66666667% + } + .col-sm-offset-1{ + margin-left:8.33333333% + } + .col-sm-offset-0{ + margin-left:0 + } +} +@media (min-width:992px){ + .col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{ + float:left + } + .col-md-12{ + width:100% + } + .col-md-11{ + width:91.66666667% + } + .col-md-10{ + width:83.33333333% + } + .col-md-9{ + width:75% + } + .col-md-8{ + width:66.66666667% + } + .col-md-7{ + width:58.33333333% + } + .col-md-6{ + width:50% + } + .col-md-5{ + width:41.66666667% + } + .col-md-4{ + width:33.33333333% + } + .col-md-3{ + width:25% + } + .col-md-2{ + width:16.66666667% + } + .col-md-1{ + width:8.33333333% + } + .col-md-pull-12{ + right:100% + } + .col-md-pull-11{ + right:91.66666667% + } + .col-md-pull-10{ + right:83.33333333% + } + .col-md-pull-9{ + right:75% + } + .col-md-pull-8{ + right:66.66666667% + } + .col-md-pull-7{ + right:58.33333333% + } + .col-md-pull-6{ + right:50% + } + .col-md-pull-5{ + right:41.66666667% + } + .col-md-pull-4{ + right:33.33333333% + } + .col-md-pull-3{ + right:25% + } + .col-md-pull-2{ + right:16.66666667% + } + .col-md-pull-1{ + right:8.33333333% + } + .col-md-pull-0{ + right:auto + } + .col-md-push-12{ + left:100% + } + .col-md-push-11{ + left:91.66666667% + } + .col-md-push-10{ + left:83.33333333% + } + .col-md-push-9{ + left:75% + } + .col-md-push-8{ + left:66.66666667% + } + .col-md-push-7{ + left:58.33333333% + } + .col-md-push-6{ + left:50% + } + .col-md-push-5{ + left:41.66666667% + } + .col-md-push-4{ + left:33.33333333% + } + .col-md-push-3{ + left:25% + } + .col-md-push-2{ + left:16.66666667% + } + .col-md-push-1{ + left:8.33333333% + } + .col-md-push-0{ + left:auto + } + .col-md-offset-12{ + margin-left:100% + } + .col-md-offset-11{ + margin-left:91.66666667% + } + .col-md-offset-10{ + margin-left:83.33333333% + } + .col-md-offset-9{ + margin-left:75% + } + .col-md-offset-8{ + margin-left:66.66666667% + } + .col-md-offset-7{ + margin-left:58.33333333% + } + .col-md-offset-6{ + margin-left:50% + } + .col-md-offset-5{ + margin-left:41.66666667% + } + .col-md-offset-4{ + margin-left:33.33333333% + } + .col-md-offset-3{ + margin-left:25% + } + .col-md-offset-2{ + margin-left:16.66666667% + } + .col-md-offset-1{ + margin-left:8.33333333% + } + .col-md-offset-0{ + margin-left:0 + } +} +@media (min-width:1200px){ + .col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{ + float:left + } + .col-lg-12{ + width:100% + } + .col-lg-11{ + width:91.66666667% + } + .col-lg-10{ + width:83.33333333% + } + .col-lg-9{ + width:75% + } + .col-lg-8{ + width:66.66666667% + } + .col-lg-7{ + width:58.33333333% + } + .col-lg-6{ + width:50% + } + .col-lg-5{ + width:41.66666667% + } + .col-lg-4{ + width:33.33333333% + } + .col-lg-3{ + width:25% + } + .col-lg-2{ + width:16.66666667% + } + .col-lg-1{ + width:8.33333333% + } + .col-lg-pull-12{ + right:100% + } + .col-lg-pull-11{ + right:91.66666667% + } + .col-lg-pull-10{ + right:83.33333333% + } + .col-lg-pull-9{ + right:75% + } + .col-lg-pull-8{ + right:66.66666667% + } + .col-lg-pull-7{ + right:58.33333333% + } + .col-lg-pull-6{ + right:50% + } + .col-lg-pull-5{ + right:41.66666667% + } + .col-lg-pull-4{ + right:33.33333333% + } + .col-lg-pull-3{ + right:25% + } + .col-lg-pull-2{ + right:16.66666667% + } + .col-lg-pull-1{ + right:8.33333333% + } + .col-lg-pull-0{ + right:auto + } + .col-lg-push-12{ + left:100% + } + .col-lg-push-11{ + left:91.66666667% + } + .col-lg-push-10{ + left:83.33333333% + } + .col-lg-push-9{ + left:75% + } + .col-lg-push-8{ + left:66.66666667% + } + .col-lg-push-7{ + left:58.33333333% + } + .col-lg-push-6{ + left:50% + } + .col-lg-push-5{ + left:41.66666667% + } + .col-lg-push-4{ + left:33.33333333% + } + .col-lg-push-3{ + left:25% + } + .col-lg-push-2{ + left:16.66666667% + } + .col-lg-push-1{ + left:8.33333333% + } + .col-lg-push-0{ + left:auto + } + .col-lg-offset-12{ + margin-left:100% + } + .col-lg-offset-11{ + margin-left:91.66666667% + } + .col-lg-offset-10{ + margin-left:83.33333333% + } + .col-lg-offset-9{ + margin-left:75% + } + .col-lg-offset-8{ + margin-left:66.66666667% + } + .col-lg-offset-7{ + margin-left:58.33333333% + } + .col-lg-offset-6{ + margin-left:50% + } + .col-lg-offset-5{ + margin-left:41.66666667% + } + .col-lg-offset-4{ + margin-left:33.33333333% + } + .col-lg-offset-3{ + margin-left:25% + } + .col-lg-offset-2{ + margin-left:16.66666667% + } + .col-lg-offset-1{ + margin-left:8.33333333% + } + .col-lg-offset-0{ + margin-left:0 + } +} +table{ + background-color:transparent +} +caption{ + padding-top:8px; + padding-bottom:8px; + color:#777; + text-align:left +} +th{ + text-align:left +} +.table{ + width:100%; + max-width:100%; + margin-bottom:20px +} +.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{ + padding:8px; + line-height:1.42857143; + vertical-align:top; + border-top:1px solid #ddd +} +.table>thead>tr>th{ + vertical-align:bottom; + border-bottom:2px solid #ddd +} +.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{ + border-top:0 +} +.table>tbody+tbody{ + border-top:2px solid #ddd +} +.table .table{ + background-color:#fff +} +.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{ + padding:5px +} +.table-bordered{ + border:1px solid #ddd +} +.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{ + border:1px solid #ddd +} +.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{ + border-bottom-width:2px +} +.table-striped>tbody>tr:nth-of-type(odd){ + background-color:#f9f9f9 +} +.table-hover>tbody>tr:hover{ + background-color:#f5f5f5 +} +table col[class*=col-]{ + position:static; + display:table-column; + float:none +} +table td[class*=col-],table th[class*=col-]{ + position:static; + display:table-cell; + float:none +} +.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{ + background-color:#f5f5f5 +} +.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{ + background-color:#e8e8e8 +} +.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{ + background-color:#dff0d8 +} +.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{ + background-color:#d0e9c6 +} +.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{ + background-color:#d9edf7 +} +.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{ + background-color:#c4e3f3 +} +.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{ + background-color:#fcf8e3 +} +.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{ + background-color:#faf2cc +} +.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{ + background-color:#f2dede +} +.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{ + background-color:#ebcccc +} +.table-responsive{ + min-height:.01%; + overflow-x:auto +} +@media screen and (max-width:767px){ + .table-responsive{ + width:100%; + margin-bottom:15px; + overflow-y:hidden; + -ms-overflow-style:-ms-autohiding-scrollbar; + border:1px solid #ddd + } + .table-responsive>.table{ + margin-bottom:0 + } + .table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{ + white-space:nowrap + } + .table-responsive>.table-bordered{ + border:0 + } + .table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{ + border-left:0 + } + .table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{ + border-right:0 + } + .table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{ + border-bottom:0 + } +} +fieldset{ + min-width:0; + padding:0; + margin:0; + border:0 +} +legend{ + display:block; + width:100%; + padding:0; + margin-bottom:20px; + font-size:21px; + line-height:inherit; + color:#333; + border:0; + border-bottom:1px solid #e5e5e5 +} +label{ + display:inline-block; + max-width:100%; + margin-bottom:5px; + font-weight:700 +} +input[type=search]{ + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box +} +input[type=checkbox],input[type=radio]{ + margin:4px 0 0; + margin-top:1px\9; + line-height:normal +} +input[type=file]{ + display:block +} +input[type=range]{ + display:block; + width:100% +} +select[multiple],select[size]{ + height:auto +} +input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{ + outline:5px auto -webkit-focus-ring-color; + outline-offset:-2px +} +output{ + display:block; + padding-top:7px; + font-size:14px; + line-height:1.42857143; + color:#555 +} +.form-control{ + display:block; + width:100%; + height:34px; + padding:6px 12px; + font-size:14px; + line-height:1.42857143; + color:#555; + background-color:#fff; + background-image:none; + border:1px solid #ccc; + border-radius:4px; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075); + box-shadow:inset 0 1px 1px rgba(0,0,0,.075); + -webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s; + -o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s; + transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s +} +.form-control:focus{ + border-color:#66afe9; + outline:0; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6); + box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6) +} +.form-control::-moz-placeholder{ + color:#999; + opacity:1 +} +.form-control:-ms-input-placeholder{ + color:#999 +} +.form-control::-webkit-input-placeholder{ + color:#999 +} +.form-control::-ms-expand{ + background-color:transparent; + border:0 +} +.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{ + background-color:#eee; + opacity:1 +} +.form-control[disabled],fieldset[disabled] .form-control{ + cursor:not-allowed +} +textarea.form-control{ + height:auto +} +input[type=search]{ + -webkit-appearance:none +} +@media screen and (-webkit-min-device-pixel-ratio:0){ + input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{ + line-height:34px + } + .input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{ + line-height:30px + } + .input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{ + line-height:46px + } +} +.form-group{ + margin-bottom:15px +} +.checkbox,.radio{ + position:relative; + display:block; + margin-top:10px; + margin-bottom:10px +} +.checkbox label,.radio label{ + min-height:20px; + padding-left:20px; + margin-bottom:0; + font-weight:400; + cursor:pointer +} +.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{ + position:absolute; + margin-top:4px\9; + margin-left:-20px +} +.checkbox+.checkbox,.radio+.radio{ + margin-top:-5px +} +.checkbox-inline,.radio-inline{ + position:relative; + display:inline-block; + padding-left:20px; + margin-bottom:0; + font-weight:400; + vertical-align:middle; + cursor:pointer +} +.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{ + margin-top:0; + margin-left:10px +} +fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{ + cursor:not-allowed +} +.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{ + cursor:not-allowed +} +.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{ + cursor:not-allowed +} +.form-control-static{ + min-height:34px; + padding-top:7px; + padding-bottom:7px; + margin-bottom:0 +} +.form-control-static.input-lg,.form-control-static.input-sm{ + padding-right:0; + padding-left:0 +} +.input-sm{ + height:30px; + padding:5px 10px; + font-size:12px; + line-height:1.5; + border-radius:3px +} +select.input-sm{ + height:30px; + line-height:30px +} +select[multiple].input-sm,textarea.input-sm{ + height:auto +} +.form-group-sm .form-control{ + height:30px; + padding:5px 10px; + font-size:12px; + line-height:1.5; + border-radius:3px +} +.form-group-sm select.form-control{ + height:30px; + line-height:30px +} +.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{ + height:auto +} +.form-group-sm .form-control-static{ + height:30px; + min-height:32px; + padding:6px 10px; + font-size:12px; + line-height:1.5 +} +.input-lg{ + height:46px; + padding:10px 16px; + font-size:18px; + line-height:1.3333333; + border-radius:6px +} +select.input-lg{ + height:46px; + line-height:46px +} +select[multiple].input-lg,textarea.input-lg{ + height:auto +} +.form-group-lg .form-control{ + height:46px; + padding:10px 16px; + font-size:18px; + line-height:1.3333333; + border-radius:6px +} +.form-group-lg select.form-control{ + height:46px; + line-height:46px +} +.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{ + height:auto +} +.form-group-lg .form-control-static{ + height:46px; + min-height:38px; + padding:11px 16px; + font-size:18px; + line-height:1.3333333 +} +.has-feedback{ + position:relative +} +.has-feedback .form-control{ + padding-right:42.5px +} +.form-control-feedback{ + position:absolute; + top:0; + right:0; + z-index:2; + display:block; + width:34px; + height:34px; + line-height:34px; + text-align:center; + pointer-events:none +} +.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{ + width:46px; + height:46px; + line-height:46px +} +.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{ + width:30px; + height:30px; + line-height:30px +} +.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{ + color:#3c763d +} +.has-success .form-control{ + border-color:#3c763d; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075); + box-shadow:inset 0 1px 1px rgba(0,0,0,.075) +} +.has-success .form-control:focus{ + border-color:#2b542c; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168; + box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168 +} +.has-success .input-group-addon{ + color:#3c763d; + background-color:#dff0d8; + border-color:#3c763d +} +.has-success .form-control-feedback{ + color:#3c763d +} +.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{ + color:#8a6d3b +} +.has-warning .form-control{ + border-color:#8a6d3b; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075); + box-shadow:inset 0 1px 1px rgba(0,0,0,.075) +} +.has-warning .form-control:focus{ + border-color:#66512c; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b; + box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b +} +.has-warning .input-group-addon{ + color:#8a6d3b; + background-color:#fcf8e3; + border-color:#8a6d3b +} +.has-warning .form-control-feedback{ + color:#8a6d3b +} +.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{ + color:#a94442 +} +.has-error .form-control{ + border-color:#a94442; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075); + box-shadow:inset 0 1px 1px rgba(0,0,0,.075) +} +.has-error .form-control:focus{ + border-color:#843534; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483; + box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483 +} +.has-error .input-group-addon{ + color:#a94442; + background-color:#f2dede; + border-color:#a94442 +} +.has-error .form-control-feedback{ + color:#a94442 +} +.has-feedback label~.form-control-feedback{ + top:25px +} +.has-feedback label.sr-only~.form-control-feedback{ + top:0 +} +.help-block{ + display:block; + margin-top:5px; + margin-bottom:10px; + color:#737373 +} +@media (min-width:768px){ + .form-inline .form-group{ + display:inline-block; + margin-bottom:0; + vertical-align:middle + } + .form-inline .form-control{ + display:inline-block; + width:auto; + vertical-align:middle + } + .form-inline .form-control-static{ + display:inline-block + } + .form-inline .input-group{ + display:inline-table; + vertical-align:middle + } + .form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{ + width:auto + } + .form-inline .input-group>.form-control{ + width:100% + } + .form-inline .control-label{ + margin-bottom:0; + vertical-align:middle + } + .form-inline .checkbox,.form-inline .radio{ + display:inline-block; + margin-top:0; + margin-bottom:0; + vertical-align:middle + } + .form-inline .checkbox label,.form-inline .radio label{ + padding-left:0 + } + .form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{ + position:relative; + margin-left:0 + } + .form-inline .has-feedback .form-control-feedback{ + top:0 + } +} +.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{ + padding-top:7px; + margin-top:0; + margin-bottom:0 +} +.form-horizontal .checkbox,.form-horizontal .radio{ + min-height:27px +} +.form-horizontal .form-group{ + margin-right:-15px; + margin-left:-15px +} +@media (min-width:768px){ + .form-horizontal .control-label{ + padding-top:7px; + margin-bottom:0; + text-align:right + } +} +.form-horizontal .has-feedback .form-control-feedback{ + right:15px +} +@media (min-width:768px){ + .form-horizontal .form-group-lg .control-label{ + padding-top:11px; + font-size:18px + } +} +@media (min-width:768px){ + .form-horizontal .form-group-sm .control-label{ + padding-top:6px; + font-size:12px + } +} +.btn{ + display:inline-block; + padding:6px 12px; + margin-bottom:0; + font-size:14px; + font-weight:400; + line-height:1.42857143; + text-align:center; + white-space:nowrap; + vertical-align:middle; + -ms-touch-action:manipulation; + touch-action:manipulation; + cursor:pointer; + -webkit-user-select:none; + -moz-user-select:none; + -ms-user-select:none; + user-select:none; + background-image:none; + border:1px solid transparent; + border-radius:4px +} +.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{ + outline:5px auto -webkit-focus-ring-color; + outline-offset:-2px +} +.btn.focus,.btn:focus,.btn:hover{ + color:#333; + text-decoration:none +} +.btn.active,.btn:active{ + background-image:none; + outline:0; + -webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125); + box-shadow:inset 0 3px 5px rgba(0,0,0,.125) +} +.btn.disabled,.btn[disabled],fieldset[disabled] .btn{ + cursor:not-allowed; + filter:alpha(opacity=65); + -webkit-box-shadow:none; + box-shadow:none; + opacity:.65 +} +a.btn.disabled,fieldset[disabled] a.btn{ + pointer-events:none +} +.btn-default{ + color:#333; + background-color:#fff; + border-color:#ccc +} +.btn-default.focus,.btn-default:focus{ + color:#333; + background-color:#e6e6e6; + border-color:#8c8c8c +} +.btn-default:hover{ + color:#333; + background-color:#e6e6e6; + border-color:#adadad +} +.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{ + color:#333; + background-color:#e6e6e6; + border-color:#adadad +} +.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{ + color:#333; + background-color:#d4d4d4; + border-color:#8c8c8c +} +.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{ + background-image:none +} +.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{ + background-color:#fff; + border-color:#ccc +} +.btn-default .badge{ + color:#fff; + background-color:#333 +} +.btn-primary{ + color:#fff; + background-color:#d11010; + border-color:#c40f0f +} +.btn-primary.focus,.btn-primary:focus{ + color:#fff; + background-color:#b20c0c; + border-color:#c40f0f +} +.btn-primary:hover{ + color:#fff; + background-color:#b20c0c; + border-color:#c40f0f +} +.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{ + color:#fff; + background-color:#b20c0c; + border-color:#c40f0f +} +.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{ + color:#fff; + background-color:#b20c0c; + border-color:#c40f0f +} +.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{ + background-image:none +} +.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{ + background-color:#b20c0c; + border-color:#c40f0f +} +.btn-primary .badge{ + color:#337ab7; + background-color:#fff +} +.btn-success{ + color:#fff; + background-color:#5cb85c; + border-color:#4cae4c +} +.btn-success.focus,.btn-success:focus{ + color:#fff; + background-color:#449d44; + border-color:#255625 +} +.btn-success:hover{ + color:#fff; + background-color:#449d44; + border-color:#398439 +} +.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{ + color:#fff; + background-color:#449d44; + border-color:#398439 +} +.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{ + color:#fff; + background-color:#398439; + border-color:#255625 +} +.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{ + background-image:none +} +.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{ + background-color:#5cb85c; + border-color:#4cae4c +} +.btn-success .badge{ + color:#5cb85c; + background-color:#fff +} +.btn-info{ + color:#fff; + background-color:#5bc0de; + border-color:#46b8da +} +.btn-info.focus,.btn-info:focus{ + color:#fff; + background-color:#31b0d5; + border-color:#1b6d85 +} +.btn-info:hover{ + color:#fff; + background-color:#31b0d5; + border-color:#269abc +} +.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{ + color:#fff; + background-color:#31b0d5; + border-color:#269abc +} +.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{ + color:#fff; + background-color:#269abc; + border-color:#1b6d85 +} +.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{ + background-image:none +} +.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{ + background-color:#5bc0de; + border-color:#46b8da +} +.btn-info .badge{ + color:#5bc0de; + background-color:#fff +} +.btn-warning{ + color:#fff; + background-color:#f0ad4e; + border-color:#eea236 +} +.btn-warning.focus,.btn-warning:focus{ + color:#fff; + background-color:#ec971f; + border-color:#985f0d +} +.btn-warning:hover{ + color:#fff; + background-color:#ec971f; + border-color:#d58512 +} +.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{ + color:#fff; + background-color:#ec971f; + border-color:#d58512 +} +.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{ + color:#fff; + background-color:#d58512; + border-color:#985f0d +} +.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{ + background-image:none +} +.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{ + background-color:#f0ad4e; + border-color:#eea236 +} +.btn-warning .badge{ + color:#f0ad4e; + background-color:#fff +} +.btn-danger{ + color:#fff; + background-color:#d9534f; + border-color:#d43f3a +} +.btn-danger.focus,.btn-danger:focus{ + color:#fff; + background-color:#c9302c; + border-color:#761c19 +} +.btn-danger:hover{ + color:#fff; + background-color:#c9302c; + border-color:#ac2925 +} +.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{ + color:#fff; + background-color:#c9302c; + border-color:#ac2925 +} +.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{ + color:#fff; + background-color:#ac2925; + border-color:#761c19 +} +.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{ + background-image:none +} +.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{ + background-color:#d9534f; + border-color:#d43f3a +} +.btn-danger .badge{ + color:#d9534f; + background-color:#fff +} +.btn-link{ + font-weight:400; + color:#337ab7; + border-radius:0 +} +.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{ + background-color:transparent; + -webkit-box-shadow:none; + box-shadow:none +} +.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{ + border-color:transparent +} +.btn-link:focus,.btn-link:hover{ + color:#23527c; + text-decoration:underline; + background-color:transparent +} +.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{ + color:#777; + text-decoration:none +} +.btn-group-lg>.btn,.btn-lg{ + padding:10px 16px; + font-size:18px; + line-height:1.3333333; + border-radius:6px +} +.btn-group-sm>.btn,.btn-sm{ + padding:5px 10px; + font-size:12px; + line-height:1.5; + border-radius:3px +} +.btn-group-xs>.btn,.btn-xs{ + padding:1px 5px; + font-size:12px; + line-height:1.5; + border-radius:3px +} +.btn-block{ + display:block; + width:100% +} +.btn-block+.btn-block{ + margin-top:5px +} +input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{ + width:100% +} +.fade{ + opacity:0; + -webkit-transition:opacity .15s linear; + -o-transition:opacity .15s linear; + transition:opacity .15s linear +} +.fade.in{ + opacity:1 +} +.collapse{ + display:none +} +.collapse.in{ + display:block +} +tr.collapse.in{ + display:table-row +} +tbody.collapse.in{ + display:table-row-group +} +.collapsing{ + position:relative; + height:0; + overflow:hidden; + -webkit-transition-timing-function:ease; + -o-transition-timing-function:ease; + transition-timing-function:ease; + -webkit-transition-duration:.35s; + -o-transition-duration:.35s; + transition-duration:.35s; + -webkit-transition-property:height,visibility; + -o-transition-property:height,visibility; + transition-property:height,visibility +} +.caret{ + display:inline-block; + width:0; + height:0; + margin-left:2px; + vertical-align:middle; + border-top:4px dashed; + border-top:4px solid\9; + border-right:4px solid transparent; + border-left:4px solid transparent +} +.dropdown,.dropup{ + position:relative +} +.dropdown-toggle:focus{ + outline:0 +} +.dropdown-menu{ + position:absolute; + top:100%; + left:0; + z-index:1000; + display:none; + float:left; + min-width:160px; + padding:5px 0; + margin:2px 0 0; + font-size:14px; + text-align:left; + list-style:none; + background-color:#fff; + -webkit-background-clip:padding-box; + background-clip:padding-box; + border:1px solid #ccc; + border:1px solid rgba(0,0,0,.15); + border-radius:4px; + -webkit-box-shadow:0 6px 12px rgba(0,0,0,.175); + box-shadow:0 6px 12px rgba(0,0,0,.175) +} +.dropdown-menu.pull-right{ + right:0; + left:auto +} +.dropdown-menu .divider{ + height:1px; + margin:9px 0; + overflow:hidden; + background-color:#e5e5e5 +} +.dropdown-menu>li>a{ + display:block; + padding:3px 20px; + clear:both; + font-weight:400; + line-height:1.42857143; + color:#333; + white-space:nowrap +} +.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{ + color:#262626; + text-decoration:none; + background-color:#f5f5f5 +} +.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{ + color:#fff; + text-decoration:none; + background-color:#337ab7; + outline:0 +} +.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{ + color:#777 +} +.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{ + text-decoration:none; + cursor:not-allowed; + background-color:transparent; + background-image:none; + filter:progid:DXImageTransform.Microsoft.gradient(enabled=false) +} +.open>.dropdown-menu{ + display:block +} +.open>a{ + outline:0 +} +.dropdown-menu-right{ + right:0; + left:auto +} +.dropdown-menu-left{ + right:auto; + left:0 +} +.dropdown-header{ + display:block; + padding:3px 20px; + font-size:12px; + line-height:1.42857143; + color:#777; + white-space:nowrap +} +.dropdown-backdrop{ + position:fixed; + top:0; + right:0; + bottom:0; + left:0; + z-index:990 +} +.pull-right>.dropdown-menu{ + right:0; + left:auto +} +.dropup .caret,.navbar-fixed-bottom .dropdown .caret{ + content:""; + border-top:0; + border-bottom:4px dashed; + border-bottom:4px solid\9 +} +.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{ + top:auto; + bottom:100%; + margin-bottom:2px +} +@media (min-width:768px){ + .navbar-right .dropdown-menu{ + right:0; + left:auto + } + .navbar-right .dropdown-menu-left{ + right:auto; + left:0 + } +} +.btn-group,.btn-group-vertical{ + position:relative; + display:inline-block; + vertical-align:middle +} +.btn-group-vertical>.btn,.btn-group>.btn{ + position:relative; + float:left +} +.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{ + z-index:2 +} +.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{ + margin-left:-1px +} +.btn-toolbar{ + margin-left:-5px +} +.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{ + float:left +} +.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{ + margin-left:5px +} +.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){ + border-radius:0 +} +.btn-group>.btn:first-child{ + margin-left:0 +} +.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){ + border-top-right-radius:0; + border-bottom-right-radius:0 +} +.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){ + border-top-left-radius:0; + border-bottom-left-radius:0 +} +.btn-group>.btn-group{ + float:left +} +.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{ + border-radius:0 +} +.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{ + border-top-right-radius:0; + border-bottom-right-radius:0 +} +.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{ + border-top-left-radius:0; + border-bottom-left-radius:0 +} +.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{ + outline:0 +} +.btn-group>.btn+.dropdown-toggle{ + padding-right:8px; + padding-left:8px +} +.btn-group>.btn-lg+.dropdown-toggle{ + padding-right:12px; + padding-left:12px +} +.btn-group.open .dropdown-toggle{ + -webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125); + box-shadow:inset 0 3px 5px rgba(0,0,0,.125) +} +.btn-group.open .dropdown-toggle.btn-link{ + -webkit-box-shadow:none; + box-shadow:none +} +.btn .caret{ + margin-left:0 +} +.btn-lg .caret{ + border-width:5px 5px 0; + border-bottom-width:0 +} +.dropup .btn-lg .caret{ + border-width:0 5px 5px +} +.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{ + display:block; + float:none; + width:100%; + max-width:100% +} +.btn-group-vertical>.btn-group>.btn{ + float:none +} +.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{ + margin-top:-1px; + margin-left:0 +} +.btn-group-vertical>.btn:not(:first-child):not(:last-child){ + border-radius:0 +} +.btn-group-vertical>.btn:first-child:not(:last-child){ + border-top-left-radius:4px; + border-top-right-radius:4px; + border-bottom-right-radius:0; + border-bottom-left-radius:0 +} +.btn-group-vertical>.btn:last-child:not(:first-child){ + border-top-left-radius:0; + border-top-right-radius:0; + border-bottom-right-radius:4px; + border-bottom-left-radius:4px +} +.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{ + border-radius:0 +} +.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{ + border-bottom-right-radius:0; + border-bottom-left-radius:0 +} +.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{ + border-top-left-radius:0; + border-top-right-radius:0 +} +.btn-group-justified{ + display:table; + width:100%; + table-layout:fixed; + border-collapse:separate +} +.btn-group-justified>.btn,.btn-group-justified>.btn-group{ + display:table-cell; + float:none; + width:1% +} +.btn-group-justified>.btn-group .btn{ + width:100% +} +.btn-group-justified>.btn-group .dropdown-menu{ + left:auto +} +[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{ + position:absolute; + clip:rect(0,0,0,0); + pointer-events:none +} +.input-group{ + position:relative; + display:table; + border-collapse:separate +} +.input-group[class*=col-]{ + float:none; + padding-right:0; + padding-left:0 +} +.input-group .form-control{ + position:relative; + z-index:2; + float:left; + width:100%; + margin-bottom:0 +} +.input-group .form-control:focus{ + z-index:3 +} +.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{ + height:46px; + padding:10px 16px; + font-size:18px; + line-height:1.3333333; + border-radius:6px +} +select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{ + height:46px; + line-height:46px +} +select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{ + height:auto +} +.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{ + height:30px; + padding:5px 10px; + font-size:12px; + line-height:1.5; + border-radius:3px +} +select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{ + height:30px; + line-height:30px +} +select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{ + height:auto +} +.input-group .form-control,.input-group-addon,.input-group-btn{ + display:table-cell +} +.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){ + border-radius:0 +} +.input-group-addon,.input-group-btn{ + width:1%; + white-space:nowrap; + vertical-align:middle +} +.input-group-addon{ + padding:6px 12px; + font-size:14px; + font-weight:400; + line-height:1; + color:#555; + text-align:center; + background-color:#eee; + border:1px solid #ccc; + border-radius:4px +} +.input-group-addon.input-sm{ + padding:5px 10px; + font-size:12px; + border-radius:3px +} +.input-group-addon.input-lg{ + padding:10px 16px; + font-size:18px; + border-radius:6px +} +.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{ + margin-top:0 +} +.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){ + border-top-right-radius:0; + border-bottom-right-radius:0 +} +.input-group-addon:first-child{ + border-right:0 +} +.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{ + border-top-left-radius:0; + border-bottom-left-radius:0 +} +.input-group-addon:last-child{ + border-left:0 +} +.input-group-btn{ + position:relative; + font-size:0; + white-space:nowrap +} +.input-group-btn>.btn{ + position:relative +} +.input-group-btn>.btn+.btn{ + margin-left:-1px +} +.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{ + z-index:2 +} +.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{ + margin-right:-1px +} +.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{ + z-index:2; + margin-left:-1px +} +.nav{ + padding-left:0; + margin-bottom:0; + list-style:none +} +.nav>li{ + position:relative; + display:block +} +.nav>li>a{ + position:relative; + display:block; + padding:10px 15px +} +.nav>li>a:focus,.nav>li>a:hover{ + text-decoration:none; + background-color:#eee +} +.nav>li.disabled>a{ + color:#777 +} +.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{ + color:#777; + text-decoration:none; + cursor:not-allowed; + background-color:transparent +} +.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{ + background-color:#eee; + border-color:#337ab7 +} +.nav .nav-divider{ + height:1px; + margin:9px 0; + overflow:hidden; + background-color:#e5e5e5 +} +.nav>li>a>img{ + max-width:none +} +.nav-tabs{ + border-bottom:1px solid #ddd +} +.nav-tabs>li{ + float:left; + margin-bottom:-1px +} +.nav-tabs>li>a{ + margin-right:2px; + line-height:1.42857143; + border:1px solid transparent; + border-radius:4px 4px 0 0 +} +.nav-tabs>li>a:hover{ + border-color:#eee #eee #ddd +} +.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{ + color:#555; + cursor:default; + background-color:#fff; + border:1px solid #ddd; + border-bottom-color:transparent +} +.nav-tabs.nav-justified{ + width:100%; + border-bottom:0 +} +.nav-tabs.nav-justified>li{ + float:none +} +.nav-tabs.nav-justified>li>a{ + margin-bottom:5px; + text-align:center +} +.nav-tabs.nav-justified>.dropdown .dropdown-menu{ + top:auto; + left:auto +} +@media (min-width:768px){ + .nav-tabs.nav-justified>li{ + display:table-cell; + width:1% + } + .nav-tabs.nav-justified>li>a{ + margin-bottom:0 + } +} +.nav-tabs.nav-justified>li>a{ + margin-right:0; + border-radius:4px +} +.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{ + border:1px solid #ddd +} +@media (min-width:768px){ + .nav-tabs.nav-justified>li>a{ + border-bottom:1px solid #ddd; + border-radius:4px 4px 0 0 + } + .nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{ + border-bottom-color:#fff + } +} +.nav-pills>li{ + float:left +} +.nav-pills>li>a{ + border-radius:4px +} +.nav-pills>li+li{ + margin-left:2px +} +.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{ + color:#fff; + background-color:#337ab7 +} +.nav-stacked>li{ + float:none +} +.nav-stacked>li+li{ + margin-top:2px; + margin-left:0 +} +.nav-justified{ + width:100% +} +.nav-justified>li{ + float:none +} +.nav-justified>li>a{ + margin-bottom:5px; + text-align:center +} +.nav-justified>.dropdown .dropdown-menu{ + top:auto; + left:auto +} +@media (min-width:768px){ + .nav-justified>li{ + display:table-cell; + width:1% + } + .nav-justified>li>a{ + margin-bottom:0 + } +} +.nav-tabs-justified{ + border-bottom:0 +} +.nav-tabs-justified>li>a{ + margin-right:0; + border-radius:4px +} +.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{ + border:1px solid #ddd +} +@media (min-width:768px){ + .nav-tabs-justified>li>a{ + border-bottom:1px solid #ddd; + border-radius:4px 4px 0 0 + } + .nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{ + border-bottom-color:#fff + } +} +.tab-content>.tab-pane{ + display:none +} +.tab-content>.active{ + display:block +} +.nav-tabs .dropdown-menu{ + margin-top:-1px; + border-top-left-radius:0; + border-top-right-radius:0 +} +.navbar{ + position:relative; + min-height:50px; + margin-bottom:20px; + border:1px solid transparent +} +@media (min-width:768px){ + .navbar{ + border-radius:4px + } +} +@media (min-width:768px){ + .navbar-header{ + float:left + } +} +.navbar-collapse{ + padding-right:15px; + padding-left:15px; + overflow-x:visible; + -webkit-overflow-scrolling:touch; + border-top:1px solid transparent; + -webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1); + box-shadow:inset 0 1px 0 rgba(255,255,255,.1) +} +.navbar-collapse.in{ + overflow-y:auto +} +@media (min-width:768px){ + .navbar-collapse{ + width:auto; + border-top:0; + -webkit-box-shadow:none; + box-shadow:none + } + .navbar-collapse.collapse{ + display:block!important; + height:auto!important; + padding-bottom:0; + overflow:visible!important + } + .navbar-collapse.in{ + overflow-y:visible + } + .navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{ + padding-right:0; + padding-left:0 + } +} +.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{ + max-height:340px +} +@media (max-device-width:480px) and (orientation:landscape){ + .navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{ + max-height:200px + } +} +.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{ + margin-right:-15px; + margin-left:-15px +} +@media (min-width:768px){ + .container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{ + margin-right:0; + margin-left:0 + } +} +.navbar-static-top{ + z-index:1000; + border-width:0 0 1px +} +@media (min-width:768px){ + .navbar-static-top{ + border-radius:0 + } +} +.navbar-fixed-bottom,.navbar-fixed-top{ + position:fixed; + right:0; + left:0; + z-index:1030 +} +@media (min-width:768px){ + .navbar-fixed-bottom,.navbar-fixed-top{ + border-radius:0 + } +} +.navbar-fixed-top{ + top:0; + border-width:0 0 1px +} +.navbar-fixed-bottom{ + bottom:0; + margin-bottom:0; + border-width:1px 0 0 +} +.navbar-brand{ + float:left; + height:50px; + padding:15px 15px; + font-size:18px; + line-height:20px +} +.navbar-brand:focus,.navbar-brand:hover{ + text-decoration:none +} +.navbar-brand>img{ + display:block +} +@media (min-width:768px){ + .navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{ + margin-left:-15px + } +} +.navbar-toggle{ + position:relative; + float:right; + padding:9px 10px; + margin-top:8px; + margin-right:15px; + margin-bottom:8px; + background-color:transparent; + background-image:none; + border:1px solid transparent; + border-radius:4px +} +.navbar-toggle:focus{ + outline:0 +} +.navbar-toggle .icon-bar{ + display:block; + width:22px; + height:2px; + border-radius:1px +} +.navbar-toggle .icon-bar+.icon-bar{ + margin-top:4px +} +@media (min-width:768px){ + .navbar-toggle{ + display:none + } +} +.navbar-nav{ + margin:7.5px -15px +} +.navbar-nav>li>a{ + padding-top:10px; + padding-bottom:10px; + line-height:20px +} +@media (max-width:767px){ + .navbar-nav .open .dropdown-menu{ + position:static; + float:none; + width:auto; + margin-top:0; + background-color:transparent; + border:0; + -webkit-box-shadow:none; + box-shadow:none + } + .navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{ + padding:5px 15px 5px 25px + } + .navbar-nav .open .dropdown-menu>li>a{ + line-height:20px + } + .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{ + background-image:none + } +} +@media (min-width:768px){ + .navbar-nav{ + float:left; + margin:0 + } + .navbar-nav>li{ + float:left + } + .navbar-nav>li>a{ + padding-top:15px; + padding-bottom:15px + } +} +.navbar-form{ + padding:10px 15px; + margin-top:8px; + margin-right:-15px; + margin-bottom:8px; + margin-left:-15px; + border-top:1px solid transparent; + border-bottom:1px solid transparent; + -webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1); + box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1) +} +@media (min-width:768px){ + .navbar-form .form-group{ + display:inline-block; + margin-bottom:0; + vertical-align:middle + } + .navbar-form .form-control{ + display:inline-block; + width:auto; + vertical-align:middle + } + .navbar-form .form-control-static{ + display:inline-block + } + .navbar-form .input-group{ + display:inline-table; + vertical-align:middle + } + .navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{ + width:auto + } + .navbar-form .input-group>.form-control{ + width:100% + } + .navbar-form .control-label{ + margin-bottom:0; + vertical-align:middle + } + .navbar-form .checkbox,.navbar-form .radio{ + display:inline-block; + margin-top:0; + margin-bottom:0; + vertical-align:middle + } + .navbar-form .checkbox label,.navbar-form .radio label{ + padding-left:0 + } + .navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{ + position:relative; + margin-left:0 + } + .navbar-form .has-feedback .form-control-feedback{ + top:0 + } +} +@media (max-width:767px){ + .navbar-form .form-group{ + margin-bottom:5px + } + .navbar-form .form-group:last-child{ + margin-bottom:0 + } +} +@media (min-width:768px){ + .navbar-form{ + width:auto; + padding-top:0; + padding-bottom:0; + margin-right:0; + margin-left:0; + border:0; + -webkit-box-shadow:none; + box-shadow:none + } +} +.navbar-nav>li>.dropdown-menu{ + margin-top:0; + border-top-left-radius:0; + border-top-right-radius:0 +} +.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{ + margin-bottom:0; + border-top-left-radius:4px; + border-top-right-radius:4px; + border-bottom-right-radius:0; + border-bottom-left-radius:0 +} +.navbar-btn{ + margin-top:8px; + margin-bottom:8px +} +.navbar-btn.btn-sm{ + margin-top:10px; + margin-bottom:10px +} +.navbar-btn.btn-xs{ + margin-top:14px; + margin-bottom:14px +} +.navbar-text{ + margin-top:15px; + margin-bottom:15px +} +@media (min-width:768px){ + .navbar-text{ + float:left; + margin-right:15px; + margin-left:15px + } +} +@media (min-width:768px){ + .navbar-left{ + float:left!important + } + .navbar-right{ + float:right!important; + margin-right:-15px + } + .navbar-right~.navbar-right{ + margin-right:0 + } +} +.navbar-default{ + background-color:#f8f8f8; + border-color:#e7e7e7 +} +.navbar-default .navbar-brand{ + color:#777 +} +.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{ + color:#5e5e5e; + background-color:transparent +} +.navbar-default .navbar-text{ + color:#777 +} +.navbar-default .navbar-nav>li>a{ + color:#777 +} +.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{ + color:#333; + background-color:transparent +} +.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{ + color:#555; + background-color:#e7e7e7 +} +.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{ + color:#ccc; + background-color:transparent +} +.navbar-default .navbar-toggle{ + border-color:#ddd +} +.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{ + background-color:#ddd +} +.navbar-default .navbar-toggle .icon-bar{ + background-color:#888 +} +.navbar-default .navbar-collapse,.navbar-default .navbar-form{ + border-color:#e7e7e7 +} +.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{ + color:#555; + background-color:#e7e7e7 +} +@media (max-width:767px){ + .navbar-default .navbar-nav .open .dropdown-menu>li>a{ + color:#777 + } + .navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{ + color:#333; + background-color:transparent + } + .navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{ + color:#555; + background-color:#e7e7e7 + } + .navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{ + color:#ccc; + background-color:transparent + } +} +.navbar-default .navbar-link{ + color:#777 +} +.navbar-default .navbar-link:hover{ + color:#333 +} +.navbar-default .btn-link{ + color:#777 +} +.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{ + color:#333 +} +.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{ + color:#ccc +} +.navbar-inverse{ + background-color:#222; + border-color:#080808 +} +.navbar-inverse .navbar-brand{ + color:#9d9d9d +} +.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{ + color:#fff; + background-color:transparent +} +.navbar-inverse .navbar-text{ + color:#9d9d9d +} +.navbar-inverse .navbar-nav>li>a{ + color:#9d9d9d +} +.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{ + color:#fff; + background-color:transparent +} +.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{ + color:#fff; + background-color:#080808 +} +.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{ + color:#444; + background-color:transparent +} +.navbar-inverse .navbar-toggle{ + border-color:#333 +} +.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{ + background-color:#333 +} +.navbar-inverse .navbar-toggle .icon-bar{ + background-color:#fff +} +.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{ + border-color:#101010 +} +.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{ + color:#fff; + background-color:#080808 +} +@media (max-width:767px){ + .navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{ + border-color:#080808 + } + .navbar-inverse .navbar-nav .open .dropdown-menu .divider{ + background-color:#080808 + } + .navbar-inverse .navbar-nav .open .dropdown-menu>li>a{ + color:#9d9d9d + } + .navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{ + color:#fff; + background-color:transparent + } + .navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{ + color:#fff; + background-color:#080808 + } + .navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{ + color:#444; + background-color:transparent + } +} +.navbar-inverse .navbar-link{ + color:#9d9d9d +} +.navbar-inverse .navbar-link:hover{ + color:#fff +} +.navbar-inverse .btn-link{ + color:#9d9d9d +} +.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{ + color:#fff +} +.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{ + color:#444 +} +.breadcrumb{ + padding:8px 15px; + margin-bottom:20px; + list-style:none; + background-color:#f5f5f5; + border-radius:4px +} +.breadcrumb>li{ + display:inline-block +} +.breadcrumb>li+li:before{ + padding:0 5px; + color:#ccc; + content:"/\00a0" +} +.breadcrumb>.active{ + color:#777 +} +.pagination{ + display:inline-block; + padding-left:0; + margin:20px 0; + border-radius:4px +} +.pagination>li{ + display:inline +} +.pagination>li>a,.pagination>li>span{ + position:relative; + float:left; + padding:6px 12px; + margin-left:-1px; + line-height:1.42857143; + color:#337ab7; + text-decoration:none; + background-color:#fff; + border:1px solid #ddd +} +.pagination>li:first-child>a,.pagination>li:first-child>span{ + margin-left:0; + border-top-left-radius:4px; + border-bottom-left-radius:4px +} +.pagination>li:last-child>a,.pagination>li:last-child>span{ + border-top-right-radius:4px; + border-bottom-right-radius:4px +} +.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{ + z-index:2; + color:#23527c; + background-color:#eee; + border-color:#ddd +} +.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{ + z-index:3; + color:#fff; + cursor:default; + background-color:#337ab7; + border-color:#337ab7 +} +.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{ + color:#777; + cursor:not-allowed; + background-color:#fff; + border-color:#ddd +} +.pagination-lg>li>a,.pagination-lg>li>span{ + padding:10px 16px; + font-size:18px; + line-height:1.3333333 +} +.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{ + border-top-left-radius:6px; + border-bottom-left-radius:6px +} +.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{ + border-top-right-radius:6px; + border-bottom-right-radius:6px +} +.pagination-sm>li>a,.pagination-sm>li>span{ + padding:5px 10px; + font-size:12px; + line-height:1.5 +} +.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{ + border-top-left-radius:3px; + border-bottom-left-radius:3px +} +.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{ + border-top-right-radius:3px; + border-bottom-right-radius:3px +} +.pager{ + padding-left:0; + margin:20px 0; + text-align:center; + list-style:none +} +.pager li{ + display:inline +} +.pager li>a,.pager li>span{ + display:inline-block; + padding:5px 14px; + background-color:#fff; + border:1px solid #ddd; + border-radius:15px +} +.pager li>a:focus,.pager li>a:hover{ + text-decoration:none; + background-color:#eee +} +.pager .next>a,.pager .next>span{ + float:right +} +.pager .previous>a,.pager .previous>span{ + float:left +} +.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{ + color:#777; + cursor:not-allowed; + background-color:#fff +} +.label{ + display:inline; + padding:.2em .6em .3em; + font-size:75%; + font-weight:700; + line-height:1; + color:#fff; + text-align:center; + white-space:nowrap; + vertical-align:baseline; + border-radius:.25em +} +a.label:focus,a.label:hover{ + color:#fff; + text-decoration:none; + cursor:pointer +} +.label:empty{ + display:none +} +.btn .label{ + position:relative; + top:-1px +} +.label-default{ + background-color:#777 +} +.label-default[href]:focus,.label-default[href]:hover{ + background-color:#5e5e5e +} +.label-primary{ + background-color:#337ab7 +} +.label-primary[href]:focus,.label-primary[href]:hover{ + background-color:#286090 +} +.label-success{ + background-color:#5cb85c +} +.label-success[href]:focus,.label-success[href]:hover{ + background-color:#449d44 +} +.label-info{ + background-color:#5bc0de +} +.label-info[href]:focus,.label-info[href]:hover{ + background-color:#31b0d5 +} +.label-warning{ + background-color:#f0ad4e +} +.label-warning[href]:focus,.label-warning[href]:hover{ + background-color:#ec971f +} +.label-danger{ + background-color:#d9534f +} +.label-danger[href]:focus,.label-danger[href]:hover{ + background-color:#c9302c +} +.badge{ + display:inline-block; + min-width:10px; + padding:3px 7px; + font-size:12px; + font-weight:700; + line-height:1; + color:#fff; + text-align:center; + white-space:nowrap; + vertical-align:middle; + background-color:#777; + border-radius:10px +} +.badge:empty{ + display:none +} +.btn .badge{ + position:relative; + top:-1px +} +.btn-group-xs>.btn .badge,.btn-xs .badge{ + top:0; + padding:1px 5px +} +a.badge:focus,a.badge:hover{ + color:#fff; + text-decoration:none; + cursor:pointer +} +.list-group-item.active>.badge,.nav-pills>.active>a>.badge{ + color:#337ab7; + background-color:#fff +} +.list-group-item>.badge{ + float:right +} +.list-group-item>.badge+.badge{ + margin-right:5px +} +.nav-pills>li>a>.badge{ + margin-left:3px +} +.jumbotron{ + padding-top:30px; + padding-bottom:30px; + margin-bottom:30px; + color:inherit; + background-color:#eee +} +.jumbotron .h1,.jumbotron h1{ + color:inherit +} +.jumbotron p{ + margin-bottom:15px; + font-size:21px; + font-weight:200 +} +.jumbotron>hr{ + border-top-color:#d5d5d5 +} +.container .jumbotron,.container-fluid .jumbotron{ + padding-right:15px; + padding-left:15px; + border-radius:6px +} +.jumbotron .container{ + max-width:100% +} +@media screen and (min-width:768px){ + .jumbotron{ + padding-top:48px; + padding-bottom:48px + } + .container .jumbotron,.container-fluid .jumbotron{ + padding-right:60px; + padding-left:60px + } + .jumbotron .h1,.jumbotron h1{ + font-size:63px + } +} +.thumbnail{ + display:block; + padding:4px; + margin-bottom:20px; + line-height:1.42857143; + background-color:#fff; + border:1px solid #ddd; + border-radius:4px; + -webkit-transition:border .2s ease-in-out; + -o-transition:border .2s ease-in-out; + transition:border .2s ease-in-out +} +.thumbnail a>img,.thumbnail>img{ + margin-right:auto; + margin-left:auto +} +a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{ + border-color:#337ab7 +} +.thumbnail .caption{ + padding:9px; + color:#333 +} +.alert{ + padding:15px; + margin-bottom:20px; + border:1px solid transparent; + border-radius:4px +} +.alert h4{ + margin-top:0; + color:inherit +} +.alert .alert-link{ + font-weight:700 +} +.alert>p,.alert>ul{ + margin-bottom:0 +} +.alert>p+p{ + margin-top:5px +} +.alert-dismissable,.alert-dismissible{ + padding-right:35px +} +.alert-dismissable .close,.alert-dismissible .close{ + position:relative; + top:-2px; + right:-21px; + color:inherit +} +.alert-success{ + color:#3c763d; + background-color:#dff0d8; + border-color:#d6e9c6 +} +.alert-success hr{ + border-top-color:#c9e2b3 +} +.alert-success .alert-link{ + color:#2b542c +} +.alert-info{ + color:#31708f; + background-color:#d9edf7; + border-color:#bce8f1 +} +.alert-info hr{ + border-top-color:#a6e1ec +} +.alert-info .alert-link{ + color:#245269 +} +.alert-warning{ + color:#8a6d3b; + background-color:#fcf8e3; + border-color:#faebcc +} +.alert-warning hr{ + border-top-color:#f7e1b5 +} +.alert-warning .alert-link{ + color:#66512c +} +.alert-danger{ + color:#a94442; + background-color:#f2dede; + border-color:#ebccd1 +} +.alert-danger hr{ + border-top-color:#e4b9c0 +} +.alert-danger .alert-link{ + color:#843534 +} +@-webkit-keyframes progress-bar-stripes{ + from{ + background-position:40px 0 + } + to{ + background-position:0 0 + } +} +@-o-keyframes progress-bar-stripes{ + from{ + background-position:40px 0 + } + to{ + background-position:0 0 + } +} +@keyframes progress-bar-stripes{ + from{ + background-position:40px 0 + } + to{ + background-position:0 0 + } +} +.progress{ + height:20px; + margin-bottom:20px; + overflow:hidden; + background-color:#f5f5f5; + border-radius:4px; + -webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1); + box-shadow:inset 0 1px 2px rgba(0,0,0,.1) +} +.progress-bar{ + float:left; + width:0; + height:100%; + font-size:12px; + line-height:20px; + color:#fff; + text-align:center; + background-color:#337ab7; + -webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15); + box-shadow:inset 0 -1px 0 rgba(0,0,0,.15); + -webkit-transition:width .6s ease; + -o-transition:width .6s ease; + transition:width .6s ease +} +.progress-bar-striped,.progress-striped .progress-bar{ + background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + -webkit-background-size:40px 40px; + background-size:40px 40px +} +.progress-bar.active,.progress.active .progress-bar{ + -webkit-animation:progress-bar-stripes 2s linear infinite; + -o-animation:progress-bar-stripes 2s linear infinite; + animation:progress-bar-stripes 2s linear infinite +} +.progress-bar-success{ + background-color:#5cb85c +} +.progress-striped .progress-bar-success{ + background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent) +} +.progress-bar-info{ + background-color:#5bc0de +} +.progress-striped .progress-bar-info{ + background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent) +} +.progress-bar-warning{ + background-color:#f0ad4e +} +.progress-striped .progress-bar-warning{ + background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent) +} +.progress-bar-danger{ + background-color:#d9534f +} +.progress-striped .progress-bar-danger{ + background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent) +} +.media{ + margin-top:15px +} +.media:first-child{ + margin-top:0 +} +.media,.media-body{ + overflow:hidden; + zoom:1 +} +.media-body{ + width:10000px +} +.media-object{ + display:block +} +.media-object.img-thumbnail{ + max-width:none +} +.media-right,.media>.pull-right{ + padding-left:10px +} +.media-left,.media>.pull-left{ + padding-right:10px +} +.media-body,.media-left,.media-right{ + display:table-cell; + vertical-align:top +} +.media-middle{ + vertical-align:middle +} +.media-bottom{ + vertical-align:bottom +} +.media-heading{ + margin-top:0; + margin-bottom:5px +} +.media-list{ + padding-left:0; + list-style:none +} +.list-group{ + padding-left:0; + margin-bottom:20px +} +.list-group-item{ + position:relative; + display:block; + padding:10px 15px; + margin-bottom:-1px; + background-color:#fff; + border:1px solid #ddd +} +.list-group-item:first-child{ + border-top-left-radius:4px; + border-top-right-radius:4px +} +.list-group-item:last-child{ + margin-bottom:0; + border-bottom-right-radius:4px; + border-bottom-left-radius:4px +} +a.list-group-item,button.list-group-item{ + color:#555 +} +a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{ + color:#333 +} +a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{ + color:#555; + text-decoration:none; + background-color:#f5f5f5 +} +button.list-group-item{ + width:100%; + text-align:left +} +.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{ + color:#777; + cursor:not-allowed; + background-color:#eee +} +.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{ + color:inherit +} +.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{ + color:#777 +} +.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{ + z-index:2; + color:#fff; + background-color:#337ab7; + border-color:#337ab7 +} +.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{ + color:inherit +} +.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{ + color:#c7ddef +} +.list-group-item-success{ + color:#3c763d; + background-color:#dff0d8 +} +a.list-group-item-success,button.list-group-item-success{ + color:#3c763d +} +a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{ + color:inherit +} +a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{ + color:#3c763d; + background-color:#d0e9c6 +} +a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{ + color:#fff; + background-color:#3c763d; + border-color:#3c763d +} +.list-group-item-info{ + color:#31708f; + background-color:#d9edf7 +} +a.list-group-item-info,button.list-group-item-info{ + color:#31708f +} +a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{ + color:inherit +} +a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{ + color:#31708f; + background-color:#c4e3f3 +} +a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{ + color:#fff; + background-color:#31708f; + border-color:#31708f +} +.list-group-item-warning{ + color:#8a6d3b; + background-color:#fcf8e3 +} +a.list-group-item-warning,button.list-group-item-warning{ + color:#8a6d3b +} +a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{ + color:inherit +} +a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{ + color:#8a6d3b; + background-color:#faf2cc +} +a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{ + color:#fff; + background-color:#8a6d3b; + border-color:#8a6d3b +} +.list-group-item-danger{ + color:#a94442; + background-color:#f2dede +} +a.list-group-item-danger,button.list-group-item-danger{ + color:#a94442 +} +a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{ + color:inherit +} +a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{ + color:#a94442; + background-color:#ebcccc +} +a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{ + color:#fff; + background-color:#a94442; + border-color:#a94442 +} +.list-group-item-heading{ + margin-top:0; + margin-bottom:5px +} +.list-group-item-text{ + margin-bottom:0; + line-height:1.3 +} +.panel{ + margin-bottom:20px; + background-color:#fff; + border:1px solid transparent; + border-radius:4px; + -webkit-box-shadow:0 1px 1px rgba(0,0,0,.05); + box-shadow:0 1px 1px rgba(0,0,0,.05) +} +.panel-body{ + padding:15px +} +.panel-heading{ + padding:10px 15px; + border-bottom:1px solid transparent; + border-top-left-radius:3px; + border-top-right-radius:3px +} +.panel-heading>.dropdown .dropdown-toggle{ + color:inherit +} +.panel-title{ + margin-top:0; + margin-bottom:0; + font-size:16px; + color:inherit +} +.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{ + color:inherit +} +.panel-footer{ + padding:10px 15px; + background-color:#f5f5f5; + border-top:1px solid #ddd; + border-bottom-right-radius:3px; + border-bottom-left-radius:3px +} +.panel>.list-group,.panel>.panel-collapse>.list-group{ + margin-bottom:0 +} +.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{ + border-width:1px 0; + border-radius:0 +} +.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{ + border-top:0; + border-top-left-radius:3px; + border-top-right-radius:3px +} +.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{ + border-bottom:0; + border-bottom-right-radius:3px; + border-bottom-left-radius:3px +} +.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{ + border-top-left-radius:0; + border-top-right-radius:0 +} +.panel-heading+.list-group .list-group-item:first-child{ + border-top-width:0 +} +.list-group+.panel-footer{ + border-top-width:0 +} +.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{ + margin-bottom:0 +} +.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{ + padding-right:15px; + padding-left:15px +} +.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{ + border-top-left-radius:3px; + border-top-right-radius:3px +} +.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{ + border-top-left-radius:3px; + border-top-right-radius:3px +} +.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{ + border-top-left-radius:3px +} +.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{ + border-top-right-radius:3px +} +.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{ + border-bottom-right-radius:3px; + border-bottom-left-radius:3px +} +.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{ + border-bottom-right-radius:3px; + border-bottom-left-radius:3px +} +.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{ + border-bottom-left-radius:3px +} +.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{ + border-bottom-right-radius:3px +} +.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{ + border-top:1px solid #ddd +} +.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{ + border-top:0 +} +.panel>.table-bordered,.panel>.table-responsive>.table-bordered{ + border:0 +} +.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{ + border-left:0 +} +.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{ + border-right:0 +} +.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{ + border-bottom:0 +} +.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{ + border-bottom:0 +} +.panel>.table-responsive{ + margin-bottom:0; + border:0 +} +.panel-group{ + margin-bottom:20px +} +.panel-group .panel{ + margin-bottom:0; + border-radius:4px +} +.panel-group .panel+.panel{ + margin-top:5px +} +.panel-group .panel-heading{ + border-bottom:0 +} +.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{ + border-top:1px solid #ddd +} +.panel-group .panel-footer{ + border-top:0 +} +.panel-group .panel-footer+.panel-collapse .panel-body{ + border-bottom:1px solid #ddd +} +.panel-default{ + border-color:#ddd +} +.panel-default>.panel-heading{ + color:#333; + background-color:#f5f5f5; + border-color:#ddd +} +.panel-default>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#ddd +} +.panel-default>.panel-heading .badge{ + color:#f5f5f5; + background-color:#333 +} +.panel-default>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#ddd +} +.panel-primary{ + border-color:#337ab7 +} +.panel-primary>.panel-heading{ + color:#fff; + background-color:#337ab7; + border-color:#337ab7 +} +.panel-primary>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#337ab7 +} +.panel-primary>.panel-heading .badge{ + color:#337ab7; + background-color:#fff +} +.panel-primary>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#337ab7 +} +.panel-success{ + border-color:#d6e9c6 +} +.panel-success>.panel-heading{ + color:#3c763d; + background-color:#dff0d8; + border-color:#d6e9c6 +} +.panel-success>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#d6e9c6 +} +.panel-success>.panel-heading .badge{ + color:#dff0d8; + background-color:#3c763d +} +.panel-success>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#d6e9c6 +} +.panel-info{ + border-color:#bce8f1 +} +.panel-info>.panel-heading{ + color:#31708f; + background-color:#d9edf7; + border-color:#bce8f1 +} +.panel-info>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#bce8f1 +} +.panel-info>.panel-heading .badge{ + color:#d9edf7; + background-color:#31708f +} +.panel-info>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#bce8f1 +} +.panel-warning{ + border-color:#faebcc +} +.panel-warning>.panel-heading{ + color:#8a6d3b; + background-color:#fcf8e3; + border-color:#faebcc +} +.panel-warning>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#faebcc +} +.panel-warning>.panel-heading .badge{ + color:#fcf8e3; + background-color:#8a6d3b +} +.panel-warning>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#faebcc +} +.panel-danger{ + border-color:#ebccd1 +} +.panel-danger>.panel-heading{ + color:#a94442; + background-color:#f2dede; + border-color:#ebccd1 +} +.panel-danger>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#ebccd1 +} +.panel-danger>.panel-heading .badge{ + color:#f2dede; + background-color:#a94442 +} +.panel-danger>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#ebccd1 +} +.embed-responsive{ + position:relative; + display:block; + height:0; + padding:0; + overflow:hidden +} +.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{ + position:absolute; + top:0; + bottom:0; + left:0; + width:100%; + height:100%; + border:0 +} +.embed-responsive-16by9{ + padding-bottom:56.25% +} +.embed-responsive-4by3{ + padding-bottom:75% +} +.well{ + min-height:20px; + padding:19px; + margin-bottom:20px; + background-color:#f5f5f5; + border:1px solid #e3e3e3; + border-radius:4px; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05); + box-shadow:inset 0 1px 1px rgba(0,0,0,.05) +} +.well blockquote{ + border-color:#ddd; + border-color:rgba(0,0,0,.15) +} +.well-lg{ + padding:24px; + border-radius:6px +} +.well-sm{ + padding:9px; + border-radius:3px +} +.close{ + float:right; + font-size:21px; + font-weight:700; + line-height:1; + color:#000; + text-shadow:0 1px 0 #fff; + filter:alpha(opacity=20); + opacity:.2 +} +.close:focus,.close:hover{ + color:#000; + text-decoration:none; + cursor:pointer; + filter:alpha(opacity=50); + opacity:.5 +} +button.close{ + -webkit-appearance:none; + padding:0; + cursor:pointer; + background:0 0; + border:0 +} +.modal-open{ + overflow:hidden +} +.modal{ + position:fixed; + top:0; + right:0; + bottom:0; + left:0; + z-index:1050; + display:none; + overflow:hidden; + -webkit-overflow-scrolling:touch; + outline:0 +} +.modal.fade .modal-dialog{ + -webkit-transition:-webkit-transform .3s ease-out; + -o-transition:-o-transform .3s ease-out; + transition:transform .3s ease-out; + -webkit-transform:translate(0,-25%); + -ms-transform:translate(0,-25%); + -o-transform:translate(0,-25%); + transform:translate(0,-25%) +} +.modal.in .modal-dialog{ + -webkit-transform:translate(0,0); + -ms-transform:translate(0,0); + -o-transform:translate(0,0); + transform:translate(0,0) +} +.modal-open .modal{ + overflow-x:hidden; + overflow-y:auto +} +.modal-dialog{ + position:relative; + width:auto; + margin:10px +} +.modal-content{ + position:relative; + background-color:#fff; + -webkit-background-clip:padding-box; + background-clip:padding-box; + border:1px solid #999; + border:1px solid rgba(0,0,0,.2); + border-radius:6px; + outline:0; + -webkit-box-shadow:0 3px 9px rgba(0,0,0,.5); + box-shadow:0 3px 9px rgba(0,0,0,.5) +} +.modal-backdrop{ + position:fixed; + top:0; + right:0; + bottom:0; + left:0; + z-index:1040; + background-color:#000 +} +.modal-backdrop.fade{ + filter:alpha(opacity=0); + opacity:0 +} +.modal-backdrop.in{ + filter:alpha(opacity=50); + opacity:.5 +} +.modal-header{ + padding:15px; + border-bottom:1px solid #e5e5e5 +} +.modal-header .close{ + margin-top:-2px +} +.modal-title{ + margin:0; + line-height:1.42857143 +} +.modal-body{ + position:relative; + padding:15px +} +.modal-footer{ + padding:15px; + text-align:right; + border-top:1px solid #e5e5e5 +} +.modal-footer .btn+.btn{ + margin-bottom:0; + margin-left:5px +} +.modal-footer .btn-group .btn+.btn{ + margin-left:-1px +} +.modal-footer .btn-block+.btn-block{ + margin-left:0 +} +.modal-scrollbar-measure{ + position:absolute; + top:-9999px; + width:50px; + height:50px; + overflow:scroll +} +@media (min-width:768px){ + .modal-dialog{ + width:600px; + margin:30px auto + } + .modal-content{ + -webkit-box-shadow:0 5px 15px rgba(0,0,0,.5); + box-shadow:0 5px 15px rgba(0,0,0,.5) + } + .modal-sm{ + width:300px + } +} +@media (min-width:992px){ + .modal-lg{ + width:900px + } +} +.tooltip{ + position:absolute; + z-index:1070; + display:block; + font-family:"Helvetica Neue",Helvetica,Arial,sans-serif; + font-size:12px; + font-style:normal; + font-weight:400; + line-height:1.42857143; + text-align:left; + text-align:start; + text-decoration:none; + text-shadow:none; + text-transform:none; + letter-spacing:normal; + word-break:normal; + word-spacing:normal; + word-wrap:normal; + white-space:normal; + filter:alpha(opacity=0); + opacity:0; + line-break:auto +} +.tooltip.in{ + filter:alpha(opacity=90); + opacity:.9 +} +.tooltip.top{ + padding:5px 0; + margin-top:-3px +} +.tooltip.right{ + padding:0 5px; + margin-left:3px +} +.tooltip.bottom{ + padding:5px 0; + margin-top:3px +} +.tooltip.left{ + padding:0 5px; + margin-left:-3px +} +.tooltip-inner{ + max-width:200px; + padding:3px 8px; + color:#fff; + text-align:center; + background-color:#000; + border-radius:4px +} +.tooltip-arrow{ + position:absolute; + width:0; + height:0; + border-color:transparent; + border-style:solid +} +.tooltip.top .tooltip-arrow{ + bottom:0; + left:50%; + margin-left:-5px; + border-width:5px 5px 0; + border-top-color:#000 +} +.tooltip.top-left .tooltip-arrow{ + right:5px; + bottom:0; + margin-bottom:-5px; + border-width:5px 5px 0; + border-top-color:#000 +} +.tooltip.top-right .tooltip-arrow{ + bottom:0; + left:5px; + margin-bottom:-5px; + border-width:5px 5px 0; + border-top-color:#000 +} +.tooltip.right .tooltip-arrow{ + top:50%; + left:0; + margin-top:-5px; + border-width:5px 5px 5px 0; + border-right-color:#000 +} +.tooltip.left .tooltip-arrow{ + top:50%; + right:0; + margin-top:-5px; + border-width:5px 0 5px 5px; + border-left-color:#000 +} +.tooltip.bottom .tooltip-arrow{ + top:0; + left:50%; + margin-left:-5px; + border-width:0 5px 5px; + border-bottom-color:#000 +} +.tooltip.bottom-left .tooltip-arrow{ + top:0; + right:5px; + margin-top:-5px; + border-width:0 5px 5px; + border-bottom-color:#000 +} +.tooltip.bottom-right .tooltip-arrow{ + top:0; + left:5px; + margin-top:-5px; + border-width:0 5px 5px; + border-bottom-color:#000 +} +.popover{ + position:absolute; + top:0; + left:0; + z-index:1060; + display:none; + max-width:276px; + padding:1px; + font-family:"Helvetica Neue",Helvetica,Arial,sans-serif; + font-size:14px; + font-style:normal; + font-weight:400; + line-height:1.42857143; + text-align:left; + text-align:start; + text-decoration:none; + text-shadow:none; + text-transform:none; + letter-spacing:normal; + word-break:normal; + word-spacing:normal; + word-wrap:normal; + white-space:normal; + background-color:#fff; + -webkit-background-clip:padding-box; + background-clip:padding-box; + border:1px solid #ccc; + border:1px solid rgba(0,0,0,.2); + border-radius:6px; + -webkit-box-shadow:0 5px 10px rgba(0,0,0,.2); + box-shadow:0 5px 10px rgba(0,0,0,.2); + line-break:auto +} +.popover.top{ + margin-top:-10px +} +.popover.right{ + margin-left:10px +} +.popover.bottom{ + margin-top:10px +} +.popover.left{ + margin-left:-10px +} +.popover-title{ + padding:8px 14px; + margin:0; + font-size:14px; + background-color:#f7f7f7; + border-bottom:1px solid #ebebeb; + border-radius:5px 5px 0 0 +} +.popover-content{ + padding:9px 14px +} +.popover>.arrow,.popover>.arrow:after{ + position:absolute; + display:block; + width:0; + height:0; + border-color:transparent; + border-style:solid +} +.popover>.arrow{ + border-width:11px +} +.popover>.arrow:after{ + content:""; + border-width:10px +} +.popover.top>.arrow{ + bottom:-11px; + left:50%; + margin-left:-11px; + border-top-color:#999; + border-top-color:rgba(0,0,0,.25); + border-bottom-width:0 +} +.popover.top>.arrow:after{ + bottom:1px; + margin-left:-10px; + content:" "; + border-top-color:#fff; + border-bottom-width:0 +} +.popover.right>.arrow{ + top:50%; + left:-11px; + margin-top:-11px; + border-right-color:#999; + border-right-color:rgba(0,0,0,.25); + border-left-width:0 +} +.popover.right>.arrow:after{ + bottom:-10px; + left:1px; + content:" "; + border-right-color:#fff; + border-left-width:0 +} +.popover.bottom>.arrow{ + top:-11px; + left:50%; + margin-left:-11px; + border-top-width:0; + border-bottom-color:#999; + border-bottom-color:rgba(0,0,0,.25) +} +.popover.bottom>.arrow:after{ + top:1px; + margin-left:-10px; + content:" "; + border-top-width:0; + border-bottom-color:#fff +} +.popover.left>.arrow{ + top:50%; + right:-11px; + margin-top:-11px; + border-right-width:0; + border-left-color:#999; + border-left-color:rgba(0,0,0,.25) +} +.popover.left>.arrow:after{ + right:1px; + bottom:-10px; + content:" "; + border-right-width:0; + border-left-color:#fff +} +.carousel{ + position:relative +} +.carousel-inner{ + position:relative; + width:100%; + overflow:hidden +} +.carousel-inner>.item{ + position:relative; + display:none; + -webkit-transition:.6s ease-in-out left; + -o-transition:.6s ease-in-out left; + transition:.6s ease-in-out left +} +.carousel-inner>.item>a>img,.carousel-inner>.item>img{ + line-height:1 +} +@media all and (transform-3d),(-webkit-transform-3d){ + .carousel-inner>.item{ + -webkit-transition:-webkit-transform .6s ease-in-out; + -o-transition:-o-transform .6s ease-in-out; + transition:transform .6s ease-in-out; + -webkit-backface-visibility:hidden; + backface-visibility:hidden; + -webkit-perspective:1000px; + perspective:1000px + } + .carousel-inner>.item.active.right,.carousel-inner>.item.next{ + left:0; + -webkit-transform:translate3d(100%,0,0); + transform:translate3d(100%,0,0) + } + .carousel-inner>.item.active.left,.carousel-inner>.item.prev{ + left:0; + -webkit-transform:translate3d(-100%,0,0); + transform:translate3d(-100%,0,0) + } + .carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{ + left:0; + -webkit-transform:translate3d(0,0,0); + transform:translate3d(0,0,0) + } +} +.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{ + display:block +} +.carousel-inner>.active{ + left:0 +} +.carousel-inner>.next,.carousel-inner>.prev{ + position:absolute; + top:0; + width:100% +} +.carousel-inner>.next{ + left:100% +} +.carousel-inner>.prev{ + left:-100% +} +.carousel-inner>.next.left,.carousel-inner>.prev.right{ + left:0 +} +.carousel-inner>.active.left{ + left:-100% +} +.carousel-inner>.active.right{ + left:100% +} +.carousel-control{ + position:absolute; + top:0; + bottom:0; + left:0; + width:15%; + font-size:20px; + color:#fff; + text-align:center; + text-shadow:0 1px 2px rgba(0,0,0,.6); + background-color:rgba(0,0,0,0); + filter:alpha(opacity=50); + opacity:.5 +} +.carousel-control.left{ + background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%); + background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%); + background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001))); + background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); + background-repeat:repeat-x +} +.carousel-control.right{ + right:0; + left:auto; + background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%); + background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%); + background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5))); + background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); + background-repeat:repeat-x +} +.carousel-control:focus,.carousel-control:hover{ + color:#fff; + text-decoration:none; + filter:alpha(opacity=90); + outline:0; + opacity:.9 +} +.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{ + position:absolute; + top:50%; + z-index:5; + display:inline-block; + margin-top:-10px +} +.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{ + left:50%; + margin-left:-10px +} +.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{ + right:50%; + margin-right:-10px +} +.carousel-control .icon-next,.carousel-control .icon-prev{ + width:20px; + height:20px; + font-family:serif; + line-height:1 +} +.carousel-control .icon-prev:before{ + content:'\2039' +} +.carousel-control .icon-next:before{ + content:'\203a' +} +.carousel-indicators{ + position:absolute; + bottom:10px; + left:50%; + z-index:15; + width:60%; + padding-left:0; + margin-left:-30%; + text-align:center; + list-style:none +} +.carousel-indicators li{ + display:inline-block; + width:10px; + height:10px; + margin:1px; + text-indent:-999px; + cursor:pointer; + background-color:#000\9; + background-color:rgba(0,0,0,0); + border:1px solid #fff; + border-radius:10px +} +.carousel-indicators .active{ + width:12px; + height:12px; + margin:0; + background-color:#fff +} +.carousel-caption{ + position:absolute; + right:15%; + bottom:20px; + left:15%; + z-index:10; + padding-top:20px; + padding-bottom:20px; + color:#fff; + text-align:center; + text-shadow:0 1px 2px rgba(0,0,0,.6) +} +.carousel-caption .btn{ + text-shadow:none +} +@media screen and (min-width:768px){ + .carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{ + width:30px; + height:30px; + margin-top:-10px; + font-size:30px + } + .carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{ + margin-left:-10px + } + .carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{ + margin-right:-10px + } + .carousel-caption{ + right:20%; + left:20%; + padding-bottom:30px + } + .carousel-indicators{ + bottom:20px + } +} +.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{ + display:table; + content:" " +} +.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{ + clear:both +} +.center-block{ + display:block; + margin-right:auto; + margin-left:auto +} +.pull-right{ + float:right!important +} +.pull-left{ + float:left!important +} +.hide{ + display:none!important +} +.show{ + display:block!important +} +.invisible{ + visibility:hidden +} +.text-hide{ + font:0/0 a; + color:transparent; + text-shadow:none; + background-color:transparent; + border:0 +} +.hidden{ + display:none!important +} +.affix{ + position:fixed +} +@-ms-viewport{ + width:device-width +} +.visible-lg,.visible-md,.visible-sm,.visible-xs{ + display:none!important +} +.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{ + display:none!important +} +@media (max-width:767px){ + .visible-xs{ + display:block!important + } + table.visible-xs{ + display:table!important + } + tr.visible-xs{ + display:table-row!important + } + td.visible-xs,th.visible-xs{ + display:table-cell!important + } +} +@media (max-width:767px){ + .visible-xs-block{ + display:block!important + } +} +@media (max-width:767px){ + .visible-xs-inline{ + display:inline!important + } +} +@media (max-width:767px){ + .visible-xs-inline-block{ + display:inline-block!important + } +} +@media (min-width:768px) and (max-width:991px){ + .visible-sm{ + display:block!important + } + table.visible-sm{ + display:table!important + } + tr.visible-sm{ + display:table-row!important + } + td.visible-sm,th.visible-sm{ + display:table-cell!important + } +} +@media (min-width:768px) and (max-width:991px){ + .visible-sm-block{ + display:block!important + } +} +@media (min-width:768px) and (max-width:991px){ + .visible-sm-inline{ + display:inline!important + } +} +@media (min-width:768px) and (max-width:991px){ + .visible-sm-inline-block{ + display:inline-block!important + } +} +@media (min-width:992px) and (max-width:1199px){ + .visible-md{ + display:block!important + } + table.visible-md{ + display:table!important + } + tr.visible-md{ + display:table-row!important + } + td.visible-md,th.visible-md{ + display:table-cell!important + } +} +@media (min-width:992px) and (max-width:1199px){ + .visible-md-block{ + display:block!important + } +} +@media (min-width:992px) and (max-width:1199px){ + .visible-md-inline{ + display:inline!important + } +} +@media (min-width:992px) and (max-width:1199px){ + .visible-md-inline-block{ + display:inline-block!important + } +} +@media (min-width:1200px){ + .visible-lg{ + display:block!important + } + table.visible-lg{ + display:table!important + } + tr.visible-lg{ + display:table-row!important + } + td.visible-lg,th.visible-lg{ + display:table-cell!important + } +} +@media (min-width:1200px){ + .visible-lg-block{ + display:block!important + } +} +@media (min-width:1200px){ + .visible-lg-inline{ + display:inline!important + } +} +@media (min-width:1200px){ + .visible-lg-inline-block{ + display:inline-block!important + } +} +@media (max-width:767px){ + .hidden-xs{ + display:none!important + } +} +@media (min-width:768px) and (max-width:991px){ + .hidden-sm{ + display:none!important + } +} +@media (min-width:992px) and (max-width:1199px){ + .hidden-md{ + display:none!important + } +} +@media (min-width:1200px){ + .hidden-lg{ + display:none!important + } +} +.visible-print{ + display:none!important +} +@media print{ + .visible-print{ + display:block!important + } + table.visible-print{ + display:table!important + } + tr.visible-print{ + display:table-row!important + } + td.visible-print,th.visible-print{ + display:table-cell!important + } +} +.visible-print-block{ + display:none!important +} +@media print{ + .visible-print-block{ + display:block!important + } +} +.visible-print-inline{ + display:none!important +} +@media print{ + .visible-print-inline{ + display:inline!important + } +} +.visible-print-inline-block{ + display:none!important +} +@media print{ + .visible-print-inline-block{ + display:inline-block!important + } +} +@media print{ + .hidden-print{ + display:none!important + } +} +/*# sourceMappingURL=bootstrap.min.css.map */ + diff --git a/themes/black/client/src/css/01-main.css b/themes/black/client/src/css/01-main.css new file mode 100644 index 000000000..318b90e25 --- /dev/null +++ b/themes/black/client/src/css/01-main.css @@ -0,0 +1,77 @@ +body { + /*background-image: url("//*img//*LargeTriangles.svg");*/ + /*background-image: url("//*img//*RandomizedPattern.svg");*/ + /*background-image: url("//*img//*background.svg");*/ + background-color:#000000;*/ +} +canvas{ + position:absolute; + top:0; + left:0; +} +.authelia-brand { + font-weight: bold; + font-style: italic; + color: #ffffff +} +.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: white; +} +/* 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; + color: white; +} +.header { + background-color: #000000; + color: white; + margin: 0px; +} +.body { + padding: 10px; +} +h1 { + font-size: 25px; +} diff --git a/themes/black/client/src/css/02-login.css b/themes/black/client/src/css/02-login.css new file mode 100644 index 000000000..a6984267b --- /dev/null +++ b/themes/black/client/src/css/02-login.css @@ -0,0 +1,136 @@ +.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 #000; + margin-top: 20px; + padding-bottom: 20px; + background-color: #000000; + -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 1); + -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 1); + box-shadow: 0px 2px 2px rgba(0, 0, 0, 1); +} +.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; + color: white; +} +.account-wall .form-inputs +{ + margin-bottom: 10px; + border-color: #b20c0c; +} +.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; + color: white; +} + +.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; + color: white; +} + +.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 */ +} diff --git a/themes/black/client/src/css/03-errors.css b/themes/black/client/src/css/03-errors.css new file mode 100644 index 000000000..e9f97f33b --- /dev/null +++ b/themes/black/client/src/css/03-errors.css @@ -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%; +} \ No newline at end of file diff --git a/themes/black/client/src/css/03-password-reset-form.css b/themes/black/client/src/css/03-password-reset-form.css new file mode 100644 index 000000000..34066bc24 --- /dev/null +++ b/themes/black/client/src/css/03-password-reset-form.css @@ -0,0 +1,4 @@ + +.password-reset-form .header-img { + border-radius: 0%; +} diff --git a/themes/black/client/src/css/03-password-reset-request.css b/themes/black/client/src/css/03-password-reset-request.css new file mode 100644 index 000000000..1a2ad4df8 --- /dev/null +++ b/themes/black/client/src/css/03-password-reset-request.css @@ -0,0 +1,4 @@ + +.password-reset-request .header-img { + border-radius: 0%; +} diff --git a/themes/black/client/src/css/03-totp-register.css b/themes/black/client/src/css/03-totp-register.css new file mode 100644 index 000000000..cb76720ad --- /dev/null +++ b/themes/black/client/src/css/03-totp-register.css @@ -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; +} \ No newline at end of file diff --git a/themes/black/client/src/css/03-u2f-register.css b/themes/black/client/src/css/03-u2f-register.css new file mode 100644 index 000000000..e54cddf89 --- /dev/null +++ b/themes/black/client/src/css/03-u2f-register.css @@ -0,0 +1,5 @@ + +.u2f-register img { + display: block; + margin: 20px auto; +} \ No newline at end of file diff --git a/themes/black/client/src/img/RandomizedPattern.svg b/themes/black/client/src/img/RandomizedPattern.svg new file mode 100644 index 000000000..51afee6d6 --- /dev/null +++ b/themes/black/client/src/img/RandomizedPattern.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/themes/black/client/src/img/background.jpg b/themes/black/client/src/img/background.jpg new file mode 100644 index 0000000000000000000000000000000000000000..974ea273fa87adccec4b4433fc4d097c04ae4c31 GIT binary patch literal 587 zcmb7za3BG;m3jxdn?P_E@h=1|8$YR;59+9 zW`I5}XRiI4&WC@yzle_&cQR_(0pU8Ji5j{Gs2||wia~Q)aTB2CVR=3aT5jO?)niY+ P^@QfW&?n$dCIR+GQO8ew literal 0 HcmV?d00001 diff --git a/themes/black/client/src/img/icon.png b/themes/black/client/src/img/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..040d10c1ab5feaf6720fa8e8c0a0aa336402894e GIT binary patch literal 1461 zcmV;m1xosfP)e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00kXML_t(o!_`+?Y*bYgUHhE- zn3=ZI7qzh{%0mzcVu-|44KcBlMg%4JNF)*ye@ry-m;Ooo@C(p{@FNm6K%#tzfV8Dp z(gHC?u?eAyEkY@6v9v&$K4#|5J!kuI=XL2!XD|}uPVUd$_w2p)Ui)z_@E;5PlS6Y? zzD$W&;aqBgCCWIgr3%#B17TU=LfaFniBfPfGHk@o-q0}>pSg3elo_&jXcox?pxuirC( z+64_w<1wcpt~Ct9s1v|4#r%X~rnEyMkPy>M!hxWQY!F}#Xodt_W_^yC&$ChylYpwS z&=(^YTQB4VP`#+(neoZ!X+^?|Fo&2@Mx>OVR#uekyFNbs3W#>4vjG6i{zq#-D?lmO zgwb%&er)to`<;|MBis1xiRpQoq=1lspman6NDy#26twqNR#fgA_~q+c*8$*>20$i( zX43)A%7y?m0U*TPM9rNfXHy+Yt03Y#2NpCw4d56E zr8CNk8ARxwwFZ)MCdHT&Pdu5eCDZpn@^ zg!ri-09%OTsSF5HzMiccym0J>)_N^~%UQkOi}{T!LDU98Rq07el%z1z(&37?5ok7xHr~3h{1!EAv9uU=n1bkaDm3plKtq}^CpAB|@>l6g=Th{?L zO><|;%(Uw^S3mw@#S9R`U6(-E0l-S{0N(|L(*w#PM1vt?cfmoFzLeC?=_s+x9)RJv zx_T-a+gbFrCOycqq7T~N44pf2vuFUnckNe}Et`c$GGYn$Em5~-bAIoXqv}29Cef4~ z2yM@YUF1_#SCxNnnX)rE+6@p6Qr(*6e&3X1gC-+a8c+&BL+%*qK6K@t0rdTNNQVOU z`=%5Qz|c$<11C_oX0Z=|Z|P*x4y9laU$!iJR{F$j%3FW5_5>uoM=4&3>Oj<9vSgh% zpy=+qV9L1*!8#Ez-`$zbhb**wobmo&McKSC1NF&>%U#7=0}m|ebs1q0HD!v9 zy$h(TT*3Ty{Ax#AA^WnJjZ3BTP6{EanRzux5Qt1y^Q$1#gAm97P#~o>v3^Gnb?z_5 zc2NL$a)2xAmbJ20*BIQu3@cgwp7UhUS%QW_rq~p}dg}0f`!}4kbp4`eG`2hHsz#kW zZxFdbQ|=801KV!&9J_Phe@-7=xryR#eBI=9yqTE?O9J-xk&7+8_&>_uokeL63{>@u P00000NkvXXu0mjfXm6c7 literal 0 HcmV?d00001 diff --git a/themes/black/client/src/img/mail.png b/themes/black/client/src/img/mail.png new file mode 100644 index 0000000000000000000000000000000000000000..834bfce9107a94be10da4c011fb78e37a192888f GIT binary patch literal 3545 zcmWkxc{CJ$5dZF4tXtNx*0s3`#S$s!5_0FZ2?r0sk5=BUk&_Rfm`^vTD z%#kBkY*-}m^PBf(=FQA|pU;~&f6Pq0iSbnyMie6efJIMN%k0Dx{#Q8NiEUML>OFC= zAWc0B_=((uyT+Zw3^#Rcf&ici|6d_HOJ!{*N#0;>T(G&ndvJ(Tpc@DY36b{n^A2)x zy6GnEAL#LPLj`rhV5O&}VG%m<>)Dz)r!}gh`sIE>yJEBH0oyW)_DS6=t)VW~UUAj$ zm+hiLcfH+H{fex~j_(uClb=^=A>;#1uWMUy{+4Tb4*!BdqZuD{5fW-KrVz0}Qw(L} za@q)nnRz60-@;pCi~Hhm>IUq&0(@ai{WjCs-tr zACjZUSIC+XRz?w>hDYM}b7}IWaF45pAB@ivor$T$1Y)i(IxTkrrMHkX&kWKPRp1hg zo=mAMrPeyF;iZ{Vt?q%>H0ZzDPZKJ|(U#Jl;f z06i6&8@wn?^&SkpMos(hkQeM4T*f2MgzTaNS1mA$!umiOb5LgSwFu%u(u zxl5l!lfKwD2^vQ@s^A+-wh}8pN2y1)F~TmnSLe`z?}x~jT2_L8%H|axd=D5X`;&{> zP>X5~|M;ND8k#(ZG=gC1d}?gqKl-%2@dGXVHzKB%M(%~U@vF1V8GNgTYJRv*V-%RB zE?7#4CY`tCW6Rh=X>>G{9qOo_vpc4hVxP@Q_CSH)2MD614Seip{M{ETb1p+mL1AN4 zP=*G!s#NrkF=IRj2zEi(Am6vfJ0yS9t@)iN$CIJClYjK3=j^;zF+d;xVL2xn_C8sB zbt&j|^EUgp$dr>75LtlGxGnY7c;~jzrPG&&5M|^-kJE=CYq(H>N7h^*^;cOA<@|L7 zo3*HDm~%9f-e&kdFH^h|Q4$a%B0dOms8)v!9&#R~<$lrR33!A8q6wc?Y!XMKRwfOH z4*XY5U)3mL|0YU{TbM`kq>j85d(Y6R1%<^YK(Rdy|IFlW%_GBRvvkXm9gFbA>2zT4%~wgv zAa{%nEl}FuVcZ8qkO%y;u}g;FXX8CA0(V zHo$2?uxjL&5mzp7SH^_L2C;OiCX>rmMyoy;vTG$k2nW{n0cDEV02>9Bh=VB)tdf$W zrorSuy@_@0rUOW3$ZL-KDckegvIY&h_WoG-|1ck^9o(Uuv9{`?2a#*EvstqrcU;OkT%&O z&?2q|bdqN?hn4Vf7m}CLABr98mRx8-;?q+v5XK08(*u@sx_(3j* zwBweN+hiAHpXTP~5Ms>Mi#JY+7E~4JK=DjQpvuacJBq6F3_{QaKJQs^b}qae=K8>g zmjUqkf+}w1a*BP5n{MVHzz2hy<-_{Dc)dbjoQebl&j$rIfuF2d!KJx{Xan~57;aKY z*cp0oD@2eKqn|i>$qf2D({4472CcKI7Y@N>p{=|McnE1GMUYE&cW91q>S4?h5CYdc zD>jt_+|+-^so@x@KH>st-l0mkN2n*Xf7vo~s4j$KQNxqukdo#tn*4rG<1cq8nW0=t~jx@i5$Yzh}-9R7YT}D7A6DYU5 zB}Sommbv#GGl+$48vKt(LSc7%-B7@|WNpltJ)j^na_{1~R9U)o$SYR#Q*m!7C{I!- zq6B&H1TBRWX!)D}Qy8DT#!Dy$H9;TT!7N8x&)vOj*_r9}hi)PIT%O8qz!uuIyOIMX zmf!B#>W3K9xc`td+{ymLd&R7@QovuOEPBz^2?*eD1G^Svw3mY z--Ts5TPh5EQ{L4ab8gTT(`bvI&U?)fx@UC?;hR>-?)nV@{H=LNX&YboU+{G`mW3mT4zkU=M{34>Ue(w*XT0L=|s)EEY=NJQB zyq{(qvcLA3&T++b|4(_N8|OlznVJ6QlY% zd}XHHJ#=tl^)=sY=cnyIL1U&?XP5cYh7^8gE_ewLFyi9T36^c`=bVb~+_)H4GN63; ze!01GwqHW2(hWpeKI4|we%qg&+E-E<7bRi~BbBbko-X)$BihDeI7>Ei*3(%H9CW6?+!kwP)K}$%-iX&Fx-dO8OayE0>WtE?}Ceu*lDoW3u)c+rZ+>D*wT{+UNT4v z7y!ad%qetWFY*OwQ>w&{#d9_*L_QC`=Oxtk0(TaG=JPJtgT10hh7L^Ni=L+Dk-}j_Zb{DeRbsUtukh*#)(O2maD^ebRpzH&k zc1-{#JRWW@uk?3xYia$^u=P^Uf$z}^$Eya-26HZIyOKl{fJMO_#eP&R3VmL0WNyWL z>|hHtJ009@t0p!BZ))12BVh4c+K$V^w=fsnlpnfpM6JX!51A~t*kfGwF1j9})Q*Uo=fMgwFn^O>KYtsCNIrx$_O6?4zks$w|$aiP)S7HbUnC?4#0( z?9RyvUybw@hfr{p?rAz#dHK3NU30$NVMv4d%yC=7jQgzx)^|a$^*x7mKbVyr%BO3pDW&@U?SP)W=! zDc?Ui-aR+*QAzPrN%B%k(lRg6F)Yt8EY2<}@KQE7hu-4Yr*R{3Rwzk*2yw|}er{!*H?d|RE?(XmJ@9|Pe z@l#6iRZa2L)$&_X^xod}eRuVKcle{8_^zq=uB!R9vH9)o`n|dOzPkFrz5Mt0{o&yK z=jQ(M@&5Su|N8m=|NkefO3nZP04a1*PE!B}2pAb5A}ud5Nl;l_gqN7AtiQp*$ji&x z+SOM_51t${QmxXRol}500C!7L_t(I%XO1yLxV67h6AluQ2_@iwQ8v|?ok!@ z-YYn2>#nW&|35*)Th&V_rjo9T ze#S;A*_l5-mq!xibJ6!wKOoo|s*V1UP3zFvtN+N2dmiZIe60TG@1IwnMAb&U!p;(& z)}V2vi81~RiCPdeuW8l@`p5cH$Duxa;$!@DOgp9kjcwZ?dHnuDpI=KyB(DLJ)l%Oiwu0v z+v3DX>XcwFHCNP3#`xu`i0+p@$j}v zvupdO%QTv77cWeCo)rccc+eBSa%GrMK}}rR1TofuJ(!M(-Rha$>YCo`p5N@L;_j;B?zHCcxaje_>G8bj z^1kZw$L#db@AlE~_S5k8)$#Y&^7q~J_~G^W`|Nj5~ z|Ns9nkp&R|000qmQchC<2M8D$ElGrzn8C=}`_N%@rX)a0Z|ZgN7D!E%EzO4BF|T#Lrx_z7o7 zd?!cSUYQljo-O%8iN{bX7`_+_qS82+VrJ&JnEfw#z64H(U-54aTjihr0J@_p!#+G! zy3)t@`pEEzh@4#i^|Mv;7Key@{s%shl_<~B(S5fjCT5W<>1t;HU|V=x3WPOt0|0}0 z;R+};#==wo_zLLBoFPTVXzf+G2XtgWM5*d=5kONq(p*3b_?p!LkqWp59$A+NrCECi zE?74ypUGPQc2z*22i4BEz^)$kGw`7J#8S(ueFE+LvgEqg05q!2ySiGp5X?S+^Vhc> zR)P_dI{|=~6(ejrC)y2Qa66arabZmMn}^G$K_mOghco%~CLizL=l=r{;7UBdz#9|* O0000yM8sXh&8)@HnE5{yMH&b zi8!)}IJ|&4vWh&ugFU{3KE8xOzlA}+hC{%IMZkzm!;DhJkyFKyRK=22#*$UWlUBZ) zR>qT8#*|sVoM_0SZO)>6)2n{dtbo+6fz_^p&$W-)xRcttr{c%C=hM&c;Lz{k((vNc z@Z;k4?d0|EQchC<2M7rb5f~XBAS*37Npg6EmzbTd zzP`c1#K_v+#*M~}1NYv0PuyGb-`g~4NNM?g=#$?2rOjRL0O0aSsuqeyLz*gifKz@Yg191z zKL>l|7_uoQdqwWkkwdyK%Y$L$qHvJ!cjbnIeZ#AxJkseoWH&ZpK4mk%m=iCbf1Qqz zTu8Qxldn8tjL$}lm15K{;^3>#m}rMo%mX9`Uwbh*KI%1IB%rcG?JhFD&iHE|>={5R z(9G!4E&e^A{EhKx2w`_Z10fQfzcWIaePgxxPWjU$B|M90H0=dj*NGj+-Ny})4&FI4Kv;zI(5W!X~C zUYq=9P~`C<;j-pFNvaznhqborUbw;E+cw@2us2MiAKF!!+j5C68*eOdG_B3iQsUC~ zf}^?Ab#S<&p*YXzQAc}KVcy|29BtJBm-*)7Ur{cM|38x literal 0 HcmV?d00001 diff --git a/themes/black/client/src/img/notifications/warning.png b/themes/black/client/src/img/notifications/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..ab8b54ffc0ea43e214c5b0a9ca13a8b590ded293 GIT binary patch literal 580 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dy%*9TgAsieWw;%dH0CG7CJR*yM z^m`CyyfQK10+1nD;u=vBoS#-wo>-L1P+nfHmzkGcoSayYs+V7sKKq@G6j0H$0G|-o z|NsBro}_YrzQU{d+OKD7zL>B3e6H?`c{%Ew#`y9xfr~3@Z1+ro2RJG>m8qL$S znrCV?&(>?6YtTC1sCB+c>jDrpYh7&Cy4w(iq8y3c@UF36%l#cL8k7YddH`2{nu zb4W<)dbUiTaqz^g$Dclby}H6Mnt_3l+0(@_#Nu>o@X4}620T-m3x$N|_$<_Zxi6UO z#sB(s?ca`0x@1-??YV86;6(=US2wog3cr4k!4t14H$f}^j_b{hPT^vg?4v($bVq(W zKm8B)w9gM~zwM7*D_4BvynO4A|3VhRk^5e2Y)<88Xg}kvq@@4Kp5cJsmq(o|Cbcpi zSpGnCqPI~3>jRC7B9V~nvt(klsYhDhGT*w&)XQlP>IKiN(4Ay_`qsC<)~{1nmVR!PD}COZ z(3kxnC7)%nN;&_XJ{5J>)4TcQI?JNIU2Zq7t-5^t_xGRmjOKwIbEe*~^8khwgQu&X J%Q~loCIDF$0tNs8 literal 0 HcmV?d00001 diff --git a/themes/black/client/src/img/padlock.png b/themes/black/client/src/img/padlock.png new file mode 100644 index 0000000000000000000000000000000000000000..31abbaeefcbc507ac59450a76b222d0f4e2ef298 GIT binary patch literal 3265 zcmX9>2{aV`6aVgk-7GeD>sK}rA(Ry*>^gE3Epmj|b<4VQ&fYA4`JLL~x$GyxygiPF4+|0N z>#b^bO(w)_&M)^CaxMsCWK&r#aHm&lGhQsda2g)u)YZ+nA*$Y=^SD?-yyum$c%Fo+ zu$ZJykg9=Rm)m9Enp(-yxPTR<#xXC42HEHB_4DZg4O#W`_q$H2B2I8p9;H-yavPzH zq>PRmoiMumG_oM7?b?9OKx=_Dkk+mM`wBnshtOJ>eU=7mz?U3Q~$Mj}SCJX1OL zyqf1?ymH|9kdKx=DX?)8JOf!9^UJ%poV#?*v99*RQ2i$@z$~vES|EhDv3k?p4arGu`0~c! zB)j#@7}HKFW*v0jh^7s5seY+tT~QL61vGi3^n&;$V?sqb^m$mcJ2D+&AnT5%LkI5`dy*61((J>5;-?QSiAbncNza z=_&RvR%{Lm(ptkSN4tVOmF{n3?6j5{zR&sjwxo5xQ|}d}l%r)b#65Pmb-wHy%d{L5 zwPQ+6{)w~M9AWrc-NtIJ$ENRvF)A{Jcr?4>y9&B4!~eLW4p`>> zhOsvg0z;G;#Cd?1bqhHtaA;*Rw-MY>E-4btGR_itUOXXU;(CuwOo62{(s@)KAuvtf z94FvGsk{$ylrmS~owI-KK@53|%_`yM&Ga8O>wLP){j(dkJHc<&j@laS3U@w@+G6&w zEevGlg6D?SCiHJ!F%BgKr*L>vX>8T2Yz8_^rTT41_xEvRdH3@!{Y6nDd=hKj!2QFr~YXu#zGc(XeTQN|WFn zQL_-O7SLo_0!Z!Uw1xG0znZF6-!;c4eL1j}u0$=vD0_f6MZe}${`8%K>BVi{5{i64 z8|m|u%LIcFxg8~xj5`zYvL?g-%vn?irwONS$jMqAk3f>u{z3D7%BMV>yDEvMtTO}d zihwHDkS-^BS)X^O)qI+wO7Cc(2D*}y<0#8~eDLL6y<=>IF)Ap{9VFsh4qS?SFhufl z(BAyj_Xz^v|A{MSeU=T;7-%0JP+Fk$q2Jf_V0xC09?A!2-q?Aq)ofD99`8HUCp^82<@{syV+N=MB$R(;352@)bTYSwwhc~HL>YeOhXS2 z7`6_89MT2B;X|FdD&I|?onT7-6g&9z@)L`P1X{VjD@l`xFt`dSWnu-|L(T!BuDGCO zoNZT=%_xMbko*hlYL}Lr*taSNRW1F@1&kj9i@TIdC=fj&0N@eyB`7fWtiKMWbATr7 zg2A&7Cg4T5(+4hN01qd{VyFg4sxm;p3HcbRj)$mUv5*IoG^Zgs!BAG-W%*=6t-UX1 zU4QHH#qWz97EB%r^T_V%r{|qM{wSWG<4a_D^Ew7a$i-0Yk9WjL_=N`g?oYJ(W=ebM z|GFOF|0d|pVM$+nV+MucRs3I=LV87{<7gYRlcx$_ud;|+Mjjfc2%;Bv>51`;2WwAO z{c#Xh5c{q-09~!XFT$9OGxf4;JiF(&p>h>ky0Ibgx8JJ};Iy=w>Y;6~e>xfhJR=#) z?X7~{(N*kOUXV%aks!{hW#9|2?9tcRK#m>F`IY_s5%sSFibr9p&|^RqvfLL<+0hfZ zQ2#zFFl5GcWlkcj8mkvA&?_^zI}S)_xCX$VFb1XnngbM-h5}-I!jWl0to`{s>4a!>rl_SvhYH` z#z%W=pEP5bHRfR-e>gHKrz_RgoAR0_lc8 z1wgm>6)?zy0O_Y4nZOkhU=5=m57R>ePM|>OJ{JSjfjgDPqai|s(%GPC-5q!&@I~B> zSVkm*1!$!dLJMNhmHgZ1#hYDkem|p7SVlnEVB!7mzhr7(ZDwave%rx4k2Hgd2yY zu*oy(^w{+B6AuMNhcISd%EV%^s3qi>3E{XaHr<;vT zLLrQgWh590jnIhuyr>83`CRZp^K;+SWri)#8qXn&eeR2GDV6$hV3 zC;|yg5QWXs$In0}S(;{3+Q)f=6U2}}MMHFMPv@82v>(iUde7*h*H5ztB$Znr8fO>y z!y`qZhxbu(c4#2?(|Dsu>wHrr5L8Mm7v}nG-NOl%x7?KV{2zXuG5c zINn@IL5?joMBh>QMAAEjtiT~pmik?HjwA;M0d{)zA}fX%gcBdHORe)+lk^9Zr_Sa` zqG{W1mf=ek9)Nz+@^^gU;jecyJU>{AcJz_GD5nPjG8kaCTN}IbF=6ffiih+nz7Y=} zg(Wv$gUXyf=OEH{vqj2}e8wO2W(9OkmWPm8m*O-uDNz}#qhmDS>q!LhLM6GcT7m~%ocUNs)9JZz20hA~qKL^0J>S_J*M z)nRkU{h6XFfO=E#6g^4IIX9ymv=`Tp0?dRuu?g;HXq3gz7`?Bzmy1d)VRWn*uU)mN zYWD;+?tYeZ=I>*4nFQYCc=YDtY9sq84;X$x+%XA_Fj)!tqt%uuX0^Y0;;2xn*cJp` zv=$LqqjK7*r)2+UuQcF?iwJcHvD+aWYZ@o41Yr=8*nku=w5P9*7f0p$0HdYq1mjgn zqcU#ytbI(erQh)M!$DN=c zob)aNpOrt>_~Jc-Tol-c0s~2SUY7LD?svNDNk)x9cNst4vPu@PS+VaFOyBnbK1YM?0W`%*k-$2d-mZTsWZ}_GVrOLYb{)xfkykLOnSth0t7LBH#Q4w#lnEDYfbqtK!^A6}Be(TLV z()-%kH^MOnA$DAl#6;0q;(8P`@0<463$}+n65fZVc3~e`j;{ez1xa zR7M72GnlI3{adw^n53_0n$*%)F3AZFNit>LsO49xGM6sRglr-`t*zImB_($s9aRZ^ z&0Sep_H>jCoh9EvJK07LYM$|kb zpMKcaMs8yX1!vnOQ3S1L+uNGR(kMc|g8X>h+?Bjl?4OzwFoO6T89rbg>H_FjBFwpG6+EE zN%ri5OinH4dsL@S;9}~71zhj3Y|b1D%iMp!(}3tW*_M4A1FjC`Qt+|9(rrsBs=M=0 z=P+YB005w8wBBy`qm+sncXSHNI0pDs9p%c?uqAMYf5Av zN{`(i^Dk2lRfB2kc(bs=xZ_}>O$S@>*IfzJ9Y@A!D>DLplnl#flvd9#dW19lhV@6I zg7(;x6Y1R@esAAx9~EFH;#6!88D3Eo6OPeuc-V6VT`DY^)HSJcR#NXt4T&eU4}~d; z0Fn1_5&O5cWUe_WJTSb1w%n5aNVNNEyPOYMZwL;2F;NPSZX9Zi3L)HWibmQLj+Q(y z0|vJdKi|%eB4*b1?5dt^k4}gfY_i!2PY>}oezLHK-qSK`o|;jK9wVpcG5}UeqKKKV zwAkJnxh$?{^^vB_=9Cg$q-j=1o0^9CBc7Z^iN${{7~#b*!f)!zH+Xc-o)P}&iG3)h zkER+>l~ovZnn`*;i)yw=^L|J6U>XclESv1t-E7u9rt|iD(?qErP8e0iHuK3F21!Gg zhSkOKt|Ciw5UxfwXHkKcRXrWPvM8IVz|T`6$E^b`eY&tCdfJh>$AhoHLQwt+$EpO! z6qjCaD;a!aRH(InPN0EbSzqnEh(f9~=_5zh?iXvux`#GflkhLWzmsNU2WbVYg=#98 zR!{FzrukEN33nzgVQe%2RGq^S`$UjYG&sDS3At2f#P-eX#vVUSAf)_QU z3AI#$B!`)QqMm}ttH8SbL16sA2YJYMtCP)Da0RI2KJmKf zk?bV%RwA>;1o3;{QWk*Yswqj~UyA@R)(pI><)rc0%1zMg`1JYp+ADycv0%6eUnjOD zpjRCuzug>@^hTa0cD17lC_BcE5mulOf^dB%HKV3>ZL_UJ!y`6fZM~6IY6LTnDTSE6ZpA->X_J`@mnH*#(6w=9ulM zF&}ZjdH}YkBZz5=8aNVG!>SYZx{?&xY{Y5SBIzQ8Jn5Nse8K9>=9DBj9!N*te{{wzHdE1LHD0SFMO8uS$w15z|ff$5W~VZ?dA~#x&f7)3^VDFqjU?~x3iTCKPY)su;gfIrWzWt zmY;zU#mjxz_8jIeVK~BRY(i+UNsTK?pd8>KX~aDYM~A@lfk1sd8INvqz#p1*VP|sv z6{M(1n_N`2!2u@_8gOib+#6(VAGC)e3I=RW zy}poa@S*3i@zaZFH*z?V0 zZB}+k$~M2@GE?x}J0CZ#NKAbymHTw~w|u}b{vJJEAO7WH_=tFXT7iR0Tr8Szk{iak z$#SA6*7Sa6t;@q0y>b)E5d$d|Y@c|VrT;+LRFZB%&-YNz(ku(a|0Mrqk-fk>JIhVR zRW*>HMqwwFSuf@BH?oBqO0u%qq?+xL zi?KbyZN#e+D$Y>mt9S!g^7{m&_nicl1+?{!5skjq?T+645K*SH6W7{CS5^(RRnWnJ zu-*}(r+t`J;^h+-mC7F7R1k1?_bN&-EFkF>ubQO5tN%<1+ttb(nP5_1*QDPi2e`EL zEY70`;CH@E1Q`l?p#JQ!Dizgg)l~aAB63}`WX4QuUXBYWWK@=qOm)-gp!NSrUD>wb z`&5h{PU$g14}q&Z#goj779|-*)vx2-ytB*DAF56zyo3Lg!}3V-x3njX{{-`3ElULW z4KL!Oa3qmcYDl_gZ`0rUNg!6{^QlIS(&$J;khg@ueo|3ere~{xQ{{SF0z)o108z*m z{!O|{d1(bNr;2gj>s)J*%~)y|+NiC2HG*A#Q$rRplR)6YbyXt`LXG3a1fSHnhzB|oJlDT;8qDTuHM*ux? zEq^i5^@aY6=L$#|Y;4LWE}vd!^UJ;K3~TgGOWfauWzGz;@6&Wh^s$OJ^To6|yG`g% zHcEO>7uZsP*xQp7Bi&H%n@8`6cRABq`2ZhDBQbqWlr(m{j%lrB#Lk0=~ac5t)t|Z>Q4jH30;8kH$qkz)OmxP^z%n# zHVReRmb=7d0aq%{>^6wnX?C|2k+Z8VMX6f`ns9SSpP2GrWZN74=-djRJqLuJ zdd_1KD)m+WOq5k&&9B}mo1DH7NnghlGHbT~Hfc>?%q6t4LcQ&Hs zGz;n#p~(4*d4EDN0TT#v2`HsD;(#aLZ@Ao=;K*I~*WgWr2^$b{uxMu7VqfUn7muy= zlPs##Z`6iXy`8JsFe~?!K$A2LjiiX$X<`?&_7kCKLkUNvE4|y;1MwnUBn-lBClYYL`wT;ySc6LAr+HJ0qIS$BW zW+9q1)lG1I;~@K@BKI2vV_^^yK1#fVh>(pEETGjZ0h5 zhbF0{A1<-AOhG4(N%nV*a)MmydUw{{(@K}+fhrB^Tt2tp_UBh5gL}Gh$ zWNynbo30F7KSq=my*=9TWcV*rHl+TnD~T(}88f8Z5<}^xx^e(b=GagHMkWG-mf4F( z;=Zn(lqU&U4zZ#AMOyo~*yZ$30NTH8=#T?=0adgYV$f+GWnEMOcz;bfA=rVQD>qZg yfK{G;*sMIT^q}!}G=q*C=Lfs;R>;VR z$X=mHWZd7o&-e4k@4jC5^*qlx=Q+gJU`;0Wjzk14Kr2 zu=aZFj1G8jZ37D;`U@gDMxbk^E62`y1F$Uo{bPtXm}F7lP9NPfKIWI5ef;dboWRe| zPsZ(%ySIb=6(^a?UM^`1YJw^rtlsTWbmwuD!;DB%b?}U>BPr*-HV6K#5krRGRm-9 z$5_1ZA<|iRr?ly@qMTxS>fxa+^Is{+QLIMv;nu^Cs@f0Sh(FC4!V~br!kfBEZX+*{ zHOSF!r@C}H-*t$0jdvz?rRP)_dEd^zHNn17;yph+{<1yNp8ny&YUrP$!f&m$&faQ- zAc7`7BkRo?vc6c z0<-nnd#{u&*2GL*ml!r6UHJCpQ)_R~1i&ocb>33(%f-Sy%Dq-rJL0 z%@@6di!&emRP?wh&j9{X!HRK`*zEHZLr1WJJNeDrs=LCzEX`f>yHz_vejtT^zF*1RtrrIo!F_j4Ef`}>UWp5OND z%&DkR;*N>KSKkW|;8xV4j<=vLmqxSq{0(^y$l#;1`Hm7N;tw=?Z!xr_Q!{J!c!R9kXI2heg$7akcGqZIWikBl#xb5=fi-~bD7xU63(VmHW@61}0@z^5Y`d16 zBInALloRu$7ARjPII^BJ*y$eRb@E^U_|8YaN@Zm=eRtgK{lFYhgeJ;9oU)FHE!|r} zfYO>$5hL^4^$~qiS3OvjaPV20UZB!)$8~;xeF4w{l$#E|&pJv@J}-PZs(Gw)=s2sA zPR^6@i;?ms6N?dZ8$`)vh9g+G+ASBjX`4Z01G@W`zM9BtUsz|_0Ybmz_%Bg z=-&eGg^sBfIJf^!QW$D$Wdic^O&KzW)3u4U$=<+sR1wX=N`n_e<(GQW*S3YYy(%gE z_{8ElEVLe|+wsvM_3M#lp!_JanT4r&~g{&YS{q zoh-`(%lKCcZ#fcz#0ezBU1vxjW>WtG=haEQjXe%SF$6>DFzU1DzlGE~4( zhMMsykhrj~KbP;wKD5voof-m^e?VNDGSlX+hC}AIfF=sZZcvz0po6dwei8?USd?DF z8bcAea+)fbC7bFc3Mv1O3bfLKYzIy`R?J_naT0r;zKiwfp@Gay9Sf*+V7c* zFzh}-vJ3Hgj)g7lU{lAgk~ItYowK!JeRr&%r26<#O}>VFuri-fzQmQvE5wFxH574& z_*>d({ACwCO>Dlswg`ES&B{&75%ILC6eix!AV*^T+b)+ z+%)H219+3$`~Bphr&tR8ZE4>`@2kE$mP}xA zymwFSeM!4NGH)*S;Auj6N*b6Cac;GnYM3m?&DHM|+AIG8CJIB;#h`qTK@PBJ3#@mS zbLe-Z`A#?5`60`T(P%@U>z1*J(_S# zYgCtd=6Vgx`gd}#HC+PIUhMG?Rbm-7k=V4*%db{Np7C;ilZvDb= zM*NJzAU~sQx8`u2efkcd){^(Mz_C6`9Vol-Sd<|4=T8ob-h$it`P;=HRS`1A7^$Al z0q-azOA(NelC1-k7-k6buuY}g$@RIp zxu&Fo-lin?-o{@dm+s%7HOaQ&pq8ur*DhG$u*@mn#9qlAG0Zt0x;RA_TbFF(smm*N z?C2|+n#7<1LJ{?Ql+e69EO?VvKduclW$g2-nCaW2KbdH=jnDL==|%Z*2U!i}0z^$| zKV45=t@+`N-BW-XzK*!k#X#e0nBYLd9JCFLFP;mZpwEt8Rp@q#ZBoQ+93jL%Y_Ij; z414O{YoM$z0yIhNi%NK=?YWL4sXzLrxP3Qs^3b*YML+iscO1T6bK46bllZKYfyC4> z!GdJ-^v3Jo5f(ZadbUBg=QDO!lV(3nHB9JT+hI;vx=pz!11@IPwGd&rPx;LDuyA+0 zmctth9HnC3uVR*}g-=Yl4TvhfO#_2`$1K5LmUGIBJMkoymH62Lj7#esIe#hBYulnQ z!hy+K{>TRdIj@2+a{hx)C3}DT3J%)(&K50AkWspF(ljjlgQ+IaM6loaAl=Ne7Kp?5 z?3a2%TQaPpFYwI+DcYD}K}y;d#B(zhh_t?$H0jDc5!r8%-STGv(ZmlH zv&h{B-W1yotL@6l5B$Si)165L0hnVSWf)*{&qxjw3FM)oE)P%Ju6R#&N{K~Nb8QZN-qYz|vO-mLsyEF+fxTnYxL*hnRK;E<^ZiEGB zV%WRPATK42hRR7%&K!u3>BOROB7tpUhKm2d@daQS_aAtI8Kz7vQR-Zsk#sE03C-I8 z`?$)V!4*Ot+WAn9;}@i$JL?q+J{Cz~LkFPP;4fSu(zswT#`dVQOq8S>!|>hmoZ)-8 zvpX_dYFAjqMOR;aI$Qi22|9mX>H7gmEB5pNnJ=RU8owsWHtH{zCfF=8U?GIFJUjtO z2RZd==uh>J(~$_yvwV|tBEI8+?$f;^doMdS+(?yZY#^xd&ohAF$7c?=UpO4)WWyDC z1v32x{PQ*d!C05C-wKN*?%%=?hs~PRp?>ZqAbMl}#0PPwUH@xlx9aGVckHaLphgmp zU5tzo7!<&xa|1f9D^sM-66_tD_>qy*9k{+BKW#)5JZ@ zSk%{1(0J3zGu-RrzcVrMH_%!Oq#p7};J-07ff-2%&AsPoK6x+ha*gp3-&yU-Rbfj0 z#&aBSJNce?(_0DU56it^q|8TkW+;>s?WQP4s(Vsjg)TprT+giAdcZ2Ax*Lw&t5`RP ztb~=Lo(%-M?=P^OU$))WZSf;#of$7O0=MJWAH1RG08J~SIJuABO^XxCcuJktTMWVr zx!d3UKmc22YEz8y4^6kqfz2AL%Rt)ZLq4`oH2h{j=|ek`N7E@XzzPyb>F za`=~KcBnb=Ux_QOo9aVtbwmm$^WWN3o|=$yW`Gma&FqRXeX0JRM=VArth_PEv^m0Z zZ%YVE=@%N2Ieu{UeAZnaH5+VJU?%1at9ZRIpiZ;x!3no0%*PKZf#GLi(O?4WHFo5Q zO*X2JP}pEd@!}t#$<%Yt6N;3cB7JOvdW>vB`q($4MIs0*55jDIG8U7icszzp3lM|{ zIpG+0ffsJkjQ=f^o}-@QBVxhlP;0CGuZp_R=}yd0V&;e{a)U~*8{&n5Pior#)?a6q zr8qqtjq|^9Jo zkA?3=_dTW*qx%9Al;Y1BOjY1W9;caR==IuQSK?0pQ771S(E+ohhms)NWyqFe?>Fy3 zD7tW3ey6f~$pw^A{)a~@(kF~zptCz`uoOdS>WG|c{GHu9bcua z{wKB<>n3XrgTl%T;F5SQ)LNsT4J_ius^t=gme@3}KWB%dBV=Z}mvz}LJnVPzZ#sxIxN~XFm=e zHO34vL006>;`3t7bf2`v3-p)ICaDBuPX}KWv1A=`0O?Pm-&(upwy%79F}cH5N8>3a+)j#c~M@;4^; zel2p;mu926lT>dB?fg!N{onTUckx@K|Hm5j4}$C;8E|VoSA5#oXshz=Kh1<3DM+ znhU?B4KnR#LM(ToSY&Q`z=q%Yu|sIAo8ViMiwf&|;{7topfa z4aZU83gzvkeJ7Snoh6?$qiUR)^uve+l>t2a(-k^s8=vm}U1I=?I4iVsm^u^KGs*>_ zI^JfHU{Eu6$0g*Rm;a!VKPpXOCvQs><=8c@opO75p~y#M)0`9_*VIyk;5z=_xN# z0`rF>TNg^_vy9Qe@yV@_{%%meZk5Ymw=|#hb73}Zr`4j>J&nQ7TP3JRhS`vNI@`F@ zEA@`RpIDB&0n|c2vjHP%9}^y!9(0<{PCU)nmcsLBW-b>)Ym>NPWc$3!@-t`Z3(?Uc~|8Bkcn?o$f%jiPGZuY^-4TJSAHUDY;9WdQ0P%W;CJ^udUzzN36 z8*YrqSFs)O#c85>vKm#vy<%txeX3rG5(1yk>=xLtM>|D|AEc5+T0ICh@|QYd+H87 zjlmioUFGWVML!nJ_6-iTR_3ngm-6Eh|tbr$7LQ8W2Lkt z`-_$g-k>v$M-(nx#CvAze%M+ws&KJ=zsG8RTudEgJ;;^}c+R;O6PjnHY^f7%fuZFD zic8B6RuXc?g{APQ=AQ?R^CWG*8!&QHm9qzndFPepCoGd&r?ewaQ5%!xE&Ilh0%q5| z+Q%vw45D@BqOYLNmZZW!B<-)6sGImFezP><92Yn2CI-1)K;ZmjLG8=?Q(29>xdhRO zI89X`A@}bcu88ytKQY0|S0nNUm!;@J3!juEA>>jk+2Ec#qw512AjhgDBskIZoPdqU zP+hTpII$>hE}4s^5U{FLMGDv-Seu>X7ni{jkOeEWJjs6DjOc3*_7h9HbqPLknhlngQJ`( zU5QxCx4p}o3U%`ln(tLWj{kZ+xq^Z$wl>BUPap7J)kS@R>X(CT!*`VYVjv zWIH}tg9jL^D!TOP|+c&KR|ifYSVbz>1=5IeJrRsbi_F;8LNFXCfQvvi+lO~bKPXh?#Q(@ zk0x7rUqBv{e4MNJ_I*hnO5(#z_=ik>T2sxqc6)h>FTXtMRsUY}J9I((E-q>({gFu%7jo0_>I8H0YfRO8S#q;8YL$e+j(RPv7q;Gw3T&=i! zpzd(R`zonJfnR@gi~qS$dw6NQxMI&EmZT@2RGg>O=p0T9x)1t)$FZZ`3K-~~(0QS4 Hcm4kWRQYbi literal 0 HcmV?d00001 diff --git a/themes/black/client/src/img/sharingan.png b/themes/black/client/src/img/sharingan.png new file mode 100644 index 0000000000000000000000000000000000000000..526787d3be6a78d936b6a21941291994d0eca38e GIT binary patch literal 9213 zcmW++Wk3{98$U`)y1NcIN~F8<=n#-b8bl-|rMtUB8tLvhq`OlZq(MqL-u=HH_Gb6P z%|5d;^E|(pa5WV<3^Yyph0zz*L4P3+5axMWH};j z5QrKCm6rJ6o^_h-;SILPKJV80o_h#h6JSjI95ZHqMxv7Zh-;v>h(LgpB!jc46N7ae zQ3d(2%^Z+ljZ)XP9dX9;V;dBK@^%R3Fs8JpWSPd2Sf6-yo!xcxTrk6NSasB%=;tk# z`!ZHo)>hV*V=OZH_udezWBhl=xY)a2-W%eS zVv{j~=TYhNpxyHX;~Go1uiO1U_fnzT4&qDZmk~CGK*MN&_cf3NiT0 z`FZ&fDF~<+eYA$3LN!45#w}&ujU~{>}wAp%2}NZ$r$W5kCUg+XxFrb_JxiG2vMDtI{6$xsWgvd zMj=o8>A@OMIDRU3;$?zNiAt5pRH3#OTXiAA%BFzk+H+u4W$EjyCie7w>rNdm%r}&e zJUQV+m{g;7?VV-vbNtT+v9DJiTVdD z)}8qGp}!pSo_woHdJh`Dd-;FSX18vRB`fWHe_$yb2TO0qm=-N38 z(y#GyOr$=3+X*kfxykgu)hOQjVbFbo0IQB8NY!QK&1iMLCne0}ocE{O!k&zYy`1Ue z|NXqKVOUM@DSY`CeHFty2VS+J(Ss#sNn;2Ms+A_!ThITk+GdKng>aL4;vRC=gft1lPRU~ zkRl=?YJ3vLUH=&6Ka7$%EWN;3Cb3B|-)OMBI-n3)nIAAkg?e${DRiJi6hIdD%q*ih zvX&YDeI;|ojhiHDwcQB;m|#j0l1-0TFO~LNhQN6H@@xV^Aqq=$3{Se~H+03q+|ND3 z?+d0O?eKWlcZ^E}=S-eAh?`@iL^|w8KYFutP^8q!BZKa)&lVj9@x30Qn@S2b#Dpv$ zcn5Z;rG*M-Vww2cd7gS#(^JJ>o#Q?2W!YKVrFp|9%$eIMabV04c;REm ztCKrOXCAELnG|t-$9D55CFU@%>c33YVmWynlw^e*>aPizaZ@|de84})!Erazrh066B6H+R1#)yhWfjdSWIu54cmmj%^ zQ+VeWS%c$f<(+n&{?sU0YFgujv1DJ1^W+g#Kt3T^r3pmADB^tQ&U-b-i)ipVyv-Ml z^lJ@iW9VgDvmCbEz*PLno>#d+evTT*ci-mVjm6BzL0niV8*zGlquz@a89jbRQ*>xc zhb<8!JP|30xS2WxO?8&6?-^P%Ja%s`-%=)E+?*q8X{kburmmS4lTb=n+PkV@+1vrk zDeHy=2?oYna}Ixdm40{!=RxdFOgy@8aKAnt3>zO)Uwl=OV{@ziI7nNN&*#xYhXPM_ zGM}|ux;2l=-X)#&GW=dO8l!$iPbOP}hxy%CZQXMB^)2gERZ(~c0;d6${*9M%qQ3 zIqJCbb`Z`xtpxCDEre8J^CP&>oOuC60rO4lWlcOjk~N{45_C3;R#B}~Q8mucs=%+Z zw*8Zl5`$EEkQ@Mp3UhM8d?}BzRp{Gcx|56*xQYzn3eXri+Mq;Kpn}P};Ht7J zNG6~#;JsCr4FluJ<0>gYc?zbk>HXd4aF&mAJn!2*`Bm;;D&R1S+?_r_HUzZef3@OP#Do$`im7WoI8B$V_LCZ z`Ofsel8KB_%C|@@`ASj}qQG7Q_Irt-t%pvdv(ff#Ksdi@Z1iP|S6KE-ctjslN<7pA zb*j2S^K`h8ri5)W!oMy5XY%B`voZcVA_Td+p6NjPHH%sRTxA(*4(jr}n@{U?s%78Y z*@}nDAthso6REc{dLKcH^M~jn5=NmQG>5qPZ8rd8)*8wG)zFJ`Tvl_Sr`9~-^G(OZ zgm{RBt%?m#M(fZ#?0(`6>8Obv+=(I*NE z@Q$&+iE)BF?Umwq)RxCfZDEU~v*!mH$Z^m)$q!>UP~N%C1ntzbWxJ$RmJzufTpu9C zr!owncmAzQLc>jS>Qj$@8xNKwln4(varG081 z$=8cPO->Cu;4jY4YRxW*k zjd|FaUo2gGl)Do0xz{e~&}^ZXfgY6XQ6k1?+%13JvhJKca^?PI8o|TI%s$@l^aZu) zk-dP=v4cQ^vS@8++gxdbvB?0>P^qMa>>tt|a&L*m7UY{kfI zXhi3lYHDHVSy((jU1J==J7_<^0b60IF}O%NJODOMVMx63YhIL)_9w5~;WaDY?1m)X1?n-< z0d}{_?}@W@et(A+GUuZO!1TNb+5<@#Jkjg0g+-pTNrZ?B)K=Y%uyxWShfttLZTtR~ZZ%9b?y3mz1nnCEi7>@wpZK8&MUm08S&&a-UQ;v`S19mK@;5981)NJ^iH#gz;=A;t=(R9RQA?P{F-vu zh=e5LtF?AK#@2>q#cEoJaLfA|V?0(qB@$l?6xgjIj566Zsfqz1i~xS}pLxYSgktbHzi zmCCxzV4j+Y`tXttAGfu~LXW*84tY=U;8Ir*Ql9aa6b7^?~996AtKDL19Or!L-D+{ zh#aCFZ7G<149ug!LQBhm^PVIfiuccg$Pi8scukZ581fO3qjq$8#lCVL0b#)({vdk9GY7P+nr31yVI%oZHox22 zA;1l|`g|-<%e-Tigfu?aS-!L!Da#(_?R?1m&WUGYW&Kmr52hcJ9ATy*owCEOVluK= zo6S{w7#`vDA>I>(cI>TMYvBGkU%&?Y&# zJwt{zLaUd*`P20$R^aSR3a+2vOtBwnP!tEfF&Jv<&3xG?BY}n_0g$RwCt`PXogwqY z&WwJ72{10Azv58_nkUzM>B3P<$!7{iZHo_!>BR*giVCey9o5rha|<(>-)@R-tX6~7 z=?F!hq3}6Oa1KXFUO1naFRqGj{#fXq|BWJD%-Y3oAQ`tE(OL&k=u?ZA)C4x~XE=J7 zWKU2ab-t!bUAq0>x%Ic|T#h$gHN7al~ z_*uD+%(6i6#f9@TcYTFPa`DpPg%Ek;YQ3pQqsNi9tg;V#uo1Ry88t{xLXD+cY;FB# zCrMzZEVcsa;fuZK_+Qq~Uog)`ug2vE%jWl0>U%L9F^}IMUtBUcF>W zOomRC(0Bc69)!+FZ)222&085Glv24pvbWHY+$+y1P(ouZp#7LO0FWtwV74^W7D14B zHVD-+q4Gyx_@5CpCi&^~9XZokBSH9dvU(D`87V4Vjdnau!bgaAI~)Pd#G`yka$Osy z1>ukFe0u~+<+7Zya#n;qWePS131LiDY|1)aN~q$pn_du_5(rTsY(qpdUiw1xAD6Ac zllMP{w#u7|L1l(S@|=SFB+z5POU0vV^|#Ar8M%^4D05c&(R{S~!ThT*$3+HcYIX8TS_>|9~AX?TOXBcwm=1 z$oY@tkah_>;ZBrm89VSa+D5?7Mkja{7aYp<(KA((sYwRDr@;6SW2YNfy7kax;mtJ~ zcbD>H*(I`ww@AWW$#t{%xr_JOMVN{gXkdm zIK<(98lP<51W+sSeGr%}o2YQS80BHC8_qdA2T1P)pQ=%CZ3F`o^vU);-B^SxFEUn@ zsK2HH@!#2t!5xA61mF)Ar#7;4i)3k4-08R=EHqDX(UBNZCe;tE=(2poQBNlQf-kO) zj1Syg`>GFE9J+TiXC7BGeRmakvRr@s9=0M}Eq*@nB=&BZOrRbAt)4vJoTOEJR93uD zqJM9O%~sSVGxE`i`eT=%BolW4Y%o*L!(?7o&x+sAQwq+<=PAteNav@!8Aq3Mi(8wb zvVe1y&0)pRljvebuOXqn%9UHn%_z1XzZ2R8D`x7{_hx0DF%xdiQxaCbQC42j1#Y3H z&3ynE9lM7pXyQ(rCsb3da(&JlL^s5+t57?EgMA~A;p(@Dj&wdEW$vx+i=scaim;ar z4qZ3-anQ_U!LBNgE0RvYHY(fpO3eUWb#lO(jEYSuVg%%y|38#itd=Qvjz|cn@ABlf zfn3s9v(o7Vp$TBmWUmcS-30O&p~p-sx^}e-VdmH@!>mGa6icfm-7uK%_H>&l zP%bA$MClu$gg+b!NiyS*{6fB>Wwa_Of~>+*u>^b;CD$-}Y{d)pz9h zm_yRlMmWbKnJeuDdH;G$ew^kFHULmS{`dc)biSs}U3(P#{e3;aGthyyClEodrNfG> z`ln0(yo~3&tF6NeC4a@*72Ex+l~o!2M@cxItGkCSSo9QEyKcZY%`(;bm!$Yj|p$y zAP66m{6W2SQ0Xkvn3ju|maH9w)4lJ^`=V>)6HMt!ub~C%5XW)~j_am6y;VsKW=Y~{ zDm(1Q$Z^2Zkp@Xiz>2kqKyi&Zz&pO(F=EH}mKX?ky zFM5pcYw3SFA=&-pv(RdFNsC6;3}1y;d`qjyL84fk>;$Rby|U4mU~Ub-2Ss^ z^`}WILzcBuiQXUd45!0b*n#}CD+>VfzrZgdlcyiSW0Nua1``ZlxdVQr zk!qo4U<|zFbE!qdbF>$zVeB=%tsUt1{y|fqr>gQF5hyo!VRiUm6S~5F&l8siefoCi zuFhasu07gnvo~t|mb{2h&d!j=C1Y-MDJAinrcIw3@6}cj?u~191om3C!=qiez z#v7G9K+fJC-A=xeBv#_Q#ESMU_TPg2--`CLCtAio&o_)oVadRfGix3xXlnRLYRFks% z-9t}jeE<);3$MfDjFt?$P`8JRJlnNBygT)8H)q2OGKe|G+M;im1J1YS*|O{ifxm8) zXDrL&g*ZX^ohkR*GslW`0(Q3^tv}uNS!GTOqS?G#>&p)YZU@bt?8LS5d_W%BB??vL zUB9C81DMuPknas~zE*1Ew;JTB%@ro?7C(!euyQz+rj6(Y*R%!UU#E8L7&QA1Hq1f; zVai`di$|si#LC^ROLHauEOk>m-R346=%o1RtCjNI84qIZXk!U!|M9l!+!hv0QgT@9 zqBt4CK~IfVKo{q*aHfa;r^+u*T6?$4j=Hrk;PcF94vT}w*pczA+^E^Ste+fBBA{#2_^Hgq|434?xxWZ}dqcJT z8Tm#j)SaC}zr|O}Mj()SP|xSlp*mynE278U1XNU2QPN02xFGlbYAH;^@FA;XR(*7s zS-Z-FwkP`0?fm@wA|QCQV!EzaveQqqgnbB6m@?97=Y^7z-^ab}v7>K(=y{E7v<`c3 zZ<^*MrIlMLLdd^k;J|y{lHEz&pKHbq_~ahV<#|6Mv;(N8EE+E!<9iNG6S$zDNv}cT zXl1|xFjQOZ}!?EN`uOIN5y@GcawB!TvTru z>7MkBV^`n}MPT#dMjqdT$1;r&-XW4&+P|g8dn-qGSw@RQ{JzZ9(=@>|pC0yN;%7A= zcUZRs)PzgHZgqT*mbbGbE}| z1NAQY%7>h6_QL2N%SCr42IzM%umi}0WJY(xi;ldnc%HC=zPf4o{y5F^Ex8{brCGff zim&hl^Jjt(@~>*i+Jk^-9*JqCvjf2HvQ|*yFk&~U@WA-@_7crOG7Q;RkQ?v( zBP|o0Ukpx5u##eA>!w5!Ao&B5#N+6J)kpiLOCYC^h^T?%H>cy_?9YCW&+ZgR7l8qD zizV}Siz}2bn;!qSwrS+xxLfS?Pmyw;-Q*rLg~N9PE;qmCf-Nz^P{A*I6g#Uc4*|56 z#~8A>35BG-Vu=o%Jy+`-FT0eoj1dhPV-aSy8VPsx3f8V11{#apqddQXBNOJ8Jwx{~q1S*i3QY-XYx(d9IuG*OM&;NPgb} zJRz@kKKzCWHP-aqmCow=PI0L?06x;{ApCXYnX#`^LbeUTb$sXk@4l|`?qc>V8W51| z{>5CSdBst@gxmOrN#J|LmdD5PG}V+!=GmA{Lv&7k=zE>{gM&JzO14*Et&k$$2kdh( z$TcmQ9p>4QUSzFbde4-3(5P3_7E>KWrhRyfWd0qmmR9{Q)BSSfn;IDb-YRP8-CyI# zlJ&_|df7RV0KriE?!4~$8qq@)d({ersq7`a!`$3v7R zgMn2o7L3K$P&1;fRqImY;(bBPceXKHeiY$|()xQtd2$3-XFlKtyrvdKa;Kv(?7oyg~8>*Q?d>I8{rAN z2mH9(geFezLXF?c~&7U^Mu@4*bi28)O|^7P;Gt@nv)*2jNEu76PK$F~z9t6N=P(K4#rL40GLbwsWK<7X*+jL%jL;9FO zdrLj7fZrid#qiEP{kQI09#|o8Pb}v88_)6xFEi_=*CeLsA-JOt^)4jTfDKI3jxR3ys?^jbM?*lwmlxSEffKLU0l7P;71CGsstmPpx1pm48eoyM&OR8baDAYmwg6@}yq z)&jn`3<|Bp+W}lR{(49lFhpnDxGY#$+Cg;fT>hnCK=seo8SMakR>1=(I$P?5~Q3r4ReR0XxEDLNG?|m+i}W zfodpWkcBdOWI$=8wz3~m}iSk;j)t3|QV z&L5}Yo-X)VDtNC0(x$Nig4{q#1o$X1XmeCg3j1iJqaQ$$31Fbq-I9w8HDjKZu;ULk zdP-uq5VjBm^N=(57@&fN2+F7sD~4D;vKR?5Ku_K(ooD9xz=tD7MisxE%!i<4ebnlu z4GbshG+S<)32;v>NKxfZ?z$@kL&>9qTB={)HD57DP}uP*_fmu?u5-DA+h z+}sb4&#zo<@BM%Kx2M%63>t74!-Gta?LqeNmz>Q!WJ-YLIOs9qDQJ23+F487+*zBt z9}%XeaxuKeD>|ECOi+~$rVn5Lk;^z3;sMaBRBf^2@*#SCd-1Aie)h2vP~m|%<<&d- z?)R=9_n#Ak6Ulk9j85}MO?x2`PnM;*RFpy=9jN6qhsS)YMd8eH&elw5CvyB^_l517 zJO>3xQ3*IvphNLOeAHy)d&$AnAnJ|5Vc23EhCI?C1A}@mXJoRLwh(YrK!@P*8=V{F z$>EP6FN9T+28x>}ZCW;C zhNBCHur9`L!fCzaH+okmn%14t{8ednI54Ys+N zyt~LmBC=v!$!fv{h?_=+C1>H#@<{oS_=)%_K8~*nd!x1<{36_4K~+TvRhQ56t@!15HTxsKi=o^Lvr7`NT z^6ihlviy+Kp3qnWP6ynIhGZH4m8G;K>~lH&vR+L4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/themes/black/client/src/img/stores/googleplay-badge.svg b/themes/black/client/src/img/stores/googleplay-badge.svg new file mode 100644 index 000000000..9e33e3aa2 --- /dev/null +++ b/themes/black/client/src/img/stores/googleplay-badge.svg @@ -0,0 +1,429 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/themes/black/client/src/img/success.png b/themes/black/client/src/img/success.png new file mode 100644 index 0000000000000000000000000000000000000000..ee9d6841bbb7208220a831c1c42917eb8facffa5 GIT binary patch literal 3147 zcmV-R47Br!P)@~8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H13&=@CK~#9!?VWj0Ro5NIWpwOJQDRbwNDwr}T5D`fYExS^ZNS!! z({?)3WhP^$({{SdG*eL?n@DR!L!g~W+NQSuv}t7VDXU9R+=7CjxFCufZipa@@1Y=Q z0fk@B56^R+j|cDGckj99-22XPzT=Dw+=`0CLjfNNgnC0Sw=jZrT^)0zv?CqKirDB=}*$K3Qy8 z0m#XXB}uo)ojuDZ2JEA;mJ)zo>nkK+#i=mr-oOa>%SoIPs_I~$EjO(IrB0#^a(-M+lx<9lN`o{IqVIbS~NiSdXS z#6y4#+4;gG!%zX}@U9%;6Klk9xgi43i5)q%FXr4q1XBU%l&+lB6MGhMEK32>WhV=t zbS47Ol|1sgZ;tzc#kvYW*YL?@T;^I{Hcn3gY_d~L7n?2u(7S%QdRK>hyX-yq-N}z3 znAFu7~3~!grS-_H?$ML&Q-^1W1)o)xkf}aUEDS zXKy#WS(dKX@un&fK(6jp0srJ3cJN)TsPU9;tBFSyfR$Ka zd)I0$W;x!Q0MQcg=vhqQ*W9@QJld}-vWEn~JeRu~UMbZiL9{mkHcH5&!#{nO4}5H| z@5p!}0A^CoDwww?#ozPU=uLo133>$hpKXBJyN%r7clAAhXR=m5B>*NQI~L~d_Dh1w zrv#uS*ZU=G@!(^7r^-GIzAF>6VqwmwYy7El9_%i_G6~x|{4VmS0-$Z zh1t83)t=uncL6eabTBT7di&VFV8i+CU~jB~e_YuQf2hfVm_zS#hrdJs`1PG_Fk@S) zPXRE&n^(fD9Z71=G1FauGNU{At)rXZ(!Cb1Cyq;{{(89-7O-72H!A$IoZ(+0@TYHE zs|db(UMx;jdY)zO0<;*}!JEiWI!<)>YUjF|;d_VwY$$wOpI6t}#1;Obs;}|&gAf2c_BYu$vKA@y7)sad{=9{?}TNC zGnpRSw-7%0#NpX45cpABwZnHMY?}>U^|43<1y~_6d{?`1-vQrcBmu%d&l&#tyDhrH zcjfuf=MPq(0BNGbe#g z>Rjl$#TEX!JGYp^cjejK)$o>w3e-M>eC{6(T{j!bOv~xH8b;KQ(l`Gr0`~ zaOf?-%9?CLJN0sRtIw6Q`1keqTDihMBMAH_5|E$(eR>Pt7 zZATNlU{4kXzAH=wz#T1ICQ!)7J6_{G;p1gO!uLLn%;XaA!-^01&7!$UfH#P|;h%Ok zLHHK&;M2##0VX$rU)}ddXnt^q-pY4~L!-c(xknO=02F!J%GekAc5E`5&K^ zF<7?Vfqn_!M<(0jx|j_FVG5d#Lm;+O&PJHJB}r)bbfT!8=}_?353Gao+m`|g{z->9 z;A5ud*{(643_=Mi9DyOh3C9fxGYNdUXr#uF>0qpxbHq_E;R}|Cg7_VVEeKItHOoxx zxC!B7rWGXo#n2^T{#MTtxmDk8fGN3&rh`u}0Y5iMu|$jr`|&+n1%V%$6K@&#bO2~Oh*~5{gss_DhL8E;&g7qh z$YtVC0^VD+%iFG468@x|Rgb)ui37n3MZUptw{Cgs6_{*6_;d|G9hjw5ERT(whg&aO z7Ct6CFaGynree`X?%~6+eJjI%HfL2Yn6X?uO2B$`Z$r1!((vj0e*u{Fg%K#hEk-Mi zdmi>en29HLlnwq26_tDm%>2q2-1*b`zq`7^fAFXuUM@_tG<-Vue;%yx(x~Vm2|9Iw zfB#`$K)}b1ptE-+$ADE{8;3i7+B8al@V{KR-tzDt5B=Jjz-lj!T&h6=gg>X?-GG3P znO>CmHdyi1u}jM`pyffQgg<(Ls5@Zgmq%m4s2e>_Mev=yJ@BOff=_S$;=$^#kAHpt zI{4_~K^PeHz84YqDCS(PhDly(?pryb_PCp%<@NDYfC!Rq(IvoQh>Lb-K+^Fd*wuIz z>bu%ufAa-ccWM{RFMcnez|%#agENW~=V=yzN?f)qcl}Oe#7qFUrvj&xK)k#DsG@}VYI&*_fJ(63EcXL}B0+dw{Ca;_`XvCB zAj3?rfU~j$5e4zP{pF}dfFKgPlz{tCFIIUAGF$++rR4jh1l)=|-Ed#xZMXnbf(axi zqy&b~@jG5AT{}@LKg|MA2|`6%O>AWe$m3r3%ezyi>cLJI0jLC#QUdmQE_d~VIiIeX zr588d1aSK#ke35?+q&-0C{CQICo^3IaJwgvH-`Gx^=+S4kPxOfFWm)jdpeMhi26P1 zogvr!&#_>|L;$yOf($ugsBLEN#p1-RTAuT{3E(koEawi?*YbZjYiH6gSo2{nfZGI; zoJCY#!ljMe>CZHRjUfWKO(mzxMJ1!};33!hJ!+?PO(B5C$|AYEZ20qiPh?@jFOBA5 zxBzZz3~gpvZOl>v@@zjqz<;cC4WA_h@VGa~4@3#@4xc>NpPsiTWjap=Tm|shfm9v{ z65w?{JTJa!ZpoS-h|u>!2;i|HwbZH-gxcd;UfPlL6OsB{C;>d`6BLQ~#GqCJ{HUL@ z&327^+`lDaUkfRKN4c6*(PkNaNr10pCC~ErrWYkQ%Z2PeMs@COMQUL#i2&V5l z$fR#RVfp%-7WzFh>Gupad;ghUfDxn!p*NB%=z9@!=^Iij=)01y)3>Jg(KzUz)zW`c lLjO-1{TleSLaf+V{|9F)WMK{Ekgos$002ovPDHLkV1l3q_6h(1 literal 0 HcmV?d00001 diff --git a/themes/black/client/src/img/user.png b/themes/black/client/src/img/user.png new file mode 100644 index 0000000000000000000000000000000000000000..00941399d34f0f2d85d323f61daa67c2c00c494d GIT binary patch literal 2933 zcmV-*3ySoKP)>+Y_1buC zheF*1yf*GR)m>@otnGcw%sqGZe)vfU+q>t^|F^R<_ujcTYIJlo%4jV9WAaR39I1IcZ1yCpS zIY0`~LOAsCXaEv{=cNF_2S1lxgn0!F-rv{6d0@U2VDjK!2d)?(^zpb(O#~{z<1L%h zz`>Ihfp_q;fNbDV1ck2z&vq|HoB+`1Xdnp~jL_hfe8nynpNI$nprH$ZHW~ak_zd`^ zVFiH3O##fnhzxsNhFJ&KlZFxi8uS9|F_R$&2KI2yF@ylnkj21V8EVk<1HThm0M?5q z!wen#x-@Ay#-LU3Q-KpQK-IVm#0Vh(6gr)CbjaY;1?MwNCoKRJI2)*zVQI!~;5m{4 zKw;knT4hL@Q-{ZzlmKWYD~#$v`_Iw@bH~becUG-S%q-brIc!hQ%CTE>@`@1rH~t^~ z41WiI7ypKEd+>ucn}h)DJO4JN0|WtfTGFky((<~-_O71$J^cftp@4sjuYs?HuSraT zYBjD2!VN!$U7T<~AWPy*p3UYD9S&zzZK#1y2EI1F2fi1fQN^iT1YmRXE_461$D3Uj zt`?WMJ^jO41wR&iFMLmYZvqmW;3a^GcjNIpyUo>oPp{y|g71wr5Rf26R{^k#lJlU> z;E|J`7V-*yELaPHq3y-G2*4(4_H+EMA1JSBYNZms53C7m<0t@*x4fXK060v^Myhx< z^3f-GCAX-B?*nV&Ji7@;T&HL$KpM}}+_t|Sw^I$@2iC~>tH7*@061HPO<3U4u(#Ll zsCIk$ywnB28nIT+BZv!B6#(a}u_;U3%(j=$=Do z#TDQ{E@c6dl>%I)Smctph;09_P|%>BeJ>}(((xfktJ z7s0;&KLKzvB#H$t|I_wH(ZlzFZ6PYb%qX@a7P;cyPd?-@iU8Oaq5{l|V%wsTEAFGy z*NTiH0Jepw04t){-gxASJA0|%rcngIwottjh+=#5NC6^K0BaQ6B~uCznF16>v8__2 z0Ffy`B??e41&CMy8c~2ADL}*u(1`*(ASuA*0;d!}b9zyLhw=;%u>#yj0luVI+|iR) z3XLLwHTR!XJA#H#02jsLHvDC;VR{11e>-6#D!>C2powB}vGJxJqX>XY&=VEFjRI6q zowRWAT5*}^;a|S)yiN7^;*#@GRF-=mZ=;7B#TC_}hF@9V(it0Xb`w6nHnOvU6tjZv zQCu5%GVtsb8)v#heEXR*D8MP|`{X294%5Wjv`g+KzVLjis&La`#O6b=`CF3!uv2g`gE;3*M` z#;x4QFBQM@Sb;#{W9HL+)51GMDB4+3-$eJ~&*NPZXX+P7fL8JkhK%LUCb~0)Po2vz z6DWM-oDh$Ojp1QkbGw`1CJ`gcS6NyF6W{}-0KXTd@WgEg$(;rE9XVelSop{vlmbkn zIxoQA5N1tN8<~EArgql=;phJ*x&WLxc7cc+qBP*Wtyu+B1lX9gzgWOezti6eA)5ti zfVZT)u7UgBK&6$ng5CG~*y2xspNrIpH&+Myu$D9>mOOMdV9Tq;UhoxC%|Js zpGXZc?K;kFCGqsE9J`?5w@)Hw&vy)6A®tx`|Cc!K{R~B2jc1N|ng#YPY z01+^g*}>)+v;a(kg*ZQ7)0sYS5YJw62)S4!2N7^)@}f|(3`qbcL4r0*!k9bJT`jgp}PPI5fXrZ%isracGF5;IdBtzN$?_Y z46k-0FnAp9&IZ2EGXt&yC`6b8>;=AxfZ)FdvVb2@IQAq2P>3)S*uhSVMm*K;1fHjK z%t;BL5Mc(81b7S(JhovA@HE9^O;P}b2rkn{6#(u#g!>KLW&ICB?B8ANXZSm~9Q{dlI$z1o fINvtfu|oa}IMcn@FFyTa00000NkvXXu0mjfV5=n> literal 0 HcmV?d00001 diff --git a/themes/black/client/src/img/warning.png b/themes/black/client/src/img/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..c6acd953b142a8e43e293c353a60e6313a8d39eb GIT binary patch literal 4038 zcmZ8kc{CK>7k@Km%nZhmB)g(al4J@g%*Ybi@{R0UV<~HPGGh#)vXiopt-hj2MImcp zY$;2&EGf;GWSK0JWq#AYzjw}k@4R>Jx$oW2UEdvZQv)=Q7!LpdZFo`Fg0oWo5|W!U zw`;lda28mgj^Py~XG9{237kF3|DsJG09+w|2|^mC_HqtIgY>L}{_%4U3Uvu^1EHaz zO4oh80%A^>WZHKCL(h;uQN z$(4fz2^gkydwnSstYvq?z;jH?4dHpjOIsi?F~5mv-=X~B1+IfT=c@TV0XW+s38jp3 zIm`8ol6U1Y3*FZ4G|uCdEr5XCymsumB+=8f9+kg$8eP;^u9f!rnogW5jie4Qx&UZq%;ioEF43zF#7yy~zM-eI zO?nd_=$d}*1Zdj89sj6I?h<22KC>b!%CwF;CBL2iff7f&(SHL%Y3gOm9%rm>z`BQ< zags$B!oM7@imA=`0F#zKMyK{>K{K;0WBK$OE(4l07$MAiJTzu}?T}$Eu6~)JEgc2l zf=SEgU@ShC_l!E7E{fG`pTiY)xdsO#!EcwQcM9VpAIi>eEYTaUlkwl)(ssK%=%={( zyLCh_G^+Jtx;(pcl>ozzx!j?_lkK#SDH7&caoLxQxBIRw{7*%VTL*&Jr`VX~UcGAs z20}q%Os|>1VApPeKz|^ki=-gYS z9Y@17L~Ki*qd&(F9GL7rg&nPk`xaYE{WG~yqtOG|AGd;=yeICQ3?P8Wl(?P zFZ7V8e_`3g%Z~$IWK8`8B+2Uaw1D@FC+7 z-$J8b@&2r&R`X87_vAo=1n`SqVsgn1)n)~(GkKZ^+y9#v%9lDe!PdCl<}d?4l&Z)= zLn&Pk*;lOwuF)@Xq&>Lrv$yIxxEBk9P*f6q;C&Gxctr z>UH@f5#<~SKVIH7{8TvYp}h5)?t8@rj)0$ z=(|UP)`a4gb}Nfh$}-Aot~qRv-2NG&Pw?!YJ_L{-cI0;gx6C17f#<(JU-shN4s}Xm zsr`bk<+y&nAzmdTfd%Fm{E6d?o6#b_8AlL0OZR6X0~$FN0^=Va&iV>~OZ62FfY!fr z+G@7W*EE5ULNUF6+k=dc=OuP(y~w^QFmG}IU~z1DYm-#g5&k$r%25&3Dt&RF(P6Qy z!hED53NQMhpoEN40!T@U!U(PZ!f|Zebkh%KFy7&Ljg0S%@80Y9{`L*_p0Gq(-Xa_n zWwVhZa3tlb{&BvDJc*;YmC03wmleLmlfhCIJloH!f053eR9|}uD3}(DuE;rORKDF~ z8OF`gF;dSjm&vc;j;H-8dLiCxQey{Hz=`iQ)BzERRTZji7F7reYFTV&VRD5#mM;P( zOnzY7S=<_1pZRu#%rT>*GCwrZtj>RI@Q?16O+_|h&X4@_qT z{7RG|7N$Y~RTfv^0XLX@uwvGl4+Up|W7er#Ssc0MwmZo`pUyz>8<(%}N)*8iLT<*$ zO7nn-T=u0({;o{JGM*TzB-T(t)5aqsCE#OXne$tyGP|&oKl$KN3NP@5DkD^g0|1p@ z4(AS`US%VnTz+8Z^%`KGKzv~a`sOjR&m!IZiOqmEvGch4*@fGRIo{5gpM0we7|#}f z^CTPH)gw)SBkBRwS>h6OBTukHNYUs@{TU=wUvp<|w38svkaGfXF17 zWRj^r4BA@-?+x!da`Cm2p9d}nF!q(p6NOeC5{5={Kr7n#qM8v0M{@o_vGlW5W}(rbH_w)a22X{DsMW>q`VKlT zXk-Y086nM7%Po?soy))lmj}z#H&QEp9}4F_KI_@4j-*^)ygEts*RkiKn62eo$G03- zOi`)M-~=((Ir@vJgpNwW2eeFB3y_uX|F=QcfJs>&llMuD- z;!BoDIu2Ioq}!HTPb2l-x0v3yVVB%8h?y?A85*|%wy3iFW1CtV+P>P+YD!=Qp&6nh zpIe}j|SXo%pJ_yAdzPv0Hd-05QBz!d!eVX!e_ z-js2+CCJYh%8^@Wg9~fkF6Qu>bT&Ro^dIN&*5>tgan0OP%w$V%fA-%-^*%>}*H>=U z!jTaxwGN=$~V1I2jb9T`^h|OQK?o+N?D|4 z%z=m!488|@gJ9jym%RV<;yy5CvE=1Zp4VzOXMit87iM+GU@MG@F&tH|m(TSZJC+>> zVV7QsOjvSjJ`@G(vuKq+ovB&m7i#;On+EK^sDZq)O`%>Aot~S$T6tk$6miZ z37nON_ttN@aBChO!q9G=2`91_S^1OMbP-+Qzb~7M7k%ZmY*tzbI>2DcboU%unRGe_ z^g6>eZQIZhx<$yz=BE-TqN+ya*KDr>e5)V=r(T0o09y}kAUswax2W8N6l5)zy{FXT zBCBMbJKg8c8{RJV4?lJfI=NJj3;36BD84{BOqIIaih|ifAHPMW@E@HB`s-w(SAV(o z8BYmb{tLZdgx&WRQ_Flp++DL9J%{!bU5L_O@OV^m-WN_@GfqN+t#6a>ZJ)fIEc54H z_Ahf?jW>B!W)Hfw>LIikDG8h3(g;8IH78vtMT}9pZk9gqaafF_b|zx9h71z{H7U<{ z+UCstV>w|G^N5qtm{_m!4V!Cc_#$GTw0CkB!PSFBD(8Zm(v9+Jmfk6V{CR2+G|9x>_Jus|!Ap5u#`Lp(Fwz*3I*OOGUT(q-M!Afy{tE@U zJ2F=Cu$IgfDXw+ZF2+tPekHA`bg@1(%I2vkB|8wwf zRU;Ls;@*5M8xL*Febm=t`gFhF?0_;=b~>tWNj@wC1yr}RMl6=> zX6-I}2_osksR_lEmhYexY@erD5yZS_@mPt{mq!Rag+PIruP1DNV3J?1b5O|b#ua+1 zKK*Qlxf{Zjnf!_bXYF=)Kg>LK0X;cl;x&6Us;m43gbnAydy`^s!rJ|y34o}Mlo51w#*DkxKu*ZSO^rjzY4`S%iTMPk?%#D9Hq0u zXO7WS9c7da`CWXmSEK4H`Zo6u$TGFji_X0wF zYHis}o|pf`w?RCV<8AhzFDtAF=BVX(@&7)kpvZHP-ER?mQZc1eo%81ghI*#D&vcxr F{{xrIJYfI; literal 0 HcmV?d00001 diff --git a/themes/black/client/src/index.ts b/themes/black/client/src/index.ts new file mode 100644 index 000000000..802004a8f --- /dev/null +++ b/themes/black/client/src/index.ts @@ -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 () { + (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, (global as any).u2f); + 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, (global as any).u2f); + 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); +})(); diff --git a/themes/black/client/src/lib/GetPromised.ts b/themes/black/client/src/lib/GetPromised.ts new file mode 100644 index 000000000..779139654 --- /dev/null +++ b/themes/black/client/src/lib/GetPromised.ts @@ -0,0 +1,14 @@ +import BluebirdPromise = require("bluebird"); + +export default function ($: JQueryStatic, url: string, data: Object, fn: any, + dataType: string): BluebirdPromise { + return new BluebirdPromise((resolve, reject) => { + $.get(url, {}, undefined, dataType) + .done((data: any) => { + resolve(data); + }) + .fail((xhr: JQueryXHR, textStatus: string) => { + reject(textStatus); + }); + }); +} \ No newline at end of file diff --git a/themes/black/client/src/lib/INotifier.ts b/themes/black/client/src/lib/INotifier.ts new file mode 100644 index 000000000..df947538f --- /dev/null +++ b/themes/black/client/src/lib/INotifier.ts @@ -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; +} \ No newline at end of file diff --git a/themes/black/client/src/lib/Notifier.ts b/themes/black/client/src/lib/Notifier.ts new file mode 100644 index 000000000..c0252b9b9 --- /dev/null +++ b/themes/black/client/src/lib/Notifier.ts @@ -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('status %s\ + %s', 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); + } +} \ No newline at end of file diff --git a/themes/black/client/src/lib/QueryParametersRetriever.ts b/themes/black/client/src/lib/QueryParametersRetriever.ts new file mode 100644 index 000000000..a529adb6e --- /dev/null +++ b/themes/black/client/src/lib/QueryParametersRetriever.ts @@ -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, " ")); + } +} \ No newline at end of file diff --git a/themes/black/client/src/lib/SafeRedirect.ts b/themes/black/client/src/lib/SafeRedirect.ts new file mode 100644 index 000000000..7e7684b8a --- /dev/null +++ b/themes/black/client/src/lib/SafeRedirect.ts @@ -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(); +} \ No newline at end of file diff --git a/themes/black/client/src/lib/firstfactor/FirstFactorValidator.ts b/themes/black/client/src/lib/firstfactor/FirstFactorValidator.ts new file mode 100644 index 000000000..eaa496fdd --- /dev/null +++ b/themes/black/client/src/lib/firstfactor/FirstFactorValidator.ts @@ -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 { + return new BluebirdPromise(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)); + }); + }); +} diff --git a/themes/black/client/src/lib/firstfactor/UISelectors.ts b/themes/black/client/src/lib/firstfactor/UISelectors.ts new file mode 100644 index 000000000..0e971b3c3 --- /dev/null +++ b/themes/black/client/src/lib/firstfactor/UISelectors.ts @@ -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"; diff --git a/themes/black/client/src/lib/firstfactor/index.ts b/themes/black/client/src/lib/firstfactor/index.ts new file mode 100644 index 000000000..24affee2d --- /dev/null +++ b/themes/black/client/src/lib/firstfactor/index.ts @@ -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); + }); +} + diff --git a/themes/black/client/src/lib/reset-password/constants.ts b/themes/black/client/src/lib/reset-password/constants.ts new file mode 100644 index 000000000..d48d4e67d --- /dev/null +++ b/themes/black/client/src/lib/reset-password/constants.ts @@ -0,0 +1,2 @@ + +export const FORM_SELECTOR = ".form-signin"; \ No newline at end of file diff --git a/themes/black/client/src/lib/reset-password/reset-password-form.ts b/themes/black/client/src/lib/reset-password/reset-password-form.ts new file mode 100644 index 000000000..b94279cde --- /dev/null +++ b/themes/black/client/src/lib/reset-password/reset-password-form.ts @@ -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); + }); +} diff --git a/themes/black/client/src/lib/reset-password/reset-password-request.ts b/themes/black/client/src/lib/reset-password/reset-password-request.ts new file mode 100644 index 000000000..846226d75 --- /dev/null +++ b/themes/black/client/src/lib/reset-password/reset-password-request.ts @@ -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); + }); +} + diff --git a/themes/black/client/src/lib/secondfactor/TOTPValidator.ts b/themes/black/client/src/lib/secondfactor/TOTPValidator.ts new file mode 100644 index 000000000..5394139a6 --- /dev/null +++ b/themes/black/client/src/lib/secondfactor/TOTPValidator.ts @@ -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 { + return new BluebirdPromise(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)); + }); + }); +} \ No newline at end of file diff --git a/themes/black/client/src/lib/secondfactor/U2FValidator.ts b/themes/black/client/src/lib/secondfactor/U2FValidator.ts new file mode 100644 index 000000000..5812922f7 --- /dev/null +++ b/themes/black/client/src/lib/secondfactor/U2FValidator.ts @@ -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 { + return new BluebirdPromise(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 { + 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, $); + }); +} diff --git a/themes/black/client/src/lib/secondfactor/constants.ts b/themes/black/client/src/lib/secondfactor/constants.ts new file mode 100644 index 000000000..50bba7571 --- /dev/null +++ b/themes/black/client/src/lib/secondfactor/constants.ts @@ -0,0 +1,3 @@ + +export const TOTP_FORM_SELECTOR = ".form-signin.totp"; +export const TOTP_TOKEN_SELECTOR = ".form-signin #token"; diff --git a/themes/black/client/src/lib/secondfactor/index.ts b/themes/black/client/src/lib/secondfactor/index.ts new file mode 100644 index 000000000..279723dce --- /dev/null +++ b/themes/black/client/src/lib/secondfactor/index.ts @@ -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); + }); +} \ No newline at end of file diff --git a/themes/black/client/src/lib/totp-register/totp-register.ts b/themes/black/client/src/lib/totp-register/totp-register.ts new file mode 100644 index 000000000..6a9aa7ee0 --- /dev/null +++ b/themes/black/client/src/lib/totp-register/totp-register.ts @@ -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); +} diff --git a/themes/black/client/src/lib/totp-register/ui-selector.ts b/themes/black/client/src/lib/totp-register/ui-selector.ts new file mode 100644 index 000000000..9d43fabea --- /dev/null +++ b/themes/black/client/src/lib/totp-register/ui-selector.ts @@ -0,0 +1,2 @@ + +export const QRCODE_ID_SELECTOR = "#qrcode"; \ No newline at end of file diff --git a/themes/black/client/src/lib/u2f-register/u2f-register.ts b/themes/black/client/src/lib/u2f-register/u2f-register.ts new file mode 100644 index 000000000..abf40ee05 --- /dev/null +++ b/themes/black/client/src/lib/u2f-register/u2f-register.ts @@ -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 { + return new BluebirdPromise(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 { + 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); + }); + }); +} diff --git a/themes/black/client/src/thirdparties/qrcode.min.js b/themes/black/client/src/thirdparties/qrcode.min.js new file mode 100644 index 000000000..993e88f39 --- /dev/null +++ b/themes/black/client/src/thirdparties/qrcode.min.js @@ -0,0 +1 @@ +var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); \ No newline at end of file diff --git a/themes/black/client/src/thirdparties/u2f-api.js b/themes/black/client/src/thirdparties/u2f-api.js new file mode 100644 index 000000000..8c7801e3b --- /dev/null +++ b/themes/black/client/src/thirdparties/u2f-api.js @@ -0,0 +1,749 @@ +//Copyright 2014-2015 Google Inc. All rights reserved. + +//Use of this source code is governed by a BSD-style +//license that can be found in the LICENSE file or at +//https://developers.google.com/open-source/licenses/bsd + +/** + * @fileoverview The U2F api. + */ +'use strict'; + + +/** + * Namespace for the U2F api. + * @type {Object} + */ +var u2f = u2f || {}; + +/** + * FIDO U2F Javascript API Version + * @number + */ +var js_api_version; + +/** + * The U2F extension id + * @const {string} + */ +// The Chrome packaged app extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the package Chrome app and does not require installing the U2F Chrome extension. + u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; +// The U2F Chrome extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the U2F Chrome extension to authenticate. +// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; + + +/** + * Message types for messsages to/from the extension + * @const + * @enum {string} + */ +u2f.MessageTypes = { + 'U2F_REGISTER_REQUEST': 'u2f_register_request', + 'U2F_REGISTER_RESPONSE': 'u2f_register_response', + 'U2F_SIGN_REQUEST': 'u2f_sign_request', + 'U2F_SIGN_RESPONSE': 'u2f_sign_response', + 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', + 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' +}; + + +/** + * Response status codes + * @const + * @enum {number} + */ +u2f.ErrorCodes = { + 'OK': 0, + 'OTHER_ERROR': 1, + 'BAD_REQUEST': 2, + 'CONFIGURATION_UNSUPPORTED': 3, + 'DEVICE_INELIGIBLE': 4, + 'TIMEOUT': 5 +}; + + +/** + * A message for registration requests + * @typedef {{ + * type: u2f.MessageTypes, + * appId: ?string, + * timeoutSeconds: ?number, + * requestId: ?number + * }} + */ +u2f.U2fRequest; + + +/** + * A message for registration responses + * @typedef {{ + * type: u2f.MessageTypes, + * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), + * requestId: ?number + * }} + */ +u2f.U2fResponse; + + +/** + * An error object for responses + * @typedef {{ + * errorCode: u2f.ErrorCodes, + * errorMessage: ?string + * }} + */ +u2f.Error; + +/** + * Data object for a single sign request. + * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} + */ +u2f.Transport; + + +/** + * Data object for a single sign request. + * @typedef {Array} + */ +u2f.Transports; + +/** + * Data object for a single sign request. + * @typedef {{ + * version: string, + * challenge: string, + * keyHandle: string, + * appId: string + * }} + */ +u2f.SignRequest; + + +/** + * Data object for a sign response. + * @typedef {{ + * keyHandle: string, + * signatureData: string, + * clientData: string + * }} + */ +u2f.SignResponse; + + +/** + * Data object for a registration request. + * @typedef {{ + * version: string, + * challenge: string + * }} + */ +u2f.RegisterRequest; + + +/** + * Data object for a registration response. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: Transports, + * appId: string + * }} + */ +u2f.RegisterResponse; + + +/** + * Data object for a registered key. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: ?Transports, + * appId: ?string + * }} + */ +u2f.RegisteredKey; + + +/** + * Data object for a get API register response. + * @typedef {{ + * js_api_version: number + * }} + */ +u2f.GetJsApiVersionResponse; + + +//Low level MessagePort API support + +/** + * Sets up a MessagePort to the U2F extension using the + * available mechanisms. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + */ +u2f.getMessagePort = function(callback) { + if (typeof chrome != 'undefined' && chrome.runtime) { + // The actual message here does not matter, but we need to get a reply + // for the callback to run. Thus, send an empty signature request + // in order to get a failure response. + var msg = { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: [] + }; + chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { + if (!chrome.runtime.lastError) { + // We are on a whitelisted origin and can talk directly + // with the extension. + u2f.getChromeRuntimePort_(callback); + } else { + // chrome.runtime was available, but we couldn't message + // the extension directly, use iframe + u2f.getIframePort_(callback); + } + }); + } else if (u2f.isAndroidChrome_()) { + u2f.getAuthenticatorPort_(callback); + } else if (u2f.isIosChrome_()) { + u2f.getIosPort_(callback); + } else { + // chrome.runtime was not available at all, which is normal + // when this origin doesn't have access to any extensions. + u2f.getIframePort_(callback); + } +}; + +/** + * Detect chrome running on android based on the browser's useragent. + * @private + */ +u2f.isAndroidChrome_ = function() { + var userAgent = navigator.userAgent; + return userAgent.indexOf('Chrome') != -1 && + userAgent.indexOf('Android') != -1; +}; + +/** + * Detect chrome running on iOS based on the browser's platform. + * @private + */ +u2f.isIosChrome_ = function() { + return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; +}; + +/** + * Connects directly to the extension via chrome.runtime.connect. + * @param {function(u2f.WrappedChromeRuntimePort_)} callback + * @private + */ +u2f.getChromeRuntimePort_ = function(callback) { + var port = chrome.runtime.connect(u2f.EXTENSION_ID, + {'includeTlsChannelId': true}); + setTimeout(function() { + callback(new u2f.WrappedChromeRuntimePort_(port)); + }, 0); +}; + +/** + * Return a 'port' abstraction to the Authenticator app. + * @param {function(u2f.WrappedAuthenticatorPort_)} callback + * @private + */ +u2f.getAuthenticatorPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedAuthenticatorPort_()); + }, 0); +}; + +/** + * Return a 'port' abstraction to the iOS client app. + * @param {function(u2f.WrappedIosPort_)} callback + * @private + */ +u2f.getIosPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedIosPort_()); + }, 0); +}; + +/** + * A wrapper for chrome.runtime.Port that is compatible with MessagePort. + * @param {Port} port + * @constructor + * @private + */ +u2f.WrappedChromeRuntimePort_ = function(port) { + this.port_ = port; +}; + +/** + * Format and return a sign request compliant with the JS API version supported by the extension. + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatSignRequest_ = + function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: challenge, + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: signRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + appId: appId, + challenge: challenge, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; +}; + +/** + * Format and return a register request compliant with the JS API version supported by the extension.. + * @param {Array} signRequests + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatRegisterRequest_ = + function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + for (var i = 0; i < registerRequests.length; i++) { + registerRequests[i].appId = appId; + } + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: registerRequests[0], + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + signRequests: signRequests, + registerRequests: registerRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + appId: appId, + registerRequests: registerRequests, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; +}; + + +/** + * Posts a message on the underlying channel. + * @param {Object} message + */ +u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { + this.port_.postMessage(message); +}; + + +/** + * Emulates the HTML 5 addEventListener interface. Works only for the + * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedChromeRuntimePort_.prototype.addEventListener = + function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message' || name == 'onmessage') { + this.port_.onMessage.addListener(function(message) { + // Emulate a minimal MessageEvent object + handler({'data': message}); + }); + } else { + console.error('WrappedChromeRuntimePort only supports onMessage'); + } +}; + +/** + * Wrap the Authenticator app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedAuthenticatorPort_ = function() { + this.requestId_ = -1; + this.requestObject_ = null; +} + +/** + * Launch the Authenticator intent. + * @param {Object} message + */ +u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { + var intentUrl = + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + + ';S.request=' + encodeURIComponent(JSON.stringify(message)) + + ';end'; + document.location = intentUrl; +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { + return "WrappedAuthenticatorPort_"; +}; + + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message') { + var self = this; + /* Register a callback to that executes when + * chrome injects the response. */ + window.addEventListener( + 'message', self.onRequestUpdate_.bind(self, handler), false); + } else { + console.error('WrappedAuthenticatorPort only supports message'); + } +}; + +/** + * Callback invoked when a response is received from the Authenticator. + * @param function({data: Object}) callback + * @param {Object} message message Object + */ +u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = + function(callback, message) { + var messageObject = JSON.parse(message.data); + var intentUrl = messageObject['intentURL']; + + var errorCode = messageObject['errorCode']; + var responseObject = null; + if (messageObject.hasOwnProperty('data')) { + responseObject = /** @type {Object} */ ( + JSON.parse(messageObject['data'])); + } + + callback({'data': responseObject}); +}; + +/** + * Base URL for intents to Authenticator. + * @const + * @private + */ +u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = + 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; + +/** + * Wrap the iOS client app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedIosPort_ = function() {}; + +/** + * Launch the iOS client app request + * @param {Object} message + */ +u2f.WrappedIosPort_.prototype.postMessage = function(message) { + var str = JSON.stringify(message); + var url = "u2f://auth?" + encodeURI(str); + location.replace(url); +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedIosPort_.prototype.getPortType = function() { + return "WrappedIosPort_"; +}; + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name !== 'message') { + console.error('WrappedIosPort only supports message'); + } +}; + +/** + * Sets up an embedded trampoline iframe, sourced from the extension. + * @param {function(MessagePort)} callback + * @private + */ +u2f.getIframePort_ = function(callback) { + // Create the iframe + var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; + var iframe = document.createElement('iframe'); + iframe.src = iframeOrigin + '/u2f-comms.html'; + iframe.setAttribute('style', 'display:none'); + document.body.appendChild(iframe); + + var channel = new MessageChannel(); + var ready = function(message) { + if (message.data == 'ready') { + channel.port1.removeEventListener('message', ready); + callback(channel.port1); + } else { + console.error('First event on iframe port was not "ready"'); + } + }; + channel.port1.addEventListener('message', ready); + channel.port1.start(); + + iframe.addEventListener('load', function() { + // Deliver the port to the iframe and initialize + iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); + }); +}; + + +//High-level JS API + +/** + * Default extension response timeout in seconds. + * @const + */ +u2f.EXTENSION_TIMEOUT_SEC = 30; + +/** + * A singleton instance for a MessagePort to the extension. + * @type {MessagePort|u2f.WrappedChromeRuntimePort_} + * @private + */ +u2f.port_ = null; + +/** + * Callbacks waiting for a port + * @type {Array} + * @private + */ +u2f.waitingForPort_ = []; + +/** + * A counter for requestIds. + * @type {number} + * @private + */ +u2f.reqCounter_ = 0; + +/** + * A map from requestIds to client callbacks + * @type {Object.} + * @private + */ +u2f.callbackMap_ = {}; + +/** + * Creates or retrieves the MessagePort singleton to use. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + * @private + */ +u2f.getPortSingleton_ = function(callback) { + if (u2f.port_) { + callback(u2f.port_); + } else { + if (u2f.waitingForPort_.length == 0) { + u2f.getMessagePort(function(port) { + u2f.port_ = port; + u2f.port_.addEventListener('message', + /** @type {function(Event)} */ (u2f.responseHandler_)); + + // Careful, here be async callbacks. Maybe. + while (u2f.waitingForPort_.length) + u2f.waitingForPort_.shift()(u2f.port_); + }); + } + u2f.waitingForPort_.push(callback); + } +}; + +/** + * Handles response messages from the extension. + * @param {MessageEvent.} message + * @private + */ +u2f.responseHandler_ = function(message) { + var response = message.data; + var reqId = response['requestId']; + if (!reqId || !u2f.callbackMap_[reqId]) { + console.error('Unknown or missing requestId in response.'); + return; + } + var cb = u2f.callbackMap_[reqId]; + delete u2f.callbackMap_[reqId]; + cb(response['responseData']); +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the sign request. + * @param {string=} appId + * @param {string=} challenge + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual sign request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual sign request in the supported API version. + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * @param {string=} appId + * @param {string=} challenge + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the register request. + * @param {string=} appId + * @param {Array} registerRequests + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual register request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual register request in the supported API version. + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * @param {string=} appId + * @param {Array} registerRequests + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatRegisterRequest_( + appId, registeredKeys, registerRequests, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + + +/** + * Dispatches a message to the extension to find out the supported + * JS API version. + * If the user is on a mobile phone and is thus using Google Authenticator instead + * of the Chrome extension, don't send the request and simply return 0. + * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.getApiVersion = function(callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + // If we are using Android Google Authenticator or iOS client app, + // do not fire an intent to ask which JS API version to use. + if (port.getPortType) { + var apiVersion; + switch (port.getPortType()) { + case 'WrappedIosPort_': + case 'WrappedAuthenticatorPort_': + apiVersion = 1.1; + break; + + default: + apiVersion = 0; + break; + } + callback({ 'js_api_version': apiVersion }); + return; + } + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var req = { + type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, + timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), + requestId: reqId + }; + port.postMessage(req); + }); +}; + diff --git a/themes/black/client/test/Notifier.test.ts b/themes/black/client/test/Notifier.test.ts new file mode 100644 index 000000000..70bfea146 --- /dev/null +++ b/themes/black/client/test/Notifier.test.ts @@ -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"); + }); +}); \ No newline at end of file diff --git a/themes/black/client/test/firstfactor/FirstFactorValidator.test.ts b/themes/black/client/test/firstfactor/FirstFactorValidator.test.ts new file mode 100644 index 000000000..ac8353278 --- /dev/null +++ b/themes/black/client/test/firstfactor/FirstFactorValidator.test.ts @@ -0,0 +1,44 @@ + +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", "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", "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."); + }); + }); +}); \ No newline at end of file diff --git a/themes/black/client/test/mocks/NotifierStub.ts b/themes/black/client/test/mocks/NotifierStub.ts new file mode 100644 index 000000000..9c268d66d --- /dev/null +++ b/themes/black/client/test/mocks/NotifierStub.ts @@ -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(); + } +} \ No newline at end of file diff --git a/themes/black/client/test/mocks/jquery.ts b/themes/black/client/test/mocks/jquery.ts new file mode 100644 index 000000000..273f90861 --- /dev/null +++ b/themes/black/client/test/mocks/jquery.ts @@ -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() + }; +} diff --git a/themes/black/client/test/mocks/u2f-api.ts b/themes/black/client/test/mocks/u2f-api.ts new file mode 100644 index 000000000..d123f6a95 --- /dev/null +++ b/themes/black/client/test/mocks/u2f-api.ts @@ -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() + }; +} \ No newline at end of file diff --git a/themes/black/client/test/secondfactor/TOTPValidator.test.ts b/themes/black/client/test/secondfactor/TOTPValidator.test.ts new file mode 100644 index 000000000..5dd6f15c3 --- /dev/null +++ b/themes/black/client/test/secondfactor/TOTPValidator.test.ts @@ -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(); + }); + }); +}); \ No newline at end of file diff --git a/themes/black/client/test/totp-register/totp-register.test.ts b/themes/black/client/test/totp-register/totp-register.test.ts new file mode 100644 index 000000000..86fc455a1 --- /dev/null +++ b/themes/black/client/test/totp-register/totp-register.test.ts @@ -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); + }); +}); \ No newline at end of file diff --git a/themes/black/client/tsconfig.json b/themes/black/client/tsconfig.json new file mode 100644 index 000000000..0bb4d62ff --- /dev/null +++ b/themes/black/client/tsconfig.json @@ -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/**/*" + ] +} diff --git a/themes/black/client/tslint.json b/themes/black/client/tslint.json new file mode 100644 index 000000000..c2c1b7501 --- /dev/null +++ b/themes/black/client/tslint.json @@ -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 + } +} diff --git a/themes/black/server/.directory b/themes/black/server/.directory new file mode 100644 index 000000000..b7754766c --- /dev/null +++ b/themes/black/server/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,58,20 +Version=3 +ViewMode=1 diff --git a/themes/black/server/src/index.ts b/themes/black/server/src/index.ts new file mode 100755 index 000000000..fcbf4d022 --- /dev/null +++ b/themes/black/server/src/index.ts @@ -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 "); + 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); diff --git a/themes/black/server/src/lib/.directory b/themes/black/server/src/lib/.directory new file mode 100644 index 000000000..006b379ad --- /dev/null +++ b/themes/black/server/src/lib/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,59,13 +Version=3 +ViewMode=1 diff --git a/themes/black/server/src/lib/AuthenticationSessionHandler.ts b/themes/black/server/src/lib/AuthenticationSessionHandler.ts new file mode 100644 index 000000000..57361bf8a --- /dev/null +++ b/themes/black/server/src/lib/AuthenticationSessionHandler.ts @@ -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; + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/ErrorReplies.ts b/themes/black/server/src/lib/ErrorReplies.ts new file mode 100644 index 000000000..f1c5f4fd1 --- /dev/null +++ b/themes/black/server/src/lib/ErrorReplies.ts @@ -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 }); +} \ No newline at end of file diff --git a/themes/black/server/src/lib/Exceptions.ts b/themes/black/server/src/lib/Exceptions.ts new file mode 100644 index 000000000..83fa4eb6b --- /dev/null +++ b/themes/black/server/src/lib/Exceptions.ts @@ -0,0 +1,88 @@ + +export class LdapSearchError extends Error { + constructor(message?: string) { + super(message); + this.name = "LdapSearchError"; + (Object).setPrototypeOf(this, LdapSearchError.prototype); + } +} + +export class LdapBindError extends Error { + constructor(message?: string) { + super(message); + this.name = "LdapBindError"; + (Object).setPrototypeOf(this, LdapBindError.prototype); + } +} + +export class LdapError extends Error { + constructor(message?: string) { + super(message); + this.name = "LdapError"; + (Object).setPrototypeOf(this, LdapError.prototype); + } +} + +export class IdentityError extends Error { + constructor(message?: string) { + super(message); + this.name = "IdentityError"; + (Object).setPrototypeOf(this, IdentityError.prototype); + } +} + +export class AccessDeniedError extends Error { + constructor(message?: string) { + super(message); + this.name = "AccessDeniedError"; + (Object).setPrototypeOf(this, AccessDeniedError.prototype); + } +} + +export class AuthenticationRegulationError extends Error { + constructor(message?: string) { + super(message); + this.name = "AuthenticationRegulationError"; + (Object).setPrototypeOf(this, AuthenticationRegulationError.prototype); + } +} + +export class InvalidTOTPError extends Error { + constructor(message?: string) { + super(message); + this.name = "InvalidTOTPError"; + (Object).setPrototypeOf(this, InvalidTOTPError.prototype); + } +} + +export class NotAuthenticatedError extends Error { + constructor(message?: string) { + super(message); + this.name = "NotAuthenticatedError"; + (Object).setPrototypeOf(this, NotAuthenticatedError.prototype); + } +} + +export class NotAuthorizedError extends Error { + constructor(message?: string) { + super(message); + this.name = "NotAuthanticatedError"; + (Object).setPrototypeOf(this, NotAuthorizedError.prototype); + } +} + +export class FirstFactorValidationError extends Error { + constructor(message?: string) { + super(message); + this.name = "FirstFactorValidationError"; + (Object).setPrototypeOf(this, FirstFactorValidationError.prototype); + } +} + +export class SecondFactorValidationError extends Error { + constructor(message?: string) { + super(message); + this.name = "SecondFactorValidationError"; + (Object).setPrototypeOf(this, FirstFactorValidationError.prototype); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/FirstFactorValidator.ts b/themes/black/server/src/lib/FirstFactorValidator.ts new file mode 100644 index 000000000..231060002 --- /dev/null +++ b/themes/black/server/src/lib/FirstFactorValidator.ts @@ -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 { + 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(); + }); +} \ No newline at end of file diff --git a/themes/black/server/src/lib/IdentityCheckMiddleware.spec.ts b/themes/black/server/src/lib/IdentityCheckMiddleware.spec.ts new file mode 100644 index 000000000..842ed6bcb --- /dev/null +++ b/themes/black/server/src/lib/IdentityCheckMiddleware.spec.ts @@ -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")); + }); + }); + }); +}); diff --git a/themes/black/server/src/lib/IdentityCheckMiddleware.ts b/themes/black/server/src/lib/IdentityCheckMiddleware.ts new file mode 100644 index 000000000..e72ea4db1 --- /dev/null +++ b/themes/black/server/src/lib/IdentityCheckMiddleware.ts @@ -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 { + + 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 { + 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 { + 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 { + + let authSession: AuthenticationSession; + const identityToken = objectPath.get( + 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 { + 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)); + }; +} diff --git a/themes/black/server/src/lib/IdentityCheckPreValidationTemplate.ts b/themes/black/server/src/lib/IdentityCheckPreValidationTemplate.ts new file mode 100644 index 000000000..0161ce406 --- /dev/null +++ b/themes/black/server/src/lib/IdentityCheckPreValidationTemplate.ts @@ -0,0 +1,3 @@ + + +export const PRE_VALIDATION_TEMPLATE = "need-identity-validation"; \ No newline at end of file diff --git a/themes/black/server/src/lib/IdentityValidable.ts b/themes/black/server/src/lib/IdentityValidable.ts new file mode 100644 index 000000000..075580c9e --- /dev/null +++ b/themes/black/server/src/lib/IdentityValidable.ts @@ -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; + postValidationInit(req: Express.Request): Bluebird; + + // 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; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/IdentityValidableStub.spec.ts b/themes/black/server/src/lib/IdentityValidableStub.spec.ts new file mode 100644 index 000000000..20a977140 --- /dev/null +++ b/themes/black/server/src/lib/IdentityValidableStub.spec.ts @@ -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 { + return this.preValidationInitStub(req); + } + + postValidationInit(req: Express.Request): Bluebird { + 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(); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/Server.spec.ts b/themes/black/server/src/lib/Server.spec.ts new file mode 100644 index 000000000..365163254 --- /dev/null +++ b/themes/black/server/src/lib/Server.spec.ts @@ -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(); + }); + }); +}); diff --git a/themes/black/server/src/lib/Server.ts b/themes/black/server/src/lib/Server.ts new file mode 100644 index 000000000..4090f6294 --- /dev/null +++ b/themes/black/server/src/lib/Server.ts @@ -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 { + 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((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 { + 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(); + } +} + diff --git a/themes/black/server/src/lib/ServerVariables.ts b/themes/black/server/src/lib/ServerVariables.ts new file mode 100644 index 000000000..cd3dd6dc8 --- /dev/null +++ b/themes/black/server/src/lib/ServerVariables.ts @@ -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; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/ServerVariablesInitializer.ts b/themes/black/server/src/lib/ServerVariablesInitializer.ts new file mode 100644 index 000000000..df79238cc --- /dev/null +++ b/themes/black/server/src/lib/ServerVariablesInitializer.ts @@ -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 { + 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 { + + 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); + }); + } +} diff --git a/themes/black/server/src/lib/ServerVariablesMockBuilder.spec.ts b/themes/black/server/src/lib/ServerVariablesMockBuilder.spec.ts new file mode 100644 index 000000000..7874702a0 --- /dev/null +++ b/themes/black/server/src/lib/ServerVariablesMockBuilder.spec.ts @@ -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 + }; + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/Level.ts b/themes/black/server/src/lib/authentication/Level.ts new file mode 100644 index 000000000..57b6a2346 --- /dev/null +++ b/themes/black/server/src/lib/authentication/Level.ts @@ -0,0 +1,5 @@ +export enum Level { + NOT_AUTHENTICATED = 0, + ONE_FACTOR = 1, + TWO_FACTOR = 2 +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/GroupsAndEmails.ts b/themes/black/server/src/lib/authentication/backends/GroupsAndEmails.ts new file mode 100644 index 000000000..3434ba66f --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/GroupsAndEmails.ts @@ -0,0 +1,5 @@ + +export interface GroupsAndEmails { + groups: string[]; + emails: string[]; +} diff --git a/themes/black/server/src/lib/authentication/backends/IUsersDatabase.ts b/themes/black/server/src/lib/authentication/backends/IUsersDatabase.ts new file mode 100644 index 000000000..d7fa13b7a --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/IUsersDatabase.ts @@ -0,0 +1,10 @@ +import Bluebird = require("bluebird"); + +import { GroupsAndEmails } from "./GroupsAndEmails"; + +export interface IUsersDatabase { + checkUserPassword(username: string, password: string): Bluebird; + getEmails(username: string): Bluebird; + getGroups(username: string): Bluebird; + updatePassword(username: string, newPassword: string): Bluebird; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts b/themes/black/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts new file mode 100644 index 000000000..19341a5dd --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts @@ -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 { + return this.checkUserPasswordStub(username, password); + } + + getEmails(username: string): Bluebird { + return this.getEmailsStub(username); + } + + getGroups(username: string): Bluebird { + return this.getGroupsStub(username); + } + + updatePassword(username: string, newPassword: string): Bluebird { + return this.updatePasswordStub(username, newPassword); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts b/themes/black/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts new file mode 100644 index 000000000..a258a78f7 --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts @@ -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()); + }); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/file/FileUsersDatabase.ts b/themes/black/server/src/lib/authentication/backends/file/FileUsersDatabase.ts new file mode 100644 index 000000000..d34dde21e --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/file/FileUsersDatabase.ts @@ -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 { + return new Bluebird((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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + return this.readDatabase() + .then((database) => { + return this.checkUserExists(database, username) + .then(() => this.retrieveEmails(database, username)); + }); + } + + getGroups(username: string): Bluebird { + return this.readDatabase() + .then((database) => { + return this.checkUserExists(database, username) + .then(() => this.retrieveGroups(database, username)); + }); + } + + updatePassword(username: string, newPassword: string): Bluebird { + return this.readDatabase() + .then((database) => { + return this.checkUserExists(database, username) + .then(() => this.replacePassword(database, username, newPassword)); + }); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/file/ReadWriteQueue.ts b/themes/black/server/src/lib/authentication/backends/file/ReadWriteQueue.ts new file mode 100644 index 000000000..957ddaec5 --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/file/ReadWriteQueue.ts @@ -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(); + } + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/ISession.ts b/themes/black/server/src/lib/authentication/backends/ldap/ISession.ts new file mode 100644 index 000000000..da2c74433 --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/ldap/ISession.ts @@ -0,0 +1,12 @@ + +import BluebirdPromise = require("bluebird"); + +export interface ISession { + open(): BluebirdPromise; + close(): BluebirdPromise; + + searchUserDn(username: string): BluebirdPromise; + searchEmails(username: string): BluebirdPromise; + searchGroups(username: string): BluebirdPromise; + modifyPassword(username: string, newPassword: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/ISessionFactory.ts b/themes/black/server/src/lib/authentication/backends/ldap/ISessionFactory.ts new file mode 100644 index 000000000..014d1eea5 --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/ldap/ISessionFactory.ts @@ -0,0 +1,6 @@ + +import { ISession } from "./ISession"; + +export interface ISessionFactory { + create(userDN: string, password: string): ISession; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts b/themes/black/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts new file mode 100644 index 000000000..f4a6e6306 --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts @@ -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); + }) + }); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts b/themes/black/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts new file mode 100644 index 000000000..edda62ec6 --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts @@ -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 = (session: ISession) => Bluebird; + +export class LdapUsersDatabase implements IUsersDatabase { + private sessionFactory: ISessionFactory; + private configuration: LdapConfiguration; + + constructor( + sessionFactory: ISessionFactory, + configuration: LdapConfiguration) { + this.sessionFactory = sessionFactory; + this.configuration = configuration; + } + + private withSession( + username: string, + password: string, + cb: SessionCallback): Bluebird { + const session = this.sessionFactory.create(username, password); + return session.open() + .then(() => cb(session)) + .finally(() => session.close()); + } + + checkUserPassword(username: string, password: string): Bluebird { + const that = this; + function verifyUserPassword(userDN: string) { + return that.withSession( + 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 { + 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 { + 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 { + 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)); + }); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts b/themes/black/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts new file mode 100644 index 000000000..9dedfcb7c --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts @@ -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"); + }); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/SafeSession.ts b/themes/black/server/src/lib/authentication/backends/ldap/SafeSession.ts new file mode 100644 index 000000000..572209066 --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/ldap/SafeSession.ts @@ -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 { + return this.sesion.open(); + } + + close(): BluebirdPromise { + return this.sesion.close(); + } + + searchGroups(username: string): BluebirdPromise { + 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 { + 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 { + 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 { + try { + const sanitizedUsername = Sanitizer.sanitize(username); + return this.sesion.modifyPassword(sanitizedUsername, newPassword); + } + catch (e) { + return BluebirdPromise.reject(new Error(SPECIAL_CHAR_USED_MESSAGE)); + } + } +} diff --git a/themes/black/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts b/themes/black/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts new file mode 100644 index 000000000..9dd33fed4 --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts @@ -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 { 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); + }); +}); diff --git a/themes/black/server/src/lib/authentication/backends/ldap/Sanitizer.ts b/themes/black/server/src/lib/authentication/backends/ldap/Sanitizer.ts new file mode 100644 index 000000000..be74132ad --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/ldap/Sanitizer.ts @@ -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; + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/Session.spec.ts b/themes/black/server/src/lib/authentication/backends/ldap/Session.spec.ts new file mode 100644 index 000000000..d55f6a805 --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/ldap/Session.spec.ts @@ -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"]); + }); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/Session.ts b/themes/black/server/src/lib/authentication/backends/ldap/Session.ts new file mode 100644 index 000000000..e0284b3c4 --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/ldap/Session.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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(); + }); + } +} diff --git a/themes/black/server/src/lib/authentication/backends/ldap/SessionFactory.ts b/themes/black/server/src/lib/authentication/backends/ldap/SessionFactory.ts new file mode 100644 index 000000000..0b6c4bff3 --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/ldap/SessionFactory.ts @@ -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 + ) + ); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts b/themes/black/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts new file mode 100644 index 000000000..face3930d --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts @@ -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); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts b/themes/black/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts new file mode 100644 index 000000000..5faf2ba18 --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts @@ -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 { + return this.openStub(); + } + + close(): Bluebird { + return this.closeStub(); + } + + searchUserDn(username: string): Bluebird { + return this.searchUserDnStub(username); + } + + searchEmails(username: string): Bluebird { + return this.searchEmailsStub(username); + } + + searchGroups(username: string): Bluebird { + return this.searchGroupsStub(username); + } + + modifyPassword(username: string, newPassword: string): Bluebird { + return this.modifyPasswordStub(username, newPassword); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/connector/Connector.ts b/themes/black/server/src/lib/authentication/backends/ldap/connector/Connector.ts new file mode 100644 index 000000000..2542ea7f9 --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/ldap/connector/Connector.ts @@ -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; + unbindAsync(): Bluebird; + searchAsync(base: string, query: LdapJs.SearchOptions): Bluebird; + modifyAsync(userdn: string, change: LdapJs.Change): Bluebird; +} + +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 { + return this.client.bindAsync(username, password); + } + + unbindAsync(): Bluebird { + return this.client.unbindAsync(); + } + + searchAsync(base: string, query: any): Bluebird { + const that = this; + return this.client.searchAsync(base, query) + .then(function (res: EventEmitter) { + const doc: SearchEntry[] = []; + return new Bluebird((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 { + return this.client.modifyAsync(dn, changeRequest); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts b/themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts new file mode 100644 index 000000000..61fef07a4 --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts @@ -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); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts b/themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts new file mode 100644 index 000000000..d11fa6386 --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts @@ -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(); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts b/themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts new file mode 100644 index 000000000..0b78225bf --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts @@ -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 { + return this.bindAsyncStub(username, password); + } + + unbindAsync(): BluebirdPromise { + return this.unbindAsyncStub(); + } + + searchAsync(base: string, query: any): BluebirdPromise { + return this.searchAsyncStub(base, query); + } + + modifyAsync(dn: string, changeRequest: any): BluebirdPromise { + return this.modifyAsyncStub(dn, changeRequest); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/connector/IConnector.ts b/themes/black/server/src/lib/authentication/backends/ldap/connector/IConnector.ts new file mode 100644 index 000000000..1e63ab193 --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/ldap/connector/IConnector.ts @@ -0,0 +1,9 @@ +import Bluebird = require("bluebird"); +import EventEmitter = require("events"); + +export interface IConnector { + bindAsync(username: string, password: string): Bluebird; + unbindAsync(): Bluebird; + searchAsync(base: string, query: any): Bluebird; + modifyAsync(dn: string, changeRequest: any): Bluebird; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts b/themes/black/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts new file mode 100644 index 000000000..f9ed65eff --- /dev/null +++ b/themes/black/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts @@ -0,0 +1,5 @@ +import { IConnector } from "./IConnector"; + +export interface IConnectorFactory { + create(): IConnector; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/totp/ITotpHandler.ts b/themes/black/server/src/lib/authentication/totp/ITotpHandler.ts new file mode 100644 index 000000000..d600d31e4 --- /dev/null +++ b/themes/black/server/src/lib/authentication/totp/ITotpHandler.ts @@ -0,0 +1,6 @@ +import { TOTPSecret } from "../../../../types/TOTPSecret"; + +export interface ITotpHandler { + generate(label: string, issuer: string): TOTPSecret; + validate(token: string, secret: string): boolean; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/totp/TotpHandler.spec.ts b/themes/black/server/src/lib/authentication/totp/TotpHandler.spec.ts new file mode 100644 index 000000000..67cffa638 --- /dev/null +++ b/themes/black/server/src/lib/authentication/totp/TotpHandler.spec.ts @@ -0,0 +1,39 @@ + +import { TotpHandler } from "./TotpHandler"; +import Sinon = require("sinon"); +import Speakeasy = require("speakeasy"); +import Assert = require("assert"); + +describe("authentication/totp/TotpHandler", function() { + let totpValidator: TotpHandler; + let validateStub: Sinon.SinonStub; + + beforeEach(() => { + validateStub = Sinon.stub(Speakeasy.totp, "verify"); + totpValidator = new TotpHandler(Speakeasy); + }); + + afterEach(function() { + validateStub.restore(); + }); + + it("should validate the TOTP token", function() { + const totp_secret = "NBD2ZV64R9UV1O7K"; + const token = "token"; + validateStub.withArgs({ + secret: totp_secret, + token: token, + encoding: "base32", + window: 1 + }).returns(true); + Assert(totpValidator.validate(token, totp_secret)); + }); + + it("should not validate a wrong TOTP token", function() { + const totp_secret = "NBD2ZV64R9UV1O7K"; + const token = "wrong token"; + validateStub.returns(false); + Assert(!totpValidator.validate(token, totp_secret)); + }); +}); + diff --git a/themes/black/server/src/lib/authentication/totp/TotpHandler.ts b/themes/black/server/src/lib/authentication/totp/TotpHandler.ts new file mode 100644 index 000000000..dfab502a7 --- /dev/null +++ b/themes/black/server/src/lib/authentication/totp/TotpHandler.ts @@ -0,0 +1,36 @@ +import { ITotpHandler } from "./ITotpHandler"; +import { TOTPSecret } from "../../../../types/TOTPSecret"; +import Speakeasy = require("speakeasy"); + +const TOTP_ENCODING = "base32"; +const WINDOW: number = 1; + +export class TotpHandler implements ITotpHandler { + private speakeasy: typeof Speakeasy; + + constructor(speakeasy: typeof Speakeasy) { + this.speakeasy = speakeasy; + } + + generate(label: string, issuer: string): TOTPSecret { + const secret = this.speakeasy.generateSecret({ + otpauth_url: false + }) as TOTPSecret; + + secret.otpauth_url = this.speakeasy.otpauthURL({ + secret: secret.ascii, + label: label, + issuer: issuer + }); + return secret; + } + + validate(token: string, secret: string): boolean { + return this.speakeasy.totp.verify({ + secret: secret, + encoding: TOTP_ENCODING, + token: token, + window: WINDOW + }); + } +} diff --git a/themes/black/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts b/themes/black/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts new file mode 100644 index 000000000..ea93330d9 --- /dev/null +++ b/themes/black/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts @@ -0,0 +1,22 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import { ITotpHandler } from "./ITotpHandler"; +import { TOTPSecret } from "../../../../types/TOTPSecret"; + +export class TotpHandlerStub implements ITotpHandler { + generateStub: Sinon.SinonStub; + validateStub: Sinon.SinonStub; + + constructor() { + this.generateStub = Sinon.stub(); + this.validateStub = Sinon.stub(); + } + + generate(label: string, issuer: string): TOTPSecret { + return this.generateStub(label, issuer); + } + + validate(token: string, secret: string): boolean { + return this.validateStub(token, secret); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/u2f/IU2fHandler.ts b/themes/black/server/src/lib/authentication/u2f/IU2fHandler.ts new file mode 100644 index 000000000..b9b7d6f26 --- /dev/null +++ b/themes/black/server/src/lib/authentication/u2f/IU2fHandler.ts @@ -0,0 +1,9 @@ +import U2f = require("u2f"); + +export interface IU2fHandler { + request(appId: string, keyHandle?: string): U2f.Request; + checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) + : U2f.RegistrationResult | U2f.Error; + checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) + : U2f.SignatureResult | U2f.Error; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/u2f/U2fHandler.ts b/themes/black/server/src/lib/authentication/u2f/U2fHandler.ts new file mode 100644 index 000000000..bf3891e5b --- /dev/null +++ b/themes/black/server/src/lib/authentication/u2f/U2fHandler.ts @@ -0,0 +1,24 @@ +import { IU2fHandler } from "./IU2fHandler"; +import U2f = require("u2f"); + +export class U2fHandler implements IU2fHandler { + private u2f: typeof U2f; + + constructor(u2f: typeof U2f) { + this.u2f = u2f; + } + + request(appId: string, keyHandle?: string): U2f.Request { + return this.u2f.request(appId, keyHandle); + } + + checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) + : U2f.RegistrationResult | U2f.Error { + return this.u2f.checkRegistration(registrationRequest, registrationResponse); + } + + checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) + : U2f.SignatureResult | U2f.Error { + return this.u2f.checkSignature(signatureRequest, signatureResponse, publicKey); + } +} diff --git a/themes/black/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts b/themes/black/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts new file mode 100644 index 000000000..135d7eb06 --- /dev/null +++ b/themes/black/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts @@ -0,0 +1,31 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import U2f = require("u2f"); +import { IU2fHandler } from "./IU2fHandler"; + + +export class U2fHandlerStub implements IU2fHandler { + requestStub: Sinon.SinonStub; + checkRegistrationStub: Sinon.SinonStub; + checkSignatureStub: Sinon.SinonStub; + + constructor() { + this.requestStub = Sinon.stub(); + this.checkRegistrationStub = Sinon.stub(); + this.checkSignatureStub = Sinon.stub(); + } + + request(appId: string, keyHandle?: string): U2f.Request { + return this.requestStub(appId, keyHandle); + } + + checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) + : U2f.RegistrationResult | U2f.Error { + return this.checkRegistrationStub(registrationRequest, registrationResponse); + } + + checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) + : U2f.SignatureResult | U2f.Error { + return this.checkSignatureStub(signatureRequest, signatureResponse, publicKey); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authorization/Authorizer.spec.ts b/themes/black/server/src/lib/authorization/Authorizer.spec.ts new file mode 100644 index 000000000..586814042 --- /dev/null +++ b/themes/black/server/src/lib/authorization/Authorizer.spec.ts @@ -0,0 +1,372 @@ + +import Assert = require("assert"); +import winston = require("winston"); +import { Authorizer } from "./Authorizer"; +import { ACLConfiguration, ACLRule } from "../configuration/schema/AclConfiguration"; +import { Level } from "./Level"; + +describe("authorization/Authorizer", function () { + let authorizer: Authorizer; + let configuration: ACLConfiguration; + + describe("configuration is null", function() { + it("should allow access to anything, anywhere for anybody", function() { + configuration = undefined; + authorizer = new Authorizer(configuration, winston); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1", "group2"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "admin.example.com", resource: "/"}, {user: "user3", groups: ["group3"]}), Level.BYPASS); + }); + }); + + describe("configuration is not null", function () { + beforeEach(function () { + configuration = { + default_policy: "deny", + rules: [] + }; + authorizer = new Authorizer(configuration, winston); + }); + + describe("check access control with default policy to deny", function () { + beforeEach(function () { + configuration.default_policy = "deny"; + }); + + it("should deny access when no rule is provided", function () { + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should control access when multiple domain matcher is provided", function () { + configuration.rules = [{ + domain: "*.mail.example.com", + policy: "two_factor", + subject: "user:user1", + resources: [".*"] + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should allow access to all resources when resources is not provided", function () { + configuration.rules = [{ + domain: "*.mail.example.com", + policy: "two_factor", + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + describe("check user rules", function () { + it("should allow access when user has a matching allowing rule", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: [".*"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should deny to other users", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: [".*"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user2", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); + }); + + it("should allow user access only to specific resources", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: ["/private/.*", "^/begin", "/end$"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/middle/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/begin"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/not/begin"}, {user: "user1", groups: ["group1"]}), Level.DENY); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end/x"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should allow access to multiple domains", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: [".*"], + subject: "user:user1" + }, { + domain: "home1.example.com", + policy: "one_factor", + resources: [".*"], + subject: "user:user1" + }, { + domain: "home2.example.com", + policy: "deny", + resources: [".*"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home1.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); + Assert.equal(authorizer.authorization({domain: "home2.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home3.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should apply rules in order", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "one_factor", + resources: ["/my/private/resource"], + subject: "user:user1" + }, { + domain: "home.example.com", + policy: "deny", + resources: ["^/my/private/.*"], + subject: "user:user1" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/my/.*"], + subject: "user:user1" + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/poney"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/duck"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/resource"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); + }); + }); + + describe("check group rules", function () { + it("should allow access when user is in group having a matching allowing rule", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: ["^/$"], + subject: "group:group1" + }, { + domain: "home.example.com", + policy: "one_factor", + resources: ["^/test$"], + subject: "group:group2" + }, { + domain: "home.example.com", + policy: "deny", + resources: ["^/private$"], + subject: "group:group2" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.ONE_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); + }); + }); + }); + + describe("check any rules", function () { + it("should control access when any rules are defined", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "bypass", + resources: ["^/public$"] + }, { + domain: "home.example.com", + policy: "deny", + resources: ["^/private$"] + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, + {user: "user4", groups: ["group5"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, + {user: "user4", groups: ["group5"]}), Level.DENY); + }); + }); + + describe("check access control with default policy to allow", function () { + beforeEach(function () { + configuration.default_policy = "bypass"; + }); + + it("should allow access to anything when no rule is provided", function () { + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + }); + + it("should deny access to one resource when defined", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["/test"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + }); + }); + + describe("check access control with complete use case", function () { + beforeEach(function () { + configuration.default_policy = "deny"; + }); + + it("should control access of multiple user (real use case)", function () { + // Let say we have three users: admin, john, harry. + // admin is in groups ["admins"] + // john is in groups ["dev", "admin-private"] + // harry is in groups ["dev"] + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: ["^/public$", "^/$"] + }, { + domain: "home.example.com", + policy: "two_factor", + resources: [".*"], + subject: "group:admins" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/private/?.*"], + subject: "group:admin-private" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/private/john$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/private/harry"], + subject: "user:harry" + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); + }); + + it("should allow when allowed at group level and denied at user level", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"], + subject: "group:dev" + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); + }); + + it("should allow access when allowed at 'any' level and denied at user level", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"] + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); + }); + + it("should allow access when allowed at 'any' level and denied at group level", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "group:dev" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"] + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); + }); + + it("should respect rules precedence", function () { + // the priority from least to most is 'default_policy', 'all', 'group', 'user' + // and the first rules in each category as a lower priority than the latest. + // You can think of it that way: they override themselves inside each category. + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "group:dev" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"] + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + }); + }); + }); +}); diff --git a/themes/black/server/src/lib/authorization/Authorizer.ts b/themes/black/server/src/lib/authorization/Authorizer.ts new file mode 100644 index 000000000..889b7ec20 --- /dev/null +++ b/themes/black/server/src/lib/authorization/Authorizer.ts @@ -0,0 +1,85 @@ + +import { ACLConfiguration, ACLPolicy, ACLRule } from "../configuration/schema/AclConfiguration"; +import { IAuthorizer } from "./IAuthorizer"; +import { Winston } from "../../../types/Dependencies"; +import { MultipleDomainMatcher } from "./MultipleDomainMatcher"; +import { Level } from "./Level"; +import { Object } from "./Object"; +import { Subject } from "./Subject"; + +function MatchDomain(actualDomain: string) { + return function (rule: ACLRule): boolean { + return MultipleDomainMatcher.match(actualDomain, rule.domain); + }; +} + +function MatchResource(actualResource: string) { + return function (rule: ACLRule): boolean { + // If resources key is not provided, the rule applies to all resources. + if (!rule.resources) return true; + + for (let i = 0; i < rule.resources.length; ++i) { + const regexp = new RegExp(rule.resources[i]); + if (regexp.test(actualResource)) return true; + } + return false; + }; +} + +function MatchSubject(subject: Subject) { + return (rule: ACLRule) => { + // If no subject, matches anybody + if (!rule.subject) return true; + + if (rule.subject.startsWith("user:")) { + const ruleUser = rule.subject.split(":")[1]; + if (subject.user == ruleUser) return true; + } + + if (rule.subject.startsWith("group:")) { + const ruleGroup = rule.subject.split(":")[1]; + if (subject.groups.indexOf(ruleGroup) > -1) return true; + } + return false; + }; +} + +export class Authorizer implements IAuthorizer { + private logger: Winston; + private readonly configuration: ACLConfiguration; + + constructor(configuration: ACLConfiguration, logger_: Winston) { + this.logger = logger_; + this.configuration = configuration; + } + + private getMatchingRules(object: Object, subject: Subject): ACLRule[] { + const rules = this.configuration.rules; + if (!rules) return []; + return rules + .filter(MatchDomain(object.domain)) + .filter(MatchResource(object.resource)) + .filter(MatchSubject(subject)); + } + + private ruleToLevel(policy: string): Level { + if (policy == "bypass") { + return Level.BYPASS; + } else if (policy == "one_factor") { + return Level.ONE_FACTOR; + } else if (policy == "two_factor") { + return Level.TWO_FACTOR; + } + return Level.DENY; + } + + authorization(object: Object, subject: Subject): Level { + if (!this.configuration) return Level.BYPASS; + + const rules = this.getMatchingRules(object, subject); + + return (rules.length > 0) + ? this.ruleToLevel(rules[0].policy) // extract the policy of the first matching rule + : this.ruleToLevel(this.configuration.default_policy); // otherwise use the default policy + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authorization/AuthorizerStub.spec.ts b/themes/black/server/src/lib/authorization/AuthorizerStub.spec.ts new file mode 100644 index 000000000..9bd6f4a85 --- /dev/null +++ b/themes/black/server/src/lib/authorization/AuthorizerStub.spec.ts @@ -0,0 +1,17 @@ +import Sinon = require("sinon"); +import { IAuthorizer } from "./IAuthorizer"; +import { Level } from "./Level"; +import { Object } from "./Object"; +import { Subject } from "./Subject"; + +export class AuthorizerStub implements IAuthorizer { + authorizationMock: Sinon.SinonStub; + + constructor() { + this.authorizationMock = Sinon.stub(); + } + + authorization(object: Object, subject: Subject): Level { + return this.authorizationMock(object, subject); + } +} diff --git a/themes/black/server/src/lib/authorization/IAuthorizer.ts b/themes/black/server/src/lib/authorization/IAuthorizer.ts new file mode 100644 index 000000000..fe7ba367a --- /dev/null +++ b/themes/black/server/src/lib/authorization/IAuthorizer.ts @@ -0,0 +1,7 @@ +import { Level } from "./Level"; +import { Subject } from "./Subject"; +import { Object } from "./Object"; + +export interface IAuthorizer { + authorization(object: Object, subject: Subject): Level; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authorization/Level.ts b/themes/black/server/src/lib/authorization/Level.ts new file mode 100644 index 000000000..d12802610 --- /dev/null +++ b/themes/black/server/src/lib/authorization/Level.ts @@ -0,0 +1,6 @@ +export enum Level { + BYPASS = 0, + ONE_FACTOR = 1, + TWO_FACTOR = 2, + DENY = 3 +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authorization/MultipleDomainMatcher.ts b/themes/black/server/src/lib/authorization/MultipleDomainMatcher.ts new file mode 100644 index 000000000..64c647a4a --- /dev/null +++ b/themes/black/server/src/lib/authorization/MultipleDomainMatcher.ts @@ -0,0 +1,12 @@ + +export class MultipleDomainMatcher { + static match(domain: string, pattern: string): boolean { + if (pattern.startsWith("*") && + domain.endsWith(pattern.substr(1))) { + return true; + } + else if (domain == pattern) { + return true; + } + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authorization/Object.ts b/themes/black/server/src/lib/authorization/Object.ts new file mode 100644 index 000000000..5411b0d24 --- /dev/null +++ b/themes/black/server/src/lib/authorization/Object.ts @@ -0,0 +1,5 @@ + +export interface Object { + domain: string; + resource: string; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/authorization/Subject.ts b/themes/black/server/src/lib/authorization/Subject.ts new file mode 100644 index 000000000..310d6b4c0 --- /dev/null +++ b/themes/black/server/src/lib/authorization/Subject.ts @@ -0,0 +1,5 @@ + +export interface Subject { + user: string; + groups: string[]; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/ConfigurationParser.spec.ts b/themes/black/server/src/lib/configuration/ConfigurationParser.spec.ts new file mode 100644 index 000000000..60c0f6182 --- /dev/null +++ b/themes/black/server/src/lib/configuration/ConfigurationParser.spec.ts @@ -0,0 +1,171 @@ +import * as Assert from "assert"; +import { Configuration } from "./schema/Configuration"; +import { ACLConfiguration } from "./schema/AclConfiguration"; +import { ConfigurationParser } from "./ConfigurationParser"; + +describe("configuration/ConfigurationParser", function () { + function buildYamlConfig(): Configuration { + const yaml_config: Configuration = { + port: 8080, + authentication_backend: { + ldap: { + url: "http://ldap", + base_dn: "dc=example,dc=com", + additional_users_dn: "ou=users", + additional_groups_dn: "ou=groups", + user: "user", + password: "pass" + }, + }, + session: { + domain: "example.com", + secret: "secret", + expiration: 40000 + }, + storage: { + local: { + path: "/mydirectory" + } + }, + regulation: { + max_retries: 3, + find_time: 5 * 60, + ban_time: 5 * 60 + }, + logs_level: "debug", + notifier: { + email: { + username: "user", + password: "password", + sender: "admin@example.com", + service: "gmail" + } + } + }; + return yaml_config; + } + + describe("port", function () { + it("should read the port from the yaml file", function () { + const yaml_config = buildYamlConfig(); + yaml_config.port = 7070; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.port, 7070); + }); + + it("should default the port to 8080 if not provided", function () { + const yaml_config = buildYamlConfig(); + delete yaml_config.port; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.port, 8080); + }); + }); + + describe("test session configuration", function() { + it("should get the session attributes", function () { + const yaml_config = buildYamlConfig(); + yaml_config.session = { + domain: "example.com", + secret: "secret", + expiration: 3600, + inactivity: 4000 + }; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.session.domain, "example.com"); + Assert.equal(config.session.secret, "secret"); + Assert.equal(config.session.expiration, 3600); + Assert.equal(config.session.inactivity, 4000); + }); + + it("should be ok not specifying inactivity", function () { + const yaml_config = buildYamlConfig(); + yaml_config.session = { + domain: "example.com", + secret: "secret", + expiration: 3600 + }; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.session.domain, "example.com"); + Assert.equal(config.session.secret, "secret"); + Assert.equal(config.session.expiration, 3600); + Assert.equal(config.session.inactivity, undefined); + }); + }); + + it("should get the log level", function () { + const yaml_config = buildYamlConfig(); + yaml_config.logs_level = "debug"; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.logs_level, "debug"); + }); + + it("should get the notifier config", function () { + const userConfig = buildYamlConfig(); + userConfig.notifier = { + email: { + username: "user", + password: "pass", + sender: "admin@example.com", + service: "gmail" + } + }; + const config = ConfigurationParser.parse(userConfig); + Assert.deepEqual(config.notifier, { + email: { + username: "user", + password: "pass", + sender: "admin@example.com", + service: "gmail" + } + }); + }); + + describe("access_control", function() { + it("should adapt access_control when it is already ok", function () { + const userConfig = buildYamlConfig(); + userConfig.access_control = { + default_policy: "deny", + rules: [{ + domain: "www.example.com", + policy: "two_factor", + subject: "user:user" + }, { + domain: "public.example.com", + policy: "two_factor" + }] + }; + const config = ConfigurationParser.parse(userConfig); + Assert.deepEqual(config.access_control, { + default_policy: "deny", + rules: [{ + domain: "www.example.com", + policy: "two_factor", + subject: "user:user" + }, { + domain: "public.example.com", + policy: "two_factor" + }] + } as ACLConfiguration); + }); + + + it("should adapt access_control when it is empty", function () { + const userConfig = buildYamlConfig(); + userConfig.access_control = {} as any; + const config = ConfigurationParser.parse(userConfig); + Assert.deepEqual(config.access_control, { + default_policy: "bypass", + rules: [] + }); + }); + }); + + describe("default_redirection_url", function() { + it("should parse default_redirection_url", function() { + const userConfig = buildYamlConfig(); + userConfig.default_redirection_url = "dummy_url"; + const config = ConfigurationParser.parse(userConfig); + Assert.deepEqual(config.default_redirection_url, "dummy_url"); + }); + }); +}); diff --git a/themes/black/server/src/lib/configuration/ConfigurationParser.ts b/themes/black/server/src/lib/configuration/ConfigurationParser.ts new file mode 100644 index 000000000..d92d163c4 --- /dev/null +++ b/themes/black/server/src/lib/configuration/ConfigurationParser.ts @@ -0,0 +1,39 @@ + +import * as ObjectPath from "object-path"; +import { Configuration, complete } from "./schema/Configuration"; +import Ajv = require("ajv"); +import Path = require("path"); +import Util = require("util"); + +export class ConfigurationParser { + private static parseTypes(configuration: Configuration): string[] { + const schema = require(Path.resolve(__dirname, "./Configuration.schema.json")); + const ajv = new Ajv({ + allErrors: true, + missingRefs: "fail" + }); + ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json")); + const valid = ajv.validate(schema, configuration); + if (!valid) + return ajv.errors.map( + (e: Ajv.ErrorObject) => { return ajv.errorsText([e]); }); + return []; + } + + static parse(configuration: Configuration): Configuration { + const validationErrors = this.parseTypes(configuration); + if (validationErrors.length > 0) { + validationErrors.forEach((e: string) => { console.log(e); }); + throw new Error("Malformed configuration (schema). Please double-check your configuration file."); + } + + const [newConfiguration, completionErrors] = complete(configuration); + + if (completionErrors.length > 0) { + completionErrors.forEach((e: string) => { console.log(e); }); + throw new Error("Malformed configuration (validator). Please double-check your configuration file."); + } + return newConfiguration; + } +} + diff --git a/themes/black/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts b/themes/black/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts new file mode 100644 index 000000000..d4a3093ee --- /dev/null +++ b/themes/black/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts @@ -0,0 +1,149 @@ +import { SessionConfigurationBuilder } from "./SessionConfigurationBuilder"; +import { Configuration } from "./schema/Configuration"; +import { GlobalDependencies } from "../../../types/Dependencies"; + +import ExpressSession = require("express-session"); +import ConnectRedis = require("connect-redis"); +import Sinon = require("sinon"); +import Assert = require("assert"); + +describe("configuration/SessionConfigurationBuilder", function () { + const configuration: Configuration = { + access_control: { + default_policy: "deny", + rules: [] + }, + totp: { + issuer: "authelia.com" + }, + authentication_backend: { + ldap: { + url: "ldap://ldap", + user: "user", + base_dn: "dc=example,dc=com", + password: "password", + additional_groups_dn: "ou=groups", + additional_users_dn: "ou=users", + group_name_attribute: "", + groups_filter: "", + mail_attribute: "", + users_filter: "" + }, + }, + logs_level: "debug", + notifier: { + filesystem: { + filename: "/test" + } + }, + port: 8080, + session: { + name: "authelia_session", + domain: "example.com", + expiration: 3600, + secret: "secret" + }, + regulation: { + max_retries: 3, + ban_time: 5 * 60, + find_time: 5 * 60 + }, + storage: { + local: { + in_memory: true + } + } + }; + + const deps: GlobalDependencies = { + ConnectRedis: Sinon.spy() as any, + ldapjs: Sinon.spy() as any, + nedb: Sinon.spy() as any, + session: Sinon.spy() as any, + speakeasy: Sinon.spy() as any, + u2f: Sinon.spy() as any, + winston: Sinon.spy() as any, + Redis: Sinon.spy() as any + }; + + it("should return session options without redis options", function () { + const options = SessionConfigurationBuilder.build(configuration, deps); + const expectedOptions = { + name: "authelia_session", + secret: "secret", + resave: false, + saveUninitialized: true, + cookie: { + secure: true, + httpOnly: true, + maxAge: 3600, + domain: "example.com" + } + }; + + Assert.deepEqual(expectedOptions, options); + }); + + it("should return session options with redis options", function () { + configuration.session["redis"] = { + host: "redis.example.com", + port: 6379 + }; + const RedisStoreMock = Sinon.spy(); + const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); + + deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; + deps.Redis = { + createClient: Sinon.mock().returns(redisClient) + } as any; + + const options = SessionConfigurationBuilder.build(configuration, deps); + + const expectedOptions: ExpressSession.SessionOptions = { + secret: "secret", + resave: false, + saveUninitialized: true, + name: "authelia_session", + cookie: { + secure: true, + httpOnly: true, + maxAge: 3600, + domain: "example.com" + }, + store: Sinon.match.object as any + }; + + Assert((deps.ConnectRedis as Sinon.SinonStub).calledWith(deps.session)); + Assert.equal(options.secret, expectedOptions.secret); + Assert.equal(options.resave, expectedOptions.resave); + Assert.equal(options.saveUninitialized, expectedOptions.saveUninitialized); + Assert.deepEqual(options.cookie, expectedOptions.cookie); + Assert(options.store != undefined); + }); + + it("should return session options with redis password", function () { + configuration.session["redis"] = { + host: "redis.example.com", + port: 6379, + password: "authelia_pass" + }; + const RedisStoreMock = Sinon.spy(); + const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); + const createClientStub = Sinon.stub(); + + deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; + deps.Redis = { + createClient: createClientStub + } as any; + + createClientStub.returns(redisClient); + + const options = SessionConfigurationBuilder.build(configuration, deps); + + Assert(createClientStub.calledWith({ + host: "redis.example.com", + port: 6379, + password: "authelia_pass" + })); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/SessionConfigurationBuilder.ts b/themes/black/server/src/lib/configuration/SessionConfigurationBuilder.ts new file mode 100644 index 000000000..6ce643d9d --- /dev/null +++ b/themes/black/server/src/lib/configuration/SessionConfigurationBuilder.ts @@ -0,0 +1,52 @@ +import ExpressSession = require("express-session"); +import Redis = require("redis"); + +import { Configuration } from "./schema/Configuration"; +import { GlobalDependencies } from "../../../types/Dependencies"; +import { RedisStoreOptions } from "connect-redis"; + +export class SessionConfigurationBuilder { + + static build(configuration: Configuration, deps: GlobalDependencies): ExpressSession.SessionOptions { + const sessionOptions: ExpressSession.SessionOptions = { + name: configuration.session.name, + secret: configuration.session.secret, + resave: false, + saveUninitialized: true, + cookie: { + secure: true, + httpOnly: true, + maxAge: configuration.session.expiration, + domain: configuration.session.domain + }, + }; + + if (configuration.session.redis) { + let redisOptions; + const options: Redis.ClientOpts = { + host: configuration.session.redis.host, + port: configuration.session.redis.port + }; + + if (configuration.session.redis.password) { + options["password"] = configuration.session.redis.password; + } + const client = deps.Redis.createClient(options); + + client.on("error", function (err: Error) { + console.error("Redis error:", err); + }); + + redisOptions = { + client: client, + logErrors: true + }; + + if (redisOptions) { + const RedisStore = deps.ConnectRedis(deps.session); + sessionOptions.store = new RedisStore(redisOptions); + } + } + return sessionOptions; + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/AclConfiguration.spec.ts b/themes/black/server/src/lib/configuration/schema/AclConfiguration.spec.ts new file mode 100644 index 000000000..d1e2a03a2 --- /dev/null +++ b/themes/black/server/src/lib/configuration/schema/AclConfiguration.spec.ts @@ -0,0 +1,34 @@ +import { ACLConfiguration, complete } from "./AclConfiguration"; +import Assert = require("assert"); + +describe("configuration/schema/AclConfiguration", function() { + it("should complete ACLConfiguration", function() { + const configuration: ACLConfiguration = {}; + const [newConfiguration, errors] = complete(configuration); + + Assert.deepEqual(newConfiguration.default_policy, "bypass"); + Assert.deepEqual(newConfiguration.rules, []); + }); + + it("should return errors when subject is not good", function() { + const configuration: ACLConfiguration = { + default_policy: "deny", + rules: [{ + domain: "dev.example.com", + subject: "user:abc", + policy: "bypass" + }, { + domain: "dev.example.com", + subject: "user:def", + policy: "bypass" + }, { + domain: "dev.example.com", + subject: "badkey:abc", + policy: "bypass" + }] + }; + const [newConfiguration, errors] = complete(configuration); + + Assert.deepEqual(errors, ["Rule 2 has wrong subject. It should be starting with user: or group:."]); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/AclConfiguration.ts b/themes/black/server/src/lib/configuration/schema/AclConfiguration.ts new file mode 100644 index 000000000..40401dd64 --- /dev/null +++ b/themes/black/server/src/lib/configuration/schema/AclConfiguration.ts @@ -0,0 +1,41 @@ + +export type ACLPolicy = "deny" | "bypass" | "one_factor" | "two_factor"; + +export type ACLRule = { + domain: string; + resources?: string[]; + subject?: string; + policy: ACLPolicy; +}; + +export interface ACLConfiguration { + default_policy?: ACLPolicy; + rules?: ACLRule[]; +} + +export function complete(configuration: ACLConfiguration): [ACLConfiguration, string[]] { + const newConfiguration: ACLConfiguration = (configuration) + ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.default_policy) { + newConfiguration.default_policy = "bypass"; + } + + if (!newConfiguration.rules) { + newConfiguration.rules = []; + } + + if (newConfiguration.rules.length > 0) { + const errors: string[] = []; + newConfiguration.rules.forEach((r, idx) => { + if (r.subject && !r.subject.match(/^(user|group):[a-zA-Z0-9]+$/)) { + errors.push(`Rule ${idx} has wrong subject. It should be starting with user: or group:.`); + } + }); + if (errors.length > 0) { + return [newConfiguration, errors]; + } + } + + return [newConfiguration, []]; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts b/themes/black/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts new file mode 100644 index 000000000..3ca86381c --- /dev/null +++ b/themes/black/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts @@ -0,0 +1,11 @@ +import { AuthenticationBackendConfiguration, complete } from "./AuthenticationBackendConfiguration"; +import Assert = require("assert"); + +describe("configuration/schema/AuthenticationBackendConfiguration", function() { + it("should ensure there is at least one key", function() { + const configuration: AuthenticationBackendConfiguration = {} as any; + const [newConfiguration, error] = complete(configuration); + + Assert.equal(error, "Authentication backend must have one of the following keys:`ldap` or `file`"); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts b/themes/black/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts new file mode 100644 index 000000000..7f77f894f --- /dev/null +++ b/themes/black/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts @@ -0,0 +1,25 @@ +import { LdapConfiguration } from "./LdapConfiguration"; +import { FileUsersDatabaseConfiguration } from "./FileUsersDatabaseConfiguration"; + +export interface AuthenticationBackendConfiguration { + ldap?: LdapConfiguration; + file?: FileUsersDatabaseConfiguration; +} + +export function complete( + configuration: AuthenticationBackendConfiguration) + : [AuthenticationBackendConfiguration, string] { + + const newConfiguration: AuthenticationBackendConfiguration = (configuration) + ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (Object.keys(newConfiguration).length != 1) { + return [ + newConfiguration, + "Authentication backend must have one of the following keys:" + + "`ldap` or `file`" + ]; + } + + return [newConfiguration, undefined]; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/Configuration.ts b/themes/black/server/src/lib/configuration/schema/Configuration.ts new file mode 100644 index 000000000..8d16a5fb2 --- /dev/null +++ b/themes/black/server/src/lib/configuration/schema/Configuration.ts @@ -0,0 +1,68 @@ +import { ACLConfiguration, complete as AclConfigurationComplete } from "./AclConfiguration"; +import { AuthenticationBackendConfiguration, complete as AuthenticationBackendComplete } from "./AuthenticationBackendConfiguration"; +import { NotifierConfiguration, complete as NotifierConfigurationComplete } from "./NotifierConfiguration"; +import { RegulationConfiguration, complete as RegulationConfigurationComplete } from "./RegulationConfiguration"; +import { SessionConfiguration, complete as SessionConfigurationComplete } from "./SessionConfiguration"; +import { StorageConfiguration, complete as StorageConfigurationComplete } from "./StorageConfiguration"; +import { TotpConfiguration, complete as TotpConfigurationComplete } from "./TotpConfiguration"; + +export interface Configuration { + access_control?: ACLConfiguration; + authentication_backend: AuthenticationBackendConfiguration; + default_redirection_url?: string; + logs_level?: string; + notifier?: NotifierConfiguration; + port?: number; + regulation?: RegulationConfiguration; + session?: SessionConfiguration; + storage?: StorageConfiguration; + totp?: TotpConfiguration; +} + +export function complete( + configuration: Configuration): + [Configuration, string[]] { + + const newConfiguration: Configuration = JSON.parse( + JSON.stringify(configuration)); + const errors: string[] = []; + + const [acls, aclsErrors] = AclConfigurationComplete( + newConfiguration.access_control); + + newConfiguration.access_control = acls; + if (aclsErrors.length > 0) { + errors.concat(aclsErrors); + } + + const [backend, error] = + AuthenticationBackendComplete( + newConfiguration.authentication_backend); + + if (error) errors.push(error); + newConfiguration.authentication_backend = backend; + + if (!newConfiguration.logs_level) { + newConfiguration.logs_level = "info"; + } + + const [notifier, notifierError] = NotifierConfigurationComplete( + newConfiguration.notifier); + newConfiguration.notifier = notifier; + if (notifierError) errors.push(notifierError); + + if (!newConfiguration.port) { + newConfiguration.port = 8080; + } + + newConfiguration.regulation = RegulationConfigurationComplete( + newConfiguration.regulation); + newConfiguration.session = SessionConfigurationComplete( + newConfiguration.session); + newConfiguration.storage = StorageConfigurationComplete( + newConfiguration.storage); + newConfiguration.totp = TotpConfigurationComplete( + newConfiguration.totp); + + return [newConfiguration, errors]; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts b/themes/black/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts new file mode 100644 index 000000000..d19002ba9 --- /dev/null +++ b/themes/black/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts @@ -0,0 +1,4 @@ + +export interface FileUsersDatabaseConfiguration { + path: string; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/LdapConfiguration.spec.ts b/themes/black/server/src/lib/configuration/schema/LdapConfiguration.spec.ts new file mode 100644 index 000000000..cc73d1085 --- /dev/null +++ b/themes/black/server/src/lib/configuration/schema/LdapConfiguration.spec.ts @@ -0,0 +1,25 @@ +import Assert = require("assert"); +import { LdapConfiguration, complete } from "./LdapConfiguration"; + +describe("configuration/schema/AuthenticationMethodsConfiguration", function() { + it("should ensure at least one key is provided", function() { + const configuration: LdapConfiguration = { + url: "ldap.example.com", + base_dn: "dc=example,dc=com", + user: "admin", + password: "password" + }; + const newConfiguration = complete(configuration); + + Assert.deepEqual(newConfiguration, { + url: "ldap.example.com", + base_dn: "dc=example,dc=com", + user: "admin", + password: "password", + users_filter: "cn={0}", + group_name_attribute: "cn", + groups_filter: "member={dn}", + mail_attribute: "mail" + }); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/LdapConfiguration.ts b/themes/black/server/src/lib/configuration/schema/LdapConfiguration.ts new file mode 100644 index 000000000..5dacb9390 --- /dev/null +++ b/themes/black/server/src/lib/configuration/schema/LdapConfiguration.ts @@ -0,0 +1,40 @@ +import Util = require("util"); + +export interface LdapConfiguration { + url: string; + base_dn: string; + + additional_users_dn?: string; + users_filter?: string; + + additional_groups_dn?: string; + groups_filter?: string; + + group_name_attribute?: string; + mail_attribute?: string; + + user: string; // admin username + password: string; // admin password +} + +export function complete(configuration: LdapConfiguration): LdapConfiguration { + const newConfiguration: LdapConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.users_filter) { + newConfiguration.users_filter = "cn={0}"; + } + + if (!newConfiguration.groups_filter) { + newConfiguration.groups_filter = "member={dn}"; + } + + if (!newConfiguration.group_name_attribute) { + newConfiguration.group_name_attribute = "cn"; + } + + if (!newConfiguration.mail_attribute) { + newConfiguration.mail_attribute = "mail"; + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts b/themes/black/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts new file mode 100644 index 000000000..6c576e8e0 --- /dev/null +++ b/themes/black/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts @@ -0,0 +1,40 @@ +import Assert = require("assert"); +import { NotifierConfiguration, complete } from "./NotifierConfiguration"; + +describe("configuration/schema/NotifierConfiguration", function() { + it("should use a default notifier when none is provided", function() { + const configuration: NotifierConfiguration = {}; + const [newConfiguration, error] = complete(configuration); + + Assert.deepEqual(newConfiguration.filesystem, {filename: "/tmp/authelia/notification.txt"}); + }); + + it("should ensure correct key is provided", function() { + const configuration = { + abc: "badvalue" + }; + const [newConfiguration, error] = complete(configuration as any); + + Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"); + }); + + it("should ensure there is no more than one key", function() { + const configuration: NotifierConfiguration = { + smtp: { + host: "smtp.example.com", + port: 25, + secure: false, + sender: "test@example.com" + }, + email: { + username: "test", + password: "test", + sender: "test@example.com", + service: "gmail" + } + }; + const [newConfiguration, error] = complete(configuration); + + Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/NotifierConfiguration.ts b/themes/black/server/src/lib/configuration/schema/NotifierConfiguration.ts new file mode 100644 index 000000000..7bcce15c2 --- /dev/null +++ b/themes/black/server/src/lib/configuration/schema/NotifierConfiguration.ts @@ -0,0 +1,45 @@ + +export interface EmailNotifierConfiguration { + username: string; + password: string; + sender: string; + service: string; +} + +export interface SmtpNotifierConfiguration { + username?: string; + password?: string; + host: string; + port: number; + secure: boolean; + sender: string; +} + +export interface FileSystemNotifierConfiguration { + filename: string; +} + +export interface NotifierConfiguration { + email?: EmailNotifierConfiguration; + smtp?: SmtpNotifierConfiguration; + filesystem?: FileSystemNotifierConfiguration; +} + +export function complete(configuration: NotifierConfiguration): [NotifierConfiguration, string] { + const newConfiguration: NotifierConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (Object.keys(newConfiguration).length == 0) + newConfiguration.filesystem = { filename: "/tmp/authelia/notification.txt" }; + + const ERROR = "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"; + + if (Object.keys(newConfiguration).length != 1) + return [newConfiguration, ERROR]; + + const key = Object.keys(newConfiguration)[0]; + + if (key != "filesystem" && key != "smtp" && key != "email") + return [newConfiguration, ERROR]; + + return [newConfiguration, undefined]; +} diff --git a/themes/black/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts b/themes/black/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts new file mode 100644 index 000000000..dce2caf4e --- /dev/null +++ b/themes/black/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts @@ -0,0 +1,13 @@ +import Assert = require("assert"); +import { RegulationConfiguration, complete } from "./RegulationConfiguration"; + +describe("configuration/schema/RegulationConfiguration", function() { + it("should return default regulation configuration", function() { + const configuration: RegulationConfiguration = {}; + const newConfiguration = complete(configuration); + + Assert.equal(newConfiguration.ban_time, 300); + Assert.equal(newConfiguration.find_time, 120); + Assert.equal(newConfiguration.max_retries, 3); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/RegulationConfiguration.ts b/themes/black/server/src/lib/configuration/schema/RegulationConfiguration.ts new file mode 100644 index 000000000..117463f43 --- /dev/null +++ b/themes/black/server/src/lib/configuration/schema/RegulationConfiguration.ts @@ -0,0 +1,23 @@ +export interface RegulationConfiguration { + max_retries?: number; + find_time?: number; + ban_time?: number; +} + +export function complete(configuration: RegulationConfiguration): RegulationConfiguration { + const newConfiguration: RegulationConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.max_retries) { + newConfiguration.max_retries = 3; + } + + if (!newConfiguration.find_time) { + newConfiguration.find_time = 120; // seconds + } + + if (!newConfiguration.ban_time) { + newConfiguration.ban_time = 300; // seconds + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/SessionConfiguration.spec.ts b/themes/black/server/src/lib/configuration/schema/SessionConfiguration.spec.ts new file mode 100644 index 000000000..e54010837 --- /dev/null +++ b/themes/black/server/src/lib/configuration/schema/SessionConfiguration.spec.ts @@ -0,0 +1,16 @@ +import Assert = require("assert"); +import { SessionConfiguration, complete } from "./SessionConfiguration"; + +describe("configuration/schema/SessionConfiguration", function() { + it("should return default regulation configuration", function() { + const configuration: SessionConfiguration = { + domain: "example.com", + secret: "unsecure_secret" + }; + const newConfiguration = complete(configuration); + + Assert.equal(newConfiguration.name, 'authelia_session'); + Assert.equal(newConfiguration.expiration, 3600000); + Assert.equal(newConfiguration.inactivity, undefined); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/SessionConfiguration.ts b/themes/black/server/src/lib/configuration/schema/SessionConfiguration.ts new file mode 100644 index 000000000..2c88bb215 --- /dev/null +++ b/themes/black/server/src/lib/configuration/schema/SessionConfiguration.ts @@ -0,0 +1,32 @@ +export interface SessionRedisOptions { + host: string; + port: number; + password?: string; +} + +export interface SessionConfiguration { + name?: string; + domain: string; + secret: string; + expiration?: number; + inactivity?: number; + redis?: SessionRedisOptions; +} + +export function complete(configuration: SessionConfiguration): SessionConfiguration { + const newConfiguration: SessionConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.name) { + newConfiguration.name = "authelia_session"; + } + + if (!newConfiguration.expiration) { + newConfiguration.expiration = 3600000; // 1 hour + } + + if (!newConfiguration.inactivity) { + newConfiguration.inactivity = undefined; // disabled + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/StorageConfiguration.spec.ts b/themes/black/server/src/lib/configuration/schema/StorageConfiguration.spec.ts new file mode 100644 index 000000000..9d02a11b6 --- /dev/null +++ b/themes/black/server/src/lib/configuration/schema/StorageConfiguration.spec.ts @@ -0,0 +1,15 @@ +import Assert = require("assert"); +import { StorageConfiguration, complete } from "./StorageConfiguration"; + +describe("configuration/schema/StorageConfiguration", function() { + it("should return default regulation configuration", function() { + const configuration: StorageConfiguration = {}; + const newConfiguration = complete(configuration); + + Assert.deepEqual(newConfiguration, { + local: { + in_memory: true + } + }); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/StorageConfiguration.ts b/themes/black/server/src/lib/configuration/schema/StorageConfiguration.ts new file mode 100644 index 000000000..47e356ef4 --- /dev/null +++ b/themes/black/server/src/lib/configuration/schema/StorageConfiguration.ts @@ -0,0 +1,30 @@ +export interface MongoStorageConfiguration { + url: string; + database: string; + auth?: { + username: string; + password: string; + }; +} + +export interface LocalStorageConfiguration { + path?: string; + in_memory?: boolean; +} + +export interface StorageConfiguration { + local?: LocalStorageConfiguration; + mongo?: MongoStorageConfiguration; +} + +export function complete(configuration: StorageConfiguration): StorageConfiguration { + const newConfiguration: StorageConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.local && !newConfiguration.mongo) { + newConfiguration.local = { + in_memory: true + }; + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/TotpConfiguration.ts b/themes/black/server/src/lib/configuration/schema/TotpConfiguration.ts new file mode 100644 index 000000000..683135639 --- /dev/null +++ b/themes/black/server/src/lib/configuration/schema/TotpConfiguration.ts @@ -0,0 +1,13 @@ +export interface TotpConfiguration { + issuer: string; +} + +export function complete(configuration: TotpConfiguration): TotpConfiguration { + const newConfiguration: TotpConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.issuer) { + newConfiguration.issuer = "authelia.com"; + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts b/themes/black/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts new file mode 100644 index 000000000..8008b4833 --- /dev/null +++ b/themes/black/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts @@ -0,0 +1,9 @@ + +export interface UserInfo { + username: string; + password_hash: string; + email: string; + groups?: string[]; +} + +export type UserDatabaseConfiguration = UserInfo[]; \ No newline at end of file diff --git a/themes/black/server/src/lib/connectors/mongo/IMongoClient.d.ts b/themes/black/server/src/lib/connectors/mongo/IMongoClient.d.ts new file mode 100644 index 000000000..36cb4b8bf --- /dev/null +++ b/themes/black/server/src/lib/connectors/mongo/IMongoClient.d.ts @@ -0,0 +1,6 @@ +import MongoDB = require("mongodb"); +import Bluebird = require("bluebird"); + +export interface IMongoClient { + collection(name: string): Bluebird +} \ No newline at end of file diff --git a/themes/black/server/src/lib/connectors/mongo/MongoClient.spec.ts b/themes/black/server/src/lib/connectors/mongo/MongoClient.spec.ts new file mode 100644 index 000000000..ca0c68593 --- /dev/null +++ b/themes/black/server/src/lib/connectors/mongo/MongoClient.spec.ts @@ -0,0 +1,119 @@ +import Assert = require("assert"); +import Bluebird = require("bluebird"); +import MongoDB = require("mongodb"); +import Sinon = require("sinon"); + +import { MongoClient } from "./MongoClient"; +import { GlobalLoggerStub } from "../../logging/GlobalLoggerStub.spec"; +import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration"; + +describe("connectors/mongo/MongoClient", function () { + let MongoClientStub: any; + let mongoClientStub: any; + let mongoDatabaseStub: any; + let logger: GlobalLoggerStub = new GlobalLoggerStub(); + + const configuration: MongoStorageConfiguration = { + url: "mongo://url", + database: "databasename" + }; + + describe("connection", () => { + before(() => { + mongoClientStub = { + db: Sinon.stub() + }; + mongoDatabaseStub = { + on: Sinon.stub(), + collection: Sinon.stub() + } + MongoClientStub = Sinon.stub( + MongoDB.MongoClient, "connect"); + MongoClientStub.yields( + undefined, mongoClientStub); + mongoClientStub.db.returns( + mongoDatabaseStub); + }); + + after(() => { + MongoClientStub.restore(); + }); + + it("should use credentials from configuration", () => { + configuration.auth = { + username: "authelia", + password: "authelia_pass" + }; + + const client = new MongoClient(configuration, logger); + return client.collection("test") + .then(() => { + Assert(MongoClientStub.calledWith("mongo://url", { + auth: { + user: "authelia", + password: "authelia_pass" + } + })) + }); + }); + }); + + describe("collection", () => { + before(function() { + mongoClientStub = { + db: Sinon.stub() + }; + mongoDatabaseStub = { + on: Sinon.stub(), + collection: Sinon.stub() + } + }); + + describe("Connection to mongo is ok", function() { + before(function () { + MongoClientStub = Sinon.stub( + MongoDB.MongoClient, "connect"); + MongoClientStub.yields( + undefined, mongoClientStub); + mongoClientStub.db.returns( + mongoDatabaseStub); + }); + + after(function () { + MongoClientStub.restore(); + }); + + it("should create a collection", function () { + const COLLECTION_NAME = "mycollection"; + const client = new MongoClient(configuration, logger); + + mongoDatabaseStub.collection.returns("COL"); + return client.collection(COLLECTION_NAME) + .then((collection) => mongoDatabaseStub.collection.calledWith(COLLECTION_NAME)); + }); + }); + + describe("Connection to mongo is broken", function() { + before(function () { + MongoClientStub = Sinon.stub( + MongoDB.MongoClient, "connect"); + MongoClientStub.yields( + new Error("Failed connection"), undefined); + }); + + after(function () { + MongoClientStub.restore(); + }); + + it("should fail creating the collection", function() { + const COLLECTION_NAME = "mycollection"; + const client = new MongoClient(configuration, logger); + + mongoDatabaseStub.collection.returns("COL"); + return client.collection(COLLECTION_NAME) + .then((collection) => Bluebird.reject(new Error("should not be here."))) + .catch((err) => Bluebird.resolve()); + }); + }) + }); +}); diff --git a/themes/black/server/src/lib/connectors/mongo/MongoClient.ts b/themes/black/server/src/lib/connectors/mongo/MongoClient.ts new file mode 100644 index 000000000..d15731e97 --- /dev/null +++ b/themes/black/server/src/lib/connectors/mongo/MongoClient.ts @@ -0,0 +1,76 @@ + +import MongoDB = require("mongodb"); +import { IMongoClient } from "./IMongoClient"; +import Bluebird = require("bluebird"); +import { AUTHENTICATION_FAILED } from "../../../../../shared/UserMessages"; +import { IGlobalLogger } from "../../logging/IGlobalLogger"; +import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration"; + +export class MongoClient implements IMongoClient { + private configuration: MongoStorageConfiguration; + + private database: MongoDB.Db; + private client: MongoDB.MongoClient; + private logger: IGlobalLogger; + + constructor( + configuration: MongoStorageConfiguration, + logger: IGlobalLogger) { + + this.configuration = configuration; + this.logger = logger; + } + + connect(): Bluebird { + const that = this; + const options: MongoDB.MongoClientOptions = {}; + if (that.configuration.auth) { + options["auth"] = { + user: that.configuration.auth.username, + password: that.configuration.auth.password + }; + } + + return new Bluebird((resolve, reject) => { + MongoDB.MongoClient.connect( + this.configuration.url, + options, + function(err, client) { + if (err) { + reject(err); + return; + } + resolve(client); + }); + }) + .then(function (client: MongoDB.MongoClient) { + that.database = client.db(that.configuration.database); + that.database.on("close", () => { + that.logger.info("[MongoClient] Lost connection."); + }); + that.database.on("reconnect", () => { + that.logger.info("[MongoClient] Reconnected."); + }); + that.client = client; + }); + } + + close(): Bluebird { + if (this.client) { + this.client.close(); + this.database = undefined; + this.client = undefined; + } + return Bluebird.resolve(); + } + + collection(name: string): Bluebird { + if (!this.client) { + const that = this; + return this.connect() + .then(() => Bluebird.resolve(that.database.collection(name))); + } + + return Bluebird.resolve(this.database.collection(name)); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/connectors/mongo/MongoClientStub.spec.ts b/themes/black/server/src/lib/connectors/mongo/MongoClientStub.spec.ts new file mode 100644 index 000000000..1cfd48e32 --- /dev/null +++ b/themes/black/server/src/lib/connectors/mongo/MongoClientStub.spec.ts @@ -0,0 +1,16 @@ +import Sinon = require("sinon"); +import MongoDB = require("mongodb"); +import Bluebird = require("bluebird"); +import { IMongoClient } from "../../../../src/lib/connectors/mongo/IMongoClient"; + +export class MongoClientStub implements IMongoClient { + public collectionStub: Sinon.SinonStub; + + constructor() { + this.collectionStub = Sinon.stub(); + } + + collection(name: string): Bluebird { + return this.collectionStub(name); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/logging/GlobalLogger.ts b/themes/black/server/src/lib/logging/GlobalLogger.ts new file mode 100644 index 000000000..4da7acf49 --- /dev/null +++ b/themes/black/server/src/lib/logging/GlobalLogger.ts @@ -0,0 +1,34 @@ +import { IGlobalLogger } from "./IGlobalLogger"; +import Util = require("util"); +import Express = require("express"); +import Winston = require("winston"); + +declare module "express" { + interface Request { + id: string; + } +} + +export class GlobalLogger implements IGlobalLogger { + private winston: typeof Winston; + constructor(winston: typeof Winston) { + this.winston = winston; + } + + private buildMessage(message: string, ...args: any[]): string { + return Util.format("date='%s' message='%s'", new Date(), + Util.format(message, ...args)); + } + + info(message: string, ...args: any[]): void { + this.winston.info(this.buildMessage(message, ...args)); + } + + debug(message: string, ...args: any[]): void { + this.winston.debug(this.buildMessage(message, ...args)); + } + + error(message: string, ...args: any[]): void { + this.winston.debug(this.buildMessage(message, ...args)); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/logging/GlobalLoggerStub.spec.ts b/themes/black/server/src/lib/logging/GlobalLoggerStub.spec.ts new file mode 100644 index 000000000..d4bb13710 --- /dev/null +++ b/themes/black/server/src/lib/logging/GlobalLoggerStub.spec.ts @@ -0,0 +1,38 @@ +import Sinon = require("sinon"); +import { GlobalLogger } from "./GlobalLogger"; +import Winston = require("winston"); +import Express = require("express"); +import { IGlobalLogger } from "./IGlobalLogger"; + +export class GlobalLoggerStub implements IGlobalLogger { + infoStub: Sinon.SinonStub; + debugStub: Sinon.SinonStub; + errorStub: Sinon.SinonStub; + private globalLogger: IGlobalLogger; + + constructor(enableLogging?: boolean) { + this.infoStub = Sinon.stub(); + this.debugStub = Sinon.stub(); + this.errorStub = Sinon.stub(); + if (enableLogging) + this.globalLogger = new GlobalLogger(Winston); + } + + info(message: string, ...args: any[]): void { + if (this.globalLogger) + this.globalLogger.info(message, ...args); + this.infoStub(message, ...args); + } + + debug(message: string, ...args: any[]): void { + if (this.globalLogger) + this.globalLogger.info(message, ...args); + this.debugStub(message, ...args); + } + + error(message: string, ...args: any[]): void { + if (this.globalLogger) + this.globalLogger.info(message, ...args); + this.errorStub(message, ...args); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/logging/IGlobalLogger.ts b/themes/black/server/src/lib/logging/IGlobalLogger.ts new file mode 100644 index 000000000..548515ec7 --- /dev/null +++ b/themes/black/server/src/lib/logging/IGlobalLogger.ts @@ -0,0 +1,5 @@ +export interface IGlobalLogger { + info(message: string, ...args: any[]): void; + debug(message: string, ...args: any[]): void; + error(message: string, ...args: any[]): void; +} diff --git a/themes/black/server/src/lib/logging/IRequestLogger.ts b/themes/black/server/src/lib/logging/IRequestLogger.ts new file mode 100644 index 000000000..126a601fe --- /dev/null +++ b/themes/black/server/src/lib/logging/IRequestLogger.ts @@ -0,0 +1,7 @@ +import Express = require("express"); + +export interface IRequestLogger { + info(req: Express.Request, message: string, ...args: any[]): void; + debug(req: Express.Request, message: string, ...args: any[]): void; + error(req: Express.Request, message: string, ...args: any[]): void; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/logging/RequestLogger.ts b/themes/black/server/src/lib/logging/RequestLogger.ts new file mode 100644 index 000000000..c45c66018 --- /dev/null +++ b/themes/black/server/src/lib/logging/RequestLogger.ts @@ -0,0 +1,45 @@ +import { IRequestLogger } from "./IRequestLogger"; +import Util = require("util"); +import Express = require("express"); +import Winston = require("winston"); + +declare module "express" { + interface Request { + id: string; + } +} + +export class RequestLogger implements IRequestLogger { + private winston: typeof Winston; + + constructor(winston: typeof Winston) { + this.winston = winston; + } + + private formatHeader(req: Express.Request) { + const clientIP = req.ip; // The IP of the original client going through the proxy chain. + return Util.format("date='%s' method='%s', path='%s' requestId='%s' sessionId='%s' ip='%s'", + new Date(), req.method, req.path, req.id, req.sessionID, clientIP); + } + + private formatBody(message: string) { + return Util.format("message='%s'", message); + } + + private formatMessage(req: Express.Request, message: string) { + return Util.format("%s %s", this.formatHeader(req), + this.formatBody(message)); + } + + info(req: Express.Request, message: string, ...args: any[]): void { + this.winston.info(this.formatMessage(req, message), ...args); + } + + debug(req: Express.Request, message: string, ...args: any[]): void { + this.winston.debug(this.formatMessage(req, message), ...args); + } + + error(req: Express.Request, message: string, ...args: any[]): void { + this.winston.error(this.formatMessage(req, message), ...args); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/logging/RequestLoggerStub.spec.ts b/themes/black/server/src/lib/logging/RequestLoggerStub.spec.ts new file mode 100644 index 000000000..b0e375210 --- /dev/null +++ b/themes/black/server/src/lib/logging/RequestLoggerStub.spec.ts @@ -0,0 +1,38 @@ +import { IRequestLogger } from "./IRequestLogger"; +import Sinon = require("sinon"); +import { RequestLogger } from "./RequestLogger"; +import Winston = require("winston"); +import Express = require("express"); + +export class RequestLoggerStub implements IRequestLogger { + infoStub: Sinon.SinonStub; + debugStub: Sinon.SinonStub; + errorStub: Sinon.SinonStub; + private requestLogger: RequestLogger; + + constructor(enableLogging?: boolean) { + this.infoStub = Sinon.stub(); + this.debugStub = Sinon.stub(); + this.errorStub = Sinon.stub(); + if (enableLogging) + this.requestLogger = new RequestLogger(Winston); + } + + info(req: Express.Request, message: string, ...args: any[]): void { + if (this.requestLogger) + this.requestLogger.info(req, message, ...args); + this.infoStub(req, message, ...args); + } + + debug(req: Express.Request, message: string, ...args: any[]): void { + if (this.requestLogger) + this.requestLogger.info(req, message, ...args); + this.debugStub(req, message, ...args); + } + + error(req: Express.Request, message: string, ...args: any[]): void { + if (this.requestLogger) + this.requestLogger.info(req, message, ...args); + this.errorStub(req, message, ...args); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/AbstractEmailNotifier.ts b/themes/black/server/src/lib/notifiers/AbstractEmailNotifier.ts new file mode 100644 index 000000000..198e4e5db --- /dev/null +++ b/themes/black/server/src/lib/notifiers/AbstractEmailNotifier.ts @@ -0,0 +1,23 @@ + +import { INotifier } from "../notifiers/INotifier"; +import { Identity } from "../../../types/Identity"; + +import Fs = require("fs"); +import Path = require("path"); +import Ejs = require("ejs"); +import BluebirdPromise = require("bluebird"); + +const email_template = Fs.readFileSync(Path.join(__dirname, "../../resources/email-template.ejs"), "UTF-8"); + +export abstract class AbstractEmailNotifier implements INotifier { + notify(to: string, subject: string, link: string): BluebirdPromise { + const d = { + url: link, + button_title: "Continue", + title: subject + }; + return this.sendEmail(to, subject, Ejs.render(email_template, d)); + } + + abstract sendEmail(to: string, subject: string, content: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/EmailNotifier.spec.ts b/themes/black/server/src/lib/notifiers/EmailNotifier.spec.ts new file mode 100644 index 000000000..8211bbc02 --- /dev/null +++ b/themes/black/server/src/lib/notifiers/EmailNotifier.spec.ts @@ -0,0 +1,54 @@ +import * as sinon from "sinon"; +import * as Assert from "assert"; +import BluebirdPromise = require("bluebird"); + +import { MailSenderStub } from "./MailSenderStub.spec"; +import EmailNotifier = require("./EmailNotifier"); + + +describe("notifiers/EmailNotifier", function () { + it("should send an email to given user", function () { + const mailSender = new MailSenderStub(); + const options = { + username: "user_gmail", + password: "pass_gmail", + sender: "admin@example.com", + service: "gmail" + }; + + mailSender.sendStub.returns(BluebirdPromise.resolve()); + const sender = new EmailNotifier.EmailNotifier(options, mailSender); + const subject = "subject"; + const url = "http://test.com"; + + return sender.notify("user@example.com", subject, url) + .then(function () { + Assert.equal(mailSender.sendStub.getCall(0).args[0].to, "user@example.com"); + Assert.equal(mailSender.sendStub.getCall(0).args[0].subject, "subject"); + return BluebirdPromise.resolve(); + }); + }); + + it("should fail while sending an email", function () { + const mailSender = new MailSenderStub(); + const options = { + username: "user_gmail", + password: "pass_gmail", + sender: "admin@example.com", + service: "gmail" + }; + + mailSender.sendStub.returns(BluebirdPromise.reject(new Error("Failed to send mail"))); + const sender = new EmailNotifier.EmailNotifier(options, mailSender); + const subject = "subject"; + const url = "http://test.com"; + + return sender.notify("user@example.com", subject, url) + .then(function () { + return BluebirdPromise.reject(new Error()); + }, function() { + Assert.equal(mailSender.sendStub.getCall(0).args[0].from, "admin@example.com"); + return BluebirdPromise.resolve(); + }); + }); +}); diff --git a/themes/black/server/src/lib/notifiers/EmailNotifier.ts b/themes/black/server/src/lib/notifiers/EmailNotifier.ts new file mode 100644 index 000000000..4df7c861e --- /dev/null +++ b/themes/black/server/src/lib/notifiers/EmailNotifier.ts @@ -0,0 +1,27 @@ + +import * as BluebirdPromise from "bluebird"; + +import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; +import { EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; +import { IMailSender } from "./IMailSender"; + +export class EmailNotifier extends AbstractEmailNotifier { + private mailSender: IMailSender; + private sender: string; + + constructor(options: EmailNotifierConfiguration, mailSender: IMailSender) { + super(); + this.mailSender = mailSender; + this.sender = options.sender; + } + + sendEmail(to: string, subject: string, content: string) { + const mailOptions = { + from: this.sender, + to: to, + subject: subject, + html: content + }; + return this.mailSender.send(mailOptions); + } +} diff --git a/themes/black/server/src/lib/notifiers/FileSystemNotifier.ts b/themes/black/server/src/lib/notifiers/FileSystemNotifier.ts new file mode 100644 index 000000000..23f6242c4 --- /dev/null +++ b/themes/black/server/src/lib/notifiers/FileSystemNotifier.ts @@ -0,0 +1,22 @@ +import * as BluebirdPromise from "bluebird"; +import * as util from "util"; +import * as Fs from "fs"; +import { INotifier } from "./INotifier"; +import { Identity } from "../../../types/Identity"; + +import { FileSystemNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; + +export class FileSystemNotifier implements INotifier { + private filename: string; + + constructor(options: FileSystemNotifierConfiguration) { + this.filename = options.filename; + } + + notify(to: string, subject: string, link: string): BluebirdPromise { + const content = util.format("Date: %s\nEmail: %s\nSubject: %s\nLink: %s", + new Date().toString(), to, subject, link); + const writeFilePromised: any = BluebirdPromise.promisify(Fs.writeFile); + return writeFilePromised(this.filename, content); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/IMailSender.ts b/themes/black/server/src/lib/notifiers/IMailSender.ts new file mode 100644 index 000000000..34ac464a8 --- /dev/null +++ b/themes/black/server/src/lib/notifiers/IMailSender.ts @@ -0,0 +1,6 @@ +import BluebirdPromise = require("bluebird"); +import Nodemailer = require("nodemailer"); + +export interface IMailSender { + send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/IMailSenderBuilder.ts b/themes/black/server/src/lib/notifiers/IMailSenderBuilder.ts new file mode 100644 index 000000000..36d4dcdf7 --- /dev/null +++ b/themes/black/server/src/lib/notifiers/IMailSenderBuilder.ts @@ -0,0 +1,7 @@ +import { IMailSender } from "./IMailSender"; +import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; + +export interface IMailSenderBuilder { + buildEmail(options: EmailNotifierConfiguration): IMailSender; + buildSmtp(options: SmtpNotifierConfiguration): IMailSender; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/INotifier.ts b/themes/black/server/src/lib/notifiers/INotifier.ts new file mode 100644 index 000000000..b9a6b138c --- /dev/null +++ b/themes/black/server/src/lib/notifiers/INotifier.ts @@ -0,0 +1,5 @@ +import * as BluebirdPromise from "bluebird"; + +export interface INotifier { + notify(to: string, subject: string, link: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/MailSender.ts b/themes/black/server/src/lib/notifiers/MailSender.ts new file mode 100644 index 000000000..536a88e63 --- /dev/null +++ b/themes/black/server/src/lib/notifiers/MailSender.ts @@ -0,0 +1,42 @@ +import { IMailSender } from "./IMailSender"; +import Nodemailer = require("nodemailer"); +import NodemailerDirectTransport = require("nodemailer-direct-transport"); +import NodemailerSmtpTransport = require("nodemailer-smtp-transport"); +import BluebirdPromise = require("bluebird"); + +export class MailSender implements IMailSender { + private transporter: Nodemailer.Transporter; + + constructor(options: NodemailerDirectTransport.DirectOptions | + NodemailerSmtpTransport.SmtpOptions, nodemailer: typeof Nodemailer) { + this.transporter = nodemailer.createTransport(options); + } + + verify(): BluebirdPromise { + const that = this; + return new BluebirdPromise(function (resolve, reject) { + that.transporter.verify(function (error: Error, success: any) { + if (error) { + reject(new Error("Unable to connect to SMTP server. \ + Please check the service is running and your credentials are correct.")); + return; + } + resolve(); + }); + }); + } + + send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise { + const that = this; + return new BluebirdPromise(function (resolve, reject) { + that.transporter.sendMail(mailOptions, (error: Error, + data: Nodemailer.SentMessageInfo) => { + if (error) { + reject(new Error("Error while sending email: " + error.message)); + return; + } + resolve(); + }); + }); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/MailSenderBuilder.spec.ts b/themes/black/server/src/lib/notifiers/MailSenderBuilder.spec.ts new file mode 100644 index 000000000..41e0db426 --- /dev/null +++ b/themes/black/server/src/lib/notifiers/MailSenderBuilder.spec.ts @@ -0,0 +1,67 @@ + +import { MailSenderBuilder } from ".//MailSenderBuilder"; +import Nodemailer = require("nodemailer"); +import Sinon = require("sinon"); +import Assert = require("assert"); + +describe("notifiers/MailSenderBuilder", function() { + let createTransportStub: Sinon.SinonStub; + beforeEach(function() { + createTransportStub = Sinon.stub(Nodemailer, "createTransport"); + }); + + afterEach(function() { + createTransportStub.restore(); + }); + + it("should create a email mail sender", function() { + const mailSenderBuilder = new MailSenderBuilder(Nodemailer); + mailSenderBuilder.buildEmail({ + username: "user_gmail", + password: "pass_gmail", + sender: "admin@example.com", + service: "gmail" + }); + Assert.equal(createTransportStub.getCall(0).args[0].auth.user, "user_gmail"); + Assert.equal(createTransportStub.getCall(0).args[0].auth.pass, "pass_gmail"); + Assert.equal(createTransportStub.getCall(0).args[0].service, "gmail"); + }); + + describe("build smtp mail sender", function() { + it("should create a smtp mail sender with authenticated user", function() { + const mailSenderBuilder = new MailSenderBuilder(Nodemailer); + mailSenderBuilder.buildSmtp({ + host: "mail.example.com", + password: "password", + port: 25, + secure: true, + username: "user", + sender: "admin@example.com" + }); + Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { + host: "mail.example.com", + auth: { + pass: "password", + user: "user" + }, + port: 25, + secure: true, + }); + }); + + it("should create a smtp mail sender with anonymous user", function() { + const mailSenderBuilder = new MailSenderBuilder(Nodemailer); + mailSenderBuilder.buildSmtp({ + host: "mail.example.com", + port: 25, + secure: true, + sender: "admin@example.com" + }); + Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { + host: "mail.example.com", + port: 25, + secure: true, + }); + }); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/MailSenderBuilder.ts b/themes/black/server/src/lib/notifiers/MailSenderBuilder.ts new file mode 100644 index 000000000..1d06be52f --- /dev/null +++ b/themes/black/server/src/lib/notifiers/MailSenderBuilder.ts @@ -0,0 +1,42 @@ +import { IMailSender } from "./IMailSender"; +import { IMailSenderBuilder } from "./IMailSenderBuilder"; +import { MailSender } from "./MailSender"; +import Nodemailer = require("nodemailer"); +import NodemailerSmtpTransport = require("nodemailer-smtp-transport"); +import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; + +export class MailSenderBuilder implements IMailSenderBuilder { + private nodemailer: typeof Nodemailer; + + constructor(nodemailer: typeof Nodemailer) { + this.nodemailer = nodemailer; + } + + buildEmail(options: EmailNotifierConfiguration): IMailSender { + const emailOptions = { + service: options.service, + auth: { + user: options.username, + pass: options.password + } + }; + return new MailSender(emailOptions, this.nodemailer); + } + + buildSmtp(options: SmtpNotifierConfiguration): IMailSender { + const smtpOptions: NodemailerSmtpTransport.SmtpOptions = { + host: options.host, + port: options.port, + secure: options.secure, // upgrade later with STARTTLS + }; + + if (options.username && options.password) { + smtpOptions.auth = { + user: options.username, + pass: options.password + }; + } + + return new MailSender(smtpOptions, this.nodemailer); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts b/themes/black/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts new file mode 100644 index 000000000..5b76f6e56 --- /dev/null +++ b/themes/black/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts @@ -0,0 +1,25 @@ +import { IMailSenderBuilder } from "../../../src/lib/notifiers/IMailSenderBuilder"; +import BluebirdPromise = require("bluebird"); +import Nodemailer = require("nodemailer"); +import Sinon = require("sinon"); +import { IMailSender } from "../../../src/lib/notifiers/IMailSender"; +import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../../../src/lib/configuration/schema/NotifierConfiguration"; + +export class MailSenderBuilderStub implements IMailSenderBuilder { + buildEmailStub: Sinon.SinonStub; + buildSmtpStub: Sinon.SinonStub; + + constructor() { + this.buildEmailStub = Sinon.stub(); + this.buildSmtpStub = Sinon.stub(); + } + + buildEmail(options: EmailNotifierConfiguration): IMailSender { + return this.buildEmailStub(options); + } + + buildSmtp(options: SmtpNotifierConfiguration): IMailSender { + return this.buildSmtpStub(options); + } + +} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/MailSenderStub.spec.ts b/themes/black/server/src/lib/notifiers/MailSenderStub.spec.ts new file mode 100644 index 000000000..d57c458fc --- /dev/null +++ b/themes/black/server/src/lib/notifiers/MailSenderStub.spec.ts @@ -0,0 +1,16 @@ +import { IMailSender } from "../../../src/lib/notifiers/IMailSender"; +import BluebirdPromise = require("bluebird"); +import Nodemailer = require("nodemailer"); +import Sinon = require("sinon"); + +export class MailSenderStub implements IMailSender { + sendStub: Sinon.SinonStub; + + constructor() { + this.sendStub = Sinon.stub(); + } + + send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise { + return this.sendStub(mailOptions); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/NotifierFactory.spec.ts b/themes/black/server/src/lib/notifiers/NotifierFactory.spec.ts new file mode 100644 index 000000000..f15e7667f --- /dev/null +++ b/themes/black/server/src/lib/notifiers/NotifierFactory.spec.ts @@ -0,0 +1,42 @@ + +import * as sinon from "sinon"; +import * as BluebirdPromise from "bluebird"; +import * as assert from "assert"; + +import { NotifierFactory } from "./NotifierFactory"; +import { EmailNotifier } from "./EmailNotifier"; +import { SmtpNotifier } from "./SmtpNotifier"; +import { MailSenderBuilderStub } from "./MailSenderBuilderStub.spec"; + + +describe("notifiers/NotifierFactory", function () { + let mailSenderBuilderStub: MailSenderBuilderStub; + it("should build a Email Notifier", function () { + const options = { + email: { + username: "abc", + password: "password", + sender: "admin@example.com", + service: "gmail" + } + }; + mailSenderBuilderStub = new MailSenderBuilderStub(); + assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof EmailNotifier); + }); + + it("should build a SMTP Notifier", function () { + const options = { + smtp: { + username: "user", + password: "pass", + secure: true, + host: "localhost", + port: 25, + sender: "admin@example.com" + } + }; + + mailSenderBuilderStub = new MailSenderBuilderStub(); + assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof SmtpNotifier); + }); +}); diff --git a/themes/black/server/src/lib/notifiers/NotifierFactory.ts b/themes/black/server/src/lib/notifiers/NotifierFactory.ts new file mode 100644 index 000000000..a89155feb --- /dev/null +++ b/themes/black/server/src/lib/notifiers/NotifierFactory.ts @@ -0,0 +1,33 @@ + +import { NotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; +import Nodemailer = require("nodemailer"); +import { INotifier } from "./INotifier"; + +import { FileSystemNotifier } from "./FileSystemNotifier"; +import { EmailNotifier } from "./EmailNotifier"; +import { SmtpNotifier } from "./SmtpNotifier"; +import { IMailSender } from "./IMailSender"; +import { IMailSenderBuilder } from "./IMailSenderBuilder"; + +export class NotifierFactory { + static build(options: NotifierConfiguration, mailSenderBuilder: IMailSenderBuilder): INotifier { + if ("email" in options) { + const mailSender = mailSenderBuilder.buildEmail(options.email); + return new EmailNotifier(options.email, mailSender); + } + else if ("smtp" in options) { + const mailSender = mailSenderBuilder.buildSmtp(options.smtp); + return new SmtpNotifier(options.smtp, mailSender); + } + else if ("filesystem" in options) { + return new FileSystemNotifier(options.filesystem); + } + else { + throw new Error("No available notifier option detected."); + } + } +} + + + + diff --git a/themes/black/server/src/lib/notifiers/NotifierStub.spec.ts b/themes/black/server/src/lib/notifiers/NotifierStub.spec.ts new file mode 100644 index 000000000..f99231b50 --- /dev/null +++ b/themes/black/server/src/lib/notifiers/NotifierStub.spec.ts @@ -0,0 +1,16 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); + +import { INotifier } from "./INotifier"; + +export class NotifierStub implements INotifier { + notifyStub: Sinon.SinonStub; + + constructor() { + this.notifyStub = Sinon.stub(); + } + + notify(to: string, subject: string, link: string): BluebirdPromise { + return this.notifyStub(to, subject, link); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/SmtpNotifier.ts b/themes/black/server/src/lib/notifiers/SmtpNotifier.ts new file mode 100644 index 000000000..f93a6d4a0 --- /dev/null +++ b/themes/black/server/src/lib/notifiers/SmtpNotifier.ts @@ -0,0 +1,30 @@ + + +import * as BluebirdPromise from "bluebird"; + +import { IMailSender } from "./IMailSender"; +import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; +import { SmtpNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; + +export class SmtpNotifier extends AbstractEmailNotifier { + private mailSender: IMailSender; + private sender: string; + + constructor(options: SmtpNotifierConfiguration, + mailSender: IMailSender) { + super(); + this.mailSender = mailSender; + this.sender = options.sender; + } + + sendEmail(to: string, subject: string, content: string) { + const mailOptions = { + from: this.sender, + to: to, + subject: subject, + html: content + }; + const that = this; + return this.mailSender.send(mailOptions); + } +} diff --git a/themes/black/server/src/lib/regulation/IRegulator.ts b/themes/black/server/src/lib/regulation/IRegulator.ts new file mode 100644 index 000000000..c49425b24 --- /dev/null +++ b/themes/black/server/src/lib/regulation/IRegulator.ts @@ -0,0 +1,6 @@ +import BluebirdPromise = require("bluebird"); + +export interface IRegulator { + mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; + regulate(userId: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/regulation/Regulator.spec.ts b/themes/black/server/src/lib/regulation/Regulator.spec.ts new file mode 100644 index 000000000..f9c6e6086 --- /dev/null +++ b/themes/black/server/src/lib/regulation/Regulator.spec.ts @@ -0,0 +1,186 @@ + +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import Assert = require("assert"); + +import { Regulator } from "./Regulator"; +import MockDate = require("mockdate"); +import exceptions = require("../Exceptions"); +import { UserDataStoreStub } from "../storage/UserDataStoreStub.spec"; + +describe("regulation/Regulator", function () { + const USER1 = "USER1"; + const USER2 = "USER2"; + let userDataStoreStub: UserDataStoreStub; + + beforeEach(function () { + userDataStoreStub = new UserDataStoreStub(); + const dataStore: { [userId: string]: { userId: string, date: Date, isAuthenticationSuccessful: boolean }[] } = { + [USER1]: [], + [USER2]: [] + }; + + userDataStoreStub.saveAuthenticationTraceStub.callsFake(function (userId, isAuthenticationSuccessful) { + dataStore[userId].unshift({ + userId: userId, + date: new Date(), + isAuthenticationSuccessful: isAuthenticationSuccessful, + }); + return BluebirdPromise.resolve(); + }); + + userDataStoreStub.retrieveLatestAuthenticationTracesStub.callsFake(function (userId, count) { + const ret = (dataStore[userId].length <= count) ? dataStore[userId] : dataStore[userId].slice(0, 3); + return BluebirdPromise.resolve(ret); + }); + }); + + afterEach(function () { + MockDate.reset(); + }); + + function markAuthenticationAt(regulator: Regulator, user: string, time: string, success: boolean) { + MockDate.set(time); + return regulator.mark(user, success); + } + + it("should mark 2 authentication and regulate (accept)", function () { + const regulator = new Regulator(userDataStoreStub, 3, 10, 10); + + return regulator.mark(USER1, false) + .then(function () { + return regulator.mark(USER1, true); + }) + .then(function () { + return regulator.regulate(USER1); + }); + }); + + it("should mark 3 authentications and regulate (reject)", function () { + const regulator = new Regulator(userDataStoreStub, 3, 10, 10); + + return regulator.mark(USER1, false) + .then(function () { + return regulator.mark(USER1, false); + }) + .then(function () { + return regulator.mark(USER1, false); + }) + .then(function () { + return regulator.regulate(USER1); + }) + .then(function () { return BluebirdPromise.reject(new Error("should not be here!")); }) + .catch(exceptions.AuthenticationRegulationError, function () { + return BluebirdPromise.resolve(); + }); + }); + + it("should mark 1 failed, 1 successful and 1 failed authentications within minimum time and regulate (accept)", function () { + const regulator = new Regulator(userDataStoreStub, 3, 60, 30); + + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", true); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:20", false); + }) + .then(function () { + return regulator.regulate(USER1); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:30", false); + }) + .then(function () { + return regulator.regulate(USER1); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:39", false); + }) + .then(function () { + return regulator.regulate(USER1); + }) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here!")); + }, + function () { + return BluebirdPromise.resolve(); + }); + }); + + it("should regulate user if number of failures is greater than 3 in allowed time lapse", function () { + function markAuthentications(regulator: Regulator, user: string) { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:45", false); + }) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:01:05", false); + }) + .then(function () { + return regulator.regulate(user); + }); + } + + const regulator1 = new Regulator(userDataStoreStub, 3, 60, 60); + const regulator2 = new Regulator(userDataStoreStub, 3, 2 * 60, 60); + + const p1 = markAuthentications(regulator1, USER1); + const p2 = markAuthentications(regulator2, USER2); + + return BluebirdPromise.join(p1, p2) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here...")); + }, function () { + Assert(p1.isFulfilled()); + Assert(p2.isRejected()); + }); + }); + + it("should user wait after regulation to authenticate again", function () { + function markAuthentications(regulator: Regulator, user: string) { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:10", false); + }) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:15", false); + }) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:25", false); + }) + .then(function () { + MockDate.set("1/2/2000 00:00:54"); + return regulator.regulate(user); + }) + .then(function () { + return BluebirdPromise.reject(new Error("should fail at this time")); + }, function () { + MockDate.set("1/2/2000 00:00:56"); + return regulator.regulate(user); + }); + } + + const regulator = new Regulator(userDataStoreStub, 4, 30, 30); + return markAuthentications(regulator, USER1); + }); + + it("should disable regulation when max_retries is set to 0", function () { + const maxRetries = 0; + const regulator = new Regulator(userDataStoreStub, maxRetries, 60, 30); + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", false); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:15", false); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:25", false); + }) + .then(function () { + MockDate.set("1/2/2000 00:00:26"); + return regulator.regulate(USER1); + }); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/regulation/Regulator.ts b/themes/black/server/src/lib/regulation/Regulator.ts new file mode 100644 index 000000000..1037a6a17 --- /dev/null +++ b/themes/black/server/src/lib/regulation/Regulator.ts @@ -0,0 +1,55 @@ + +import * as BluebirdPromise from "bluebird"; +import exceptions = require("../Exceptions"); +import { IUserDataStore } from "../storage/IUserDataStore"; +import { AuthenticationTraceDocument } from "../storage/AuthenticationTraceDocument"; +import { IRegulator } from "./IRegulator"; + +export class Regulator implements IRegulator { + private userDataStore: IUserDataStore; + private banTime: number; + private findTime: number; + private maxRetries: number; + + constructor(userDataStore: any, maxRetries: number, findTime: number, banTime: number) { + this.userDataStore = userDataStore; + this.banTime = banTime; + this.findTime = findTime; + this.maxRetries = maxRetries; + } + + // Mark authentication + mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { + return this.userDataStore.saveAuthenticationTrace(userId, isAuthenticationSuccessful); + } + + regulate(userId: string): BluebirdPromise { + const that = this; + + if (that.maxRetries <= 0) return BluebirdPromise.resolve(); + + return this.userDataStore.retrieveLatestAuthenticationTraces(userId, that.maxRetries) + .then((docs: AuthenticationTraceDocument[]) => { + // less than the max authorized number of authentication in time range, thus authorizing access + if (docs.length < that.maxRetries) return BluebirdPromise.resolve(); + + const numberOfFailedAuth = docs + .map(function (d: AuthenticationTraceDocument) { return d.isAuthenticationSuccessful == false ? 1 : 0; }) + .reduce(function (acc, v) { return acc + v; }, 0); + + if (numberOfFailedAuth < this.maxRetries) return BluebirdPromise.resolve(); + + const newestDocument = docs[0]; + const oldestDocument = docs[that.maxRetries - 1]; + + const authenticationsTimeRangeInSeconds = (newestDocument.date.getTime() - oldestDocument.date.getTime()) / 1000; + const tooManyAuthInTimelapse = (authenticationsTimeRangeInSeconds < this.findTime); + const stillInBannedTimeRange = (new Date(new Date().getTime() - this.banTime * 1000) < newestDocument.date); + + if (tooManyAuthInTimelapse && stillInBannedTimeRange) + throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes."); + + return BluebirdPromise.resolve(); + }); + } +} diff --git a/themes/black/server/src/lib/regulation/RegulatorStub.spec.ts b/themes/black/server/src/lib/regulation/RegulatorStub.spec.ts new file mode 100644 index 000000000..ca8a00fb1 --- /dev/null +++ b/themes/black/server/src/lib/regulation/RegulatorStub.spec.ts @@ -0,0 +1,22 @@ +import Bluebird = require("bluebird"); +import Sinon = require("sinon"); +import { IRegulator } from "./IRegulator"; + + +export class RegulatorStub implements IRegulator { + markStub: Sinon.SinonStub; + regulateStub: Sinon.SinonStub; + + constructor() { + this.markStub = Sinon.stub(); + this.regulateStub = Sinon.stub(); + } + + mark(userId: string, isAuthenticationSuccessful: boolean): Bluebird { + return this.markStub(userId, isAuthenticationSuccessful); + } + + regulate(userId: string): Bluebird { + return this.regulateStub(userId); + } +} diff --git a/themes/black/server/src/lib/routes/error/401/get.spec.ts b/themes/black/server/src/lib/routes/error/401/get.spec.ts new file mode 100644 index 000000000..9fdac9c3c --- /dev/null +++ b/themes/black/server/src/lib/routes/error/401/get.spec.ts @@ -0,0 +1,61 @@ +import Sinon = require("sinon"); +import Express = require("express"); +import Assert = require("assert"); +import Get401 from "./get"; +import { ServerVariables } from "../../../ServerVariables"; +import { ServerVariablesMockBuilder, ServerVariablesMock } + from "../../../ServerVariablesMockBuilder.spec"; + +describe("routes/error/401/get", function () { + let vars: ServerVariables; + let mocks: ServerVariablesMock; + let req: any; + let res: any; + let renderSpy: Sinon.SinonSpy; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + vars = s.variables; + mocks = s.mocks; + + renderSpy = Sinon.spy(); + req = { + headers: {} + }; + res = { + render: renderSpy + }; + }); + + it("should set redirection url to the default redirection url", function () { + vars.config.default_redirection_url = "http://default-redirection"; + return Get401(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/401", { + redirection_url: "http://default-redirection" + })); + }); + }); + + it("should set redirection url to the referer", function () { + req.headers["referer"] = "http://redirection"; + return Get401(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/401", { + redirection_url: "http://redirection" + })); + }); + }); + + it("should render without redirecting the user", function () { + return Get401(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/401", { + redirection_url: undefined + })); + }); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/error/401/get.ts b/themes/black/server/src/lib/routes/error/401/get.ts new file mode 100644 index 000000000..ca4a3963d --- /dev/null +++ b/themes/black/server/src/lib/routes/error/401/get.ts @@ -0,0 +1,15 @@ + +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import redirector from "../redirector"; +import { ServerVariables } from "../../../ServerVariables"; + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response): BluebirdPromise { + const redirectionUrl = redirector(req, vars); + res.render("errors/401", { + redirection_url: redirectionUrl + }); + return BluebirdPromise.resolve(); + }; +} diff --git a/themes/black/server/src/lib/routes/error/403/get.spec.ts b/themes/black/server/src/lib/routes/error/403/get.spec.ts new file mode 100644 index 000000000..22eb84853 --- /dev/null +++ b/themes/black/server/src/lib/routes/error/403/get.spec.ts @@ -0,0 +1,61 @@ +import Sinon = require("sinon"); +import Express = require("express"); +import Assert = require("assert"); +import Get403 from "./get"; +import { ServerVariables } from "../../../ServerVariables"; +import { ServerVariablesMockBuilder, ServerVariablesMock } + from "../../../ServerVariablesMockBuilder.spec"; + +describe("routes/error/403/get", function () { + let vars: ServerVariables; + let mocks: ServerVariablesMock; + let req: any; + let res: any; + let renderSpy: Sinon.SinonSpy; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + vars = s.variables; + mocks = s.mocks; + + renderSpy = Sinon.spy(); + req = { + headers: {} + }; + res = { + render: renderSpy + }; + }); + + it("should set redirection url to the default redirection url", function () { + vars.config.default_redirection_url = "http://default-redirection"; + return Get403(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/403", { + redirection_url: "http://default-redirection" + })); + }); + }); + + it("should set redirection url to the referer", function () { + req.headers["referer"] = "http://redirection"; + return Get403(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/403", { + redirection_url: "http://redirection" + })); + }); + }); + + it("should render without redirecting the user", function () { + return Get403(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/403", { + redirection_url: undefined + })); + }); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/error/403/get.ts b/themes/black/server/src/lib/routes/error/403/get.ts new file mode 100644 index 000000000..3ab0319e5 --- /dev/null +++ b/themes/black/server/src/lib/routes/error/403/get.ts @@ -0,0 +1,15 @@ + +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import redirector from "../redirector"; +import { ServerVariables } from "../../../ServerVariables"; + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response): BluebirdPromise { + const redirectionUrl = redirector(req, vars); + res.render("errors/403", { + redirection_url: redirectionUrl + }); + return BluebirdPromise.resolve(); + }; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/error/404/get.spec.ts b/themes/black/server/src/lib/routes/error/404/get.spec.ts new file mode 100644 index 000000000..73e4e6cef --- /dev/null +++ b/themes/black/server/src/lib/routes/error/404/get.spec.ts @@ -0,0 +1,19 @@ +import Sinon = require("sinon"); +import Express = require("express"); +import Assert = require("assert"); +import Get404 from "./get"; + +describe("routes/error/404/get", function () { + it("should render the page", function () { + const req = {} as Express.Request; + const res = { + render: Sinon.stub() + }; + + return Get404(req, res as any) + .then(function () { + Assert(res.render.calledOnce); + Assert(res.render.calledWith("errors/404")); + }); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/error/404/get.ts b/themes/black/server/src/lib/routes/error/404/get.ts new file mode 100644 index 000000000..6693b6fc8 --- /dev/null +++ b/themes/black/server/src/lib/routes/error/404/get.ts @@ -0,0 +1,8 @@ + +import BluebirdPromise = require("bluebird"); +import express = require("express"); + +export default function (req: express.Request, res: express.Response): BluebirdPromise { + res.render("errors/404"); + return BluebirdPromise.resolve(); +} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/error/redirector.ts b/themes/black/server/src/lib/routes/error/redirector.ts new file mode 100644 index 000000000..b1a3ccc11 --- /dev/null +++ b/themes/black/server/src/lib/routes/error/redirector.ts @@ -0,0 +1,13 @@ +import Express = require("express"); +import { ServerVariables } from "../../ServerVariables"; + +export default function (req: Express.Request, vars: ServerVariables): string { + let redirectionUrl: string; + + if (req.headers && req.headers["referer"]) + redirectionUrl = "" + req.headers["referer"]; + else if (vars.config.default_redirection_url) + redirectionUrl = vars.config.default_redirection_url; + + return redirectionUrl; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/firstfactor/get.ts b/themes/black/server/src/lib/routes/firstfactor/get.ts new file mode 100644 index 000000000..d94f656c8 --- /dev/null +++ b/themes/black/server/src/lib/routes/firstfactor/get.ts @@ -0,0 +1,72 @@ + +import express = require("express"); +import Endpoints = require("../../../../../shared/api"); +import BluebirdPromise = require("bluebird"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Constants = require("../../../../../shared/constants"); +import Util = require("util"); +import { ServerVariables } from "../../ServerVariables"; +import { SafeRedirector } from "../../utils/SafeRedirection"; +import { Level } from "../../authentication/Level"; + +function getRedirectParam( + req: express.Request) { + return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; +} + +function redirectToSecondFactorPage( + req: express.Request, + res: express.Response) { + + const redirectUrl = getRedirectParam(req); + if (!redirectUrl) + res.redirect(Endpoints.SECOND_FACTOR_GET); + else + res.redirect( + Util.format("%s?%s=%s", + Endpoints.SECOND_FACTOR_GET, + Constants.REDIRECT_QUERY_PARAM, + redirectUrl)); +} + +function redirectToService( + req: express.Request, + res: express.Response, + redirector: SafeRedirector) { + const redirectUrl = getRedirectParam(req); + if (!redirectUrl) { + res.redirect(Endpoints.LOGGED_IN); + } else { + redirector.redirectOrElse(res, redirectUrl, Endpoints.LOGGED_IN); + } +} + +function renderFirstFactor( + res: express.Response) { + + res.render("firstfactor", { + first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST, + reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET + }); +} + +export default function ( + vars: ServerVariables) { + + const redirector = new SafeRedirector(vars.config.session.domain); + return function (req: express.Request, res: express.Response): BluebirdPromise { + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (authSession.authentication_level == Level.ONE_FACTOR) { + redirectToSecondFactorPage(req, res); + } else if (authSession.authentication_level == Level.TWO_FACTOR) { + redirectToService(req, res, redirector); + } else { + renderFirstFactor(res); + } + resolve(); + }); + }; +} diff --git a/themes/black/server/src/lib/routes/firstfactor/post.spec.ts b/themes/black/server/src/lib/routes/firstfactor/post.spec.ts new file mode 100644 index 000000000..e1d078cdd --- /dev/null +++ b/themes/black/server/src/lib/routes/firstfactor/post.spec.ts @@ -0,0 +1,136 @@ + +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import Assert = require("assert"); +import FirstFactorPost = require("./post"); +import exceptions = require("../../Exceptions"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../types/AuthenticationSession"; +import Endpoints = require("../../../../../shared/api"); +import AuthenticationRegulatorMock = require("../../regulation/RegulatorStub.spec"); +import ExpressMock = require("../../stubs/express.spec"); +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../ServerVariables"; + +describe("routes/firstfactor/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let emails: string[]; + let groups: string[]; + let vars: ServerVariables; + let mocks: ServerVariablesMock; + let authSession: AuthenticationSession; + + beforeEach(function () { + emails = ["test_ok@example.com"]; + groups = ["group1", "group2" ]; + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + mocks.authorizer.authorizationMock.returns(true); + mocks.regulator.regulateStub.returns(BluebirdPromise.resolve()); + mocks.regulator.markStub.returns(BluebirdPromise.resolve()); + + req = { + originalUrl: "/api/firstfactor", + body: { + username: "username", + password: "password" + }, + query: { + redirect: "http://redirect.url" + }, + session: { + cookie: {} + }, + headers: { + host: "home.example.com" + } + }; + + res = ExpressMock.ResponseMock(); + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + }); + + it("should reply with 204 if success", function () { + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.resolve({ + emails: emails, + groups: groups + })); + return FirstFactorPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal("username", authSession.userid); + Assert(res.send.calledOnce); + }); + }); + + describe("keep me logged in", () => { + beforeEach(() => { + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.resolve({ + emails: emails, + groups: groups + })); + req.body.keepMeLoggedIn = "true"; + return FirstFactorPost.default(vars)(req as any, res as any); + }); + + it("should set keep_me_logged_in session variable to true", function () { + Assert.equal(authSession.keep_me_logged_in, true); + }); + + it("should set cookie maxAge to one year", function () { + Assert.equal(req.session.cookie.maxAge, 365 * 24 * 60 * 60 * 1000); + }); + }); + + it("should retrieve email from LDAP", function () { + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.resolve([{ mail: ["test@example.com"] }])); + return FirstFactorPost.default(vars)(req as any, res as any); + }); + + it("should set first email address as user session variable", function () { + const emails = ["test_ok@example.com"]; + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.resolve({ + emails: emails, + groups: groups + })); + + return FirstFactorPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal("test_ok@example.com", authSession.email); + }); + }); + + it("should return error message when LDAP authenticator throws", function () { + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.reject(new exceptions.LdapBindError("Bad credentials"))); + + return FirstFactorPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.equal(mocks.regulator.markStub.getCall(0).args[0], "username"); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + }); + }); + + it("should return error message when regulator rejects authentication", function () { + const err = new exceptions.AuthenticationRegulationError("Authentication regulation..."); + mocks.regulator.regulateStub.returns(BluebirdPromise.reject(err)); + return FirstFactorPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + }); + }); +}); + + diff --git a/themes/black/server/src/lib/routes/firstfactor/post.ts b/themes/black/server/src/lib/routes/firstfactor/post.ts new file mode 100644 index 000000000..565681d6a --- /dev/null +++ b/themes/black/server/src/lib/routes/firstfactor/post.ts @@ -0,0 +1,101 @@ + +import Exceptions = require("../../Exceptions"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import Endpoint = require("../../../../../shared/api"); +import ErrorReplies = require("../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Constants = require("../../../../../shared/constants"); +import UserMessages = require("../../../../../shared/UserMessages"); +import { ServerVariables } from "../../ServerVariables"; +import { AuthenticationSession } from "../../../../types/AuthenticationSession"; +import { GroupsAndEmails } from "../../authentication/backends/GroupsAndEmails"; +import { Level as AuthenticationLevel } from "../../authentication/Level"; +import { Level as AuthorizationLevel } from "../../authorization/Level"; +import { URLDecomposer } from "../../utils/URLDecomposer"; + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response) + : BluebirdPromise { + const username: string = req.body.username; + const password: string = req.body.password; + const keepMeLoggedIn: boolean = req.body.keepMeLoggedIn && + req.body.keepMeLoggedIn === "true"; + let authSession: AuthenticationSession; + + if (keepMeLoggedIn) { + // Stay connected for 1 year. + vars.logger.debug(req, "User requested to stay logged in for one year."); + req.session.cookie.maxAge = 365 * 24 * 60 * 60 * 1000; + } + + return BluebirdPromise.resolve() + .then(function () { + if (!username || !password) { + return BluebirdPromise.reject(new Error("No username or password.")); + } + vars.logger.info(req, "Starting authentication of user \"%s\"", username); + authSession = AuthenticationSessionHandler.get(req, vars.logger); + return vars.regulator.regulate(username); + }) + .then(function () { + vars.logger.info(req, "No regulation applied."); + return vars.usersDatabase.checkUserPassword(username, password); + }) + .then(function (groupsAndEmails: GroupsAndEmails) { + vars.logger.info(req, + "LDAP binding successful. Retrieved information about user are %s", + JSON.stringify(groupsAndEmails)); + authSession.userid = username; + authSession.keep_me_logged_in = keepMeLoggedIn; + authSession.authentication_level = AuthenticationLevel.ONE_FACTOR; + const redirectUrl: string = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined" + // Fuck, don't know why it is a string! + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : ""; + + const emails: string[] = groupsAndEmails.emails; + const groups: string[] = groupsAndEmails.groups; + const decomposition = URLDecomposer.fromUrl(redirectUrl); + const authorizationLevel = (decomposition) + ? vars.authorizer.authorization( + {domain: decomposition.domain, resource: decomposition.path}, + {user: username, groups: groups}) + : AuthorizationLevel.TWO_FACTOR; + + if (emails.length > 0) + authSession.email = emails[0]; + authSession.groups = groups; + + vars.logger.debug(req, "Mark successful authentication to regulator."); + vars.regulator.mark(username, true); + + if (authorizationLevel <= AuthorizationLevel.ONE_FACTOR) { + let newRedirectionUrl: string = redirectUrl; + if (!newRedirectionUrl) + newRedirectionUrl = Endpoint.LOGGED_IN; + res.send({ + redirect: newRedirectionUrl + }); + vars.logger.debug(req, "Redirect to '%s'", redirectUrl); + } + else { + let newRedirectUrl = Endpoint.SECOND_FACTOR_GET; + if (redirectUrl) { + newRedirectUrl += "?" + Constants.REDIRECT_QUERY_PARAM + "=" + + redirectUrl; + } + vars.logger.debug(req, "Redirect to '%s'", newRedirectUrl); + res.send({ + redirect: newRedirectUrl + }); + } + return BluebirdPromise.resolve(); + }) + .catch(Exceptions.LdapBindError, function (err: Error) { + vars.regulator.mark(username, false); + return ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)(err); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)); + }; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/loggedin/get.ts b/themes/black/server/src/lib/routes/loggedin/get.ts new file mode 100644 index 000000000..283a041b1 --- /dev/null +++ b/themes/black/server/src/lib/routes/loggedin/get.ts @@ -0,0 +1,23 @@ +import Express = require("express"); +import Endpoints = require("../../../../../shared/api"); +import BluebirdPromise = require("bluebird"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { ServerVariables } from "../../ServerVariables"; +import ErrorReplies = require("../../ErrorReplies"); + +export default function (vars: ServerVariables) { + function handler(req: Express.Request, res: Express.Response): BluebirdPromise { + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + res.render("already-logged-in", { + logout_endpoint: Endpoints.LOGOUT_GET, + username: authSession.userid, + redirection_url: vars.config.default_redirection_url + }); + resolve(); + }) + .catch(ErrorReplies.replyWithError401(req, res, vars.logger)); + } + + return handler; +} diff --git a/themes/black/server/src/lib/routes/logout/get.ts b/themes/black/server/src/lib/routes/logout/get.ts new file mode 100644 index 000000000..4d5112146 --- /dev/null +++ b/themes/black/server/src/lib/routes/logout/get.ts @@ -0,0 +1,20 @@ + +import express = require("express"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Constants = require("../../../../../shared/constants"); +import { ServerVariables } from "../../ServerVariables"; + +function getRedirectParam(req: express.Request) { + return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; +} + +export default function (vars: ServerVariables) { + return function(req: express.Request, res: express.Response) { + const redirect_param = getRedirectParam(req); + const redirect_url = redirect_param || "/"; + AuthenticationSessionHandler.reset(req); + res.redirect(redirect_url); + }; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/password-reset/constants.ts b/themes/black/server/src/lib/routes/password-reset/constants.ts new file mode 100644 index 000000000..5c639e92a --- /dev/null +++ b/themes/black/server/src/lib/routes/password-reset/constants.ts @@ -0,0 +1,2 @@ + +export const CHALLENGE = "reset-password"; \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/password-reset/form/post.spec.ts b/themes/black/server/src/lib/routes/password-reset/form/post.spec.ts new file mode 100644 index 000000000..ed029c906 --- /dev/null +++ b/themes/black/server/src/lib/routes/password-reset/form/post.spec.ts @@ -0,0 +1,122 @@ + +import PasswordResetFormPost = require("./post"); +import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; +import { UserDataStore } from "../../../storage/UserDataStore"; +import Sinon = require("sinon"); +import Assert = require("assert"); +import BluebirdPromise = require("bluebird"); +import ExpressMock = require("../../../stubs/express.spec"); +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../ServerVariables"; +import { Level } from "../../../authentication/Level"; + +describe("routes/password-reset/form/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let vars: ServerVariables; + let mocks: ServerVariablesMock; + let authSession: AuthenticationSession; + + beforeEach(function () { + req = { + originalUrl: "/api/password-reset", + body: { + userid: "user" + }, + session: {}, + headers: { + host: "localhost" + } + }; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + + mocks.config.authentication_backend.ldap = { + url: "ldap://ldapjs", + mail_attribute: "mail", + user: "user", + password: "password", + additional_users_dn: "ou=users", + additional_groups_dn: "ou=groups", + base_dn: "dc=example,dc=com", + users_filter: "user", + group_name_attribute: "cn", + groups_filter: "groups" + }; + + res = ExpressMock.ResponseMock(); + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + authSession.userid = "user"; + authSession.email = "user@example.com"; + authSession.authentication_level = Level.ONE_FACTOR; + }); + + describe("test reset password post", () => { + it("should update the password and reset auth_session for reauthentication", function () { + req.body = {}; + req.body.password = "new-password"; + + mocks.usersDatabase.updatePasswordStub.returns(BluebirdPromise.resolve()); + + authSession.identity_check = { + userid: "user", + challenge: "reset-password" + }; + return PasswordResetFormPost.default(vars)(req as any, res as any) + .then(function () { + return AuthenticationSessionHandler.get(req as any, vars.logger); + }).then(function (_authSession) { + Assert.equal(res.status.getCall(0).args[0], 204); + Assert.equal(_authSession.authentication_level, Level.NOT_AUTHENTICATED); + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if identity_challenge does not exist", function () { + authSession.identity_check = { + userid: "user", + challenge: undefined + }; + return PasswordResetFormPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "An error occurred during password reset. Your password has not been changed." + }); + }); + }); + + it("should fail when ldap fails", function () { + req.body = {}; + req.body.password = "new-password"; + + mocks.usersDatabase.updatePasswordStub + .returns(BluebirdPromise.reject("Internal error with LDAP")); + + authSession.identity_check = { + challenge: "reset-password", + userid: "user" + }; + return PasswordResetFormPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "An error occurred during password reset. Your password has not been changed." + }); + return BluebirdPromise.resolve(); + }); + }); + }); +}); diff --git a/themes/black/server/src/lib/routes/password-reset/form/post.ts b/themes/black/server/src/lib/routes/password-reset/form/post.ts new file mode 100644 index 000000000..fccd7471b --- /dev/null +++ b/themes/black/server/src/lib/routes/password-reset/form/post.ts @@ -0,0 +1,50 @@ + +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); +import exceptions = require("../../../Exceptions"); +import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; +import ErrorReplies = require("../../../ErrorReplies"); +import UserMessages = require("../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../ServerVariables"; + +import Constants = require("./../constants"); + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const newPassword = objectPath.get(req, "body.password"); + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (!authSession.identity_check) { + reject(new Error("No identity check initiated")); + return; + } + + vars.logger.info(req, "User %s wants to reset his/her password.", + authSession.identity_check.userid); + vars.logger.debug(req, "Challenge %s", authSession.identity_check.challenge); + + if (authSession.identity_check.challenge != Constants.CHALLENGE) { + reject(new Error("Bad challenge.")); + return; + } + resolve(); + }) + .then(function () { + return vars.usersDatabase.updatePassword(authSession.identity_check.userid, newPassword); + }) + .then(function () { + vars.logger.info(req, "Password reset for user '%s'", + authSession.identity_check.userid); + AuthenticationSessionHandler.reset(req); + res.status(204); + res.send(); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.RESET_PASSWORD_FAILED)); + }; +} diff --git a/themes/black/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts b/themes/black/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts new file mode 100644 index 000000000..ac6a41754 --- /dev/null +++ b/themes/black/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts @@ -0,0 +1,92 @@ + +import PasswordResetHandler + from "./PasswordResetHandler"; +import { UserDataStore } from "../../../storage/UserDataStore"; +import Sinon = require("sinon"); +import winston = require("winston"); +import assert = require("assert"); +import BluebirdPromise = require("bluebird"); +import ExpressMock = require("../../../stubs/express.spec"); +import { ServerVariablesMock, ServerVariablesMockBuilder } + from "../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../ServerVariables"; + +describe("routes/password-reset/identity/PasswordResetHandler", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + req = { + originalUrl: "/non-api/xxx", + query: { + userid: "user" + }, + session: { + auth: { + userid: "user", + email: "user@example.com", + first_factor: true, + second_factor: false + } + }, + headers: { + host: "localhost" + } + }; + + const options = { + inMemoryOnly: true + }; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + mocks.userDataStore.saveU2FRegistrationStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub + .returns(BluebirdPromise.resolve({})); + res = ExpressMock.ResponseMock(); + }); + + describe("test reset password identity pre check", () => { + it("should fail when no userid is provided", function () { + req.query.userid = undefined; + const handler = new PasswordResetHandler(vars.logger, + vars.usersDatabase); + return handler.preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject("It should fail"); + }) + .catch(function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if ldap fail", function () { + mocks.usersDatabase.getEmailsStub + .returns(BluebirdPromise.reject("Internal error")); + new PasswordResetHandler(vars.logger, vars.usersDatabase) + .preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here")); + }, + function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should returns identity when ldap replies", function () { + mocks.usersDatabase.getEmailsStub + .returns(BluebirdPromise.resolve(["test@example.com"])); + return new PasswordResetHandler(vars.logger, vars.usersDatabase) + .preValidationInit(req as any); + }); + }); +}); diff --git a/themes/black/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts b/themes/black/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts new file mode 100644 index 000000000..42ae92cda --- /dev/null +++ b/themes/black/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts @@ -0,0 +1,69 @@ +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); + +import exceptions = require("../../../Exceptions"); +import { Identity } from "../../../../../types/Identity"; +import { IdentityValidable } from "../../../IdentityValidable"; +import { PRE_VALIDATION_TEMPLATE } from "../../../IdentityCheckPreValidationTemplate"; +import Constants = require("../constants"); +import { IRequestLogger } from "../../../logging/IRequestLogger"; +import { IUsersDatabase } from "../../../authentication/backends/IUsersDatabase"; + +export const TEMPLATE_NAME = "password-reset-form"; + +export default class PasswordResetHandler implements IdentityValidable { + private logger: IRequestLogger; + private usersDatabase: IUsersDatabase; + + constructor(logger: IRequestLogger, usersDatabase: IUsersDatabase) { + this.logger = logger; + this.usersDatabase = usersDatabase; + } + + challenge(): string { + return Constants.CHALLENGE; + } + + preValidationInit(req: express.Request): BluebirdPromise { + const that = this; + const userid: string = + objectPath.get(req, "query.userid"); + return BluebirdPromise.resolve() + .then(function () { + that.logger.debug(req, "User '%s' requested a password reset", userid); + if (!userid) + return BluebirdPromise.reject( + new exceptions.AccessDeniedError("No user id provided")); + + return that.usersDatabase.getEmails(userid); + }) + .then(function (emails: string[]) { + if (!emails && emails.length <= 0) throw new Error("No email found"); + const identity = { + email: emails[0], + userid: userid + }; + return BluebirdPromise.resolve(identity); + }) + .catch(function (err: Error) { + return BluebirdPromise.reject(new exceptions.IdentityError(err.message)); + }); + } + + preValidationResponse(req: express.Request, res: express.Response) { + res.render(PRE_VALIDATION_TEMPLATE); + } + + postValidationInit(req: express.Request) { + return BluebirdPromise.resolve(); + } + + postValidationResponse(req: express.Request, res: express.Response) { + res.render(TEMPLATE_NAME); + } + + mailSubject(): string { + return "Reset your password"; + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/password-reset/request/get.ts b/themes/black/server/src/lib/routes/password-reset/request/get.ts new file mode 100644 index 000000000..8f3ae2b4b --- /dev/null +++ b/themes/black/server/src/lib/routes/password-reset/request/get.ts @@ -0,0 +1,13 @@ + +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); +import exceptions = require("../../../Exceptions"); + +import Constants = require("./../constants"); + +const TEMPLATE_NAME = "password-reset-request"; + +export default function (req: express.Request, res: express.Response) { + res.render(TEMPLATE_NAME); +} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/secondfactor/get.spec.ts b/themes/black/server/src/lib/routes/secondfactor/get.spec.ts new file mode 100644 index 000000000..6c77e1f69 --- /dev/null +++ b/themes/black/server/src/lib/routes/secondfactor/get.spec.ts @@ -0,0 +1,44 @@ +import SecondFactorGet from "./get"; +import { ServerVariablesMockBuilder, ServerVariablesMock } + from "../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../ServerVariables"; +import Sinon = require("sinon"); +import ExpressMock = require("../../stubs/express.spec"); +import Assert = require("assert"); +import Endpoints = require("../../../../../shared/api"); +import BluebirdPromise = require("bluebird"); + +describe("routes/secondfactor/get", function () { + let mocks: ServerVariablesMock; + let vars: ServerVariables; + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false + } + }; + }); + + describe("test rendering", function () { + it("should render second factor page", function () { + req.session.auth.second_factor = false; + return SecondFactorGet(vars)(req as any, res as any) + .then(function () { + Assert(res.render.calledWith("secondfactor")); + return BluebirdPromise.resolve(); + }); + }); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/secondfactor/get.ts b/themes/black/server/src/lib/routes/secondfactor/get.ts new file mode 100644 index 000000000..9f6deb4c6 --- /dev/null +++ b/themes/black/server/src/lib/routes/secondfactor/get.ts @@ -0,0 +1,28 @@ + +import Express = require("express"); +import Endpoints = require("../../../../../shared/api"); +import BluebirdPromise = require("bluebird"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { ServerVariables } from "../../ServerVariables"; + +const TEMPLATE_NAME = "secondfactor"; + +export default function (vars: ServerVariables) { + function handler(req: Express.Request, res: Express.Response) + : BluebirdPromise { + + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + + res.render(TEMPLATE_NAME, { + username: authSession.userid, + totp_identity_start_endpoint: + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, + u2f_identity_start_endpoint: + Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET + }); + resolve(); + }); + } + return handler; +} diff --git a/themes/black/server/src/lib/routes/secondfactor/redirect.spec.ts b/themes/black/server/src/lib/routes/secondfactor/redirect.spec.ts new file mode 100644 index 000000000..ea66e6dca --- /dev/null +++ b/themes/black/server/src/lib/routes/secondfactor/redirect.spec.ts @@ -0,0 +1,41 @@ +import Redirect from "./redirect"; +import ExpressMock = require("../../stubs/express.spec"); +import { ServerVariablesMockBuilder, ServerVariablesMock } +from "../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../ServerVariables"; +import Assert = require("assert"); + +describe("routes/secondfactor/redirect", function() { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + }); + + it("should redirect to default_redirection_url", function() { + vars.config.default_redirection_url = "http://default_redirection_url"; + Redirect(vars)(req as any, res as any) + .then(function() { + Assert(res.json.calledWith({ + redirect: "http://default_redirection_url" + })); + }); + }); + + it("should redirect to /", function() { + Redirect(vars)(req as any, res as any) + .then(function() { + Assert(res.json.calledWith({ + redirect: "/" + })); + }); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/secondfactor/redirect.ts b/themes/black/server/src/lib/routes/secondfactor/redirect.ts new file mode 100644 index 000000000..5d84d9ebf --- /dev/null +++ b/themes/black/server/src/lib/routes/secondfactor/redirect.ts @@ -0,0 +1,30 @@ + +import express = require("express"); +import objectPath = require("object-path"); +import Endpoints = require("../../../../../shared/api"); +import { ServerVariables } from "../../ServerVariables"; +import BluebirdPromise = require("bluebird"); +import ErrorReplies = require("../../ErrorReplies"); +import UserMessages = require("../../../../../shared/UserMessages"); +import { RedirectionMessage } from "../../../../../shared/RedirectionMessage"; +import Constants = require("../../../../../shared/constants"); + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response) + : BluebirdPromise { + + return new BluebirdPromise(function (resolve, reject) { + let redirectUrl: string = "/"; + if (vars.config.default_redirection_url) { + redirectUrl = vars.config.default_redirection_url; + } + vars.logger.debug(req, "Request redirection to \"%s\".", redirectUrl); + res.json({ + redirect: redirectUrl + } as RedirectionMessage); + return resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + }; +} diff --git a/themes/black/server/src/lib/routes/secondfactor/totp/constants.ts b/themes/black/server/src/lib/routes/secondfactor/totp/constants.ts new file mode 100644 index 000000000..7b5a1dcfe --- /dev/null +++ b/themes/black/server/src/lib/routes/secondfactor/totp/constants.ts @@ -0,0 +1,4 @@ + +export const CHALLENGE = "totp-register"; +export const TEMPLATE_NAME = "totp-register"; + diff --git a/themes/black/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts b/themes/black/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts new file mode 100644 index 000000000..78b8ea3ea --- /dev/null +++ b/themes/black/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts @@ -0,0 +1,116 @@ +import Sinon = require("sinon"); +import RegistrationHandler from "./RegistrationHandler"; +import { Identity } from "../../../../../../types/Identity"; +import { UserDataStore } from "../../../../storage/UserDataStore"; +import BluebirdPromise = require("bluebird"); +import ExpressMock = require("../../../../stubs/express.spec"); +import { ServerVariablesMock, ServerVariablesMockBuilder } + from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; +import Assert = require("assert"); + +describe("routes/secondfactor/totp/identity/RegistrationHandler", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + req.session = { + auth: { + userid: "user", + email: "user@example.com", + first_factor: true, + second_factor: false, + identity_check: { + userid: "user", + challenge: "totp-register" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.saveTOTPSecretStub + .returns(BluebirdPromise.resolve({})); + + res = ExpressMock.ResponseMock(); + }); + + describe("test totp registration pre validation", function () { + it("should fail if first_factor has not been passed", function () { + req.session.auth.first_factor = false; + return new RegistrationHandler(vars.logger, vars.userDataStore, + vars.totpHandler, vars.config.totp) + .preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("It should fail")); + }) + .catch(function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if userid is missing", function (done) { + req.session.auth.first_factor = false; + req.session.auth.userid = undefined; + + new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler, + vars.config.totp) + .preValidationInit(req as any) + .catch(function (err: Error) { + done(); + }); + }); + + it("should fail if email is missing", function (done) { + req.session.auth.first_factor = false; + req.session.auth.email = undefined; + + new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler, + vars.config.totp) + .preValidationInit(req as any) + .catch(function (err: Error) { + done(); + }); + }); + + it("should succeed if first factor passed, userid and email are provided", + function () { + return new RegistrationHandler(vars.logger, vars.userDataStore, + vars.totpHandler, vars.config.totp) + .preValidationInit(req as any); + }); + }); + + describe("test totp registration post validation", function () { + it("should generate a secret using userId as label and issuer defined in config", function () { + vars.config.totp = { + issuer: "issuer" + }; + return new RegistrationHandler(vars.logger, vars.userDataStore, + vars.totpHandler, vars.config.totp) + .postValidationResponse(req as any, res as any) + .then(function() { + Assert(mocks.totpHandler.generateStub.calledWithExactly("user", "issuer")); + }); + }); + }); +}); diff --git a/themes/black/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts b/themes/black/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts new file mode 100644 index 000000000..b39b6d045 --- /dev/null +++ b/themes/black/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts @@ -0,0 +1,112 @@ + +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); + +import { Identity } from "../../../../../../types/Identity"; +import { IdentityValidable } from "../../../../IdentityValidable"; +import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; +import Constants = require("../constants"); +import Endpoints = require("../../../../../../../shared/api"); +import ErrorReplies = require("../../../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import FirstFactorValidator = require("../../../../FirstFactorValidator"); +import { IRequestLogger } from "../../../../logging/IRequestLogger"; +import { IUserDataStore } from "../../../../storage/IUserDataStore"; +import { ITotpHandler } from "../../../../authentication/totp/ITotpHandler"; +import { TOTPSecret } from "../../../../../../types/TOTPSecret"; +import { TotpConfiguration } from "../../../../configuration/schema/TotpConfiguration"; + + +export default class RegistrationHandler implements IdentityValidable { + private logger: IRequestLogger; + private userDataStore: IUserDataStore; + private totp: ITotpHandler; + private configuration: TotpConfiguration; + + constructor(logger: IRequestLogger, + userDataStore: IUserDataStore, + totp: ITotpHandler, configuration: TotpConfiguration) { + this.logger = logger; + this.userDataStore = userDataStore; + this.totp = totp; + this.configuration = configuration; + } + + challenge(): string { + return Constants.CHALLENGE; + } + + private retrieveIdentity(req: express.Request): BluebirdPromise { + const that = this; + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, that.logger); + const userid = authSession.userid; + const email = authSession.email; + + if (!(userid && email)) { + return reject(new Error("User ID or email is missing")); + } + + const identity = { + email: email, + userid: userid + }; + return resolve(identity); + }); + } + + preValidationInit(req: express.Request): BluebirdPromise { + const that = this; + return FirstFactorValidator.validate(req, this.logger) + .then(function () { + return that.retrieveIdentity(req); + }); + } + + preValidationResponse(req: express.Request, res: express.Response) { + res.render(PRE_VALIDATION_TEMPLATE); + } + + postValidationInit(req: express.Request) { + return FirstFactorValidator.validate(req, this.logger); + } + + postValidationResponse(req: express.Request, res: express.Response) + : BluebirdPromise { + const that = this; + let secret: TOTPSecret; + let userId: string; + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, that.logger); + userId = authSession.userid; + + if (authSession.identity_check.challenge != Constants.CHALLENGE + || !userId) + return reject(new Error("Bad challenge.")); + + resolve(); + }) + .then(function () { + secret = that.totp.generate(userId, + that.configuration.issuer); + that.logger.debug(req, "Save the TOTP secret in DB"); + return that.userDataStore.saveTOTPSecret(userId, secret); + }) + .then(function () { + AuthenticationSessionHandler.reset(req); + + res.render(Constants.TEMPLATE_NAME, { + base32_secret: secret.base32, + otpauth_url: secret.otpauth_url, + login_endpoint: Endpoints.FIRST_FACTOR_GET + }); + }) + .catch(ErrorReplies.replyWithError200(req, res, that.logger, UserMessages.OPERATION_FAILED)); + } + + mailSubject(): string { + return "Set up Authelia's one-time password"; + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts b/themes/black/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts new file mode 100644 index 000000000..70a20d39d --- /dev/null +++ b/themes/black/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts @@ -0,0 +1,76 @@ + +import BluebirdPromise = require("bluebird"); +import Sinon = require("sinon"); +import Assert = require("assert"); +import Exceptions = require("../../../../Exceptions"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import SignPost = require("./post"); +import { ServerVariables } from "../../../../ServerVariables"; + +import ExpressMock = require("../../../../stubs/express.spec"); +import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; +import { Level } from "../../../../authentication/Level"; + +describe("routes/secondfactor/totp/sign/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let authSession: AuthenticationSession; + let vars: ServerVariables; + let mocks: ServerVariablesMock; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + vars = s.variables; + mocks = s.mocks; + const app_get = Sinon.stub(); + req = { + originalUrl: "/api/totp-register", + app: {}, + body: { + token: "abc" + }, + session: {}, + query: { + redirect: "http://redirect" + } + }; + res = ExpressMock.ResponseMock(); + + const doc = { + userid: "user", + secret: { + base32: "ABCDEF" + } + }; + mocks.userDataStore.retrieveTOTPSecretStub.returns(BluebirdPromise.resolve(doc)); + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + authSession.userid = "user"; + authSession.authentication_level = Level.ONE_FACTOR; + }); + + + it("should send status code 200 when totp is valid", function () { + mocks.totpHandler.validateStub.returns(true); + return SignPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(authSession.authentication_level, Level.TWO_FACTOR); + return BluebirdPromise.resolve(); + }); + }); + + it("should send error message when totp is not valid", function () { + mocks.totpHandler.validateStub.returns(false); + return SignPost.default(vars)(req as any, res as any) + .then(function () { + Assert.notEqual(authSession.authentication_level, Level.TWO_FACTOR); + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); +}); + diff --git a/themes/black/server/src/lib/routes/secondfactor/totp/sign/post.ts b/themes/black/server/src/lib/routes/secondfactor/totp/sign/post.ts new file mode 100644 index 000000000..34a276d12 --- /dev/null +++ b/themes/black/server/src/lib/routes/secondfactor/totp/sign/post.ts @@ -0,0 +1,42 @@ +import Bluebird = require("bluebird"); +import Express = require("express"); + +import { TOTPSecretDocument } from "../../../../storage/TOTPSecretDocument"; +import Endpoints = require("../../../../../../../shared/api"); +import Redirect from "../../redirect"; +import ErrorReplies = require("../../../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../../ServerVariables"; +import { Level } from "../../../../authentication/Level"; + +const UNAUTHORIZED_MESSAGE = "Unauthorized access"; + +export default function (vars: ServerVariables) { + function handler(req: Express.Request, res: Express.Response): Bluebird { + let authSession: AuthenticationSession; + const token = req.body.token; + + return new Bluebird(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + vars.logger.info(req, "Initiate TOTP validation for user \"%s\".", authSession.userid); + resolve(); + }) + .then(function () { + return vars.userDataStore.retrieveTOTPSecret(authSession.userid); + }) + .then(function (doc: TOTPSecretDocument) { + if (!vars.totpHandler.validate(token, doc.secret.base32)) + return Bluebird.reject(new Error("Invalid TOTP token.")); + + vars.logger.debug(req, "TOTP validation succeeded."); + authSession.authentication_level = Level.TWO_FACTOR; + Redirect(vars)(req, res); + return Bluebird.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + return handler; +} diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts new file mode 100644 index 000000000..7f16c0ee8 --- /dev/null +++ b/themes/black/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts @@ -0,0 +1,11 @@ + +import util = require("util"); +import express = require("express"); + +function extract_app_id(req: express.Request): string { + return util.format("https://%s", req.headers.host); +} + +export = { + extract_app_id: extract_app_id +}; \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts new file mode 100644 index 000000000..a54bfbfe6 --- /dev/null +++ b/themes/black/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts @@ -0,0 +1,96 @@ +import Sinon = require("sinon"); +import Assert = require("assert"); +import BluebirdPromise = require("bluebird"); + +import { Identity } from "../../../../../../types/Identity"; +import RegistrationHandler from "./RegistrationHandler"; +import ExpressMock = require("../../../../stubs/express.spec"); +import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; + +describe("routes/secondfactor/u2f/identity/RegistrationHandler", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + req.app = {}; + req.session = { + auth: { + userid: "user", + email: "user@example.com", + first_factor: true, + second_factor: false + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + + res = ExpressMock.ResponseMock(); + res.send = Sinon.spy(); + res.json = Sinon.spy(); + res.status = Sinon.spy(); + }); + + describe("test u2f registration check", test_registration_check); + + function test_registration_check() { + it("should fail if first_factor has not been passed", function () { + req.session.auth.first_factor = false; + return new RegistrationHandler(vars.logger).preValidationInit(req as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if userid is missing", function () { + req.session.auth.first_factor = false; + req.session.auth.userid = undefined; + + return new RegistrationHandler(vars.logger).preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here")); + }, + function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if email is missing", function () { + req.session.auth.first_factor = false; + req.session.auth.email = undefined; + + return new RegistrationHandler(vars.logger).preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here")); + }, + function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should succeed if first factor passed, userid and email are provided", function () { + req.session.auth.first_factor = true; + req.session.auth.email = "admin@example.com"; + req.session.auth.userid = "user"; + return new RegistrationHandler(vars.logger).preValidationInit(req as any); + }); + } +}); diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts new file mode 100644 index 000000000..bc4713c77 --- /dev/null +++ b/themes/black/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts @@ -0,0 +1,73 @@ + +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import objectPath = require("object-path"); + +import { IdentityValidable } from "../../../../IdentityValidable"; +import { Identity } from "../../../../../../types/Identity"; +import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; +import FirstFactorValidator = require("../../../../FirstFactorValidator"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { IRequestLogger } from "../../../../logging/IRequestLogger"; + +const CHALLENGE = "u2f-register"; +const MAIL_SUBJECT = "Register your security key with Authelia"; + +const POST_VALIDATION_TEMPLATE_NAME = "u2f-register"; + + +export default class RegistrationHandler implements IdentityValidable { + private logger: IRequestLogger; + + constructor(logger: IRequestLogger) { + this.logger = logger; + } + + challenge(): string { + return CHALLENGE; + } + + private retrieveIdentity(req: express.Request): BluebirdPromise { + const that = this; + return new BluebirdPromise(function(resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, that.logger); + const userid = authSession.userid; + const email = authSession.email; + + if (!(userid && email)) { + return reject(new Error("User ID or email is missing")); + } + + const identity = { + email: email, + userid: userid + }; + return resolve(identity); + }); + } + + preValidationInit(req: express.Request): BluebirdPromise { + const that = this; + return FirstFactorValidator.validate(req, this.logger) + .then(function () { + return that.retrieveIdentity(req); + }); + } + + preValidationResponse(req: express.Request, res: express.Response) { + res.render(PRE_VALIDATION_TEMPLATE); + } + + postValidationInit(req: express.Request) { + return FirstFactorValidator.validate(req, this.logger); + } + + postValidationResponse(req: express.Request, res: express.Response) { + res.render(POST_VALIDATION_TEMPLATE_NAME); + } + + mailSubject(): string { + return MAIL_SUBJECT; + } +} + diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts new file mode 100644 index 000000000..de3347a21 --- /dev/null +++ b/themes/black/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts @@ -0,0 +1,146 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import assert = require("assert"); +import U2FRegisterPost = require("./post"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import ExpressMock = require("../../../../stubs/express.spec"); +import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; + + +describe("routes/secondfactor/u2f/register/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + let authSession: AuthenticationSession; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.originalUrl = "/api/xxxx"; + req.app = {}; + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false, + identity_check: { + challenge: "u2f-register", + userid: "user" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + }); + + describe("test registration", test_registration); + + + function test_registration() { + it("should save u2f meta and return status code 200", function () { + const expectedStatus = { + keyHandle: "keyHandle", + publicKey: "pbk", + certificate: "cert" + }; + mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve(expectedStatus)); + + authSession.register_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { + assert.equal("user", mocks.userDataStore.saveU2FRegistrationStub.getCall(0).args[0]); + assert.equal(authSession.identity_check, undefined); + }); + }); + + it("should return error message on finishRegistration error", function () { + mocks.u2f.checkRegistrationStub.returns({ errorCode: 500 }); + + authSession.register_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function () { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); + + it("should return error message when register_request is not provided", function () { + mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); + authSession.register_request = undefined; + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function () { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); + + it("should return error message when no auth request has been initiated", function () { + mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); + authSession.register_request = undefined; + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function () { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); + + it("should return error message when identity has not been verified", function () { + authSession.identity_check = undefined; + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function () { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); + } +}); + diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/register/post.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/register/post.ts new file mode 100644 index 000000000..7296ccbe5 --- /dev/null +++ b/themes/black/server/src/lib/routes/secondfactor/u2f/register/post.ts @@ -0,0 +1,64 @@ + +import { UserDataStore } from "../../../../storage/UserDataStore"; +import objectPath = require("object-path"); +import u2f_common = require("../U2FCommon"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import U2f = require("u2f"); +import { U2FRegistration } from "../../../../../../types/U2FRegistration"; +import redirect from "../../redirect"; +import ErrorReplies = require("../../../../ErrorReplies"); +import { ServerVariables } from "../../../../ServerVariables"; +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; + + +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const appid = u2f_common.extract_app_id(req); + const registrationResponse: U2f.RegistrationData = req.body; + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + const registrationRequest = authSession.register_request; + + if (!registrationRequest) { + return reject(new Error("No registration request")); + } + + if (!authSession.identity_check + || authSession.identity_check.challenge != "u2f-register") { + return reject(new Error("Bad challenge for registration request")); + } + + vars.logger.info(req, "Finishing registration"); + vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); + vars.logger.debug(req, "RegistrationResponse = %s", JSON.stringify(registrationResponse)); + + return resolve(vars.u2f.checkRegistration(registrationRequest, registrationResponse)); + }) + .then(function (u2fResult: U2f.RegistrationResult | U2f.Error): BluebirdPromise { + if (objectPath.has(u2fResult, "errorCode")) + return BluebirdPromise.reject(new Error("Error while registering.")); + + const registrationResult: U2f.RegistrationResult = u2fResult as U2f.RegistrationResult; + vars.logger.info(req, "Store registration and reply"); + vars.logger.debug(req, "RegistrationResult = %s", JSON.stringify(registrationResult)); + const registration: U2FRegistration = { + keyHandle: registrationResult.keyHandle, + publicKey: registrationResult.publicKey + }; + return vars.userDataStore.saveU2FRegistration(authSession.userid, appid, registration); + }) + .then(function () { + authSession.identity_check = undefined; + redirect(vars)(req, res); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + return handler; +} diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts new file mode 100644 index 000000000..a207c9109 --- /dev/null +++ b/themes/black/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts @@ -0,0 +1,86 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import Assert = require("assert"); +import U2FRegisterRequestGet = require("./get"); +import ExpressMock = require("../../../../stubs/express.spec"); +import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; + +describe("routes/secondfactor/u2f/register_request/get", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.originalUrl = "/api/xxxx"; + req.app = {}; + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false, + identity_check: { + challenge: "u2f-register", + userid: "user" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + }); + + describe("test registration request", () => { + it("should send back the registration request and save it in the session", function () { + const expectedRequest = { + test: "abc" + }; + mocks.u2f.requestStub.returns(BluebirdPromise.resolve(expectedRequest)); + return U2FRegisterRequestGet.default(vars)(req as any, res as any) + .then(function () { + Assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); + }); + }); + + it("should return internal error on registration request", function () { + res.send = sinon.spy(); + const user_key_container = {}; + mocks.u2f.requestStub.returns(BluebirdPromise.reject("Internal error")); + return U2FRegisterRequestGet.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + }); + }); + + it("should return forbidden if identity has not been verified", function () { + req.session.auth.identity_check = undefined; + return U2FRegisterRequestGet.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(403, res.status.getCall(0).args[0]); + }); + }); + }); +}); + diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/register_request/get.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/register_request/get.ts new file mode 100644 index 000000000..f611af933 --- /dev/null +++ b/themes/black/server/src/lib/routes/secondfactor/u2f/register_request/get.ts @@ -0,0 +1,43 @@ + +import { UserDataStore } from "../../../../storage/UserDataStore"; + +import objectPath = require("object-path"); +import u2f_common = require("../U2FCommon"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import U2f = require("u2f"); +import ErrorReplies = require("../../../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../../ServerVariables"; + +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const appid: string = u2f_common.extract_app_id(req); + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (!authSession.identity_check + || authSession.identity_check.challenge != "u2f-register") { + res.status(403); + res.send(); + return reject(new Error("Bad challenge.")); + } + + vars.logger.info(req, "Starting registration for appId '%s'", appid); + return resolve(vars.u2f.request(appid)); + }) + .then(function (registrationRequest: U2f.Request) { + vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); + authSession.register_request = registrationRequest; + res.json(registrationRequest); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + + return handler; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts new file mode 100644 index 000000000..9b137e66d --- /dev/null +++ b/themes/black/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts @@ -0,0 +1,101 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import Assert = require("assert"); +import U2FSignPost = require("./post"); +import { ServerVariables } from "../../../../ServerVariables"; +import winston = require("winston"); + +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; +import ExpressMock = require("../../../../stubs/express.spec"); +import U2FMock = require("../../../../stubs/u2f.spec"); +import U2f = require("u2f"); +import { Level } from "../../../../authentication/Level"; + +describe("routes/secondfactor/u2f/sign/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.app = {}; + req.originalUrl = "/api/xxxx"; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req.session = { + auth: { + userid: "user", + authentication_level: Level.ONE_FACTOR, + identity_check: { + challenge: "u2f-register", + userid: "user" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const options = { + inMemoryOnly: true + }; + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + }); + + it("should return status code 204", function () { + const expectedStatus = { + keyHandle: "keyHandle", + publicKey: "pbk", + certificate: "cert" + }; + mocks.u2f.checkSignatureStub.returns(expectedStatus); + + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ + registration: { + publicKey: "PUBKEY" + } + })); + + req.session.auth.sign_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + return U2FSignPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(req.session.auth.authentication_level, Level.TWO_FACTOR); + }); + }); + + it("should return unauthorized error on registration request internal error", function () { + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ + registration: { + publicKey: "PUBKEY" + } + })); + mocks.u2f.checkSignatureStub.returns({ errorCode: 500 }); + + req.session.auth.sign_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + return U2FSignPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], + { error: "Operation failed." }); + }); + }); +}); + diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/sign/post.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/sign/post.ts new file mode 100644 index 000000000..7ee711c2c --- /dev/null +++ b/themes/black/server/src/lib/routes/secondfactor/u2f/sign/post.ts @@ -0,0 +1,57 @@ + +import objectPath = require("object-path"); +import u2f_common = require("../U2FCommon"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import { UserDataStore } from "../../../../storage/UserDataStore"; +import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; +import { Winston } from "../../../../../../types/Dependencies"; +import U2f = require("u2f"); +import exceptions = require("../../../../Exceptions"); +import redirect from "../../redirect"; +import ErrorReplies = require("../../../../ErrorReplies"); +import { ServerVariables } from "../../../../ServerVariables"; +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import { Level } from "../../../../authentication/Level"; + +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const appId = u2f_common.extract_app_id(req); + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (!authSession.sign_request) { + const err = new Error("No sign request"); + ErrorReplies.replyWithError401(req, res, vars.logger)(err); + return reject(err); + } + resolve(); + }) + .then(function () { + const userid = authSession.userid; + return vars.userDataStore.retrieveU2FRegistration(userid, appId); + }) + .then(function (doc: U2FRegistrationDocument): BluebirdPromise { + const signRequest = authSession.sign_request; + const signData: U2f.SignatureData = req.body; + vars.logger.info(req, "Finish authentication"); + return BluebirdPromise.resolve(vars.u2f.checkSignature(signRequest, signData, doc.registration.publicKey)); + }) + .then(function (result: U2f.SignatureResult | U2f.Error): BluebirdPromise { + if (objectPath.has(result, "errorCode")) + return BluebirdPromise.reject(new Error("Error while signing")); + vars.logger.info(req, "Successful authentication"); + authSession.authentication_level = Level.TWO_FACTOR; + redirect(vars)(req, res); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + + return handler; +} + diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts new file mode 100644 index 000000000..dd52b27e0 --- /dev/null +++ b/themes/black/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts @@ -0,0 +1,68 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import assert = require("assert"); +import U2FSignRequestGet = require("./get"); +import ExpressMock = require("../../../../stubs/express.spec"); +import { Request } from "u2f"; +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; + +import { SignMessage } from "../../../../../../../shared/SignMessage"; + +describe("routes/secondfactor/u2f/sign_request/get", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.originalUrl = "/api/xxxx"; + req.app = {}; + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false, + identity_check: { + challenge: "u2f-register", + userid: "user" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + }); + + it("should send back the sign request and save it in the session", function () { + const expectedRequest: Request = { + version: "U2F_V2", + appId: 'app', + challenge: 'challenge!' + }; + mocks.u2f.requestStub.returns(expectedRequest); + mocks.userDataStore.retrieveU2FRegistrationStub + .returns(BluebirdPromise.resolve({ + registration: { + keyHandle: "KeyHandle" + } + })); + + return U2FSignRequestGet.default(vars)(req as any, res as any) + .then(() => { + assert.deepEqual(expectedRequest, req.session.auth.sign_request); + assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); + }); + }); +}); + diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts new file mode 100644 index 000000000..9e93dde06 --- /dev/null +++ b/themes/black/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts @@ -0,0 +1,42 @@ + +import u2f_common = require("../../../secondfactor/u2f/U2FCommon"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; +import exceptions = require("../../../../Exceptions"); +import ErrorReplies = require("../../../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../../ServerVariables"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; + +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const appId = u2f_common.extract_app_id(req); + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + resolve(); + }) + .then(function () { + return vars.userDataStore.retrieveU2FRegistration(authSession.userid, appId); + }) + .then(function (doc: U2FRegistrationDocument): BluebirdPromise { + if (!doc) + return BluebirdPromise.reject(new exceptions.AccessDeniedError("No U2F registration document found.")); + + const appId: string = u2f_common.extract_app_id(req); + vars.logger.info(req, "Start authentication of app '%s'", appId); + vars.logger.debug(req, "AppId = %s, keyHandle = %s", appId, JSON.stringify(doc.registration.keyHandle)); + + const request = vars.u2f.request(appId, doc.registration.keyHandle); + authSession.sign_request = request; + res.json(request); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + return handler; +} diff --git a/themes/black/server/src/lib/routes/verify/access_control.ts b/themes/black/server/src/lib/routes/verify/access_control.ts new file mode 100644 index 000000000..136239aeb --- /dev/null +++ b/themes/black/server/src/lib/routes/verify/access_control.ts @@ -0,0 +1,51 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import Util = require("util"); + +import Exceptions = require("../../Exceptions"); + +import { Level as AuthorizationLevel } from "../../authorization/Level"; +import { Level as AuthenticationLevel } from "../../authentication/Level"; +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { ServerVariables } from "../../ServerVariables"; + +function isAuthorized( + authorization: AuthorizationLevel, + authentication: AuthenticationLevel): boolean { + + if (authorization == AuthorizationLevel.BYPASS) { + return true; + } else if (authorization == AuthorizationLevel.ONE_FACTOR && + authentication >= AuthenticationLevel.ONE_FACTOR) { + return true; + } else if (authorization == AuthorizationLevel.TWO_FACTOR && + authentication >= AuthenticationLevel.TWO_FACTOR) { + return true; + } + return false; +} + +export default function ( + req: Express.Request, + vars: ServerVariables, + domain: string, resource: string, + user: string, groups: string[], + authenticationLevel: AuthenticationLevel) { + + return new BluebirdPromise(function (resolve, reject) { + const authorizationLevel = vars.authorizer + .authorization({domain, resource}, {user, groups}); + + if (!isAuthorized(authorizationLevel, authenticationLevel)) { + if (authorizationLevel == AuthorizationLevel.DENY) { + reject(new Exceptions.NotAuthorizedError( + Util.format("User %s is not authorized to access %s%s", user, domain, resource))); + return; + } + reject(new Exceptions.NotAuthenticatedError(Util.format( + "User '%s' is not sufficiently authorized to access %s%s.", user, domain, resource))); + return; + } + resolve(); + }); +} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/verify/get.spec.ts b/themes/black/server/src/lib/routes/verify/get.spec.ts new file mode 100644 index 000000000..67cf19fb6 --- /dev/null +++ b/themes/black/server/src/lib/routes/verify/get.spec.ts @@ -0,0 +1,320 @@ + +import Assert = require("assert"); +import BluebirdPromise = require("bluebird"); +import Express = require("express"); +import Sinon = require("sinon"); +import winston = require("winston"); + +import VerifyGet = require("./get"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../types/AuthenticationSession"; +import ExpressMock = require("../../stubs/express.spec"); +import { ServerVariables } from "../../ServerVariables"; +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../ServerVariablesMockBuilder.spec"; +import { Level } from "../../authentication/Level"; +import { Level as AuthorizationLevel } from "../../authorization/Level"; + +describe("routes/verify/get", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + let authSession: AuthenticationSession; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + req.originalUrl = "/api/xxxx"; + req.query = { + redirect: "undefined" + }; + AuthenticationSessionHandler.reset(req as any); + req.headers["x-original-url"] = "https://secret.example.com/"; + const s = ServerVariablesMockBuilder.build(false); + mocks = s.mocks; + vars = s.variables; + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + }); + + describe("with session cookie", function () { + it("should be already authenticated", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + authSession.authentication_level = Level.TWO_FACTOR; + authSession.userid = "myuser"; + authSession.groups = ["mygroup", "othergroup"]; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser"); + Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); + Assert.equal(204, res.status.getCall(0).args[0]); + }); + }); + + function test_session(_authSession: AuthenticationSession, status_code: number) { + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert.equal(status_code, res.status.getCall(0).args[0]); + }); + } + + function test_non_authenticated_401(authSession: AuthenticationSession) { + return test_session(authSession, 401); + } + + function test_unauthorized_403(authSession: AuthenticationSession) { + return test_session(authSession, 403); + } + + function test_authorized(authSession: AuthenticationSession) { + return test_session(authSession, 204); + } + + describe("given user tries to access a 2-factor endpoint", function () { + before(function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + }); + + describe("given different cases of session", function () { + it("should not be authenticated when second factor is missing", function () { + return test_non_authenticated_401({ + keep_me_logged_in: false, + userid: "user", + authentication_level: Level.ONE_FACTOR, + email: undefined, + groups: [], + last_activity_datetime: new Date().getTime() + }); + }); + + it("should not be authenticated when userid is missing", function () { + return test_non_authenticated_401({ + keep_me_logged_in: false, + userid: undefined, + authentication_level: Level.TWO_FACTOR, + email: undefined, + groups: [], + last_activity_datetime: new Date().getTime() + }); + }); + + it("should not be authenticated when level is insufficient", function () { + return test_non_authenticated_401({ + keep_me_logged_in: false, + userid: "user", + authentication_level: Level.NOT_AUTHENTICATED, + email: undefined, + groups: [], + last_activity_datetime: new Date().getTime() + }); + }); + + it("should not be authenticated when session has not be initiated", function () { + return test_non_authenticated_401(undefined); + }); + + it("should not be authenticated when domain is not allowed for user", function () { + authSession.authentication_level = Level.TWO_FACTOR; + authSession.userid = "myuser"; + req.headers["x-original-url"] = "https://test.example.com/"; + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.DENY); + + return test_unauthorized_403({ + keep_me_logged_in: false, + authentication_level: Level.TWO_FACTOR, + userid: "user", + groups: ["group1", "group2"], + email: undefined, + last_activity_datetime: new Date().getTime() + }); + }); + }); + }); + + describe("given user tries to access a single factor endpoint", function () { + beforeEach(function () { + req.headers["x-original-url"] = "https://redirect.url/"; + }); + + it("should be authenticated when first factor is validated", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); + authSession.authentication_level = Level.ONE_FACTOR; + authSession.userid = "user1"; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWith(204)); + Assert(res.send.calledOnce); + }); + }); + + it("should be rejected with 401 when not authenticated", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); + authSession.authentication_level = Level.NOT_AUTHENTICATED; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWith(401)); + }); + }); + }); + + describe("inactivity period", function () { + it("should update last inactivity period on requests on /api/verify", function () { + mocks.config.session.inactivity = 200000; + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + const currentTime = new Date().getTime() - 1000; + AuthenticationSessionHandler.reset(req as any); + authSession.authentication_level = Level.TWO_FACTOR; + authSession.userid = "myuser"; + authSession.groups = ["mygroup", "othergroup"]; + authSession.last_activity_datetime = currentTime; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + return AuthenticationSessionHandler.get(req as any, vars.logger); + }) + .then(function (authSession) { + Assert(authSession.last_activity_datetime > currentTime); + }); + }); + + it("should reset session when max inactivity period has been reached", function () { + mocks.config.session.inactivity = 1; + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + const currentTime = new Date().getTime() - 1000; + AuthenticationSessionHandler.reset(req as any); + authSession.authentication_level = Level.TWO_FACTOR; + authSession.userid = "myuser"; + authSession.groups = ["mygroup", "othergroup"]; + authSession.last_activity_datetime = currentTime; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + return AuthenticationSessionHandler.get(req as any, vars.logger); + }) + .then(function (authSession) { + Assert.equal(authSession.authentication_level, Level.NOT_AUTHENTICATED); + Assert.equal(authSession.userid, undefined); + }); + }); + }); + }); + + describe("response type 401 | 302", function() { + it("should return error code 401", function() { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( + "Invalid credentials")); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should redirect to provided redirection url", function() { + const REDIRECT_URL = "http://redirection_url.com"; + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( + "Invalid credentials")); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + req.query["rd"] = REDIRECT_URL; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.redirect.calledWithExactly(REDIRECT_URL)); + }); + }); + }); + + describe("with basic auth", function () { + it("should authenticate correctly", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.returns({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "john"); + Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); + Assert.equal(204, res.status.getCall(0).args[0]); + }); + }); + + it("should fail when endpoint is protected by two factors", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.config.access_control.rules = [{ + domain: "secret.example.com", + policy: "two_factor" + }]; + mocks.usersDatabase.checkUserPasswordStub.resolves({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should fail when base64 token is not valid", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.resolves({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic i_m*not_a_base64*token"; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should fail when base64 token has not format user:psswd", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.resolves({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzOmJhZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should fail when bad user password is provided", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( + "Invalid credentials")); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should fail when resource is restricted", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.resolves({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + }); +}); + diff --git a/themes/black/server/src/lib/routes/verify/get.ts b/themes/black/server/src/lib/routes/verify/get.ts new file mode 100644 index 000000000..f73861696 --- /dev/null +++ b/themes/black/server/src/lib/routes/verify/get.ts @@ -0,0 +1,91 @@ +import BluebirdPromise = require("bluebird"); +import Express = require("express"); +import Exceptions = require("../../Exceptions"); +import ErrorReplies = require("../../ErrorReplies"); +import { ServerVariables } from "../../ServerVariables"; +import GetWithSessionCookieMethod from "./get_session_cookie"; +import GetWithBasicAuthMethod from "./get_basic_auth"; +import Constants = require("../../../../../shared/constants"); +import ObjectPath = require("object-path"); + +import { AuthenticationSessionHandler } + from "../../AuthenticationSessionHandler"; +import { AuthenticationSession } + from "../../../../types/AuthenticationSession"; + +const REMOTE_USER = "Remote-User"; +const REMOTE_GROUPS = "Remote-Groups"; + + +function verifyWithSelectedMethod(req: Express.Request, res: Express.Response, + vars: ServerVariables, authSession: AuthenticationSession) + : () => BluebirdPromise<{ username: string, groups: string[] }> { + return function () { + const authorization: string = "" + req.headers["proxy-authorization"]; + if (authorization && authorization.startsWith("Basic ")) + return GetWithBasicAuthMethod(req, res, vars, authorization); + + return GetWithSessionCookieMethod(req, res, vars, authSession); + }; +} + +function setRedirectHeader(req: Express.Request, res: Express.Response) { + return function () { + const originalUrl = ObjectPath.get( + req, "headers.x-original-url"); + res.set("Redirect", originalUrl); + return BluebirdPromise.resolve(); + }; +} + +function setUserAndGroupsHeaders(res: Express.Response) { + return function (u: { username: string, groups: string[] }) { + res.setHeader(REMOTE_USER, u.username); + res.setHeader(REMOTE_GROUPS, u.groups.join(",")); + return BluebirdPromise.resolve(); + }; +} + +function replyWith200(res: Express.Response) { + return function () { + res.status(204); + res.send(); + }; +} + +function getRedirectParam(req: Express.Request) { + return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; +} + +export default function (vars: ServerVariables) { + return function (req: Express.Request, res: Express.Response) + : BluebirdPromise { + let authSession: AuthenticationSession; + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + resolve(); + }) + .then(setRedirectHeader(req, res)) + .then(verifyWithSelectedMethod(req, res, vars, authSession)) + .then(setUserAndGroupsHeaders(res)) + .then(replyWith200(res)) + // The user is authenticated but has restricted access -> 403 + .catch(Exceptions.NotAuthorizedError, + ErrorReplies.replyWithError403(req, res, vars.logger)) + .catch(Exceptions.NotAuthenticatedError, + ErrorReplies.replyWithError401(req, res, vars.logger)) + // The user is not yet authenticated -> 401 + .catch((err) => { + const redirectUrl = getRedirectParam(req); + if (redirectUrl) { + ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err); + } + else { + ErrorReplies.replyWithError401(req, res, vars.logger)(err); + } + }); + }; +} + diff --git a/themes/black/server/src/lib/routes/verify/get_basic_auth.ts b/themes/black/server/src/lib/routes/verify/get_basic_auth.ts new file mode 100644 index 000000000..af23c76c9 --- /dev/null +++ b/themes/black/server/src/lib/routes/verify/get_basic_auth.ts @@ -0,0 +1,55 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import ObjectPath = require("object-path"); +import { ServerVariables } from "../../ServerVariables"; +import { AuthenticationSession } + from "../../../../types/AuthenticationSession"; +import AccessControl from "./access_control"; +import { URLDecomposer } from "../../utils/URLDecomposer"; +import { Level } from "../../authentication/Level"; + +export default function (req: Express.Request, res: Express.Response, + vars: ServerVariables, authorizationHeader: string) + : BluebirdPromise<{ username: string, groups: string[] }> { + let username: string; + const uri = ObjectPath.get(req, "headers.x-original-url"); + const urlDecomposition = URLDecomposer.fromUrl(uri); + + return BluebirdPromise.resolve() + .then(() => { + const base64Re = new RegExp("^Basic ((?:[A-Za-z0-9+/]{4})*" + + "(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)$"); + const isTokenValidBase64 = base64Re.test(authorizationHeader); + + if (!isTokenValidBase64) { + return BluebirdPromise.reject(new Error("No valid base64 token found in the header")); + } + + const tokenMatches = authorizationHeader.match(base64Re); + const base64Token = tokenMatches[1]; + const decodedToken = Buffer.from(base64Token, "base64").toString(); + const splittedToken = decodedToken.split(":"); + + if (splittedToken.length != 2) { + return BluebirdPromise.reject(new Error( + "The authorization token is invalid. Expecting 'userid:password'")); + } + + username = splittedToken[0]; + const password = splittedToken[1]; + return vars.usersDatabase.checkUserPassword(username, password); + }) + .then(function (groupsAndEmails) { + return AccessControl(req, vars, urlDecomposition.domain, urlDecomposition.path, + username, groupsAndEmails.groups, Level.ONE_FACTOR) + .then(() => BluebirdPromise.resolve({ + username: username, + groups: groupsAndEmails.groups + })); + }) + .catch(function (err: Error) { + return BluebirdPromise.reject( + new Error("Unable to authenticate the user with basic auth. Cause: " + + err.message)); + }); +} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/verify/get_session_cookie.ts b/themes/black/server/src/lib/routes/verify/get_session_cookie.ts new file mode 100644 index 000000000..070344812 --- /dev/null +++ b/themes/black/server/src/lib/routes/verify/get_session_cookie.ts @@ -0,0 +1,78 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import Util = require("util"); +import ObjectPath = require("object-path"); + +import Exceptions = require("../../Exceptions"); +import { Configuration } from "../../configuration/schema/Configuration"; +import { ServerVariables } from "../../ServerVariables"; +import { IRequestLogger } from "../../logging/IRequestLogger"; +import { AuthenticationSession } + from "../../../../types/AuthenticationSession"; +import { AuthenticationSessionHandler } + from "../../AuthenticationSessionHandler"; +import AccessControl from "./access_control"; +import { URLDecomposer } from "../../utils/URLDecomposer"; + +function verify_inactivity(req: Express.Request, + authSession: AuthenticationSession, + configuration: Configuration, logger: IRequestLogger) + : BluebirdPromise { + + // If inactivity is not specified, then inactivity timeout does not apply + if (!configuration.session.inactivity || authSession.keep_me_logged_in) { + return BluebirdPromise.resolve(); + } + + const lastActivityTime = authSession.last_activity_datetime; + const currentTime = new Date().getTime(); + authSession.last_activity_datetime = currentTime; + + const inactivityPeriodMs = currentTime - lastActivityTime; + logger.debug(req, "Inactivity period was %s s and max period was %s.", + inactivityPeriodMs / 1000, configuration.session.inactivity / 1000); + if (inactivityPeriodMs < configuration.session.inactivity) { + return BluebirdPromise.resolve(); + } + + logger.debug(req, "Session has been reset after too long inactivity period."); + AuthenticationSessionHandler.reset(req); + return BluebirdPromise.reject(new Error("Inactivity period exceeded.")); +} + +export default function (req: Express.Request, res: Express.Response, + vars: ServerVariables, authSession: AuthenticationSession) + : BluebirdPromise<{ username: string, groups: string[] }> { + + return BluebirdPromise.resolve() + .then(() => { + const username = authSession.userid; + const groups = authSession.groups; + + if (!authSession.userid) { + return BluebirdPromise.reject(new Exceptions.AccessDeniedError( + "userid is missing")); + } + + const originalUrl = ObjectPath.get( + req, "headers.x-original-url"); + const originalUri = + ObjectPath.get(req, "headers.x-original-uri"); + + const d = URLDecomposer.fromUrl(originalUrl); + vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", d.domain, + d.path, username, groups.join(",")); + return AccessControl(req, vars, d.domain, d.path, username, groups, + authSession.authentication_level); + }) + .then(() => { + return verify_inactivity(req, authSession, + vars.config, vars.logger); + }) + .then(() => { + return BluebirdPromise.resolve({ + username: authSession.userid, + groups: authSession.groups + }); + }); +} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/AuthenticationTraceDocument.d.ts b/themes/black/server/src/lib/storage/AuthenticationTraceDocument.d.ts new file mode 100644 index 000000000..69818c055 --- /dev/null +++ b/themes/black/server/src/lib/storage/AuthenticationTraceDocument.d.ts @@ -0,0 +1,6 @@ + +export interface AuthenticationTraceDocument { + userId: string; + date: Date; + isAuthenticationSuccessful: boolean; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/CollectionFactoryFactory.ts b/themes/black/server/src/lib/storage/CollectionFactoryFactory.ts new file mode 100644 index 000000000..92b29abfa --- /dev/null +++ b/themes/black/server/src/lib/storage/CollectionFactoryFactory.ts @@ -0,0 +1,15 @@ +import { ICollectionFactory } from "./ICollectionFactory"; +import { NedbCollectionFactory } from "./nedb/NedbCollectionFactory"; +import { MongoCollectionFactory } from "./mongo/MongoCollectionFactory"; +import { IMongoClient } from "../connectors/mongo/IMongoClient"; + + +export class CollectionFactoryFactory { + static createNedb(options: Nedb.DataStoreOptions): ICollectionFactory { + return new NedbCollectionFactory(options); + } + + static createMongo(client: IMongoClient): ICollectionFactory { + return new MongoCollectionFactory(client); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/CollectionFactoryStub.spec.ts b/themes/black/server/src/lib/storage/CollectionFactoryStub.spec.ts new file mode 100644 index 000000000..17f8bb021 --- /dev/null +++ b/themes/black/server/src/lib/storage/CollectionFactoryStub.spec.ts @@ -0,0 +1,16 @@ +import BluebirdPromise = require("bluebird"); +import Sinon = require("sinon"); +import { ICollection } from "./ICollection"; +import { ICollectionFactory } from "./ICollectionFactory"; + +export class CollectionFactoryStub implements ICollectionFactory { + buildStub: Sinon.SinonStub; + + constructor() { + this.buildStub = Sinon.stub(); + } + + build(collectionName: string): ICollection { + return this.buildStub(collectionName); + } +} diff --git a/themes/black/server/src/lib/storage/CollectionStub.spec.ts b/themes/black/server/src/lib/storage/CollectionStub.spec.ts new file mode 100644 index 000000000..42895d672 --- /dev/null +++ b/themes/black/server/src/lib/storage/CollectionStub.spec.ts @@ -0,0 +1,39 @@ +import BluebirdPromise = require("bluebird"); +import Sinon = require("sinon"); +import { ICollection } from "./ICollection"; + +export class CollectionStub implements ICollection { + findStub: Sinon.SinonStub; + findOneStub: Sinon.SinonStub; + updateStub: Sinon.SinonStub; + removeStub: Sinon.SinonStub; + insertStub: Sinon.SinonStub; + + constructor() { + this.findStub = Sinon.stub(); + this.findOneStub = Sinon.stub(); + this.updateStub = Sinon.stub(); + this.removeStub = Sinon.stub(); + this.insertStub = Sinon.stub(); + } + + find(filter: any, sortKeys: any, count: number): BluebirdPromise { + return this.findStub(filter, sortKeys, count); + } + + findOne(filter: any): BluebirdPromise { + return this.findOneStub(filter); + } + + update(filter: any, document: any, options: any): BluebirdPromise { + return this.updateStub(filter, document, options); + } + + remove(filter: any): BluebirdPromise { + return this.removeStub(filter); + } + + insert(document: any): BluebirdPromise { + return this.insertStub(document); + } +} diff --git a/themes/black/server/src/lib/storage/ICollection.d.ts b/themes/black/server/src/lib/storage/ICollection.d.ts new file mode 100644 index 000000000..caa6c2a87 --- /dev/null +++ b/themes/black/server/src/lib/storage/ICollection.d.ts @@ -0,0 +1,11 @@ +/* istanbul ignore next */ +import BluebirdPromise = require("bluebird"); + +/* istanbul ignore next */ +export interface ICollection { + find(query: any, sortKeys: any, count: number): BluebirdPromise; + findOne(query: any): BluebirdPromise; + update(query: any, updateQuery: any, options?: any): BluebirdPromise; + remove(query: any): BluebirdPromise; + insert(document: any): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/ICollectionFactory.d.ts b/themes/black/server/src/lib/storage/ICollectionFactory.d.ts new file mode 100644 index 000000000..39eb42c77 --- /dev/null +++ b/themes/black/server/src/lib/storage/ICollectionFactory.d.ts @@ -0,0 +1,6 @@ + +import { ICollection } from "./ICollection"; + +export interface ICollectionFactory { + build(collectionName: string): ICollection; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/IUserDataStore.d.ts b/themes/black/server/src/lib/storage/IUserDataStore.d.ts new file mode 100644 index 000000000..81df482aa --- /dev/null +++ b/themes/black/server/src/lib/storage/IUserDataStore.d.ts @@ -0,0 +1,21 @@ +import BluebirdPromise = require("bluebird"); +import { TOTPSecretDocument } from "./TOTPSecretDocument"; +import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; +import { U2FRegistration } from "../../../types/U2FRegistration"; +import { TOTPSecret } from "../../../types/TOTPSecret"; +import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; +import { IdentityValidationDocument } from "./IdentityValidationDocument"; + +export interface IUserDataStore { + saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise; + retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise; + + saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; + retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise; + + produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise; + consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise; + + saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise; + retrieveTOTPSecret(userId: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/IdentityValidationDocument.d.ts b/themes/black/server/src/lib/storage/IdentityValidationDocument.d.ts new file mode 100644 index 000000000..e7fd7b3f4 --- /dev/null +++ b/themes/black/server/src/lib/storage/IdentityValidationDocument.d.ts @@ -0,0 +1,7 @@ + +export interface IdentityValidationDocument { + userId: string; + token: string; + challenge: string; + maxDate: Date; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/TOTPSecretDocument.d.ts b/themes/black/server/src/lib/storage/TOTPSecretDocument.d.ts new file mode 100644 index 000000000..a6c0bf9e6 --- /dev/null +++ b/themes/black/server/src/lib/storage/TOTPSecretDocument.d.ts @@ -0,0 +1,6 @@ +import { TOTPSecret } from "../../../types/TOTPSecret"; + +export interface TOTPSecretDocument { + userid: string; + secret: TOTPSecret; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/U2FRegistrationDocument.d.ts b/themes/black/server/src/lib/storage/U2FRegistrationDocument.d.ts new file mode 100644 index 000000000..efec6cb1d --- /dev/null +++ b/themes/black/server/src/lib/storage/U2FRegistrationDocument.d.ts @@ -0,0 +1,8 @@ + +import { U2FRegistration } from "../../../types/U2FRegistration"; + +export interface U2FRegistrationDocument { + userId: string; + appId: string; + registration: U2FRegistration; +} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/UserDataStore.spec.ts b/themes/black/server/src/lib/storage/UserDataStore.spec.ts new file mode 100644 index 000000000..66fb85461 --- /dev/null +++ b/themes/black/server/src/lib/storage/UserDataStore.spec.ts @@ -0,0 +1,264 @@ + +import * as Assert from "assert"; +import * as Sinon from "sinon"; +import * as MockDate from "mockdate"; +import BluebirdPromise = require("bluebird"); + +import { UserDataStore } from "./UserDataStore"; +import { TOTPSecret } from "../../../types/TOTPSecret"; +import { U2FRegistration } from "../../../types/U2FRegistration"; +import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; +import { CollectionStub } from "./CollectionStub.spec"; +import { CollectionFactoryStub } from "./CollectionFactoryStub.spec"; + +describe("storage/UserDataStore", function () { + let factory: CollectionFactoryStub; + let collection: CollectionStub; + let userId: string; + let appId: string; + let totpSecret: TOTPSecret; + let u2fRegistration: U2FRegistration; + + beforeEach(function () { + factory = new CollectionFactoryStub(); + collection = new CollectionStub(); + + userId = "user"; + appId = "https://myappId"; + + totpSecret = { + ascii: "abc", + base32: "ABCDKZLEFZGREJK", + otpauth_url: "totp://test", + google_auth_qr: "dummy", + hex: "dummy", + qr_code_ascii: "dummy", + qr_code_base32: "dummy", + qr_code_hex: "dummy" + }; + + u2fRegistration = { + keyHandle: "KEY_HANDLE", + publicKey: "publickey" + }; + }); + + it("should correctly creates collections", function () { + new UserDataStore(factory); + + Assert.equal(4, factory.buildStub.callCount); + Assert(factory.buildStub.calledWith("authentication_traces")); + Assert(factory.buildStub.calledWith("identity_validation_tokens")); + Assert(factory.buildStub.calledWith("u2f_registrations")); + Assert(factory.buildStub.calledWith("totp_secrets")); + }); + + describe("TOTP secrets collection", function () { + it("should save a totp secret", function () { + factory.buildStub.returns(collection); + collection.updateStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.saveTOTPSecret(userId, totpSecret) + .then(function (doc) { + Assert(collection.updateStub.calledOnce); + Assert(collection.updateStub.calledWith({ userId: userId }, { + userId: userId, + secret: totpSecret + }, { upsert: true })); + return BluebirdPromise.resolve(); + }); + }); + + it("should retrieve a totp secret", function () { + factory.buildStub.returns(collection); + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.retrieveTOTPSecret(userId) + .then(function (doc) { + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ userId: userId })); + return BluebirdPromise.resolve(); + }); + }); + }); + + describe("U2F secrets collection", function () { + it("should save a U2F secret", function () { + factory.buildStub.returns(collection); + collection.updateStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.saveU2FRegistration(userId, appId, u2fRegistration) + .then(function (doc) { + Assert(collection.updateStub.calledOnce); + Assert(collection.updateStub.calledWith({ + userId: userId, + appId: appId + }, { + userId: userId, + appId: appId, + registration: u2fRegistration + }, { upsert: true })); + return BluebirdPromise.resolve(); + }); + }); + + it("should retrieve a U2F secret", function () { + factory.buildStub.returns(collection); + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.retrieveU2FRegistration(userId, appId) + .then(function (doc) { + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ + userId: userId, + appId: appId + })); + return BluebirdPromise.resolve(); + }); + }); + }); + + + describe("Regulator traces collection", function () { + it("should save a trace", function () { + factory.buildStub.returns(collection); + collection.insertStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.saveAuthenticationTrace(userId, true) + .then(function (doc) { + Assert(collection.insertStub.calledOnce); + Assert(collection.insertStub.calledWith({ + userId: userId, + date: Sinon.match.date, + isAuthenticationSuccessful: true + })); + return BluebirdPromise.resolve(); + }); + }); + + function should_retrieve_latest_authentication_traces(count: number) { + factory.buildStub.returns(collection); + collection.findStub.withArgs().returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.retrieveLatestAuthenticationTraces(userId, count) + .then(function (doc: AuthenticationTraceDocument[]) { + Assert(collection.findStub.calledOnce); + Assert(collection.findStub.calledWith({ + userId: userId, + }, { date: -1 }, count)); + return BluebirdPromise.resolve(); + }); + } + + it("should retrieve 3 latest failed authentication traces", function () { + should_retrieve_latest_authentication_traces(3); + }); + }); + + + describe("Identity validation collection", function () { + it("should save a identity validation token", function () { + factory.buildStub.returns(collection); + collection.insertStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + const maxAge = 400; + const token = "TOKEN"; + const challenge = "CHALLENGE"; + + return dataStore.produceIdentityValidationToken(userId, token, challenge, maxAge) + .then(function (doc) { + Assert(collection.insertStub.calledOnce); + Assert(collection.insertStub.calledWith({ + userId: userId, + token: token, + challenge: challenge, + maxDate: Sinon.match.date + })); + return BluebirdPromise.resolve(); + }); + }); + + it("should consume an identity token successfully", function () { + factory.buildStub.returns(collection); + + MockDate.set(100); + + const token = "TOKEN"; + const challenge = "CHALLENGE"; + + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ + userId: "USER", + token: token, + challenge: challenge, + maxDate: new Date() + })); + collection.removeStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + MockDate.set(80); + + return dataStore.consumeIdentityValidationToken(token, challenge) + .then(function (doc) { + MockDate.reset(); + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ + token: token, + challenge: challenge + })); + + Assert(collection.removeStub.calledOnce); + Assert(collection.removeStub.calledWith({ + token: token, + challenge: challenge + })); + return BluebirdPromise.resolve(); + }); + }); + + it("should consume an expired identity token", function () { + factory.buildStub.returns(collection); + + MockDate.set(0); + + const token = "TOKEN"; + const challenge = "CHALLENGE"; + + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ + userId: "USER", + token: token, + challenge: challenge, + maxDate: new Date() + })); + + const dataStore = new UserDataStore(factory); + + MockDate.set(80000); + + return dataStore.consumeIdentityValidationToken(token, challenge) + .then(function () { return BluebirdPromise.reject(new Error("should not be here")); }) + .catch(function () { + MockDate.reset(); + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ + token: token, + challenge: challenge + })); + return BluebirdPromise.resolve(); + }); + }); + }); +}); diff --git a/themes/black/server/src/lib/storage/UserDataStore.ts b/themes/black/server/src/lib/storage/UserDataStore.ts new file mode 100644 index 000000000..27b0cddbd --- /dev/null +++ b/themes/black/server/src/lib/storage/UserDataStore.ts @@ -0,0 +1,143 @@ +import * as BluebirdPromise from "bluebird"; +import * as path from "path"; +import { IUserDataStore } from "./IUserDataStore"; +import { ICollection } from "./ICollection"; +import { ICollectionFactory } from "./ICollectionFactory"; +import { TOTPSecretDocument } from "./TOTPSecretDocument"; +import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; +import { U2FRegistration } from "../../../types/U2FRegistration"; +import { TOTPSecret } from "../../../types/TOTPSecret"; +import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; +import { IdentityValidationDocument } from "./IdentityValidationDocument"; + +// Constants + +const IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME = "identity_validation_tokens"; +const AUTHENTICATION_TRACES_COLLECTION_NAME = "authentication_traces"; + +const U2F_REGISTRATIONS_COLLECTION_NAME = "u2f_registrations"; +const TOTP_SECRETS_COLLECTION_NAME = "totp_secrets"; + + +export interface U2FRegistrationKey { + userId: string; + appId: string; +} + +// Source + +export class UserDataStore implements IUserDataStore { + private u2fSecretCollection: ICollection; + private identityCheckTokensCollection: ICollection; + private authenticationTracesCollection: ICollection; + private totpSecretCollection: ICollection; + + private collectionFactory: ICollectionFactory; + + constructor(collectionFactory: ICollectionFactory) { + this.collectionFactory = collectionFactory; + + this.u2fSecretCollection = this.collectionFactory.build(U2F_REGISTRATIONS_COLLECTION_NAME); + this.identityCheckTokensCollection = this.collectionFactory.build(IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME); + this.authenticationTracesCollection = this.collectionFactory.build(AUTHENTICATION_TRACES_COLLECTION_NAME); + this.totpSecretCollection = this.collectionFactory.build(TOTP_SECRETS_COLLECTION_NAME); + } + + saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { + const newDocument: U2FRegistrationDocument = { + userId: userId, + appId: appId, + registration: registration + }; + + const filter: U2FRegistrationKey = { + userId: userId, + appId: appId + }; + + return this.u2fSecretCollection.update(filter, newDocument, { upsert: true }); + } + + retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { + const filter: U2FRegistrationKey = { + userId: userId, + appId: appId + }; + return this.u2fSecretCollection.findOne(filter); + } + + saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { + const newDocument: AuthenticationTraceDocument = { + userId: userId, + date: new Date(), + isAuthenticationSuccessful: isAuthenticationSuccessful, + }; + + return this.authenticationTracesCollection.insert(newDocument); + } + + retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { + const q = { + userId: userId + }; + + return this.authenticationTracesCollection.find(q, { date: -1 }, count); + } + + produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { + const newDocument: IdentityValidationDocument = { + userId: userId, + token: token, + challenge: challenge, + maxDate: new Date(new Date().getTime() + maxAge) + }; + + return this.identityCheckTokensCollection.insert(newDocument); + } + + consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { + const that = this; + const filter = { + token: token, + challenge: challenge + }; + + let identityValidationDocument: IdentityValidationDocument; + + return this.identityCheckTokensCollection.findOne(filter) + .then(function (doc: IdentityValidationDocument) { + if (!doc) { + return BluebirdPromise.reject(new Error("Registration token does not exist")); + } + + identityValidationDocument = doc; + const current_date = new Date(); + if (current_date > doc.maxDate) + return BluebirdPromise.reject(new Error("Registration token is not valid anymore")); + + return that.identityCheckTokensCollection.remove(filter); + }) + .then(() => { + return BluebirdPromise.resolve(identityValidationDocument); + }); + } + + saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { + const doc = { + userId: userId, + secret: secret + }; + + const filter = { + userId: userId + }; + return this.totpSecretCollection.update(filter, doc, { upsert: true }); + } + + retrieveTOTPSecret(userId: string): BluebirdPromise { + const filter = { + userId: userId + }; + return this.totpSecretCollection.findOne(filter); + } +} diff --git a/themes/black/server/src/lib/storage/UserDataStoreStub.spec.ts b/themes/black/server/src/lib/storage/UserDataStoreStub.spec.ts new file mode 100644 index 000000000..5ea27a2de --- /dev/null +++ b/themes/black/server/src/lib/storage/UserDataStoreStub.spec.ts @@ -0,0 +1,64 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); + +import { TOTPSecretDocument } from "./TOTPSecretDocument"; +import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; +import { U2FRegistration } from "../../../types/U2FRegistration"; +import { TOTPSecret } from "../../../types/TOTPSecret"; +import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; +import { IdentityValidationDocument } from "./IdentityValidationDocument"; +import { IUserDataStore } from "./IUserDataStore"; + +export class UserDataStoreStub implements IUserDataStore { + saveU2FRegistrationStub: Sinon.SinonStub; + retrieveU2FRegistrationStub: Sinon.SinonStub; + saveAuthenticationTraceStub: Sinon.SinonStub; + retrieveLatestAuthenticationTracesStub: Sinon.SinonStub; + produceIdentityValidationTokenStub: Sinon.SinonStub; + consumeIdentityValidationTokenStub: Sinon.SinonStub; + saveTOTPSecretStub: Sinon.SinonStub; + retrieveTOTPSecretStub: Sinon.SinonStub; + + constructor() { + this.saveU2FRegistrationStub = Sinon.stub(); + this.retrieveU2FRegistrationStub = Sinon.stub(); + this.saveAuthenticationTraceStub = Sinon.stub(); + this.retrieveLatestAuthenticationTracesStub = Sinon.stub(); + this.produceIdentityValidationTokenStub = Sinon.stub(); + this.consumeIdentityValidationTokenStub = Sinon.stub(); + this.saveTOTPSecretStub = Sinon.stub(); + this.retrieveTOTPSecretStub = Sinon.stub(); + } + + saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { + return this.saveU2FRegistrationStub(userId, appId, registration); + } + + retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { + return this.retrieveU2FRegistrationStub(userId, appId); + } + + saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { + return this.saveAuthenticationTraceStub(userId, isAuthenticationSuccessful); + } + + retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { + return this.retrieveLatestAuthenticationTracesStub(userId, count); + } + + produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { + return this.produceIdentityValidationTokenStub(userId, token, challenge, maxAge); + } + + consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { + return this.consumeIdentityValidationTokenStub(token, challenge); + } + + saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { + return this.saveTOTPSecretStub(userId, secret); + } + + retrieveTOTPSecret(userId: string): BluebirdPromise { + return this.retrieveTOTPSecretStub(userId); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/mongo/MongoCollection.spec.ts b/themes/black/server/src/lib/storage/mongo/MongoCollection.spec.ts new file mode 100644 index 000000000..74a773a1e --- /dev/null +++ b/themes/black/server/src/lib/storage/mongo/MongoCollection.spec.ts @@ -0,0 +1,110 @@ +import Assert = require("assert"); +import Sinon = require("sinon"); +import MongoDB = require("mongodb"); +import BluebirdPromise = require("bluebird"); +import { MongoClientStub } from "../../connectors/mongo/MongoClientStub.spec"; +import { MongoCollection } from "./MongoCollection"; + +describe("storage/mongo/MongoCollection", function () { + let mongoCollectionStub: any; + let mongoClientStub: MongoClientStub; + let findStub: Sinon.SinonStub; + let findOneStub: Sinon.SinonStub; + let insertOneStub: Sinon.SinonStub; + let updateStub: Sinon.SinonStub; + let removeStub: Sinon.SinonStub; + let countStub: Sinon.SinonStub; + const COLLECTION_NAME = "collection"; + + before(function () { + mongoClientStub = new MongoClientStub(); + mongoCollectionStub = Sinon.createStubInstance(require("mongodb").Collection as any); + findStub = mongoCollectionStub.find as Sinon.SinonStub; + findOneStub = mongoCollectionStub.findOne as Sinon.SinonStub; + insertOneStub = mongoCollectionStub.insertOne as Sinon.SinonStub; + updateStub = mongoCollectionStub.update as Sinon.SinonStub; + removeStub = mongoCollectionStub.remove as Sinon.SinonStub; + countStub = mongoCollectionStub.count as Sinon.SinonStub; + mongoClientStub.collectionStub.returns( + BluebirdPromise.resolve(mongoCollectionStub) + ); + }); + + describe("find", function () { + it("should find a document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + findStub.returns({ + sort: Sinon.stub().returns({ + limit: Sinon.stub().returns({ + toArray: Sinon.stub().returns(BluebirdPromise.resolve([])) + }) + }) + }); + + return collection.find({ key: "KEY" }) + .then(function () { + Assert(findStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("findOne", function () { + it("should find one document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + findOneStub.returns(BluebirdPromise.resolve({})); + + return collection.findOne({ key: "KEY" }) + .then(function () { + Assert(findOneStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("insert", function () { + it("should insert a document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + insertOneStub.returns(BluebirdPromise.resolve({})); + + return collection.insert({ key: "KEY" }) + .then(function () { + Assert(insertOneStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("update", function () { + it("should update a document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + updateStub.returns(BluebirdPromise.resolve({})); + + return collection.update({ key: "KEY" }, { key: "KEY", value: 1 }) + .then(function () { + Assert(updateStub.calledWith({ key: "KEY" }, { key: "KEY", value: 1 })); + }); + }); + }); + + describe("remove", function () { + it("should remove a document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + removeStub.returns(BluebirdPromise.resolve({})); + + return collection.remove({ key: "KEY" }) + .then(function () { + Assert(removeStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("count", function () { + it("should count documents in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + countStub.returns(BluebirdPromise.resolve({})); + + return collection.count({ key: "KEY" }) + .then(function () { + Assert(countStub.calledWith({ key: "KEY" })); + }); + }); + }); +}); diff --git a/themes/black/server/src/lib/storage/mongo/MongoCollection.ts b/themes/black/server/src/lib/storage/mongo/MongoCollection.ts new file mode 100644 index 000000000..9771389f1 --- /dev/null +++ b/themes/black/server/src/lib/storage/mongo/MongoCollection.ts @@ -0,0 +1,50 @@ +import Bluebird = require("bluebird"); +import { ICollection } from "../ICollection"; +import MongoDB = require("mongodb"); +import { IMongoClient } from "../../connectors/mongo/IMongoClient"; + + +export class MongoCollection implements ICollection { + private mongoClient: IMongoClient; + private collectionName: string; + + constructor(collectionName: string, mongoClient: IMongoClient) { + this.collectionName = collectionName; + this.mongoClient = mongoClient; + } + + private collection(): Bluebird { + return this.mongoClient.collection(this.collectionName); + } + + find(query: any, sortKeys?: any, count?: number): Bluebird { + return this.collection() + .then((collection) => collection.find(query).sort(sortKeys).limit(count)) + .then((query) => query.toArray()); + } + + findOne(query: any): Bluebird { + return this.collection() + .then((collection) => collection.findOne(query)); + } + + update(query: any, updateQuery: any, options?: any): Bluebird { + return this.collection() + .then((collection) => collection.update(query, updateQuery, options)); + } + + remove(query: any): Bluebird { + return this.collection() + .then((collection) => collection.remove(query)); + } + + insert(document: any): Bluebird { + return this.collection() + .then((collection) => collection.insertOne(document)); + } + + count(query: any): Bluebird { + return this.collection() + .then((collection) => collection.count(query)); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts b/themes/black/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts new file mode 100644 index 000000000..bd959cacb --- /dev/null +++ b/themes/black/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts @@ -0,0 +1,21 @@ +import Assert = require("assert"); +import Sinon = require("sinon"); +import { MongoClientStub } from "../../connectors/mongo/MongoClientStub.spec"; +import { MongoCollectionFactory } from "./MongoCollectionFactory"; + +describe("storage/mongo/MongoCollectionFactory", function () { + let mongoClient: MongoClientStub; + + before(function() { + mongoClient = new MongoClientStub(); + }); + + describe("create", function () { + it("should create a collection", function () { + const COLLECTION_NAME = "COLLECTION_NAME"; + + const factory = new MongoCollectionFactory(mongoClient); + Assert(factory.build(COLLECTION_NAME)); + }); + }); +}); diff --git a/themes/black/server/src/lib/storage/mongo/MongoCollectionFactory.ts b/themes/black/server/src/lib/storage/mongo/MongoCollectionFactory.ts new file mode 100644 index 000000000..14a8262c9 --- /dev/null +++ b/themes/black/server/src/lib/storage/mongo/MongoCollectionFactory.ts @@ -0,0 +1,19 @@ +import BluebirdPromise = require("bluebird"); +import { ICollection } from "../ICollection"; +import { ICollectionFactory } from "../ICollectionFactory"; +import { MongoCollection } from "./MongoCollection"; +import path = require("path"); +import MongoDB = require("mongodb"); +import { IMongoClient } from "../../connectors/mongo/IMongoClient"; + +export class MongoCollectionFactory implements ICollectionFactory { + private mongoClient: IMongoClient; + + constructor(mongoClient: IMongoClient) { + this.mongoClient = mongoClient; + } + + build(collectionName: string): ICollection { + return new MongoCollection(collectionName, this.mongoClient); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/nedb/NedbCollection.spec.ts b/themes/black/server/src/lib/storage/nedb/NedbCollection.spec.ts new file mode 100644 index 000000000..a69962b67 --- /dev/null +++ b/themes/black/server/src/lib/storage/nedb/NedbCollection.spec.ts @@ -0,0 +1,136 @@ +import Sinon = require("sinon"); +import Assert = require("assert"); + +import { NedbCollection } from "./NedbCollection"; + +describe("storage/nedb/NedbCollection", function () { + describe("insert", function () { + it("should insert one entry", function () { + const nedbOptions = { + inMemoryOnly: true + }; + const collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou" }); + + return collection.count({}).then(function (count: number) { + Assert.equal(1, count); + }); + }); + + it("should insert three entries", function () { + const nedbOptions = { + inMemoryOnly: true + }; + const collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou" }); + collection.insert({ key: "hello" }); + collection.insert({ key: "hey" }); + + return collection.count({}).then(function (count: number) { + Assert.equal(3, count); + }); + }); + }); + + describe("find", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "hello" }); + collection.insert({ key: "hey" }); + collection.insert({ key: "coucou", value: 2 }); + }); + + it("should find one hello", function () { + return collection.find({ key: "hello" }, { key: 1 }) + .then(function (docs: { key: string }[]) { + Assert.equal(1, docs.length); + Assert(docs[0].key == "hello"); + }); + }); + + it("should find two coucou", function () { + return collection.find({ key: "coucou" }, { value: 1 }) + .then(function (docs: { value: number }[]) { + Assert.equal(2, docs.length); + }); + }); + }); + + describe("findOne", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "coucou", value: 1 }); + }); + + it("should find two coucou", function () { + const doc = { key: "coucou", value: 1 }; + return collection.count(doc) + .then(function (count: number) { + Assert.equal(4, count); + return collection.findOne(doc); + }); + }); + }); + + describe("update", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou", value: 1 }); + }); + + it("should update the value", function () { + return collection.update({ key: "coucou" }, { key: "coucou", value: 2 }, { multi: true }) + .then(function () { + return collection.find({ key: "coucou" }); + }) + .then(function (docs: { key: string, value: number }[]) { + Assert.equal(1, docs.length); + Assert.equal(2, docs[0].value); + }); + }); + }); + + describe("update", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou" }); + collection.insert({ key: "hello" }); + }); + + it("should update the value", function () { + return collection.remove({ key: "coucou" }) + .then(function () { + return collection.count({}); + }) + .then(function (count: number) { + Assert.equal(1, count); + }); + }); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/nedb/NedbCollection.ts b/themes/black/server/src/lib/storage/nedb/NedbCollection.ts new file mode 100644 index 000000000..88a93ad05 --- /dev/null +++ b/themes/black/server/src/lib/storage/nedb/NedbCollection.ts @@ -0,0 +1,47 @@ +import BluebirdPromise = require("bluebird"); +import { ICollection } from "../ICollection"; +import Nedb = require("nedb"); + +declare module "nedb" { + export class NedbAsync extends Nedb { + constructor(pathOrOptions?: string | Nedb.DataStoreOptions); + updateAsync(query: any, updateQuery: any, options?: Nedb.UpdateOptions): BluebirdPromise; + findOneAsync(query: any): BluebirdPromise; + insertAsync(newDoc: T): BluebirdPromise; + removeAsync(query: any): BluebirdPromise; + countAsync(query: any): BluebirdPromise; + } +} + +export class NedbCollection implements ICollection { + private collection: Nedb.NedbAsync; + + constructor(options: Nedb.DataStoreOptions) { + this.collection = BluebirdPromise.promisifyAll(new Nedb(options)) as Nedb.NedbAsync; + } + + find(query: any, sortKeys?: any, count?: number): BluebirdPromise { + const q = this.collection.find(query).sort(sortKeys).limit(count); + return BluebirdPromise.promisify(q.exec, { context: q })(); + } + + findOne(query: any): BluebirdPromise { + return this.collection.findOneAsync(query); + } + + update(query: any, updateQuery: any, options?: any): BluebirdPromise { + return this.collection.updateAsync(query, updateQuery, options); + } + + remove(query: any): BluebirdPromise { + return this.collection.removeAsync(query); + } + + insert(document: any): BluebirdPromise { + return this.collection.insertAsync(document); + } + + count(query: any): BluebirdPromise { + return this.collection.countAsync(query); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts b/themes/black/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts new file mode 100644 index 000000000..da90c661f --- /dev/null +++ b/themes/black/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts @@ -0,0 +1,16 @@ +import Sinon = require("sinon"); +import Assert = require("assert"); + +import { NedbCollectionFactory } from "./NedbCollectionFactory"; + +describe("storage/nedb/NedbCollectionFactory", function() { + it("should create a nedb collection", function() { + const nedbOptions = { + inMemoryOnly: true + }; + const factory = new NedbCollectionFactory(nedbOptions); + + const collection = factory.build("mycollection"); + Assert(collection); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/nedb/NedbCollectionFactory.ts b/themes/black/server/src/lib/storage/nedb/NedbCollectionFactory.ts new file mode 100644 index 000000000..49c4dc853 --- /dev/null +++ b/themes/black/server/src/lib/storage/nedb/NedbCollectionFactory.ts @@ -0,0 +1,28 @@ +import { ICollection } from "../ICollection"; +import { ICollectionFactory } from "../ICollectionFactory"; +import { NedbCollection } from "./NedbCollection"; +import path = require("path"); +import Nedb = require("nedb"); + +export interface NedbOptions { + inMemoryOnly?: boolean; + directory?: string; +} + +export class NedbCollectionFactory implements ICollectionFactory { + private options: Nedb.DataStoreOptions; + + constructor(options: Nedb.DataStoreOptions) { + this.options = options; + } + + build(collectionName: string): ICollection { + const datastoreOptions: Nedb.DataStoreOptions = { + inMemoryOnly: this.options.inMemoryOnly || false, + autoload: true, + filename: (this.options.filename) ? path.resolve(this.options.filename, collectionName) : undefined + }; + + return new NedbCollection(datastoreOptions); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/stubs/express.spec.ts b/themes/black/server/src/lib/stubs/express.spec.ts new file mode 100644 index 000000000..48f15d7e1 --- /dev/null +++ b/themes/black/server/src/lib/stubs/express.spec.ts @@ -0,0 +1,103 @@ + +import sinon = require("sinon"); +import express = require("express"); + +export interface RequestMock { + app?: any; + body?: any; + session?: any; + headers?: any; + get?: any; + query?: any; + originalUrl: string; +} + +export interface ResponseMock { + send: sinon.SinonStub | sinon.SinonSpy; + sendStatus: sinon.SinonStub; + sendFile: sinon.SinonStub; + sendfile: sinon.SinonStub; + status: sinon.SinonStub | sinon.SinonSpy; + json: sinon.SinonStub | sinon.SinonSpy; + links: sinon.SinonStub; + jsonp: sinon.SinonStub; + download: sinon.SinonStub; + contentType: sinon.SinonStub; + type: sinon.SinonStub; + format: sinon.SinonStub; + attachment: sinon.SinonStub; + set: sinon.SinonStub; + header: sinon.SinonStub; + headersSent: boolean; + get: sinon.SinonStub; + clearCookie: sinon.SinonStub; + cookie: sinon.SinonStub; + location: sinon.SinonStub; + redirect: sinon.SinonStub | sinon.SinonSpy; + render: sinon.SinonStub | sinon.SinonSpy; + locals: sinon.SinonStub; + charset: string; + vary: sinon.SinonStub; + app: any; + write: sinon.SinonStub; + writeContinue: sinon.SinonStub; + writeHead: sinon.SinonStub; + statusCode: number; + statusMessage: string; + setHeader: sinon.SinonStub; + setTimeout: sinon.SinonStub; + sendDate: boolean; + getHeader: sinon.SinonStub; +} + +export function RequestMock(): RequestMock { + return { + originalUrl: "/non-api/xxx", + app: { + get: sinon.stub() + }, + headers: { + "x-forwarded-for": "127.0.0.1" + }, + session: {} + }; +} +export function ResponseMock(): ResponseMock { + return { + send: sinon.stub(), + status: sinon.stub(), + json: sinon.stub(), + sendStatus: sinon.stub(), + links: sinon.stub(), + jsonp: sinon.stub(), + sendFile: sinon.stub(), + sendfile: sinon.stub(), + download: sinon.stub(), + contentType: sinon.stub(), + type: sinon.stub(), + format: sinon.stub(), + attachment: sinon.stub(), + set: sinon.stub(), + header: sinon.stub(), + headersSent: true, + get: sinon.stub(), + clearCookie: sinon.stub(), + cookie: sinon.stub(), + location: sinon.stub(), + redirect: sinon.stub(), + render: sinon.stub(), + locals: sinon.stub(), + charset: "utf-8", + vary: sinon.stub(), + app: sinon.stub(), + write: sinon.stub(), + writeContinue: sinon.stub(), + writeHead: sinon.stub(), + statusCode: 200, + statusMessage: "message", + setHeader: sinon.stub(), + setTimeout: sinon.stub(), + sendDate: true, + getHeader: sinon.stub() + }; +} diff --git a/themes/black/server/src/lib/stubs/ldapjs.spec.ts b/themes/black/server/src/lib/stubs/ldapjs.spec.ts new file mode 100644 index 000000000..045c0e11b --- /dev/null +++ b/themes/black/server/src/lib/stubs/ldapjs.spec.ts @@ -0,0 +1,50 @@ + +import Sinon = require("sinon"); + +export class LdapjsMock { + createClientStub: sinon.SinonStub; + + constructor() { + this.createClientStub = Sinon.stub(); + } + + createClient(params: any) { + return this.createClientStub(params); + } +} + +export class LdapjsClientMock { + bindStub: sinon.SinonStub; + unbindStub: sinon.SinonStub; + searchStub: sinon.SinonStub; + modifyStub: sinon.SinonStub; + onStub: sinon.SinonStub; + + constructor() { + this.bindStub = Sinon.stub(); + this.unbindStub = Sinon.stub(); + this.searchStub = Sinon.stub(); + this.modifyStub = Sinon.stub(); + this.onStub = Sinon.stub(); + } + + bind() { + return this.bindStub(); + } + + unbind() { + return this.unbindStub(); + } + + search() { + return this.searchStub(); + } + + modify() { + return this.modifyStub(); + } + + on() { + return this.onStub(); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/stubs/speakeasy.spec.ts b/themes/black/server/src/lib/stubs/speakeasy.spec.ts new file mode 100644 index 000000000..023614dcd --- /dev/null +++ b/themes/black/server/src/lib/stubs/speakeasy.spec.ts @@ -0,0 +1,7 @@ + +import sinon = require("sinon"); + +export = { + totp: sinon.stub(), + generateSecret: sinon.stub() +}; diff --git a/themes/black/server/src/lib/stubs/u2f.spec.ts b/themes/black/server/src/lib/stubs/u2f.spec.ts new file mode 100644 index 000000000..234b28c11 --- /dev/null +++ b/themes/black/server/src/lib/stubs/u2f.spec.ts @@ -0,0 +1,16 @@ + +import sinon = require("sinon"); + +export interface U2FMock { + request: sinon.SinonStub; + checkSignature: sinon.SinonStub; + checkRegistration: sinon.SinonStub; +} + +export function U2FMock(): U2FMock { + return { + request: sinon.stub(), + checkSignature: sinon.stub(), + checkRegistration: sinon.stub() + }; +} diff --git a/themes/black/server/src/lib/utils/HashGenerator.spec.ts b/themes/black/server/src/lib/utils/HashGenerator.spec.ts new file mode 100644 index 000000000..f19619a65 --- /dev/null +++ b/themes/black/server/src/lib/utils/HashGenerator.spec.ts @@ -0,0 +1,18 @@ +import Assert = require("assert"); +import { HashGenerator } from "./HashGenerator"; + +describe("utils/HashGenerator", function () { + it("should compute correct ssha512 (password)", function () { + return HashGenerator.ssha512("password", 500000, "jgiCMRyGXzoqpxS3") + .then(function (hash: string) { + Assert.equal(hash, "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"); + }); + }); + + it("should compute correct ssha512 (test)", function () { + return HashGenerator.ssha512("test", 500000, "abcdefghijklmnop") + .then(function (hash: string) { + Assert.equal(hash, "{CRYPT}$6$rounds=500000$abcdefghijklmnop$sTlNGf0VO/HTQIOXemmaBbV28HUch/qhWOA1/4dsDj6CDQYhUgXbYSPL6gccAsWMr2zD5fFWwhKmPdG.yxphs."); + }); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/utils/HashGenerator.ts b/themes/black/server/src/lib/utils/HashGenerator.ts new file mode 100644 index 000000000..e67de32b7 --- /dev/null +++ b/themes/black/server/src/lib/utils/HashGenerator.ts @@ -0,0 +1,23 @@ +import BluebirdPromise = require("bluebird"); +import RandomString = require("randomstring"); +import Util = require("util"); +const crypt = require("crypt3"); + +export class HashGenerator { + static ssha512( + password: string, + rounds: number = 500000, + salt?: string): BluebirdPromise { + const saltSize = 16; + // $6 means SHA512 + const _salt = Util.format("$6$rounds=%d$%s", rounds, + (salt) ? salt : RandomString.generate(16)); + + const cryptAsync = BluebirdPromise.promisify(crypt); + + return cryptAsync(password, _salt) + .then(function (hash: string) { + return BluebirdPromise.resolve(Util.format("{CRYPT}%s", hash)); + }); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/utils/ObjectCloner.ts b/themes/black/server/src/lib/utils/ObjectCloner.ts new file mode 100644 index 000000000..3e125d749 --- /dev/null +++ b/themes/black/server/src/lib/utils/ObjectCloner.ts @@ -0,0 +1,6 @@ + +export class ObjectCloner { + static clone(obj: any): any { + return JSON.parse(JSON.stringify(obj)); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/utils/SafeRedirection.spec.ts b/themes/black/server/src/lib/utils/SafeRedirection.spec.ts new file mode 100644 index 000000000..4126949fd --- /dev/null +++ b/themes/black/server/src/lib/utils/SafeRedirection.spec.ts @@ -0,0 +1,33 @@ +import Assert = require("assert"); +import Sinon = require("sinon"); +import { SafeRedirector } from "./SafeRedirection"; + +describe("web_server/middlewares/SafeRedirection", () => { + describe("Url is in protected domain", () => { + before(() => { + this.redirector = new SafeRedirector("example.com"); + this.res = {redirect: Sinon.stub()}; + }); + + it("should redirect to provided url", () => { + this.redirector.redirectOrElse(this.res, + "https://mysubdomain.example.com:8080/abc", + "https://authelia.example.com"); + Assert(this.res.redirect.calledWith("https://mysubdomain.example.com:8080/abc")); + }); + + it("should redirect to default url when wrong domain", () => { + this.redirector.redirectOrElse(this.res, + "https://mysubdomain.domain.rtf:8080/abc", + "https://authelia.example.com"); + Assert(this.res.redirect.calledWith("https://authelia.example.com")); + }); + + it("should redirect to default url when not terminating by domain", () => { + this.redirector.redirectOrElse(this.res, + "https://mysubdomain.example.com.rtf:8080/abc", + "https://authelia.example.com"); + Assert(this.res.redirect.calledWith("https://authelia.example.com")); + }); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/utils/SafeRedirection.ts b/themes/black/server/src/lib/utils/SafeRedirection.ts new file mode 100644 index 000000000..9e6a32e0c --- /dev/null +++ b/themes/black/server/src/lib/utils/SafeRedirection.ts @@ -0,0 +1,22 @@ +import Express = require("express"); +import { DomainExtractor } from "../../../../shared/DomainExtractor"; +import { BelongToDomain } from "../../../../shared/BelongToDomain"; + + +export class SafeRedirector { + private domain: string; + + constructor(domain: string) { + this.domain = domain; + } + + redirectOrElse( + res: Express.Response, + url: string, + defaultUrl: string): void { + if (BelongToDomain(url, this.domain)) { + res.redirect(url); + } + res.redirect(defaultUrl); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/utils/URLDecomposer.spec.ts b/themes/black/server/src/lib/utils/URLDecomposer.spec.ts new file mode 100644 index 000000000..cbb038738 --- /dev/null +++ b/themes/black/server/src/lib/utils/URLDecomposer.spec.ts @@ -0,0 +1,46 @@ +import { URLDecomposer } from "./URLDecomposer"; +import Assert = require("assert"); + +describe("utils/URLDecomposer", function () { + describe("test fromUrl", function () { + it("should return domain from https url", function () { + const d = URLDecomposer.fromUrl("https://www.example.com/test/abc"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/test/abc"); + }); + + it("should return domain from http url", function () { + const d = URLDecomposer.fromUrl("http://www.example.com/test/abc"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/test/abc"); + }); + + it("should return domain when url contains port", function () { + const d = URLDecomposer.fromUrl("https://www.example.com:8080/test/abc"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/test/abc"); + }); + + it("should return default path when no path provided", function () { + const d = URLDecomposer.fromUrl("https://www.example.com:8080"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/"); + }); + + it("should return default path when provided", function () { + const d = URLDecomposer.fromUrl("https://www.example.com:8080/"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/"); + }); + + it("should return undefined when does not match", function () { + const d = URLDecomposer.fromUrl("https:///abc/test"); + Assert.equal(d, undefined); + }); + + it("should return undefined when does not match", function () { + const d = URLDecomposer.fromUrl("https:///abc/test"); + Assert.equal(d, undefined); + }); + }); +}); \ No newline at end of file diff --git a/themes/black/server/src/lib/utils/URLDecomposer.ts b/themes/black/server/src/lib/utils/URLDecomposer.ts new file mode 100644 index 000000000..9bdf2e9d2 --- /dev/null +++ b/themes/black/server/src/lib/utils/URLDecomposer.ts @@ -0,0 +1,15 @@ +export class URLDecomposer { + static fromUrl(url: string): {domain: string, path: string} { + if (!url) return; + const match = url.match(/https?:\/\/([a-z0-9_.-]+)(:[0-9]+)?(.*)/); + + if (!match) return; + + if (match[1] && !match[3]) { + return {domain: match[1], path: "/"}; + } else if (match[1] && match[3]) { + return {domain: match[1], path: match[3]}; + } + return; + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/web_server/Configurator.ts b/themes/black/server/src/lib/web_server/Configurator.ts new file mode 100644 index 000000000..6e404874a --- /dev/null +++ b/themes/black/server/src/lib/web_server/Configurator.ts @@ -0,0 +1,47 @@ +import { Configuration } from "../configuration/schema/Configuration"; +import { GlobalDependencies } from "../../../types/Dependencies"; +import { SessionConfigurationBuilder } from + "../configuration/SessionConfigurationBuilder"; +import Path = require("path"); +import Express = require("express"); +import * as BodyParser from "body-parser"; +import { RestApi } from "./RestApi"; +import { WithHeadersLogged } from "./middlewares/WithHeadersLogged"; +import { ServerVariables } from "../ServerVariables"; +import Helmet = require("helmet"); + +const addRequestId = require("express-request-id")(); + +// Constants +const TRUST_PROXY = "trust proxy"; +const X_POWERED_BY = "x-powered-by"; +const VIEWS = "views"; +const VIEW_ENGINE = "view engine"; +const PUG = "pug"; + +export class Configurator { + static configure(config: Configuration, + app: Express.Application, + vars: ServerVariables, + deps: GlobalDependencies): void { + const viewsDirectory = Path.resolve(__dirname, "../../views"); + const publicHtmlDirectory = Path.resolve(__dirname, "../../public_html"); + + const expressSessionOptions = SessionConfigurationBuilder.build(config, deps); + + app.use(Express.static(publicHtmlDirectory)); + app.use(BodyParser.urlencoded({ extended: false })); + app.use(BodyParser.json()); + app.use(deps.session(expressSessionOptions)); + app.use(addRequestId); + app.use(WithHeadersLogged.middleware(vars.logger)); + app.disable(X_POWERED_BY); + app.enable(TRUST_PROXY); + app.use(Helmet()); + + app.set(VIEWS, viewsDirectory); + app.set(VIEW_ENGINE, PUG); + + RestApi.setup(app, vars); + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/web_server/RestApi.ts b/themes/black/server/src/lib/web_server/RestApi.ts new file mode 100644 index 000000000..9144a15b9 --- /dev/null +++ b/themes/black/server/src/lib/web_server/RestApi.ts @@ -0,0 +1,125 @@ +import Express = require("express"); + +import FirstFactorGet = require("../routes/firstfactor/get"); +import SecondFactorGet = require("../routes/secondfactor/get"); + +import FirstFactorPost = require("../routes/firstfactor/post"); +import LogoutGet = require("../routes/logout/get"); +import VerifyGet = require("../routes/verify/get"); +import TOTPSignGet = require("../routes/secondfactor/totp/sign/post"); + +import IdentityCheckMiddleware = require("../IdentityCheckMiddleware"); + +import TOTPRegistrationIdentityHandler from "../routes/secondfactor/totp/identity/RegistrationHandler"; +import U2FRegistrationIdentityHandler from "../routes/secondfactor/u2f/identity/RegistrationHandler"; +import ResetPasswordIdentityHandler from "../routes/password-reset/identity/PasswordResetHandler"; + +import U2FSignPost = require("../routes/secondfactor/u2f/sign/post"); +import U2FSignRequestGet = require("../routes/secondfactor/u2f/sign_request/get"); + +import U2FRegisterPost = require("../routes/secondfactor/u2f/register/post"); +import U2FRegisterRequestGet = require("../routes/secondfactor/u2f/register_request/get"); + +import ResetPasswordFormPost = require("../routes/password-reset/form/post"); +import ResetPasswordRequestPost = require("../routes/password-reset/request/get"); + +import Error401Get = require("../routes/error/401/get"); +import Error403Get = require("../routes/error/403/get"); +import Error404Get = require("../routes/error/404/get"); + +import LoggedIn = require("../routes/loggedin/get"); + +import { ServerVariables } from "../ServerVariables"; +import Endpoints = require("../../../../shared/api"); +import { RequireValidatedFirstFactor } from "./middlewares/RequireValidatedFirstFactor"; + +function setupTotp(app: Express.Application, vars: ServerVariables) { + app.post(Endpoints.SECOND_FACTOR_TOTP_POST, + RequireValidatedFirstFactor.middleware(vars.logger), + TOTPSignGet.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, + RequireValidatedFirstFactor.middleware(vars.logger)); + + app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, + RequireValidatedFirstFactor.middleware(vars.logger)); + + IdentityCheckMiddleware.register(app, + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, + new TOTPRegistrationIdentityHandler(vars.logger, + vars.userDataStore, vars.totpHandler, vars.config.totp), + vars); +} + +function setupU2f(app: Express.Application, vars: ServerVariables) { + app.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, + RequireValidatedFirstFactor.middleware(vars.logger), + U2FSignRequestGet.default(vars)); + + app.post(Endpoints.SECOND_FACTOR_U2F_SIGN_POST, + RequireValidatedFirstFactor.middleware(vars.logger), + U2FSignPost.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, + RequireValidatedFirstFactor.middleware(vars.logger), + U2FRegisterRequestGet.default(vars)); + + app.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, + RequireValidatedFirstFactor.middleware(vars.logger), + U2FRegisterPost.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, + RequireValidatedFirstFactor.middleware(vars.logger)); + + app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, + RequireValidatedFirstFactor.middleware(vars.logger)); + + IdentityCheckMiddleware.register(app, + Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, + Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, + new U2FRegistrationIdentityHandler(vars.logger), vars); +} + +function setupResetPassword(app: Express.Application, vars: ServerVariables) { + IdentityCheckMiddleware.register(app, + Endpoints.RESET_PASSWORD_IDENTITY_START_GET, + Endpoints.RESET_PASSWORD_IDENTITY_FINISH_GET, + new ResetPasswordIdentityHandler(vars.logger, vars.usersDatabase), + vars); + + app.get(Endpoints.RESET_PASSWORD_REQUEST_GET, + ResetPasswordRequestPost.default); + app.post(Endpoints.RESET_PASSWORD_FORM_POST, + ResetPasswordFormPost.default(vars)); +} + +function setupErrors(app: Express.Application, vars: ServerVariables) { + app.get(Endpoints.ERROR_401_GET, Error401Get.default(vars)); + app.get(Endpoints.ERROR_403_GET, Error403Get.default(vars)); + app.get(Endpoints.ERROR_404_GET, Error404Get.default); +} + +export class RestApi { + static setup(app: Express.Application, vars: ServerVariables): void { + app.get(Endpoints.FIRST_FACTOR_GET, FirstFactorGet.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_GET, + RequireValidatedFirstFactor.middleware(vars.logger), + SecondFactorGet.default(vars)); + + app.get(Endpoints.LOGOUT_GET, LogoutGet.default(vars)); + + app.get(Endpoints.VERIFY_GET, VerifyGet.default(vars)); + app.post(Endpoints.FIRST_FACTOR_POST, FirstFactorPost.default(vars)); + + setupTotp(app, vars); + setupU2f(app, vars); + setupResetPassword(app, vars); + setupErrors(app, vars); + + app.get(Endpoints.LOGGED_IN, + RequireValidatedFirstFactor.middleware(vars.logger), + LoggedIn.default(vars)); + } +} diff --git a/themes/black/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts b/themes/black/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts new file mode 100644 index 000000000..ecfd75765 --- /dev/null +++ b/themes/black/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts @@ -0,0 +1,27 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import ErrorReplies = require("../../ErrorReplies"); +import { IRequestLogger } from "../../logging/IRequestLogger"; +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Exceptions = require("../../Exceptions"); +import { Level } from "../../authentication/Level"; + +export class RequireValidatedFirstFactor { + static middleware(logger: IRequestLogger) { + return function (req: Express.Request, res: Express.Response, + next: Express.NextFunction): BluebirdPromise { + + 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.")); + + next(); + resolve(); + }) + .catch(ErrorReplies.replyWithError401(req, res, logger)); + }; + } +} \ No newline at end of file diff --git a/themes/black/server/src/lib/web_server/middlewares/WithHeadersLogged.ts b/themes/black/server/src/lib/web_server/middlewares/WithHeadersLogged.ts new file mode 100644 index 000000000..139db1141 --- /dev/null +++ b/themes/black/server/src/lib/web_server/middlewares/WithHeadersLogged.ts @@ -0,0 +1,12 @@ +import Express = require("express"); +import { IRequestLogger } from "../../logging/IRequestLogger"; + +export class WithHeadersLogged { + static middleware(logger: IRequestLogger) { + return function (req: Express.Request, res: Express.Response, + next: Express.NextFunction): void { + logger.debug(req, "Headers = %s", JSON.stringify(req.headers)); + next(); + }; + } +} \ No newline at end of file diff --git a/themes/black/server/src/resources/email-template.ejs b/themes/black/server/src/resources/email-template.ejs new file mode 100644 index 000000000..f59c2f944 --- /dev/null +++ b/themes/black/server/src/resources/email-template.ejs @@ -0,0 +1,254 @@ + + + + + + Simples-Minimalistic Responsive Template + + + + + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + +
 
+ + + + + + + +
+

<%= title %>

+
+ +
 
+
+
+ + + + + + + + +
+ + + + + + +
 
+
+ + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + +
 
+ + + + + + + + + + + + + + + + + + +
+ This email has been sent to you in order to validate your identity. Please ignore it if you do not know why you received it. +
 
+ <%= button_title %> +
+
 
+
+
+ + + + + + + + +
+ + + + + + + + + + + + +
 
 
 
+
+ + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + +
+ Please ignore this email if you did not initiate the process. +
+
+
+ + + + + diff --git a/themes/black/server/src/views/already-logged-in.pug b/themes/black/server/src/views/already-logged-in.pug new file mode 100644 index 000000000..137bbea30 --- /dev/null +++ b/themes/black/server/src/views/already-logged-in.pug @@ -0,0 +1,14 @@ +extends layout/layout.pug + +block form-header + h1 Sign in + +block content + img(class="header-img" src="/img/success.png" alt="success") + if redirection_url + p You are already logged in as #{ username }.

+ | If you are not redirected in few seconds, click here.

+ | Otherwise, click here to log off. + else + p You are already logged in as #{ username }.

+ | Click here to log off. diff --git a/themes/black/server/src/views/errors/.directory b/themes/black/server/src/views/errors/.directory new file mode 100644 index 000000000..33f71bea4 --- /dev/null +++ b/themes/black/server/src/views/errors/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,59,57 +Version=3 +ViewMode=1 diff --git a/themes/black/server/src/views/errors/401.pug b/themes/black/server/src/views/errors/401.pug new file mode 100644 index 000000000..b7a222ad0 --- /dev/null +++ b/themes/black/server/src/views/errors/401.pug @@ -0,0 +1,16 @@ +extends ../layout/layout.pug + +block variables + - page_classname = "error-401"; + +block form-header + h1 Error 401 + +block content + img(class="header-img" src="/img/warning.png" alt="warning") + if redirection_url + p You are not authorized to access this resource.

+ | Please click here if you are not + | redirected in few seconds. + else + p You are not authorized to access this resource. \ No newline at end of file diff --git a/themes/black/server/src/views/errors/403.pug b/themes/black/server/src/views/errors/403.pug new file mode 100644 index 000000000..f4b5ca8a2 --- /dev/null +++ b/themes/black/server/src/views/errors/403.pug @@ -0,0 +1,16 @@ +extends ../layout/layout.pug + +block variables + - page_classname = "error-403"; + +block form-header + h1 Error 403 + +block content + img(class="header-img" src="/img/warning.png" alt="warning") + if redirection_url + p You don't have enough privileges to access this resource.

+ | Please click here if you are not + | redirected in few seconds. + else + p You don't have enough privileges to access this resource. diff --git a/themes/black/server/src/views/errors/404.pug b/themes/black/server/src/views/errors/404.pug new file mode 100644 index 000000000..06d6375fc --- /dev/null +++ b/themes/black/server/src/views/errors/404.pug @@ -0,0 +1,11 @@ +extends ../layout/layout.pug + +block variables + - page_classname = "error-404"; + +block form-header +

Error 404

+ +block content + img(class="header-img" src="/img/warning.png" alt="warning") + p Page not found. diff --git a/themes/black/server/src/views/firstfactor.pug b/themes/black/server/src/views/firstfactor.pug new file mode 100644 index 000000000..574470710 --- /dev/null +++ b/themes/black/server/src/views/firstfactor.pug @@ -0,0 +1,23 @@ +extends layout/layout.pug + +block variables + - page_classname = "firstfactor"; + +block form-header + h1 Sign in + +block content + div(class="notification") + img(class="header-img" src="/img/sharingan.png" alt="user profile") + p Enter your credentials to sign in + form(class="form-signin") + div(class="form-inputs") + input(type="text" class="form-control" id="username" placeholder="Username" required autofocus) + input(type="password" class="form-control" id="password" placeholder="Password" required) + button(id="signin" class="btn btn-lg btn-primary btn-block" type="submit") Sign in + div(class="keep-me-logged-in pull-left") + input(type="checkbox" id="keep_me_logged_in" name="keep_me_logged_in" value="true") + label(for="keep_me_logged_in") Keep me logged in + div(class="bottom-right-links pull-right") + a(href=reset_password_request_endpoint, class="link forgot-password") Forgot password? + span(class="clearfix") diff --git a/themes/black/server/src/views/layout/layout.pug b/themes/black/server/src/views/layout/layout.pug new file mode 100644 index 000000000..43247436d --- /dev/null +++ b/themes/black/server/src/views/layout/layout.pug @@ -0,0 +1,28 @@ +block variables + +doctype html +html + head + title Authelia - 2FA + meta(name="viewport", content="width=device-width, initial-scale=1.0") + meta(name="robots", content="noindex, nofollow, nosnippet, noarchive") + meta(http-equiv="Content-Security-Policy", content="default-src 'self'; img-src 'self' data:;") + link(rel="icon", href="/img/icon.png" type="image/png" sizes="32x32") + link(rel="stylesheet", type="text/css", href="/css/authelia.css") + if redirection_url + meta(http-equiv="refresh" content="4;url=" + redirection_url) + body + div(class="container") + div(class="row") + div(class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-6 col-lg-offset-3") + div(class="account-wall " + page_classname) + div(class="row header") + block form-header + div(class="row body") + div(class="form col-xs-10 col-xs-offset-1 col-sm-8 col-sm-offset-2 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2") + block content + div(class="row footer poweredby-block") + div(class="poweredby col-xs-6 col-xs-offset-4 col-sm-6 col-sm-offset-4 col-md-6 col-md-offset-4") + | Powered by Authelia + block entrypoint + script(src="/js/authelia.js", type="text/javascript") diff --git a/themes/black/server/src/views/need-identity-validation.pug b/themes/black/server/src/views/need-identity-validation.pug new file mode 100644 index 000000000..4cfd62717 --- /dev/null +++ b/themes/black/server/src/views/need-identity-validation.pug @@ -0,0 +1,12 @@ +extends layout/layout.pug + +block variables + - page_classname = "identity-validation"; + +block form-header + h1 Registration + +block content + img(class="header-img" src="/img/mail.png" alt="mail") + p A confirmation email has been sent to your mailbox. + | Please open it and click on the link within 15 minutes to confirm the registration. diff --git a/themes/black/server/src/views/password-reset-form.pug b/themes/black/server/src/views/password-reset-form.pug new file mode 100644 index 000000000..fd9311892 --- /dev/null +++ b/themes/black/server/src/views/password-reset-form.pug @@ -0,0 +1,18 @@ +extends layout/layout.pug + +block variables + - page_classname = "password-reset-form"; + +block form-header + h1 Reset password + +block content + div(class="notification") + img(class="header-img" src="/img/password_white.png" alt="password") + p Set your new password and confirm it. + form(class="form-signin") + div(class="form-inputs") + input(class="form-control" type="password" name="password1" id="password1" placeholder="New password" required="required") + input(class="form-control" type="password" name="password2" id="password2" placeholder="Password confirmation" required="required") + button(id="reset-password-button" class="btn btn-lg btn-primary btn-block" type="submit") Reset Password + span(class="clearfix") diff --git a/themes/black/server/src/views/password-reset-request.pug b/themes/black/server/src/views/password-reset-request.pug new file mode 100644 index 000000000..855b5998b --- /dev/null +++ b/themes/black/server/src/views/password-reset-request.pug @@ -0,0 +1,18 @@ +extends layout/layout.pug + +block variables + - page_classname = "password-reset-request"; + +block form-header + h1 Reset password + +block content + div(class="notification") + div + img(class="header-img" src="/img/password_white.png" alt="password") + p After giving your username, you will receive an email to change your password. + form(class="form-signin") + div(class="form-inputs") + input(type="text" class="form-control" name="username" id="username" placeholder="Your username" required="required") + button(id="reset-password-button" class="btn btn-lg btn-primary btn-block" type="submit") Reset Password + span(class="clearfix") diff --git a/themes/black/server/src/views/secondfactor.pug b/themes/black/server/src/views/secondfactor.pug new file mode 100644 index 000000000..87b578185 --- /dev/null +++ b/themes/black/server/src/views/secondfactor.pug @@ -0,0 +1,31 @@ +extends layout/layout.pug + +block variables + - page_classname = "secondfactor"; + +block form-header + h1 Sign in + +block content + div + div(class="notification notification-totp") + h3 Hi #{username} + div(class="row") + div(class="u2f-token") + img(src="/img/pendrive.png", alt="security key") + p + | Please, touch your security key
+ b Or
+ | Get a one-time password + form(class="form-signin totp") + div(class="form-inputs") + input(type="text" autocomplete="off" class="form-control" id="token" placeholder="Token" required autofocus) + button(class="btn btn-lg btn-primary btn-block totp-button" type="submit") Sign in + div(class="pull-right bottom-right-links") + div Need to register? + div + a(href=u2f_identity_start_endpoint, class="link register-u2f", data-toggle="tooltip", title="A security key is required to register.") Security key + | | + a(href=totp_identity_start_endpoint, class="link register-totp") Google Authenticator + span(class="clearfix") + script(src="/js/u2f-api.js", type="text/javascript") diff --git a/themes/black/server/src/views/totp-register.pug b/themes/black/server/src/views/totp-register.pug new file mode 100644 index 000000000..1b4d98354 --- /dev/null +++ b/themes/black/server/src/views/totp-register.pug @@ -0,0 +1,25 @@ +extends layout/layout.pug + +block variables + - page_classname = "totp-register"; + +block form-header + h1 One-time passwords + +block content + p Open Google Authenticator and add this entry + p(id="secret") #{ base32_secret } + p or scan this barcode + div(id="qrcode") #{ otpauth_url } + p + a(href=login_endpoint, id="login-button") Login + div(class="need-google-authenticator") + | Need Google Authenticator? + div(class="store-badges") + a(href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1', target="_blank") + img(alt='Get it on Google Play', src='/img/stores/googleplay-badge.svg', class="store-badge") + a(href='https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8', target="_blank") + img(alt='Get it on Apple Store' src='/img/stores/applestore-badge.svg' class="store-badge") + +block entrypoint + script(src="/js/qrcode.min.js", type="text/javascript" ) diff --git a/themes/black/server/src/views/u2f-register.pug b/themes/black/server/src/views/u2f-register.pug new file mode 100644 index 000000000..d52eba6ca --- /dev/null +++ b/themes/black/server/src/views/u2f-register.pug @@ -0,0 +1,12 @@ +extends layout/layout.pug + +block variables + - page_classname = "u2f-register"; + +block form-header + h1 Register your security key + +block content + p Touch the token to register your security key. + img(src="/img/pendrive.png" alt="pendrive") + script(src="/js/u2f-api.js", type="text/javascript") \ No newline at end of file diff --git a/themes/black/server/test/requests.ts b/themes/black/server/test/requests.ts new file mode 100644 index 000000000..93fa0de47 --- /dev/null +++ b/themes/black/server/test/requests.ts @@ -0,0 +1,94 @@ + +import BluebirdPromise = require("bluebird"); +import request = require("request"); +import assert = require("assert"); +import express = require("express"); +import nodemailer = require("nodemailer"); +import Endpoints = require("../../shared/api"); + +declare module "request" { + export interface RequestAPI { + getAsync(uri: string, options?: RequiredUriUrl): BluebirdPromise; + getAsync(uri: string): BluebirdPromise; + getAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; + + postAsync(uri: string, options?: CoreOptions): BluebirdPromise; + postAsync(uri: string): BluebirdPromise; + postAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; + } +} + +const requestAsync: typeof request = BluebirdPromise.promisifyAll(request) as typeof request; + +export = function (port: number) { + const PORT = port; + const BASE_URL = "http://localhost:" + PORT; + + function execute_totp(jar: request.CookieJar, token: string) { + return requestAsync.postAsync({ + url: BASE_URL + Endpoints.SECOND_FACTOR_TOTP_POST, + jar: jar, + form: { + token: token + } + }); + } + + function execute_u2f_authentication(jar: request.CookieJar) { + return requestAsync.getAsync({ + url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, + jar: jar + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200); + return requestAsync.postAsync({ + url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_POST, + jar: jar, + form: { + } + }); + }); + } + + function execute_verification(jar: request.CookieJar) { + return requestAsync.getAsync({ url: BASE_URL + Endpoints.VERIFY_GET, jar: jar }); + } + + function execute_login(jar: request.CookieJar) { + return requestAsync.getAsync({ url: BASE_URL + Endpoints.FIRST_FACTOR_GET, jar: jar }); + } + + function execute_first_factor(jar: request.CookieJar) { + return requestAsync.postAsync({ + url: BASE_URL + Endpoints.FIRST_FACTOR_POST, + jar: jar, + form: { + username: "test_ok", + password: "password" + } + }); + } + + function execute_failing_first_factor(jar: request.CookieJar) { + return requestAsync.postAsync({ + url: BASE_URL + Endpoints.FIRST_FACTOR_POST, + jar: jar, + form: { + username: "test_nok", + password: "password" + } + }); + } + + return { + login: execute_login, + verify: execute_verification, + u2f_authentication: execute_u2f_authentication, + first_factor: execute_first_factor, + failing_first_factor: execute_failing_first_factor, + totp: execute_totp, + }; +}; + diff --git a/themes/black/server/tsconfig.json b/themes/black/server/tsconfig.json new file mode 100644 index 000000000..ebe98c5ed --- /dev/null +++ b/themes/black/server/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "moduleResolution": "node", + "noImplicitAny": true, + "sourceMap": true, + "removeComments": true, + "outDir": "../dist", + "baseUrl": ".", + "paths": { + "*": [ + "./types/*", + "../shared/types/*" + ] + } + }, + "exclude": [ + "src/**/*.spec.ts" + ] +} diff --git a/themes/black/server/tslint.json b/themes/black/server/tslint.json new file mode 100644 index 000000000..c2c1b7501 --- /dev/null +++ b/themes/black/server/tslint.json @@ -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 + } +} diff --git a/themes/black/server/types/.directory b/themes/black/server/types/.directory new file mode 100644 index 000000000..1e65000ea --- /dev/null +++ b/themes/black/server/types/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,58,27 +Version=3 +ViewMode=1 diff --git a/themes/black/server/types/AuthenticationSession.ts b/themes/black/server/types/AuthenticationSession.ts new file mode 100644 index 000000000..bbed0e715 --- /dev/null +++ b/themes/black/server/types/AuthenticationSession.ts @@ -0,0 +1,18 @@ +import U2f = require("u2f"); +import { Level } from "../src/lib/authentication/Level"; + +export interface AuthenticationSession { + userid: string; + authentication_level: Level; + keep_me_logged_in: boolean; + last_activity_datetime: number; + identity_check?: { + challenge: string; + userid: string; + }; + register_request?: U2f.Request; + sign_request?: U2f.Request; + email: string; + groups: string[]; + redirect?: string; +} \ No newline at end of file diff --git a/themes/black/server/types/Dependencies.ts b/themes/black/server/types/Dependencies.ts new file mode 100644 index 000000000..f20404dbc --- /dev/null +++ b/themes/black/server/types/Dependencies.ts @@ -0,0 +1,29 @@ +import winston = require("winston"); +import speakeasy = require("speakeasy"); +import nodemailer = require("nodemailer"); +import session = require("express-session"); +import nedb = require("nedb"); +import ldapjs = require("ldapjs"); +import u2f = require("u2f"); +import RedisSession = require("connect-redis"); +import Redis = require("redis"); + +export type Speakeasy = typeof speakeasy; +export type Winston = typeof winston; +export type Session = typeof session; +export type Nedb = typeof nedb; +export type Ldapjs = typeof ldapjs; +export type U2f = typeof u2f; +export type ConnectRedis = typeof RedisSession; +export type Redis = typeof Redis; + +export interface GlobalDependencies { + u2f: U2f; + ldapjs: Ldapjs; + session: Session; + Redis: Redis; + ConnectRedis: ConnectRedis; + winston: Winston; + speakeasy: Speakeasy; + nedb: Nedb; +} \ No newline at end of file diff --git a/themes/black/server/types/Identity.ts b/themes/black/server/types/Identity.ts new file mode 100644 index 000000000..e985984e2 --- /dev/null +++ b/themes/black/server/types/Identity.ts @@ -0,0 +1,6 @@ + + +export interface Identity { + userid: string; + email: string; +} \ No newline at end of file diff --git a/themes/black/server/types/TOTPSecret.ts b/themes/black/server/types/TOTPSecret.ts new file mode 100644 index 000000000..d6775f2f0 --- /dev/null +++ b/themes/black/server/types/TOTPSecret.ts @@ -0,0 +1,11 @@ + +export interface TOTPSecret { + ascii: string; + hex: string; + base32: string; + qr_code_ascii: string; + qr_code_hex: string; + qr_code_base32: string; + google_auth_qr: string; + otpauth_url: string; + } \ No newline at end of file diff --git a/themes/black/server/types/U2FRegistration.ts b/themes/black/server/types/U2FRegistration.ts new file mode 100644 index 000000000..b6080af07 --- /dev/null +++ b/themes/black/server/types/U2FRegistration.ts @@ -0,0 +1,5 @@ + +export interface U2FRegistration { + keyHandle: string; + publicKey: string; +} \ No newline at end of file diff --git a/themes/black/server/types/dovehash.d.ts b/themes/black/server/types/dovehash.d.ts new file mode 100644 index 000000000..c354609c0 --- /dev/null +++ b/themes/black/server/types/dovehash.d.ts @@ -0,0 +1,4 @@ + +declare module "dovehash" { + function encode(algo: string, text: string): string; +} \ No newline at end of file diff --git a/themes/black/server/types/speakeasy.d.ts b/themes/black/server/types/speakeasy.d.ts new file mode 100644 index 000000000..6ea06948b --- /dev/null +++ b/themes/black/server/types/speakeasy.d.ts @@ -0,0 +1,96 @@ +declare module "speakeasy" { + export = speakeasy + + interface SharedOptions { + encoding?: string + algorithm?: string + } + + interface DigestOptions extends SharedOptions { + secret: string + counter: number + } + + interface HOTPOptions extends SharedOptions { + secret: string + counter: number + digest?: Buffer + digits?: number + } + + interface HOTPVerifyOptions extends SharedOptions { + secret: string + token: string + counter: number + digits?: number + window?: number + } + + interface TOTPOptions extends SharedOptions { + secret: string + time?: number + step?: number + epoch?: number + counter?: number + digits?: number + } + + interface TOTPVerifyOptions extends SharedOptions { + secret: string + token: string + time?: number + step?: number + epoch?: number + counter?: number + digits?: number + window?: number + } + + interface GenerateSecretOptions { + length?: number + symbols?: boolean + otpauth_url?: boolean + name?: string + issuer?: string + } + + interface GeneratedSecret { + ascii: string + hex: string + base32: string + qr_code_ascii: string + qr_code_hex: string + qr_code_base32: string + google_auth_qr: string + otpauth_url: string + } + + interface OTPAuthURLOptions extends SharedOptions { + secret: string + label: string + type?: string + counter?: number + issuer?: string + digits?: number + period?: number + } + + interface Speakeasy { + digest: (options: DigestOptions) => Buffer + hotp: { + (options: HOTPOptions): string, + verifyDelta: (options: HOTPVerifyOptions) => boolean, + verify: (options: HOTPVerifyOptions) => boolean, + } + totp: { + (options: TOTPOptions): string + verifyDelta: (options: TOTPVerifyOptions) => boolean, + verify: (options: TOTPVerifyOptions) => boolean, + } + generateSecret: (options?: GenerateSecretOptions) => GeneratedSecret + generateSecretASCII: (length?: number, symbols?: boolean) => string + otpauthURL: (options: OTPAuthURLOptions) => string + } + + const speakeasy: Speakeasy +} \ No newline at end of file diff --git a/themes/main/server/src/views/layout/layout.pug b/themes/main/server/src/views/layout/layout.pug index 1d845be49..39d045042 100644 --- a/themes/main/server/src/views/layout/layout.pug +++ b/themes/main/server/src/views/layout/layout.pug @@ -13,7 +13,6 @@ html meta(http-equiv="refresh" content="4;url=" + redirection_url) body canvas#canvas(width='400', height='300') - script(src='/js/matrix.js') div(class="container") div(class="row") div(class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-6 col-lg-offset-3") From 1e71815b009e272087480d53c551112262e5e556 Mon Sep 17 00:00:00 2001 From: BankaiNoJutsu Date: Tue, 18 Dec 2018 08:32:04 +0100 Subject: [PATCH 04/11] added squares and triangles themes --- Gruntfile.js | 68 +- themes/squares/client/src/css/.directory | 4 + .../client/src/css/00-bootstrap.min.css | 5768 +++++++++++++++++ themes/squares/client/src/css/01-main.css | 77 + themes/squares/client/src/css/02-login.css | 136 + themes/squares/client/src/css/03-errors.css | 12 + .../client/src/css/03-password-reset-form.css | 4 + .../src/css/03-password-reset-request.css | 4 + .../client/src/css/03-totp-register.css | 22 + .../client/src/css/03-u2f-register.css | 5 + .../squares/client/src/img/LargeTriangles.svg | 1 + .../client/src/img/RandomizedPattern.svg | 1 + themes/squares/client/src/img/background.jpg | Bin 0 -> 587 bytes themes/squares/client/src/img/background.svg | 5 + themes/squares/client/src/img/icon.png | Bin 0 -> 1461 bytes themes/squares/client/src/img/mail.png | Bin 0 -> 3545 bytes .../client/src/img/matrix_circle_128x128.png | Bin 0 -> 35750 bytes .../client/src/img/notifications/.directory | 4 + .../client/src/img/notifications/error.png | Bin 0 -> 863 bytes .../client/src/img/notifications/info.png | Bin 0 -> 732 bytes .../client/src/img/notifications/success.png | Bin 0 -> 931 bytes .../client/src/img/notifications/warning.png | Bin 0 -> 580 bytes themes/squares/client/src/img/padlock.png | Bin 0 -> 3265 bytes .../squares/client/src/img/password_white.png | Bin 0 -> 3858 bytes themes/squares/client/src/img/pendrive.png | Bin 0 -> 6721 bytes themes/squares/client/src/img/sharingan.png | Bin 0 -> 9213 bytes .../squares/client/src/img/stores/.directory | 4 + .../src/img/stores/applestore-badge.svg | 129 + .../src/img/stores/googleplay-badge.svg | 429 ++ themes/squares/client/src/img/success.png | Bin 0 -> 3147 bytes themes/squares/client/src/img/user.png | Bin 0 -> 2933 bytes themes/squares/client/src/img/warning.png | Bin 0 -> 4038 bytes themes/squares/client/src/index.ts | 34 + themes/squares/client/src/lib/GetPromised.ts | 14 + themes/squares/client/src/lib/INotifier.ts | 14 + themes/squares/client/src/lib/Notifier.ts | 83 + .../src/lib/QueryParametersRetriever.ts | 12 + themes/squares/client/src/lib/SafeRedirect.ts | 10 + .../lib/firstfactor/FirstFactorValidator.ts | 46 + .../client/src/lib/firstfactor/UISelectors.ts | 5 + .../client/src/lib/firstfactor/index.ts | 49 + .../src/lib/reset-password/constants.ts | 2 + .../lib/reset-password/reset-password-form.ts | 57 + .../reset-password/reset-password-request.ts | 56 + .../src/lib/secondfactor/TOTPValidator.ts | 28 + .../src/lib/secondfactor/U2FValidator.ts | 42 + .../client/src/lib/secondfactor/constants.ts | 3 + .../client/src/lib/secondfactor/index.ts | 59 + .../src/lib/totp-register/totp-register.ts | 11 + .../src/lib/totp-register/ui-selector.ts | 2 + .../src/lib/u2f-register/u2f-register.ts | 56 + .../client/src/thirdparties/qrcode.min.js | 1 + .../client/src/thirdparties/u2f-api.js | 749 +++ themes/squares/client/test/Notifier.test.ts | 71 + .../firstfactor/FirstFactorValidator.test.ts | 44 + .../squares/client/test/mocks/NotifierStub.ts | 33 + themes/squares/client/test/mocks/jquery.ts | 59 + themes/squares/client/test/mocks/u2f-api.ts | 14 + .../test/secondfactor/TOTPValidator.test.ts | 37 + .../test/totp-register/totp-register.test.ts | 31 + themes/squares/client/tsconfig.json | 24 + themes/squares/client/tslint.json | 60 + themes/squares/server/.directory | 4 + themes/squares/server/src/index.ts | 28 + themes/squares/server/src/lib/.directory | 4 + .../src/lib/AuthenticationSessionHandler.ts | 45 + themes/squares/server/src/lib/ErrorReplies.ts | 49 + themes/squares/server/src/lib/Exceptions.ts | 88 + .../server/src/lib/FirstFactorValidator.ts | 20 + .../src/lib/IdentityCheckMiddleware.spec.ts | 176 + .../server/src/lib/IdentityCheckMiddleware.ts | 138 + .../lib/IdentityCheckPreValidationTemplate.ts | 3 + .../server/src/lib/IdentityValidable.ts | 19 + .../src/lib/IdentityValidableStub.spec.ts | 52 + themes/squares/server/src/lib/Server.spec.ts | 81 + themes/squares/server/src/lib/Server.ts | 93 + .../squares/server/src/lib/ServerVariables.ts | 21 + .../src/lib/ServerVariablesInitializer.ts | 116 + .../lib/ServerVariablesMockBuilder.spec.ts | 87 + .../server/src/lib/authentication/Level.ts | 5 + .../backends/GroupsAndEmails.ts | 5 + .../authentication/backends/IUsersDatabase.ts | 10 + .../backends/IUsersDatabaseStub.spec.ts | 35 + .../backends/file/FileUsersDatabase.spec.ts | 224 + .../backends/file/FileUsersDatabase.ts | 182 + .../backends/file/ReadWriteQueue.ts | 60 + .../authentication/backends/ldap/ISession.ts | 12 + .../backends/ldap/ISessionFactory.ts | 6 + .../backends/ldap/LdapUsersDatabase.spec.ts | 386 ++ .../backends/ldap/LdapUsersDatabase.ts | 107 + .../backends/ldap/SafeSession.spec.ts | 76 + .../backends/ldap/SafeSession.ts | 62 + .../backends/ldap/Sanitizer.spec.ts | 25 + .../authentication/backends/ldap/Sanitizer.ts | 25 + .../backends/ldap/Session.spec.ts | 127 + .../authentication/backends/ldap/Session.ts | 156 + .../backends/ldap/SessionFactory.ts | 37 + .../backends/ldap/SessionFactoryStub.spec.ts | 16 + .../backends/ldap/SessionStub.spec.ts | 46 + .../backends/ldap/connector/Connector.ts | 69 + .../ldap/connector/ConnectorFactory.ts | 18 + .../connector/ConnectorFactoryStub.spec.ts | 17 + .../ldap/connector/ConnectorStub.spec.ts | 34 + .../backends/ldap/connector/IConnector.ts | 9 + .../ldap/connector/IConnectorFactory.ts | 5 + .../lib/authentication/totp/ITotpHandler.ts | 6 + .../authentication/totp/TotpHandler.spec.ts | 39 + .../lib/authentication/totp/TotpHandler.ts | 36 + .../totp/TotpHandlerStub.spec.ts | 22 + .../src/lib/authentication/u2f/IU2fHandler.ts | 9 + .../src/lib/authentication/u2f/U2fHandler.ts | 24 + .../authentication/u2f/U2fHandlerStub.spec.ts | 31 + .../src/lib/authorization/Authorizer.spec.ts | 372 ++ .../src/lib/authorization/Authorizer.ts | 85 + .../lib/authorization/AuthorizerStub.spec.ts | 17 + .../src/lib/authorization/IAuthorizer.ts | 7 + .../server/src/lib/authorization/Level.ts | 6 + .../authorization/MultipleDomainMatcher.ts | 12 + .../server/src/lib/authorization/Object.ts | 5 + .../server/src/lib/authorization/Subject.ts | 5 + .../configuration/ConfigurationParser.spec.ts | 171 + .../lib/configuration/ConfigurationParser.ts | 39 + .../SessionConfigurationBuilder.spec.ts | 149 + .../SessionConfigurationBuilder.ts | 52 + .../schema/AclConfiguration.spec.ts | 34 + .../configuration/schema/AclConfiguration.ts | 41 + ...AuthenticationBackendConfiguration.spec.ts | 11 + .../AuthenticationBackendConfiguration.ts | 25 + .../lib/configuration/schema/Configuration.ts | 68 + .../schema/FileUsersDatabaseConfiguration.ts | 4 + .../schema/LdapConfiguration.spec.ts | 25 + .../configuration/schema/LdapConfiguration.ts | 40 + .../schema/NotifierConfiguration.spec.ts | 40 + .../schema/NotifierConfiguration.ts | 45 + .../schema/RegulationConfiguration.spec.ts | 13 + .../schema/RegulationConfiguration.ts | 23 + .../schema/SessionConfiguration.spec.ts | 16 + .../schema/SessionConfiguration.ts | 32 + .../schema/StorageConfiguration.spec.ts | 15 + .../schema/StorageConfiguration.ts | 30 + .../configuration/schema/TotpConfiguration.ts | 13 + .../schema/UserDatabaseConfiguration.ts | 9 + .../lib/connectors/mongo/IMongoClient.d.ts | 6 + .../lib/connectors/mongo/MongoClient.spec.ts | 119 + .../src/lib/connectors/mongo/MongoClient.ts | 76 + .../connectors/mongo/MongoClientStub.spec.ts | 16 + .../server/src/lib/logging/GlobalLogger.ts | 34 + .../src/lib/logging/GlobalLoggerStub.spec.ts | 38 + .../server/src/lib/logging/IGlobalLogger.ts | 5 + .../server/src/lib/logging/IRequestLogger.ts | 7 + .../server/src/lib/logging/RequestLogger.ts | 45 + .../src/lib/logging/RequestLoggerStub.spec.ts | 38 + .../lib/notifiers/AbstractEmailNotifier.ts | 23 + .../src/lib/notifiers/EmailNotifier.spec.ts | 54 + .../server/src/lib/notifiers/EmailNotifier.ts | 27 + .../src/lib/notifiers/FileSystemNotifier.ts | 22 + .../server/src/lib/notifiers/IMailSender.ts | 6 + .../src/lib/notifiers/IMailSenderBuilder.ts | 7 + .../server/src/lib/notifiers/INotifier.ts | 5 + .../server/src/lib/notifiers/MailSender.ts | 42 + .../lib/notifiers/MailSenderBuilder.spec.ts | 67 + .../src/lib/notifiers/MailSenderBuilder.ts | 42 + .../notifiers/MailSenderBuilderStub.spec.ts | 25 + .../src/lib/notifiers/MailSenderStub.spec.ts | 16 + .../src/lib/notifiers/NotifierFactory.spec.ts | 42 + .../src/lib/notifiers/NotifierFactory.ts | 33 + .../src/lib/notifiers/NotifierStub.spec.ts | 16 + .../server/src/lib/notifiers/SmtpNotifier.ts | 30 + .../server/src/lib/regulation/IRegulator.ts | 6 + .../src/lib/regulation/Regulator.spec.ts | 186 + .../server/src/lib/regulation/Regulator.ts | 55 + .../src/lib/regulation/RegulatorStub.spec.ts | 22 + .../src/lib/routes/error/401/get.spec.ts | 61 + .../server/src/lib/routes/error/401/get.ts | 15 + .../src/lib/routes/error/403/get.spec.ts | 61 + .../server/src/lib/routes/error/403/get.ts | 15 + .../src/lib/routes/error/404/get.spec.ts | 19 + .../server/src/lib/routes/error/404/get.ts | 8 + .../server/src/lib/routes/error/redirector.ts | 13 + .../server/src/lib/routes/firstfactor/get.ts | 72 + .../src/lib/routes/firstfactor/post.spec.ts | 136 + .../server/src/lib/routes/firstfactor/post.ts | 101 + .../server/src/lib/routes/loggedin/get.ts | 23 + .../server/src/lib/routes/logout/get.ts | 20 + .../lib/routes/password-reset/constants.ts | 2 + .../routes/password-reset/form/post.spec.ts | 122 + .../lib/routes/password-reset/form/post.ts | 50 + .../identity/PasswordResetHandler.spec.ts | 92 + .../identity/PasswordResetHandler.ts | 69 + .../lib/routes/password-reset/request/get.ts | 13 + .../src/lib/routes/secondfactor/get.spec.ts | 44 + .../server/src/lib/routes/secondfactor/get.ts | 28 + .../lib/routes/secondfactor/redirect.spec.ts | 41 + .../src/lib/routes/secondfactor/redirect.ts | 30 + .../lib/routes/secondfactor/totp/constants.ts | 4 + .../totp/identity/RegistrationHandler.spec.ts | 116 + .../totp/identity/RegistrationHandler.ts | 112 + .../secondfactor/totp/sign/post.spec.ts | 76 + .../lib/routes/secondfactor/totp/sign/post.ts | 42 + .../lib/routes/secondfactor/u2f/U2FCommon.ts | 11 + .../u2f/identity/RegistrationHandler.spec.ts | 96 + .../u2f/identity/RegistrationHandler.ts | 73 + .../secondfactor/u2f/register/post.spec.ts | 146 + .../routes/secondfactor/u2f/register/post.ts | 64 + .../u2f/register_request/get.spec.ts | 86 + .../secondfactor/u2f/register_request/get.ts | 43 + .../routes/secondfactor/u2f/sign/post.spec.ts | 101 + .../lib/routes/secondfactor/u2f/sign/post.ts | 57 + .../secondfactor/u2f/sign_request/get.spec.ts | 68 + .../secondfactor/u2f/sign_request/get.ts | 42 + .../src/lib/routes/verify/access_control.ts | 51 + .../server/src/lib/routes/verify/get.spec.ts | 320 + .../server/src/lib/routes/verify/get.ts | 91 + .../src/lib/routes/verify/get_basic_auth.ts | 55 + .../lib/routes/verify/get_session_cookie.ts | 78 + .../storage/AuthenticationTraceDocument.d.ts | 6 + .../lib/storage/CollectionFactoryFactory.ts | 15 + .../lib/storage/CollectionFactoryStub.spec.ts | 16 + .../src/lib/storage/CollectionStub.spec.ts | 39 + .../server/src/lib/storage/ICollection.d.ts | 11 + .../src/lib/storage/ICollectionFactory.d.ts | 6 + .../src/lib/storage/IUserDataStore.d.ts | 21 + .../storage/IdentityValidationDocument.d.ts | 7 + .../src/lib/storage/TOTPSecretDocument.d.ts | 6 + .../lib/storage/U2FRegistrationDocument.d.ts | 8 + .../src/lib/storage/UserDataStore.spec.ts | 264 + .../server/src/lib/storage/UserDataStore.ts | 143 + .../src/lib/storage/UserDataStoreStub.spec.ts | 64 + .../lib/storage/mongo/MongoCollection.spec.ts | 110 + .../src/lib/storage/mongo/MongoCollection.ts | 50 + .../mongo/MongoCollectionFactory.spec.ts | 21 + .../storage/mongo/MongoCollectionFactory.ts | 19 + .../lib/storage/nedb/NedbCollection.spec.ts | 136 + .../src/lib/storage/nedb/NedbCollection.ts | 47 + .../nedb/NedbCollectionFactory.spec.ts | 16 + .../lib/storage/nedb/NedbCollectionFactory.ts | 28 + .../server/src/lib/stubs/express.spec.ts | 103 + .../server/src/lib/stubs/ldapjs.spec.ts | 50 + .../server/src/lib/stubs/speakeasy.spec.ts | 7 + .../squares/server/src/lib/stubs/u2f.spec.ts | 16 + .../src/lib/utils/HashGenerator.spec.ts | 18 + .../server/src/lib/utils/HashGenerator.ts | 23 + .../server/src/lib/utils/ObjectCloner.ts | 6 + .../src/lib/utils/SafeRedirection.spec.ts | 33 + .../server/src/lib/utils/SafeRedirection.ts | 22 + .../src/lib/utils/URLDecomposer.spec.ts | 46 + .../server/src/lib/utils/URLDecomposer.ts | 15 + .../server/src/lib/web_server/Configurator.ts | 47 + .../server/src/lib/web_server/RestApi.ts | 125 + .../RequireValidatedFirstFactor.ts | 27 + .../middlewares/WithHeadersLogged.ts | 12 + .../server/src/resources/email-template.ejs | 254 + .../server/src/views/already-logged-in.pug | 14 + .../server/src/views/errors/.directory | 4 + .../squares/server/src/views/errors/401.pug | 16 + .../squares/server/src/views/errors/403.pug | 16 + .../squares/server/src/views/errors/404.pug | 11 + .../squares/server/src/views/firstfactor.pug | 23 + .../server/src/views/layout/layout.pug | 28 + .../src/views/need-identity-validation.pug | 12 + .../server/src/views/password-reset-form.pug | 18 + .../src/views/password-reset-request.pug | 18 + .../squares/server/src/views/secondfactor.pug | 31 + .../server/src/views/totp-register.pug | 25 + .../squares/server/src/views/u2f-register.pug | 12 + themes/squares/server/test/requests.ts | 94 + themes/squares/server/tsconfig.json | 21 + themes/squares/server/tslint.json | 60 + themes/squares/server/types/.directory | 4 + .../server/types/AuthenticationSession.ts | 18 + themes/squares/server/types/Dependencies.ts | 29 + themes/squares/server/types/Identity.ts | 6 + themes/squares/server/types/TOTPSecret.ts | 11 + .../squares/server/types/U2FRegistration.ts | 5 + themes/squares/server/types/dovehash.d.ts | 4 + themes/squares/server/types/speakeasy.d.ts | 96 + themes/triangles/client/src/.directory | 4 + themes/triangles/client/src/css/.directory | 4 + .../client/src/css/00-bootstrap.min.css | 5768 +++++++++++++++++ themes/triangles/client/src/css/01-main.css | 77 + themes/triangles/client/src/css/02-login.css | 136 + themes/triangles/client/src/css/03-errors.css | 12 + .../client/src/css/03-password-reset-form.css | 4 + .../src/css/03-password-reset-request.css | 4 + .../client/src/css/03-totp-register.css | 22 + .../client/src/css/03-u2f-register.css | 5 + .../client/src/img/LargeTriangles.svg | 1 + .../triangles/client/src/img/background.jpg | Bin 0 -> 587 bytes themes/triangles/client/src/img/icon.png | Bin 0 -> 1461 bytes themes/triangles/client/src/img/mail.png | Bin 0 -> 3545 bytes .../client/src/img/matrix_circle_128x128.png | Bin 0 -> 35750 bytes .../client/src/img/notifications/.directory | 4 + .../client/src/img/notifications/error.png | Bin 0 -> 863 bytes .../client/src/img/notifications/info.png | Bin 0 -> 732 bytes .../client/src/img/notifications/success.png | Bin 0 -> 931 bytes .../client/src/img/notifications/warning.png | Bin 0 -> 580 bytes themes/triangles/client/src/img/padlock.png | Bin 0 -> 3265 bytes .../client/src/img/password_white.png | Bin 0 -> 3858 bytes themes/triangles/client/src/img/pendrive.png | Bin 0 -> 6721 bytes themes/triangles/client/src/img/sharingan.png | Bin 0 -> 9213 bytes .../client/src/img/stores/.directory | 4 + .../src/img/stores/applestore-badge.svg | 129 + .../src/img/stores/googleplay-badge.svg | 429 ++ themes/triangles/client/src/img/success.png | Bin 0 -> 3147 bytes themes/triangles/client/src/img/user.png | Bin 0 -> 2933 bytes themes/triangles/client/src/img/warning.png | Bin 0 -> 4038 bytes themes/triangles/client/src/index.ts | 34 + .../triangles/client/src/lib/GetPromised.ts | 14 + themes/triangles/client/src/lib/INotifier.ts | 14 + themes/triangles/client/src/lib/Notifier.ts | 83 + .../src/lib/QueryParametersRetriever.ts | 12 + .../triangles/client/src/lib/SafeRedirect.ts | 10 + .../lib/firstfactor/FirstFactorValidator.ts | 46 + .../client/src/lib/firstfactor/UISelectors.ts | 5 + .../client/src/lib/firstfactor/index.ts | 49 + .../src/lib/reset-password/constants.ts | 2 + .../lib/reset-password/reset-password-form.ts | 57 + .../reset-password/reset-password-request.ts | 56 + .../src/lib/secondfactor/TOTPValidator.ts | 28 + .../src/lib/secondfactor/U2FValidator.ts | 42 + .../client/src/lib/secondfactor/constants.ts | 3 + .../client/src/lib/secondfactor/index.ts | 59 + .../src/lib/totp-register/totp-register.ts | 11 + .../src/lib/totp-register/ui-selector.ts | 2 + .../src/lib/u2f-register/u2f-register.ts | 56 + .../client/src/thirdparties/qrcode.min.js | 1 + .../client/src/thirdparties/u2f-api.js | 749 +++ themes/triangles/client/test/Notifier.test.ts | 71 + .../firstfactor/FirstFactorValidator.test.ts | 44 + .../client/test/mocks/NotifierStub.ts | 33 + themes/triangles/client/test/mocks/jquery.ts | 59 + themes/triangles/client/test/mocks/u2f-api.ts | 14 + .../test/secondfactor/TOTPValidator.test.ts | 37 + .../test/totp-register/totp-register.test.ts | 31 + themes/triangles/client/tsconfig.json | 24 + themes/triangles/client/tslint.json | 60 + themes/triangles/server/.directory | 4 + themes/triangles/server/src/index.ts | 28 + themes/triangles/server/src/lib/.directory | 4 + .../src/lib/AuthenticationSessionHandler.ts | 45 + .../triangles/server/src/lib/ErrorReplies.ts | 49 + themes/triangles/server/src/lib/Exceptions.ts | 88 + .../server/src/lib/FirstFactorValidator.ts | 20 + .../src/lib/IdentityCheckMiddleware.spec.ts | 176 + .../server/src/lib/IdentityCheckMiddleware.ts | 138 + .../lib/IdentityCheckPreValidationTemplate.ts | 3 + .../server/src/lib/IdentityValidable.ts | 19 + .../src/lib/IdentityValidableStub.spec.ts | 52 + .../triangles/server/src/lib/Server.spec.ts | 81 + themes/triangles/server/src/lib/Server.ts | 93 + .../server/src/lib/ServerVariables.ts | 21 + .../src/lib/ServerVariablesInitializer.ts | 116 + .../lib/ServerVariablesMockBuilder.spec.ts | 87 + .../server/src/lib/authentication/Level.ts | 5 + .../backends/GroupsAndEmails.ts | 5 + .../authentication/backends/IUsersDatabase.ts | 10 + .../backends/IUsersDatabaseStub.spec.ts | 35 + .../backends/file/FileUsersDatabase.spec.ts | 224 + .../backends/file/FileUsersDatabase.ts | 182 + .../backends/file/ReadWriteQueue.ts | 60 + .../authentication/backends/ldap/ISession.ts | 12 + .../backends/ldap/ISessionFactory.ts | 6 + .../backends/ldap/LdapUsersDatabase.spec.ts | 386 ++ .../backends/ldap/LdapUsersDatabase.ts | 107 + .../backends/ldap/SafeSession.spec.ts | 76 + .../backends/ldap/SafeSession.ts | 62 + .../backends/ldap/Sanitizer.spec.ts | 25 + .../authentication/backends/ldap/Sanitizer.ts | 25 + .../backends/ldap/Session.spec.ts | 127 + .../authentication/backends/ldap/Session.ts | 156 + .../backends/ldap/SessionFactory.ts | 37 + .../backends/ldap/SessionFactoryStub.spec.ts | 16 + .../backends/ldap/SessionStub.spec.ts | 46 + .../backends/ldap/connector/Connector.ts | 69 + .../ldap/connector/ConnectorFactory.ts | 18 + .../connector/ConnectorFactoryStub.spec.ts | 17 + .../ldap/connector/ConnectorStub.spec.ts | 34 + .../backends/ldap/connector/IConnector.ts | 9 + .../ldap/connector/IConnectorFactory.ts | 5 + .../lib/authentication/totp/ITotpHandler.ts | 6 + .../authentication/totp/TotpHandler.spec.ts | 39 + .../lib/authentication/totp/TotpHandler.ts | 36 + .../totp/TotpHandlerStub.spec.ts | 22 + .../src/lib/authentication/u2f/IU2fHandler.ts | 9 + .../src/lib/authentication/u2f/U2fHandler.ts | 24 + .../authentication/u2f/U2fHandlerStub.spec.ts | 31 + .../src/lib/authorization/Authorizer.spec.ts | 372 ++ .../src/lib/authorization/Authorizer.ts | 85 + .../lib/authorization/AuthorizerStub.spec.ts | 17 + .../src/lib/authorization/IAuthorizer.ts | 7 + .../server/src/lib/authorization/Level.ts | 6 + .../authorization/MultipleDomainMatcher.ts | 12 + .../server/src/lib/authorization/Object.ts | 5 + .../server/src/lib/authorization/Subject.ts | 5 + .../configuration/ConfigurationParser.spec.ts | 171 + .../lib/configuration/ConfigurationParser.ts | 39 + .../SessionConfigurationBuilder.spec.ts | 149 + .../SessionConfigurationBuilder.ts | 52 + .../schema/AclConfiguration.spec.ts | 34 + .../configuration/schema/AclConfiguration.ts | 41 + ...AuthenticationBackendConfiguration.spec.ts | 11 + .../AuthenticationBackendConfiguration.ts | 25 + .../lib/configuration/schema/Configuration.ts | 68 + .../schema/FileUsersDatabaseConfiguration.ts | 4 + .../schema/LdapConfiguration.spec.ts | 25 + .../configuration/schema/LdapConfiguration.ts | 40 + .../schema/NotifierConfiguration.spec.ts | 40 + .../schema/NotifierConfiguration.ts | 45 + .../schema/RegulationConfiguration.spec.ts | 13 + .../schema/RegulationConfiguration.ts | 23 + .../schema/SessionConfiguration.spec.ts | 16 + .../schema/SessionConfiguration.ts | 32 + .../schema/StorageConfiguration.spec.ts | 15 + .../schema/StorageConfiguration.ts | 30 + .../configuration/schema/TotpConfiguration.ts | 13 + .../schema/UserDatabaseConfiguration.ts | 9 + .../lib/connectors/mongo/IMongoClient.d.ts | 6 + .../lib/connectors/mongo/MongoClient.spec.ts | 119 + .../src/lib/connectors/mongo/MongoClient.ts | 76 + .../connectors/mongo/MongoClientStub.spec.ts | 16 + .../server/src/lib/logging/GlobalLogger.ts | 34 + .../src/lib/logging/GlobalLoggerStub.spec.ts | 38 + .../server/src/lib/logging/IGlobalLogger.ts | 5 + .../server/src/lib/logging/IRequestLogger.ts | 7 + .../server/src/lib/logging/RequestLogger.ts | 45 + .../src/lib/logging/RequestLoggerStub.spec.ts | 38 + .../lib/notifiers/AbstractEmailNotifier.ts | 23 + .../src/lib/notifiers/EmailNotifier.spec.ts | 54 + .../server/src/lib/notifiers/EmailNotifier.ts | 27 + .../src/lib/notifiers/FileSystemNotifier.ts | 22 + .../server/src/lib/notifiers/IMailSender.ts | 6 + .../src/lib/notifiers/IMailSenderBuilder.ts | 7 + .../server/src/lib/notifiers/INotifier.ts | 5 + .../server/src/lib/notifiers/MailSender.ts | 42 + .../lib/notifiers/MailSenderBuilder.spec.ts | 67 + .../src/lib/notifiers/MailSenderBuilder.ts | 42 + .../notifiers/MailSenderBuilderStub.spec.ts | 25 + .../src/lib/notifiers/MailSenderStub.spec.ts | 16 + .../src/lib/notifiers/NotifierFactory.spec.ts | 42 + .../src/lib/notifiers/NotifierFactory.ts | 33 + .../src/lib/notifiers/NotifierStub.spec.ts | 16 + .../server/src/lib/notifiers/SmtpNotifier.ts | 30 + .../server/src/lib/regulation/IRegulator.ts | 6 + .../src/lib/regulation/Regulator.spec.ts | 186 + .../server/src/lib/regulation/Regulator.ts | 55 + .../src/lib/regulation/RegulatorStub.spec.ts | 22 + .../src/lib/routes/error/401/get.spec.ts | 61 + .../server/src/lib/routes/error/401/get.ts | 15 + .../src/lib/routes/error/403/get.spec.ts | 61 + .../server/src/lib/routes/error/403/get.ts | 15 + .../src/lib/routes/error/404/get.spec.ts | 19 + .../server/src/lib/routes/error/404/get.ts | 8 + .../server/src/lib/routes/error/redirector.ts | 13 + .../server/src/lib/routes/firstfactor/get.ts | 72 + .../src/lib/routes/firstfactor/post.spec.ts | 136 + .../server/src/lib/routes/firstfactor/post.ts | 101 + .../server/src/lib/routes/loggedin/get.ts | 23 + .../server/src/lib/routes/logout/get.ts | 20 + .../lib/routes/password-reset/constants.ts | 2 + .../routes/password-reset/form/post.spec.ts | 122 + .../lib/routes/password-reset/form/post.ts | 50 + .../identity/PasswordResetHandler.spec.ts | 92 + .../identity/PasswordResetHandler.ts | 69 + .../lib/routes/password-reset/request/get.ts | 13 + .../src/lib/routes/secondfactor/get.spec.ts | 44 + .../server/src/lib/routes/secondfactor/get.ts | 28 + .../lib/routes/secondfactor/redirect.spec.ts | 41 + .../src/lib/routes/secondfactor/redirect.ts | 30 + .../lib/routes/secondfactor/totp/constants.ts | 4 + .../totp/identity/RegistrationHandler.spec.ts | 116 + .../totp/identity/RegistrationHandler.ts | 112 + .../secondfactor/totp/sign/post.spec.ts | 76 + .../lib/routes/secondfactor/totp/sign/post.ts | 42 + .../lib/routes/secondfactor/u2f/U2FCommon.ts | 11 + .../u2f/identity/RegistrationHandler.spec.ts | 96 + .../u2f/identity/RegistrationHandler.ts | 73 + .../secondfactor/u2f/register/post.spec.ts | 146 + .../routes/secondfactor/u2f/register/post.ts | 64 + .../u2f/register_request/get.spec.ts | 86 + .../secondfactor/u2f/register_request/get.ts | 43 + .../routes/secondfactor/u2f/sign/post.spec.ts | 101 + .../lib/routes/secondfactor/u2f/sign/post.ts | 57 + .../secondfactor/u2f/sign_request/get.spec.ts | 68 + .../secondfactor/u2f/sign_request/get.ts | 42 + .../src/lib/routes/verify/access_control.ts | 51 + .../server/src/lib/routes/verify/get.spec.ts | 320 + .../server/src/lib/routes/verify/get.ts | 91 + .../src/lib/routes/verify/get_basic_auth.ts | 55 + .../lib/routes/verify/get_session_cookie.ts | 78 + .../storage/AuthenticationTraceDocument.d.ts | 6 + .../lib/storage/CollectionFactoryFactory.ts | 15 + .../lib/storage/CollectionFactoryStub.spec.ts | 16 + .../src/lib/storage/CollectionStub.spec.ts | 39 + .../server/src/lib/storage/ICollection.d.ts | 11 + .../src/lib/storage/ICollectionFactory.d.ts | 6 + .../src/lib/storage/IUserDataStore.d.ts | 21 + .../storage/IdentityValidationDocument.d.ts | 7 + .../src/lib/storage/TOTPSecretDocument.d.ts | 6 + .../lib/storage/U2FRegistrationDocument.d.ts | 8 + .../src/lib/storage/UserDataStore.spec.ts | 264 + .../server/src/lib/storage/UserDataStore.ts | 143 + .../src/lib/storage/UserDataStoreStub.spec.ts | 64 + .../lib/storage/mongo/MongoCollection.spec.ts | 110 + .../src/lib/storage/mongo/MongoCollection.ts | 50 + .../mongo/MongoCollectionFactory.spec.ts | 21 + .../storage/mongo/MongoCollectionFactory.ts | 19 + .../lib/storage/nedb/NedbCollection.spec.ts | 136 + .../src/lib/storage/nedb/NedbCollection.ts | 47 + .../nedb/NedbCollectionFactory.spec.ts | 16 + .../lib/storage/nedb/NedbCollectionFactory.ts | 28 + .../server/src/lib/stubs/express.spec.ts | 103 + .../server/src/lib/stubs/ldapjs.spec.ts | 50 + .../server/src/lib/stubs/speakeasy.spec.ts | 7 + .../server/src/lib/stubs/u2f.spec.ts | 16 + .../src/lib/utils/HashGenerator.spec.ts | 18 + .../server/src/lib/utils/HashGenerator.ts | 23 + .../server/src/lib/utils/ObjectCloner.ts | 6 + .../src/lib/utils/SafeRedirection.spec.ts | 33 + .../server/src/lib/utils/SafeRedirection.ts | 22 + .../src/lib/utils/URLDecomposer.spec.ts | 46 + .../server/src/lib/utils/URLDecomposer.ts | 15 + .../server/src/lib/web_server/Configurator.ts | 47 + .../server/src/lib/web_server/RestApi.ts | 125 + .../RequireValidatedFirstFactor.ts | 27 + .../middlewares/WithHeadersLogged.ts | 12 + .../server/src/resources/email-template.ejs | 254 + .../server/src/views/already-logged-in.pug | 14 + .../server/src/views/errors/.directory | 4 + .../triangles/server/src/views/errors/401.pug | 16 + .../triangles/server/src/views/errors/403.pug | 16 + .../triangles/server/src/views/errors/404.pug | 11 + .../server/src/views/firstfactor.pug | 23 + .../server/src/views/layout/layout.pug | 28 + .../src/views/need-identity-validation.pug | 12 + .../server/src/views/password-reset-form.pug | 18 + .../src/views/password-reset-request.pug | 18 + .../server/src/views/secondfactor.pug | 31 + .../server/src/views/totp-register.pug | 25 + .../server/src/views/u2f-register.pug | 12 + themes/triangles/server/test/requests.ts | 94 + themes/triangles/server/tsconfig.json | 21 + themes/triangles/server/tslint.json | 60 + themes/triangles/server/types/.directory | 4 + .../server/types/AuthenticationSession.ts | 18 + themes/triangles/server/types/Dependencies.ts | 29 + themes/triangles/server/types/Identity.ts | 6 + themes/triangles/server/types/TOTPSecret.ts | 11 + .../triangles/server/types/U2FRegistration.ts | 5 + themes/triangles/server/types/dovehash.d.ts | 4 + themes/triangles/server/types/speakeasy.d.ts | 96 + 550 files changed, 37963 insertions(+), 3 deletions(-) create mode 100644 themes/squares/client/src/css/.directory create mode 100644 themes/squares/client/src/css/00-bootstrap.min.css create mode 100644 themes/squares/client/src/css/01-main.css create mode 100644 themes/squares/client/src/css/02-login.css create mode 100644 themes/squares/client/src/css/03-errors.css create mode 100644 themes/squares/client/src/css/03-password-reset-form.css create mode 100644 themes/squares/client/src/css/03-password-reset-request.css create mode 100644 themes/squares/client/src/css/03-totp-register.css create mode 100644 themes/squares/client/src/css/03-u2f-register.css create mode 100644 themes/squares/client/src/img/LargeTriangles.svg create mode 100644 themes/squares/client/src/img/RandomizedPattern.svg create mode 100644 themes/squares/client/src/img/background.jpg create mode 100644 themes/squares/client/src/img/background.svg create mode 100644 themes/squares/client/src/img/icon.png create mode 100644 themes/squares/client/src/img/mail.png create mode 100644 themes/squares/client/src/img/matrix_circle_128x128.png create mode 100644 themes/squares/client/src/img/notifications/.directory create mode 100644 themes/squares/client/src/img/notifications/error.png create mode 100644 themes/squares/client/src/img/notifications/info.png create mode 100644 themes/squares/client/src/img/notifications/success.png create mode 100644 themes/squares/client/src/img/notifications/warning.png create mode 100644 themes/squares/client/src/img/padlock.png create mode 100644 themes/squares/client/src/img/password_white.png create mode 100644 themes/squares/client/src/img/pendrive.png create mode 100644 themes/squares/client/src/img/sharingan.png create mode 100644 themes/squares/client/src/img/stores/.directory create mode 100644 themes/squares/client/src/img/stores/applestore-badge.svg create mode 100644 themes/squares/client/src/img/stores/googleplay-badge.svg create mode 100644 themes/squares/client/src/img/success.png create mode 100644 themes/squares/client/src/img/user.png create mode 100644 themes/squares/client/src/img/warning.png create mode 100644 themes/squares/client/src/index.ts create mode 100644 themes/squares/client/src/lib/GetPromised.ts create mode 100644 themes/squares/client/src/lib/INotifier.ts create mode 100644 themes/squares/client/src/lib/Notifier.ts create mode 100644 themes/squares/client/src/lib/QueryParametersRetriever.ts create mode 100644 themes/squares/client/src/lib/SafeRedirect.ts create mode 100644 themes/squares/client/src/lib/firstfactor/FirstFactorValidator.ts create mode 100644 themes/squares/client/src/lib/firstfactor/UISelectors.ts create mode 100644 themes/squares/client/src/lib/firstfactor/index.ts create mode 100644 themes/squares/client/src/lib/reset-password/constants.ts create mode 100644 themes/squares/client/src/lib/reset-password/reset-password-form.ts create mode 100644 themes/squares/client/src/lib/reset-password/reset-password-request.ts create mode 100644 themes/squares/client/src/lib/secondfactor/TOTPValidator.ts create mode 100644 themes/squares/client/src/lib/secondfactor/U2FValidator.ts create mode 100644 themes/squares/client/src/lib/secondfactor/constants.ts create mode 100644 themes/squares/client/src/lib/secondfactor/index.ts create mode 100644 themes/squares/client/src/lib/totp-register/totp-register.ts create mode 100644 themes/squares/client/src/lib/totp-register/ui-selector.ts create mode 100644 themes/squares/client/src/lib/u2f-register/u2f-register.ts create mode 100644 themes/squares/client/src/thirdparties/qrcode.min.js create mode 100644 themes/squares/client/src/thirdparties/u2f-api.js create mode 100644 themes/squares/client/test/Notifier.test.ts create mode 100644 themes/squares/client/test/firstfactor/FirstFactorValidator.test.ts create mode 100644 themes/squares/client/test/mocks/NotifierStub.ts create mode 100644 themes/squares/client/test/mocks/jquery.ts create mode 100644 themes/squares/client/test/mocks/u2f-api.ts create mode 100644 themes/squares/client/test/secondfactor/TOTPValidator.test.ts create mode 100644 themes/squares/client/test/totp-register/totp-register.test.ts create mode 100644 themes/squares/client/tsconfig.json create mode 100644 themes/squares/client/tslint.json create mode 100644 themes/squares/server/.directory create mode 100755 themes/squares/server/src/index.ts create mode 100644 themes/squares/server/src/lib/.directory create mode 100644 themes/squares/server/src/lib/AuthenticationSessionHandler.ts create mode 100644 themes/squares/server/src/lib/ErrorReplies.ts create mode 100644 themes/squares/server/src/lib/Exceptions.ts create mode 100644 themes/squares/server/src/lib/FirstFactorValidator.ts create mode 100644 themes/squares/server/src/lib/IdentityCheckMiddleware.spec.ts create mode 100644 themes/squares/server/src/lib/IdentityCheckMiddleware.ts create mode 100644 themes/squares/server/src/lib/IdentityCheckPreValidationTemplate.ts create mode 100644 themes/squares/server/src/lib/IdentityValidable.ts create mode 100644 themes/squares/server/src/lib/IdentityValidableStub.spec.ts create mode 100644 themes/squares/server/src/lib/Server.spec.ts create mode 100644 themes/squares/server/src/lib/Server.ts create mode 100644 themes/squares/server/src/lib/ServerVariables.ts create mode 100644 themes/squares/server/src/lib/ServerVariablesInitializer.ts create mode 100644 themes/squares/server/src/lib/ServerVariablesMockBuilder.spec.ts create mode 100644 themes/squares/server/src/lib/authentication/Level.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/GroupsAndEmails.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/IUsersDatabase.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/file/FileUsersDatabase.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/file/ReadWriteQueue.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/ISession.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/ISessionFactory.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/SafeSession.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/Sanitizer.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/Session.spec.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/Session.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/SessionFactory.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/connector/Connector.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/connector/IConnector.ts create mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts create mode 100644 themes/squares/server/src/lib/authentication/totp/ITotpHandler.ts create mode 100644 themes/squares/server/src/lib/authentication/totp/TotpHandler.spec.ts create mode 100644 themes/squares/server/src/lib/authentication/totp/TotpHandler.ts create mode 100644 themes/squares/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts create mode 100644 themes/squares/server/src/lib/authentication/u2f/IU2fHandler.ts create mode 100644 themes/squares/server/src/lib/authentication/u2f/U2fHandler.ts create mode 100644 themes/squares/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts create mode 100644 themes/squares/server/src/lib/authorization/Authorizer.spec.ts create mode 100644 themes/squares/server/src/lib/authorization/Authorizer.ts create mode 100644 themes/squares/server/src/lib/authorization/AuthorizerStub.spec.ts create mode 100644 themes/squares/server/src/lib/authorization/IAuthorizer.ts create mode 100644 themes/squares/server/src/lib/authorization/Level.ts create mode 100644 themes/squares/server/src/lib/authorization/MultipleDomainMatcher.ts create mode 100644 themes/squares/server/src/lib/authorization/Object.ts create mode 100644 themes/squares/server/src/lib/authorization/Subject.ts create mode 100644 themes/squares/server/src/lib/configuration/ConfigurationParser.spec.ts create mode 100644 themes/squares/server/src/lib/configuration/ConfigurationParser.ts create mode 100644 themes/squares/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts create mode 100644 themes/squares/server/src/lib/configuration/SessionConfigurationBuilder.ts create mode 100644 themes/squares/server/src/lib/configuration/schema/AclConfiguration.spec.ts create mode 100644 themes/squares/server/src/lib/configuration/schema/AclConfiguration.ts create mode 100644 themes/squares/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts create mode 100644 themes/squares/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts create mode 100644 themes/squares/server/src/lib/configuration/schema/Configuration.ts create mode 100644 themes/squares/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts create mode 100644 themes/squares/server/src/lib/configuration/schema/LdapConfiguration.spec.ts create mode 100644 themes/squares/server/src/lib/configuration/schema/LdapConfiguration.ts create mode 100644 themes/squares/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts create mode 100644 themes/squares/server/src/lib/configuration/schema/NotifierConfiguration.ts create mode 100644 themes/squares/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts create mode 100644 themes/squares/server/src/lib/configuration/schema/RegulationConfiguration.ts create mode 100644 themes/squares/server/src/lib/configuration/schema/SessionConfiguration.spec.ts create mode 100644 themes/squares/server/src/lib/configuration/schema/SessionConfiguration.ts create mode 100644 themes/squares/server/src/lib/configuration/schema/StorageConfiguration.spec.ts create mode 100644 themes/squares/server/src/lib/configuration/schema/StorageConfiguration.ts create mode 100644 themes/squares/server/src/lib/configuration/schema/TotpConfiguration.ts create mode 100644 themes/squares/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts create mode 100644 themes/squares/server/src/lib/connectors/mongo/IMongoClient.d.ts create mode 100644 themes/squares/server/src/lib/connectors/mongo/MongoClient.spec.ts create mode 100644 themes/squares/server/src/lib/connectors/mongo/MongoClient.ts create mode 100644 themes/squares/server/src/lib/connectors/mongo/MongoClientStub.spec.ts create mode 100644 themes/squares/server/src/lib/logging/GlobalLogger.ts create mode 100644 themes/squares/server/src/lib/logging/GlobalLoggerStub.spec.ts create mode 100644 themes/squares/server/src/lib/logging/IGlobalLogger.ts create mode 100644 themes/squares/server/src/lib/logging/IRequestLogger.ts create mode 100644 themes/squares/server/src/lib/logging/RequestLogger.ts create mode 100644 themes/squares/server/src/lib/logging/RequestLoggerStub.spec.ts create mode 100644 themes/squares/server/src/lib/notifiers/AbstractEmailNotifier.ts create mode 100644 themes/squares/server/src/lib/notifiers/EmailNotifier.spec.ts create mode 100644 themes/squares/server/src/lib/notifiers/EmailNotifier.ts create mode 100644 themes/squares/server/src/lib/notifiers/FileSystemNotifier.ts create mode 100644 themes/squares/server/src/lib/notifiers/IMailSender.ts create mode 100644 themes/squares/server/src/lib/notifiers/IMailSenderBuilder.ts create mode 100644 themes/squares/server/src/lib/notifiers/INotifier.ts create mode 100644 themes/squares/server/src/lib/notifiers/MailSender.ts create mode 100644 themes/squares/server/src/lib/notifiers/MailSenderBuilder.spec.ts create mode 100644 themes/squares/server/src/lib/notifiers/MailSenderBuilder.ts create mode 100644 themes/squares/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts create mode 100644 themes/squares/server/src/lib/notifiers/MailSenderStub.spec.ts create mode 100644 themes/squares/server/src/lib/notifiers/NotifierFactory.spec.ts create mode 100644 themes/squares/server/src/lib/notifiers/NotifierFactory.ts create mode 100644 themes/squares/server/src/lib/notifiers/NotifierStub.spec.ts create mode 100644 themes/squares/server/src/lib/notifiers/SmtpNotifier.ts create mode 100644 themes/squares/server/src/lib/regulation/IRegulator.ts create mode 100644 themes/squares/server/src/lib/regulation/Regulator.spec.ts create mode 100644 themes/squares/server/src/lib/regulation/Regulator.ts create mode 100644 themes/squares/server/src/lib/regulation/RegulatorStub.spec.ts create mode 100644 themes/squares/server/src/lib/routes/error/401/get.spec.ts create mode 100644 themes/squares/server/src/lib/routes/error/401/get.ts create mode 100644 themes/squares/server/src/lib/routes/error/403/get.spec.ts create mode 100644 themes/squares/server/src/lib/routes/error/403/get.ts create mode 100644 themes/squares/server/src/lib/routes/error/404/get.spec.ts create mode 100644 themes/squares/server/src/lib/routes/error/404/get.ts create mode 100644 themes/squares/server/src/lib/routes/error/redirector.ts create mode 100644 themes/squares/server/src/lib/routes/firstfactor/get.ts create mode 100644 themes/squares/server/src/lib/routes/firstfactor/post.spec.ts create mode 100644 themes/squares/server/src/lib/routes/firstfactor/post.ts create mode 100644 themes/squares/server/src/lib/routes/loggedin/get.ts create mode 100644 themes/squares/server/src/lib/routes/logout/get.ts create mode 100644 themes/squares/server/src/lib/routes/password-reset/constants.ts create mode 100644 themes/squares/server/src/lib/routes/password-reset/form/post.spec.ts create mode 100644 themes/squares/server/src/lib/routes/password-reset/form/post.ts create mode 100644 themes/squares/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts create mode 100644 themes/squares/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts create mode 100644 themes/squares/server/src/lib/routes/password-reset/request/get.ts create mode 100644 themes/squares/server/src/lib/routes/secondfactor/get.spec.ts create mode 100644 themes/squares/server/src/lib/routes/secondfactor/get.ts create mode 100644 themes/squares/server/src/lib/routes/secondfactor/redirect.spec.ts create mode 100644 themes/squares/server/src/lib/routes/secondfactor/redirect.ts create mode 100644 themes/squares/server/src/lib/routes/secondfactor/totp/constants.ts create mode 100644 themes/squares/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts create mode 100644 themes/squares/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts create mode 100644 themes/squares/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts create mode 100644 themes/squares/server/src/lib/routes/secondfactor/totp/sign/post.ts create mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts create mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts create mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts create mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts create mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/register/post.ts create mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts create mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/register_request/get.ts create mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts create mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/sign/post.ts create mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts create mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts create mode 100644 themes/squares/server/src/lib/routes/verify/access_control.ts create mode 100644 themes/squares/server/src/lib/routes/verify/get.spec.ts create mode 100644 themes/squares/server/src/lib/routes/verify/get.ts create mode 100644 themes/squares/server/src/lib/routes/verify/get_basic_auth.ts create mode 100644 themes/squares/server/src/lib/routes/verify/get_session_cookie.ts create mode 100644 themes/squares/server/src/lib/storage/AuthenticationTraceDocument.d.ts create mode 100644 themes/squares/server/src/lib/storage/CollectionFactoryFactory.ts create mode 100644 themes/squares/server/src/lib/storage/CollectionFactoryStub.spec.ts create mode 100644 themes/squares/server/src/lib/storage/CollectionStub.spec.ts create mode 100644 themes/squares/server/src/lib/storage/ICollection.d.ts create mode 100644 themes/squares/server/src/lib/storage/ICollectionFactory.d.ts create mode 100644 themes/squares/server/src/lib/storage/IUserDataStore.d.ts create mode 100644 themes/squares/server/src/lib/storage/IdentityValidationDocument.d.ts create mode 100644 themes/squares/server/src/lib/storage/TOTPSecretDocument.d.ts create mode 100644 themes/squares/server/src/lib/storage/U2FRegistrationDocument.d.ts create mode 100644 themes/squares/server/src/lib/storage/UserDataStore.spec.ts create mode 100644 themes/squares/server/src/lib/storage/UserDataStore.ts create mode 100644 themes/squares/server/src/lib/storage/UserDataStoreStub.spec.ts create mode 100644 themes/squares/server/src/lib/storage/mongo/MongoCollection.spec.ts create mode 100644 themes/squares/server/src/lib/storage/mongo/MongoCollection.ts create mode 100644 themes/squares/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts create mode 100644 themes/squares/server/src/lib/storage/mongo/MongoCollectionFactory.ts create mode 100644 themes/squares/server/src/lib/storage/nedb/NedbCollection.spec.ts create mode 100644 themes/squares/server/src/lib/storage/nedb/NedbCollection.ts create mode 100644 themes/squares/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts create mode 100644 themes/squares/server/src/lib/storage/nedb/NedbCollectionFactory.ts create mode 100644 themes/squares/server/src/lib/stubs/express.spec.ts create mode 100644 themes/squares/server/src/lib/stubs/ldapjs.spec.ts create mode 100644 themes/squares/server/src/lib/stubs/speakeasy.spec.ts create mode 100644 themes/squares/server/src/lib/stubs/u2f.spec.ts create mode 100644 themes/squares/server/src/lib/utils/HashGenerator.spec.ts create mode 100644 themes/squares/server/src/lib/utils/HashGenerator.ts create mode 100644 themes/squares/server/src/lib/utils/ObjectCloner.ts create mode 100644 themes/squares/server/src/lib/utils/SafeRedirection.spec.ts create mode 100644 themes/squares/server/src/lib/utils/SafeRedirection.ts create mode 100644 themes/squares/server/src/lib/utils/URLDecomposer.spec.ts create mode 100644 themes/squares/server/src/lib/utils/URLDecomposer.ts create mode 100644 themes/squares/server/src/lib/web_server/Configurator.ts create mode 100644 themes/squares/server/src/lib/web_server/RestApi.ts create mode 100644 themes/squares/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts create mode 100644 themes/squares/server/src/lib/web_server/middlewares/WithHeadersLogged.ts create mode 100644 themes/squares/server/src/resources/email-template.ejs create mode 100644 themes/squares/server/src/views/already-logged-in.pug create mode 100644 themes/squares/server/src/views/errors/.directory create mode 100644 themes/squares/server/src/views/errors/401.pug create mode 100644 themes/squares/server/src/views/errors/403.pug create mode 100644 themes/squares/server/src/views/errors/404.pug create mode 100644 themes/squares/server/src/views/firstfactor.pug create mode 100644 themes/squares/server/src/views/layout/layout.pug create mode 100644 themes/squares/server/src/views/need-identity-validation.pug create mode 100644 themes/squares/server/src/views/password-reset-form.pug create mode 100644 themes/squares/server/src/views/password-reset-request.pug create mode 100644 themes/squares/server/src/views/secondfactor.pug create mode 100644 themes/squares/server/src/views/totp-register.pug create mode 100644 themes/squares/server/src/views/u2f-register.pug create mode 100644 themes/squares/server/test/requests.ts create mode 100644 themes/squares/server/tsconfig.json create mode 100644 themes/squares/server/tslint.json create mode 100644 themes/squares/server/types/.directory create mode 100644 themes/squares/server/types/AuthenticationSession.ts create mode 100644 themes/squares/server/types/Dependencies.ts create mode 100644 themes/squares/server/types/Identity.ts create mode 100644 themes/squares/server/types/TOTPSecret.ts create mode 100644 themes/squares/server/types/U2FRegistration.ts create mode 100644 themes/squares/server/types/dovehash.d.ts create mode 100644 themes/squares/server/types/speakeasy.d.ts create mode 100644 themes/triangles/client/src/.directory create mode 100644 themes/triangles/client/src/css/.directory create mode 100644 themes/triangles/client/src/css/00-bootstrap.min.css create mode 100644 themes/triangles/client/src/css/01-main.css create mode 100644 themes/triangles/client/src/css/02-login.css create mode 100644 themes/triangles/client/src/css/03-errors.css create mode 100644 themes/triangles/client/src/css/03-password-reset-form.css create mode 100644 themes/triangles/client/src/css/03-password-reset-request.css create mode 100644 themes/triangles/client/src/css/03-totp-register.css create mode 100644 themes/triangles/client/src/css/03-u2f-register.css create mode 100644 themes/triangles/client/src/img/LargeTriangles.svg create mode 100644 themes/triangles/client/src/img/background.jpg create mode 100644 themes/triangles/client/src/img/icon.png create mode 100644 themes/triangles/client/src/img/mail.png create mode 100644 themes/triangles/client/src/img/matrix_circle_128x128.png create mode 100644 themes/triangles/client/src/img/notifications/.directory create mode 100644 themes/triangles/client/src/img/notifications/error.png create mode 100644 themes/triangles/client/src/img/notifications/info.png create mode 100644 themes/triangles/client/src/img/notifications/success.png create mode 100644 themes/triangles/client/src/img/notifications/warning.png create mode 100644 themes/triangles/client/src/img/padlock.png create mode 100644 themes/triangles/client/src/img/password_white.png create mode 100644 themes/triangles/client/src/img/pendrive.png create mode 100644 themes/triangles/client/src/img/sharingan.png create mode 100644 themes/triangles/client/src/img/stores/.directory create mode 100644 themes/triangles/client/src/img/stores/applestore-badge.svg create mode 100644 themes/triangles/client/src/img/stores/googleplay-badge.svg create mode 100644 themes/triangles/client/src/img/success.png create mode 100644 themes/triangles/client/src/img/user.png create mode 100644 themes/triangles/client/src/img/warning.png create mode 100644 themes/triangles/client/src/index.ts create mode 100644 themes/triangles/client/src/lib/GetPromised.ts create mode 100644 themes/triangles/client/src/lib/INotifier.ts create mode 100644 themes/triangles/client/src/lib/Notifier.ts create mode 100644 themes/triangles/client/src/lib/QueryParametersRetriever.ts create mode 100644 themes/triangles/client/src/lib/SafeRedirect.ts create mode 100644 themes/triangles/client/src/lib/firstfactor/FirstFactorValidator.ts create mode 100644 themes/triangles/client/src/lib/firstfactor/UISelectors.ts create mode 100644 themes/triangles/client/src/lib/firstfactor/index.ts create mode 100644 themes/triangles/client/src/lib/reset-password/constants.ts create mode 100644 themes/triangles/client/src/lib/reset-password/reset-password-form.ts create mode 100644 themes/triangles/client/src/lib/reset-password/reset-password-request.ts create mode 100644 themes/triangles/client/src/lib/secondfactor/TOTPValidator.ts create mode 100644 themes/triangles/client/src/lib/secondfactor/U2FValidator.ts create mode 100644 themes/triangles/client/src/lib/secondfactor/constants.ts create mode 100644 themes/triangles/client/src/lib/secondfactor/index.ts create mode 100644 themes/triangles/client/src/lib/totp-register/totp-register.ts create mode 100644 themes/triangles/client/src/lib/totp-register/ui-selector.ts create mode 100644 themes/triangles/client/src/lib/u2f-register/u2f-register.ts create mode 100644 themes/triangles/client/src/thirdparties/qrcode.min.js create mode 100644 themes/triangles/client/src/thirdparties/u2f-api.js create mode 100644 themes/triangles/client/test/Notifier.test.ts create mode 100644 themes/triangles/client/test/firstfactor/FirstFactorValidator.test.ts create mode 100644 themes/triangles/client/test/mocks/NotifierStub.ts create mode 100644 themes/triangles/client/test/mocks/jquery.ts create mode 100644 themes/triangles/client/test/mocks/u2f-api.ts create mode 100644 themes/triangles/client/test/secondfactor/TOTPValidator.test.ts create mode 100644 themes/triangles/client/test/totp-register/totp-register.test.ts create mode 100644 themes/triangles/client/tsconfig.json create mode 100644 themes/triangles/client/tslint.json create mode 100644 themes/triangles/server/.directory create mode 100755 themes/triangles/server/src/index.ts create mode 100644 themes/triangles/server/src/lib/.directory create mode 100644 themes/triangles/server/src/lib/AuthenticationSessionHandler.ts create mode 100644 themes/triangles/server/src/lib/ErrorReplies.ts create mode 100644 themes/triangles/server/src/lib/Exceptions.ts create mode 100644 themes/triangles/server/src/lib/FirstFactorValidator.ts create mode 100644 themes/triangles/server/src/lib/IdentityCheckMiddleware.spec.ts create mode 100644 themes/triangles/server/src/lib/IdentityCheckMiddleware.ts create mode 100644 themes/triangles/server/src/lib/IdentityCheckPreValidationTemplate.ts create mode 100644 themes/triangles/server/src/lib/IdentityValidable.ts create mode 100644 themes/triangles/server/src/lib/IdentityValidableStub.spec.ts create mode 100644 themes/triangles/server/src/lib/Server.spec.ts create mode 100644 themes/triangles/server/src/lib/Server.ts create mode 100644 themes/triangles/server/src/lib/ServerVariables.ts create mode 100644 themes/triangles/server/src/lib/ServerVariablesInitializer.ts create mode 100644 themes/triangles/server/src/lib/ServerVariablesMockBuilder.spec.ts create mode 100644 themes/triangles/server/src/lib/authentication/Level.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/GroupsAndEmails.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/IUsersDatabase.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/file/FileUsersDatabase.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/file/ReadWriteQueue.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/ISession.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/ISessionFactory.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/SafeSession.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/Sanitizer.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/Session.spec.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/Session.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/SessionFactory.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/connector/Connector.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/connector/IConnector.ts create mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts create mode 100644 themes/triangles/server/src/lib/authentication/totp/ITotpHandler.ts create mode 100644 themes/triangles/server/src/lib/authentication/totp/TotpHandler.spec.ts create mode 100644 themes/triangles/server/src/lib/authentication/totp/TotpHandler.ts create mode 100644 themes/triangles/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts create mode 100644 themes/triangles/server/src/lib/authentication/u2f/IU2fHandler.ts create mode 100644 themes/triangles/server/src/lib/authentication/u2f/U2fHandler.ts create mode 100644 themes/triangles/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts create mode 100644 themes/triangles/server/src/lib/authorization/Authorizer.spec.ts create mode 100644 themes/triangles/server/src/lib/authorization/Authorizer.ts create mode 100644 themes/triangles/server/src/lib/authorization/AuthorizerStub.spec.ts create mode 100644 themes/triangles/server/src/lib/authorization/IAuthorizer.ts create mode 100644 themes/triangles/server/src/lib/authorization/Level.ts create mode 100644 themes/triangles/server/src/lib/authorization/MultipleDomainMatcher.ts create mode 100644 themes/triangles/server/src/lib/authorization/Object.ts create mode 100644 themes/triangles/server/src/lib/authorization/Subject.ts create mode 100644 themes/triangles/server/src/lib/configuration/ConfigurationParser.spec.ts create mode 100644 themes/triangles/server/src/lib/configuration/ConfigurationParser.ts create mode 100644 themes/triangles/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts create mode 100644 themes/triangles/server/src/lib/configuration/SessionConfigurationBuilder.ts create mode 100644 themes/triangles/server/src/lib/configuration/schema/AclConfiguration.spec.ts create mode 100644 themes/triangles/server/src/lib/configuration/schema/AclConfiguration.ts create mode 100644 themes/triangles/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts create mode 100644 themes/triangles/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts create mode 100644 themes/triangles/server/src/lib/configuration/schema/Configuration.ts create mode 100644 themes/triangles/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts create mode 100644 themes/triangles/server/src/lib/configuration/schema/LdapConfiguration.spec.ts create mode 100644 themes/triangles/server/src/lib/configuration/schema/LdapConfiguration.ts create mode 100644 themes/triangles/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts create mode 100644 themes/triangles/server/src/lib/configuration/schema/NotifierConfiguration.ts create mode 100644 themes/triangles/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts create mode 100644 themes/triangles/server/src/lib/configuration/schema/RegulationConfiguration.ts create mode 100644 themes/triangles/server/src/lib/configuration/schema/SessionConfiguration.spec.ts create mode 100644 themes/triangles/server/src/lib/configuration/schema/SessionConfiguration.ts create mode 100644 themes/triangles/server/src/lib/configuration/schema/StorageConfiguration.spec.ts create mode 100644 themes/triangles/server/src/lib/configuration/schema/StorageConfiguration.ts create mode 100644 themes/triangles/server/src/lib/configuration/schema/TotpConfiguration.ts create mode 100644 themes/triangles/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts create mode 100644 themes/triangles/server/src/lib/connectors/mongo/IMongoClient.d.ts create mode 100644 themes/triangles/server/src/lib/connectors/mongo/MongoClient.spec.ts create mode 100644 themes/triangles/server/src/lib/connectors/mongo/MongoClient.ts create mode 100644 themes/triangles/server/src/lib/connectors/mongo/MongoClientStub.spec.ts create mode 100644 themes/triangles/server/src/lib/logging/GlobalLogger.ts create mode 100644 themes/triangles/server/src/lib/logging/GlobalLoggerStub.spec.ts create mode 100644 themes/triangles/server/src/lib/logging/IGlobalLogger.ts create mode 100644 themes/triangles/server/src/lib/logging/IRequestLogger.ts create mode 100644 themes/triangles/server/src/lib/logging/RequestLogger.ts create mode 100644 themes/triangles/server/src/lib/logging/RequestLoggerStub.spec.ts create mode 100644 themes/triangles/server/src/lib/notifiers/AbstractEmailNotifier.ts create mode 100644 themes/triangles/server/src/lib/notifiers/EmailNotifier.spec.ts create mode 100644 themes/triangles/server/src/lib/notifiers/EmailNotifier.ts create mode 100644 themes/triangles/server/src/lib/notifiers/FileSystemNotifier.ts create mode 100644 themes/triangles/server/src/lib/notifiers/IMailSender.ts create mode 100644 themes/triangles/server/src/lib/notifiers/IMailSenderBuilder.ts create mode 100644 themes/triangles/server/src/lib/notifiers/INotifier.ts create mode 100644 themes/triangles/server/src/lib/notifiers/MailSender.ts create mode 100644 themes/triangles/server/src/lib/notifiers/MailSenderBuilder.spec.ts create mode 100644 themes/triangles/server/src/lib/notifiers/MailSenderBuilder.ts create mode 100644 themes/triangles/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts create mode 100644 themes/triangles/server/src/lib/notifiers/MailSenderStub.spec.ts create mode 100644 themes/triangles/server/src/lib/notifiers/NotifierFactory.spec.ts create mode 100644 themes/triangles/server/src/lib/notifiers/NotifierFactory.ts create mode 100644 themes/triangles/server/src/lib/notifiers/NotifierStub.spec.ts create mode 100644 themes/triangles/server/src/lib/notifiers/SmtpNotifier.ts create mode 100644 themes/triangles/server/src/lib/regulation/IRegulator.ts create mode 100644 themes/triangles/server/src/lib/regulation/Regulator.spec.ts create mode 100644 themes/triangles/server/src/lib/regulation/Regulator.ts create mode 100644 themes/triangles/server/src/lib/regulation/RegulatorStub.spec.ts create mode 100644 themes/triangles/server/src/lib/routes/error/401/get.spec.ts create mode 100644 themes/triangles/server/src/lib/routes/error/401/get.ts create mode 100644 themes/triangles/server/src/lib/routes/error/403/get.spec.ts create mode 100644 themes/triangles/server/src/lib/routes/error/403/get.ts create mode 100644 themes/triangles/server/src/lib/routes/error/404/get.spec.ts create mode 100644 themes/triangles/server/src/lib/routes/error/404/get.ts create mode 100644 themes/triangles/server/src/lib/routes/error/redirector.ts create mode 100644 themes/triangles/server/src/lib/routes/firstfactor/get.ts create mode 100644 themes/triangles/server/src/lib/routes/firstfactor/post.spec.ts create mode 100644 themes/triangles/server/src/lib/routes/firstfactor/post.ts create mode 100644 themes/triangles/server/src/lib/routes/loggedin/get.ts create mode 100644 themes/triangles/server/src/lib/routes/logout/get.ts create mode 100644 themes/triangles/server/src/lib/routes/password-reset/constants.ts create mode 100644 themes/triangles/server/src/lib/routes/password-reset/form/post.spec.ts create mode 100644 themes/triangles/server/src/lib/routes/password-reset/form/post.ts create mode 100644 themes/triangles/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts create mode 100644 themes/triangles/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts create mode 100644 themes/triangles/server/src/lib/routes/password-reset/request/get.ts create mode 100644 themes/triangles/server/src/lib/routes/secondfactor/get.spec.ts create mode 100644 themes/triangles/server/src/lib/routes/secondfactor/get.ts create mode 100644 themes/triangles/server/src/lib/routes/secondfactor/redirect.spec.ts create mode 100644 themes/triangles/server/src/lib/routes/secondfactor/redirect.ts create mode 100644 themes/triangles/server/src/lib/routes/secondfactor/totp/constants.ts create mode 100644 themes/triangles/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts create mode 100644 themes/triangles/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts create mode 100644 themes/triangles/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts create mode 100644 themes/triangles/server/src/lib/routes/secondfactor/totp/sign/post.ts create mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts create mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts create mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts create mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts create mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/register/post.ts create mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts create mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/register_request/get.ts create mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts create mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/sign/post.ts create mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts create mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts create mode 100644 themes/triangles/server/src/lib/routes/verify/access_control.ts create mode 100644 themes/triangles/server/src/lib/routes/verify/get.spec.ts create mode 100644 themes/triangles/server/src/lib/routes/verify/get.ts create mode 100644 themes/triangles/server/src/lib/routes/verify/get_basic_auth.ts create mode 100644 themes/triangles/server/src/lib/routes/verify/get_session_cookie.ts create mode 100644 themes/triangles/server/src/lib/storage/AuthenticationTraceDocument.d.ts create mode 100644 themes/triangles/server/src/lib/storage/CollectionFactoryFactory.ts create mode 100644 themes/triangles/server/src/lib/storage/CollectionFactoryStub.spec.ts create mode 100644 themes/triangles/server/src/lib/storage/CollectionStub.spec.ts create mode 100644 themes/triangles/server/src/lib/storage/ICollection.d.ts create mode 100644 themes/triangles/server/src/lib/storage/ICollectionFactory.d.ts create mode 100644 themes/triangles/server/src/lib/storage/IUserDataStore.d.ts create mode 100644 themes/triangles/server/src/lib/storage/IdentityValidationDocument.d.ts create mode 100644 themes/triangles/server/src/lib/storage/TOTPSecretDocument.d.ts create mode 100644 themes/triangles/server/src/lib/storage/U2FRegistrationDocument.d.ts create mode 100644 themes/triangles/server/src/lib/storage/UserDataStore.spec.ts create mode 100644 themes/triangles/server/src/lib/storage/UserDataStore.ts create mode 100644 themes/triangles/server/src/lib/storage/UserDataStoreStub.spec.ts create mode 100644 themes/triangles/server/src/lib/storage/mongo/MongoCollection.spec.ts create mode 100644 themes/triangles/server/src/lib/storage/mongo/MongoCollection.ts create mode 100644 themes/triangles/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts create mode 100644 themes/triangles/server/src/lib/storage/mongo/MongoCollectionFactory.ts create mode 100644 themes/triangles/server/src/lib/storage/nedb/NedbCollection.spec.ts create mode 100644 themes/triangles/server/src/lib/storage/nedb/NedbCollection.ts create mode 100644 themes/triangles/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts create mode 100644 themes/triangles/server/src/lib/storage/nedb/NedbCollectionFactory.ts create mode 100644 themes/triangles/server/src/lib/stubs/express.spec.ts create mode 100644 themes/triangles/server/src/lib/stubs/ldapjs.spec.ts create mode 100644 themes/triangles/server/src/lib/stubs/speakeasy.spec.ts create mode 100644 themes/triangles/server/src/lib/stubs/u2f.spec.ts create mode 100644 themes/triangles/server/src/lib/utils/HashGenerator.spec.ts create mode 100644 themes/triangles/server/src/lib/utils/HashGenerator.ts create mode 100644 themes/triangles/server/src/lib/utils/ObjectCloner.ts create mode 100644 themes/triangles/server/src/lib/utils/SafeRedirection.spec.ts create mode 100644 themes/triangles/server/src/lib/utils/SafeRedirection.ts create mode 100644 themes/triangles/server/src/lib/utils/URLDecomposer.spec.ts create mode 100644 themes/triangles/server/src/lib/utils/URLDecomposer.ts create mode 100644 themes/triangles/server/src/lib/web_server/Configurator.ts create mode 100644 themes/triangles/server/src/lib/web_server/RestApi.ts create mode 100644 themes/triangles/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts create mode 100644 themes/triangles/server/src/lib/web_server/middlewares/WithHeadersLogged.ts create mode 100644 themes/triangles/server/src/resources/email-template.ejs create mode 100644 themes/triangles/server/src/views/already-logged-in.pug create mode 100644 themes/triangles/server/src/views/errors/.directory create mode 100644 themes/triangles/server/src/views/errors/401.pug create mode 100644 themes/triangles/server/src/views/errors/403.pug create mode 100644 themes/triangles/server/src/views/errors/404.pug create mode 100644 themes/triangles/server/src/views/firstfactor.pug create mode 100644 themes/triangles/server/src/views/layout/layout.pug create mode 100644 themes/triangles/server/src/views/need-identity-validation.pug create mode 100644 themes/triangles/server/src/views/password-reset-form.pug create mode 100644 themes/triangles/server/src/views/password-reset-request.pug create mode 100644 themes/triangles/server/src/views/secondfactor.pug create mode 100644 themes/triangles/server/src/views/totp-register.pug create mode 100644 themes/triangles/server/src/views/u2f-register.pug create mode 100644 themes/triangles/server/test/requests.ts create mode 100644 themes/triangles/server/tsconfig.json create mode 100644 themes/triangles/server/tslint.json create mode 100644 themes/triangles/server/types/.directory create mode 100644 themes/triangles/server/types/AuthenticationSession.ts create mode 100644 themes/triangles/server/types/Dependencies.ts create mode 100644 themes/triangles/server/types/Identity.ts create mode 100644 themes/triangles/server/types/TOTPSecret.ts create mode 100644 themes/triangles/server/types/U2FRegistration.ts create mode 100644 themes/triangles/server/types/dovehash.d.ts create mode 100644 themes/triangles/server/types/speakeasy.d.ts diff --git a/Gruntfile.js b/Gruntfile.js index 1509cc90c..fcf4a5731 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -155,6 +155,54 @@ module.exports = function (grunt) { src: '**', dest: `${buildDir}/server/src/public_html/js/` }, + squares_resources: { + expand: true, + cwd: 'themes/squares/server/src/resources', + src: '**', + dest: `${buildDir}/server/src/resources/` + }, + squares_views: { + expand: true, + cwd: 'themes/squares/server/src/views', + src: '**', + dest: `${buildDir}/server/src/views/` + }, + squares_images: { + expand: true, + cwd: 'themes/squares/client/src/img', + src: '**', + dest: `${buildDir}/server/src/public_html/img/` + }, + squares_thirdparties: { + expand: true, + cwd: 'themes/squares/client/src/thirdparties', + src: '**', + dest: `${buildDir}/server/src/public_html/js/` + }, + triangles_resources: { + expand: true, + cwd: 'themes/triangles/server/src/resources', + src: '**', + dest: `${buildDir}/server/src/resources/` + }, + triangles_views: { + expand: true, + cwd: 'themes/triangles/server/src/views', + src: '**', + dest: `${buildDir}/server/src/views/` + }, + triangles_images: { + expand: true, + cwd: 'themes/triangles/client/src/img', + src: '**', + dest: `${buildDir}/server/src/public_html/img/` + }, + triangles_thirdparties: { + expand: true, + cwd: 'themes/triangles/client/src/thirdparties', + src: '**', + dest: `${buildDir}/server/src/public_html/js/` + }, schema: { src: schemaDir, dest: `${buildDir}/${schemaDir}` @@ -234,6 +282,14 @@ module.exports = function (grunt) { src: ['themes/black/client/src/css/*.css'], dest: `${buildDir}/server/src/public_html/css/authelia.css` }, + squares_css: { + src: ['themes/squares/client/src/css/*.css'], + dest: `${buildDir}/server/src/public_html/css/authelia.css` + }, + triangles_css: { + src: ['themes/triangles/client/src/css/*.css'], + dest: `${buildDir}/server/src/public_html/css/authelia.css` + }, }, cssmin: { target: { @@ -266,18 +322,24 @@ module.exports = function (grunt) { grunt.registerTask('test-int', ['run:test-cucumber', 'run:test-minimal-config', 'run:test-complete-config', 'run:test-inactivity']); 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('copy-resources-matrix', ['copy:matrix_resources', 'copy:matrix_views', 'copy:matrix_images', 'copy:matrix_thirdparties', 'concat:matrix_css']); grunt.registerTask('copy-resources-black', ['copy:black_resources', 'copy:black_views', 'copy:black_images', 'copy:black_thirdparties', 'concat:black_css']); + + grunt.registerTask('copy-resources-squares', ['copy:squares_resources', 'copy:squares_views', 'copy:squares_images', 'copy:squares_thirdparties', 'concat:squares_css']); + + grunt.registerTask('copy-resources-triangles', ['copy:triangles_resources', 'copy:triangles_views', 'copy:triangles_images', 'copy:triangles_thirdparties', 'concat:triangles_css']); + + grunt.registerTask('generate-config-schema', ['run:generate-config-schema', 'copy:schema']); 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-server-black', ['compile-server', 'copy-resources-black', 'generate-config-schema']); - + grunt.registerTask('build-server-squares', ['compile-server', 'copy-resources-squares', 'generate-config-schema']); + grunt.registerTask('build-server-triangles', ['compile-server', 'copy-resources-triangles', 'generate-config-schema']); + grunt.registerTask('build', ['build-client', 'build-server-'+target]); grunt.registerTask('build-dist', ['clean', 'build', 'run:minify', 'cssmin', 'run:include-minified-script']); diff --git a/themes/squares/client/src/css/.directory b/themes/squares/client/src/css/.directory new file mode 100644 index 000000000..6e4b3f639 --- /dev/null +++ b/themes/squares/client/src/css/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,56,41 +Version=3 +ViewMode=1 diff --git a/themes/squares/client/src/css/00-bootstrap.min.css b/themes/squares/client/src/css/00-bootstrap.min.css new file mode 100644 index 000000000..dfeacbb88 --- /dev/null +++ b/themes/squares/client/src/css/00-bootstrap.min.css @@ -0,0 +1,5768 @@ +/*! * Bootstrap v3.3.7 (http://getbootstrap.com) * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ +html{ + font-family:sans-serif; + -webkit-text-size-adjust:100%; + -ms-text-size-adjust:100% +} +body{ + margin:0 +} +article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{ + display:block +} +audio,canvas,progress,video{ + display:inline-block; + vertical-align:baseline +} +audio:not([controls]){ + display:none; + height:0 +} +[hidden],template{ + display:none +} +a{ + background-color:transparent +} +a:active,a:hover{ + outline:0 +} +abbr[title]{ + border-bottom:1px dotted +} +b,strong{ + font-weight:700 +} +dfn{ + font-style:italic +} +h1{ + margin:.67em 0; + font-size:2em +} +mark{ + color:#000; + background:#ff0 +} +small{ + font-size:80% +} +sub,sup{ + position:relative; + font-size:75%; + line-height:0; + vertical-align:baseline +} +sup{ + top:-.5em +} +sub{ + bottom:-.25em +} +img{ + border:0 +} +svg:not(:root){ + overflow:hidden +} +figure{ + margin:1em 40px +} +hr{ + height:0; + -webkit-box-sizing:content-box; + -moz-box-sizing:content-box; + box-sizing:content-box +} +pre{ + overflow:auto +} +code,kbd,pre,samp{ + font-family:monospace,monospace; + font-size:1em +} +button,input,optgroup,select,textarea{ + margin:0; + font:inherit; + color:inherit +} +button{ + overflow:visible +} +button,select{ + text-transform:none +} +button,html input[type=button],input[type=reset],input[type=submit]{ + -webkit-appearance:button; + cursor:pointer +} +button[disabled],html input[disabled]{ + cursor:default +} +button::-moz-focus-inner,input::-moz-focus-inner{ + padding:0; + border:0 +} +input{ + line-height:normal +} +input[type=checkbox],input[type=radio]{ + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + padding:0 +} +input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{ + height:auto +} +input[type=search]{ + -webkit-box-sizing:content-box; + -moz-box-sizing:content-box; + box-sizing:content-box; + -webkit-appearance:textfield +} +input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{ + -webkit-appearance:none +} +fieldset{ + padding:.35em .625em .75em; + margin:0 2px; + border:1px solid silver +} +legend{ + padding:0; + border:0 +} +textarea{ + overflow:auto +} +optgroup{ + font-weight:700 +} +table{ + border-spacing:0; + border-collapse:collapse +} +td,th{ + padding:0 +} +/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ +@media print{ + *,:after,:before{ + color:#000!important; + text-shadow:none!important; + background:0 0!important; + -webkit-box-shadow:none!important; + box-shadow:none!important + } + a,a:visited{ + text-decoration:underline + } + a[href]:after{ + content:" (" attr(href) ")" + } + abbr[title]:after{ + content:" (" attr(title) ")" + } + a[href^="javascript:"]:after,a[href^="#"]:after{ + content:"" + } + blockquote,pre{ + border:1px solid #999; + page-break-inside:avoid + } + thead{ + display:table-header-group + } + img,tr{ + page-break-inside:avoid + } + img{ + max-width:100%!important + } + h2,h3,p{ + orphans:3; + widows:3 + } + h2,h3{ + page-break-after:avoid + } + .navbar{ + display:none + } + .btn>.caret,.dropup>.btn>.caret{ + border-top-color:#000!important + } + .label{ + border:1px solid #000 + } + .table{ + border-collapse:collapse!important + } + .table td,.table th{ + background-color:#fff!important + } + .table-bordered td,.table-bordered th{ + border:1px solid #ddd!important + } +} +@font-face{ + font-family:'Glyphicons Halflings'; + src:url(../fonts/glyphicons-halflings-regular.eot); + src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg') +} +.glyphicon{ + position:relative; + top:1px; + display:inline-block; + font-family:'Glyphicons Halflings'; + font-style:normal; + font-weight:400; + line-height:1; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale +} +.glyphicon-asterisk:before{ + content:"\002a" +} +.glyphicon-plus:before{ + content:"\002b" +} +.glyphicon-eur:before,.glyphicon-euro:before{ + content:"\20ac" +} +.glyphicon-minus:before{ + content:"\2212" +} +.glyphicon-cloud:before{ + content:"\2601" +} +.glyphicon-envelope:before{ + content:"\2709" +} +.glyphicon-pencil:before{ + content:"\270f" +} +.glyphicon-glass:before{ + content:"\e001" +} +.glyphicon-music:before{ + content:"\e002" +} +.glyphicon-search:before{ + content:"\e003" +} +.glyphicon-heart:before{ + content:"\e005" +} +.glyphicon-star:before{ + content:"\e006" +} +.glyphicon-star-empty:before{ + content:"\e007" +} +.glyphicon-user:before{ + content:"\e008" +} +.glyphicon-film:before{ + content:"\e009" +} +.glyphicon-th-large:before{ + content:"\e010" +} +.glyphicon-th:before{ + content:"\e011" +} +.glyphicon-th-list:before{ + content:"\e012" +} +.glyphicon-ok:before{ + content:"\e013" +} +.glyphicon-remove:before{ + content:"\e014" +} +.glyphicon-zoom-in:before{ + content:"\e015" +} +.glyphicon-zoom-out:before{ + content:"\e016" +} +.glyphicon-off:before{ + content:"\e017" +} +.glyphicon-signal:before{ + content:"\e018" +} +.glyphicon-cog:before{ + content:"\e019" +} +.glyphicon-trash:before{ + content:"\e020" +} +.glyphicon-home:before{ + content:"\e021" +} +.glyphicon-file:before{ + content:"\e022" +} +.glyphicon-time:before{ + content:"\e023" +} +.glyphicon-road:before{ + content:"\e024" +} +.glyphicon-download-alt:before{ + content:"\e025" +} +.glyphicon-download:before{ + content:"\e026" +} +.glyphicon-upload:before{ + content:"\e027" +} +.glyphicon-inbox:before{ + content:"\e028" +} +.glyphicon-play-circle:before{ + content:"\e029" +} +.glyphicon-repeat:before{ + content:"\e030" +} +.glyphicon-refresh:before{ + content:"\e031" +} +.glyphicon-list-alt:before{ + content:"\e032" +} +.glyphicon-lock:before{ + content:"\e033" +} +.glyphicon-flag:before{ + content:"\e034" +} +.glyphicon-headphones:before{ + content:"\e035" +} +.glyphicon-volume-off:before{ + content:"\e036" +} +.glyphicon-volume-down:before{ + content:"\e037" +} +.glyphicon-volume-up:before{ + content:"\e038" +} +.glyphicon-qrcode:before{ + content:"\e039" +} +.glyphicon-barcode:before{ + content:"\e040" +} +.glyphicon-tag:before{ + content:"\e041" +} +.glyphicon-tags:before{ + content:"\e042" +} +.glyphicon-book:before{ + content:"\e043" +} +.glyphicon-bookmark:before{ + content:"\e044" +} +.glyphicon-print:before{ + content:"\e045" +} +.glyphicon-camera:before{ + content:"\e046" +} +.glyphicon-font:before{ + content:"\e047" +} +.glyphicon-bold:before{ + content:"\e048" +} +.glyphicon-italic:before{ + content:"\e049" +} +.glyphicon-text-height:before{ + content:"\e050" +} +.glyphicon-text-width:before{ + content:"\e051" +} +.glyphicon-align-left:before{ + content:"\e052" +} +.glyphicon-align-center:before{ + content:"\e053" +} +.glyphicon-align-right:before{ + content:"\e054" +} +.glyphicon-align-justify:before{ + content:"\e055" +} +.glyphicon-list:before{ + content:"\e056" +} +.glyphicon-indent-left:before{ + content:"\e057" +} +.glyphicon-indent-right:before{ + content:"\e058" +} +.glyphicon-facetime-video:before{ + content:"\e059" +} +.glyphicon-picture:before{ + content:"\e060" +} +.glyphicon-map-marker:before{ + content:"\e062" +} +.glyphicon-adjust:before{ + content:"\e063" +} +.glyphicon-tint:before{ + content:"\e064" +} +.glyphicon-edit:before{ + content:"\e065" +} +.glyphicon-share:before{ + content:"\e066" +} +.glyphicon-check:before{ + content:"\e067" +} +.glyphicon-move:before{ + content:"\e068" +} +.glyphicon-step-backward:before{ + content:"\e069" +} +.glyphicon-fast-backward:before{ + content:"\e070" +} +.glyphicon-backward:before{ + content:"\e071" +} +.glyphicon-play:before{ + content:"\e072" +} +.glyphicon-pause:before{ + content:"\e073" +} +.glyphicon-stop:before{ + content:"\e074" +} +.glyphicon-forward:before{ + content:"\e075" +} +.glyphicon-fast-forward:before{ + content:"\e076" +} +.glyphicon-step-forward:before{ + content:"\e077" +} +.glyphicon-eject:before{ + content:"\e078" +} +.glyphicon-chevron-left:before{ + content:"\e079" +} +.glyphicon-chevron-right:before{ + content:"\e080" +} +.glyphicon-plus-sign:before{ + content:"\e081" +} +.glyphicon-minus-sign:before{ + content:"\e082" +} +.glyphicon-remove-sign:before{ + content:"\e083" +} +.glyphicon-ok-sign:before{ + content:"\e084" +} +.glyphicon-question-sign:before{ + content:"\e085" +} +.glyphicon-info-sign:before{ + content:"\e086" +} +.glyphicon-screenshot:before{ + content:"\e087" +} +.glyphicon-remove-circle:before{ + content:"\e088" +} +.glyphicon-ok-circle:before{ + content:"\e089" +} +.glyphicon-ban-circle:before{ + content:"\e090" +} +.glyphicon-arrow-left:before{ + content:"\e091" +} +.glyphicon-arrow-right:before{ + content:"\e092" +} +.glyphicon-arrow-up:before{ + content:"\e093" +} +.glyphicon-arrow-down:before{ + content:"\e094" +} +.glyphicon-share-alt:before{ + content:"\e095" +} +.glyphicon-resize-full:before{ + content:"\e096" +} +.glyphicon-resize-small:before{ + content:"\e097" +} +.glyphicon-exclamation-sign:before{ + content:"\e101" +} +.glyphicon-gift:before{ + content:"\e102" +} +.glyphicon-leaf:before{ + content:"\e103" +} +.glyphicon-fire:before{ + content:"\e104" +} +.glyphicon-eye-open:before{ + content:"\e105" +} +.glyphicon-eye-close:before{ + content:"\e106" +} +.glyphicon-warning-sign:before{ + content:"\e107" +} +.glyphicon-plane:before{ + content:"\e108" +} +.glyphicon-calendar:before{ + content:"\e109" +} +.glyphicon-random:before{ + content:"\e110" +} +.glyphicon-comment:before{ + content:"\e111" +} +.glyphicon-magnet:before{ + content:"\e112" +} +.glyphicon-chevron-up:before{ + content:"\e113" +} +.glyphicon-chevron-down:before{ + content:"\e114" +} +.glyphicon-retweet:before{ + content:"\e115" +} +.glyphicon-shopping-cart:before{ + content:"\e116" +} +.glyphicon-folder-close:before{ + content:"\e117" +} +.glyphicon-folder-open:before{ + content:"\e118" +} +.glyphicon-resize-vertical:before{ + content:"\e119" +} +.glyphicon-resize-horizontal:before{ + content:"\e120" +} +.glyphicon-hdd:before{ + content:"\e121" +} +.glyphicon-bullhorn:before{ + content:"\e122" +} +.glyphicon-bell:before{ + content:"\e123" +} +.glyphicon-certificate:before{ + content:"\e124" +} +.glyphicon-thumbs-up:before{ + content:"\e125" +} +.glyphicon-thumbs-down:before{ + content:"\e126" +} +.glyphicon-hand-right:before{ + content:"\e127" +} +.glyphicon-hand-left:before{ + content:"\e128" +} +.glyphicon-hand-up:before{ + content:"\e129" +} +.glyphicon-hand-down:before{ + content:"\e130" +} +.glyphicon-circle-arrow-right:before{ + content:"\e131" +} +.glyphicon-circle-arrow-left:before{ + content:"\e132" +} +.glyphicon-circle-arrow-up:before{ + content:"\e133" +} +.glyphicon-circle-arrow-down:before{ + content:"\e134" +} +.glyphicon-globe:before{ + content:"\e135" +} +.glyphicon-wrench:before{ + content:"\e136" +} +.glyphicon-tasks:before{ + content:"\e137" +} +.glyphicon-filter:before{ + content:"\e138" +} +.glyphicon-briefcase:before{ + content:"\e139" +} +.glyphicon-fullscreen:before{ + content:"\e140" +} +.glyphicon-dashboard:before{ + content:"\e141" +} +.glyphicon-paperclip:before{ + content:"\e142" +} +.glyphicon-heart-empty:before{ + content:"\e143" +} +.glyphicon-link:before{ + content:"\e144" +} +.glyphicon-phone:before{ + content:"\e145" +} +.glyphicon-pushpin:before{ + content:"\e146" +} +.glyphicon-usd:before{ + content:"\e148" +} +.glyphicon-gbp:before{ + content:"\e149" +} +.glyphicon-sort:before{ + content:"\e150" +} +.glyphicon-sort-by-alphabet:before{ + content:"\e151" +} +.glyphicon-sort-by-alphabet-alt:before{ + content:"\e152" +} +.glyphicon-sort-by-order:before{ + content:"\e153" +} +.glyphicon-sort-by-order-alt:before{ + content:"\e154" +} +.glyphicon-sort-by-attributes:before{ + content:"\e155" +} +.glyphicon-sort-by-attributes-alt:before{ + content:"\e156" +} +.glyphicon-unchecked:before{ + content:"\e157" +} +.glyphicon-expand:before{ + content:"\e158" +} +.glyphicon-collapse-down:before{ + content:"\e159" +} +.glyphicon-collapse-up:before{ + content:"\e160" +} +.glyphicon-log-in:before{ + content:"\e161" +} +.glyphicon-flash:before{ + content:"\e162" +} +.glyphicon-log-out:before{ + content:"\e163" +} +.glyphicon-new-window:before{ + content:"\e164" +} +.glyphicon-record:before{ + content:"\e165" +} +.glyphicon-save:before{ + content:"\e166" +} +.glyphicon-open:before{ + content:"\e167" +} +.glyphicon-saved:before{ + content:"\e168" +} +.glyphicon-import:before{ + content:"\e169" +} +.glyphicon-export:before{ + content:"\e170" +} +.glyphicon-send:before{ + content:"\e171" +} +.glyphicon-floppy-disk:before{ + content:"\e172" +} +.glyphicon-floppy-saved:before{ + content:"\e173" +} +.glyphicon-floppy-remove:before{ + content:"\e174" +} +.glyphicon-floppy-save:before{ + content:"\e175" +} +.glyphicon-floppy-open:before{ + content:"\e176" +} +.glyphicon-credit-card:before{ + content:"\e177" +} +.glyphicon-transfer:before{ + content:"\e178" +} +.glyphicon-cutlery:before{ + content:"\e179" +} +.glyphicon-header:before{ + content:"\e180" +} +.glyphicon-compressed:before{ + content:"\e181" +} +.glyphicon-earphone:before{ + content:"\e182" +} +.glyphicon-phone-alt:before{ + content:"\e183" +} +.glyphicon-tower:before{ + content:"\e184" +} +.glyphicon-stats:before{ + content:"\e185" +} +.glyphicon-sd-video:before{ + content:"\e186" +} +.glyphicon-hd-video:before{ + content:"\e187" +} +.glyphicon-subtitles:before{ + content:"\e188" +} +.glyphicon-sound-stereo:before{ + content:"\e189" +} +.glyphicon-sound-dolby:before{ + content:"\e190" +} +.glyphicon-sound-5-1:before{ + content:"\e191" +} +.glyphicon-sound-6-1:before{ + content:"\e192" +} +.glyphicon-sound-7-1:before{ + content:"\e193" +} +.glyphicon-copyright-mark:before{ + content:"\e194" +} +.glyphicon-registration-mark:before{ + content:"\e195" +} +.glyphicon-cloud-download:before{ + content:"\e197" +} +.glyphicon-cloud-upload:before{ + content:"\e198" +} +.glyphicon-tree-conifer:before{ + content:"\e199" +} +.glyphicon-tree-deciduous:before{ + content:"\e200" +} +.glyphicon-cd:before{ + content:"\e201" +} +.glyphicon-save-file:before{ + content:"\e202" +} +.glyphicon-open-file:before{ + content:"\e203" +} +.glyphicon-level-up:before{ + content:"\e204" +} +.glyphicon-copy:before{ + content:"\e205" +} +.glyphicon-paste:before{ + content:"\e206" +} +.glyphicon-alert:before{ + content:"\e209" +} +.glyphicon-equalizer:before{ + content:"\e210" +} +.glyphicon-king:before{ + content:"\e211" +} +.glyphicon-queen:before{ + content:"\e212" +} +.glyphicon-pawn:before{ + content:"\e213" +} +.glyphicon-bishop:before{ + content:"\e214" +} +.glyphicon-knight:before{ + content:"\e215" +} +.glyphicon-baby-formula:before{ + content:"\e216" +} +.glyphicon-tent:before{ + content:"\26fa" +} +.glyphicon-blackboard:before{ + content:"\e218" +} +.glyphicon-bed:before{ + content:"\e219" +} +.glyphicon-apple:before{ + content:"\f8ff" +} +.glyphicon-erase:before{ + content:"\e221" +} +.glyphicon-hourglass:before{ + content:"\231b" +} +.glyphicon-lamp:before{ + content:"\e223" +} +.glyphicon-duplicate:before{ + content:"\e224" +} +.glyphicon-piggy-bank:before{ + content:"\e225" +} +.glyphicon-scissors:before{ + content:"\e226" +} +.glyphicon-bitcoin:before{ + content:"\e227" +} +.glyphicon-btc:before{ + content:"\e227" +} +.glyphicon-xbt:before{ + content:"\e227" +} +.glyphicon-yen:before{ + content:"\00a5" +} +.glyphicon-jpy:before{ + content:"\00a5" +} +.glyphicon-ruble:before{ + content:"\20bd" +} +.glyphicon-rub:before{ + content:"\20bd" +} +.glyphicon-scale:before{ + content:"\e230" +} +.glyphicon-ice-lolly:before{ + content:"\e231" +} +.glyphicon-ice-lolly-tasted:before{ + content:"\e232" +} +.glyphicon-education:before{ + content:"\e233" +} +.glyphicon-option-horizontal:before{ + content:"\e234" +} +.glyphicon-option-vertical:before{ + content:"\e235" +} +.glyphicon-menu-hamburger:before{ + content:"\e236" +} +.glyphicon-modal-window:before{ + content:"\e237" +} +.glyphicon-oil:before{ + content:"\e238" +} +.glyphicon-grain:before{ + content:"\e239" +} +.glyphicon-sunglasses:before{ + content:"\e240" +} +.glyphicon-text-size:before{ + content:"\e241" +} +.glyphicon-text-color:before{ + content:"\e242" +} +.glyphicon-text-background:before{ + content:"\e243" +} +.glyphicon-object-align-top:before{ + content:"\e244" +} +.glyphicon-object-align-bottom:before{ + content:"\e245" +} +.glyphicon-object-align-horizontal:before{ + content:"\e246" +} +.glyphicon-object-align-left:before{ + content:"\e247" +} +.glyphicon-object-align-vertical:before{ + content:"\e248" +} +.glyphicon-object-align-right:before{ + content:"\e249" +} +.glyphicon-triangle-right:before{ + content:"\e250" +} +.glyphicon-triangle-left:before{ + content:"\e251" +} +.glyphicon-triangle-bottom:before{ + content:"\e252" +} +.glyphicon-triangle-top:before{ + content:"\e253" +} +.glyphicon-console:before{ + content:"\e254" +} +.glyphicon-superscript:before{ + content:"\e255" +} +.glyphicon-subscript:before{ + content:"\e256" +} +.glyphicon-menu-left:before{ + content:"\e257" +} +.glyphicon-menu-right:before{ + content:"\e258" +} +.glyphicon-menu-down:before{ + content:"\e259" +} +.glyphicon-menu-up:before{ + content:"\e260" +} +*{ + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box +} +:after,:before{ + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box +} +html{ + font-size:10px; + -webkit-tap-highlight-color:rgba(0,0,0,0) +} +body{ + font-family:"Helvetica Neue",Helvetica,Arial,sans-serif; + font-size:14px; + line-height:1.42857143; + color:#333; + background-color:#fff +} +button,input,select,textarea{ + font-family:inherit; + font-size:inherit; + line-height:inherit +} +a{ + color:#337ab7; + text-decoration:none +} +a:focus,a:hover{ + color:#23527c; + text-decoration:underline +} +a:focus{ + outline:5px auto -webkit-focus-ring-color; + outline-offset:-2px +} +figure{ + margin:0 +} +img{ + vertical-align:middle +} +.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{ + display:block; + max-width:100%; + height:auto +} +.img-rounded{ + border-radius:6px +} +.img-thumbnail{ + display:inline-block; + max-width:100%; + height:auto; + padding:4px; + line-height:1.42857143; + background-color:#fff; + border:1px solid #ddd; + border-radius:4px; + -webkit-transition:all .2s ease-in-out; + -o-transition:all .2s ease-in-out; + transition:all .2s ease-in-out +} +.img-circle{ + border-radius:50% +} +hr{ + margin-top:20px; + margin-bottom:20px; + border:0; + border-top:1px solid #eee +} +.sr-only{ + position:absolute; + width:1px; + height:1px; + padding:0; + margin:-1px; + overflow:hidden; + clip:rect(0,0,0,0); + border:0 +} +.sr-only-focusable:active,.sr-only-focusable:focus{ + position:static; + width:auto; + height:auto; + margin:0; + overflow:visible; + clip:auto +} +[role=button]{ + cursor:pointer +} +.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{ + font-family:inherit; + font-weight:500; + line-height:1.1; + color:inherit +} +.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{ + font-weight:400; + line-height:1; + color:#777 +} +.h1,.h2,.h3,h1,h2,h3{ + margin-top:20px; + margin-bottom:10px +} +.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{ + font-size:65% +} +.h4,.h5,.h6,h4,h5,h6{ + margin-top:10px; + margin-bottom:10px +} +.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{ + font-size:75% +} +.h1,h1{ + font-size:36px +} +.h2,h2{ + font-size:30px +} +.h3,h3{ + font-size:24px +} +.h4,h4{ + font-size:18px +} +.h5,h5{ + font-size:14px +} +.h6,h6{ + font-size:12px +} +p{ + margin:0 0 10px +} +.lead{ + margin-bottom:20px; + font-size:16px; + font-weight:300; + line-height:1.4 +} +@media (min-width:768px){ + .lead{ + font-size:21px + } +} +.small,small{ + font-size:85% +} +.mark,mark{ + padding:.2em; + background-color:#fcf8e3 +} +.text-left{ + text-align:left +} +.text-right{ + text-align:right +} +.text-center{ + text-align:center +} +.text-justify{ + text-align:justify +} +.text-nowrap{ + white-space:nowrap +} +.text-lowercase{ + text-transform:lowercase +} +.text-uppercase{ + text-transform:uppercase +} +.text-capitalize{ + text-transform:capitalize +} +.text-muted{ + color:#777 +} +.text-primary{ + color:#337ab7 +} +a.text-primary:focus,a.text-primary:hover{ + color:#286090 +} +.text-success{ + color:#3c763d +} +a.text-success:focus,a.text-success:hover{ + color:#2b542c +} +.text-info{ + color:#31708f +} +a.text-info:focus,a.text-info:hover{ + color:#245269 +} +.text-warning{ + color:#8a6d3b +} +a.text-warning:focus,a.text-warning:hover{ + color:#66512c +} +.text-danger{ + color:#a94442 +} +a.text-danger:focus,a.text-danger:hover{ + color:#843534 +} +.bg-primary{ + color:#fff; + background-color:#337ab7 +} +a.bg-primary:focus,a.bg-primary:hover{ + background-color:#286090 +} +.bg-success{ + background-color:#dff0d8 +} +a.bg-success:focus,a.bg-success:hover{ + background-color:#c1e2b3 +} +.bg-info{ + background-color:#d9edf7 +} +a.bg-info:focus,a.bg-info:hover{ + background-color:#afd9ee +} +.bg-warning{ + background-color:#fcf8e3 +} +a.bg-warning:focus,a.bg-warning:hover{ + background-color:#f7ecb5 +} +.bg-danger{ + background-color:#f2dede +} +a.bg-danger:focus,a.bg-danger:hover{ + background-color:#e4b9b9 +} +.page-header{ + padding-bottom:9px; + margin:40px 0 20px; + border-bottom:1px solid #eee +} +ol,ul{ + margin-top:0; + margin-bottom:10px +} +ol ol,ol ul,ul ol,ul ul{ + margin-bottom:0 +} +.list-unstyled{ + padding-left:0; + list-style:none +} +.list-inline{ + padding-left:0; + margin-left:-5px; + list-style:none +} +.list-inline>li{ + display:inline-block; + padding-right:5px; + padding-left:5px +} +dl{ + margin-top:0; + margin-bottom:20px +} +dd,dt{ + line-height:1.42857143 +} +dt{ + font-weight:700 +} +dd{ + margin-left:0 +} +@media (min-width:768px){ + .dl-horizontal dt{ + float:left; + width:160px; + overflow:hidden; + clear:left; + text-align:right; + text-overflow:ellipsis; + white-space:nowrap + } + .dl-horizontal dd{ + margin-left:180px + } +} +abbr[data-original-title],abbr[title]{ + cursor:help; + border-bottom:1px dotted #777 +} +.initialism{ + font-size:90%; + text-transform:uppercase +} +blockquote{ + padding:10px 20px; + margin:0 0 20px; + font-size:17.5px; + border-left:5px solid #eee +} +blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{ + margin-bottom:0 +} +blockquote .small,blockquote footer,blockquote small{ + display:block; + font-size:80%; + line-height:1.42857143; + color:#777 +} +blockquote .small:before,blockquote footer:before,blockquote small:before{ + content:'\2014 \00A0' +} +.blockquote-reverse,blockquote.pull-right{ + padding-right:15px; + padding-left:0; + text-align:right; + border-right:5px solid #eee; + border-left:0 +} +.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{ + content:'' +} +.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{ + content:'\00A0 \2014' +} +address{ + margin-bottom:20px; + font-style:normal; + line-height:1.42857143 +} +code,kbd,pre,samp{ + font-family:Menlo,Monaco,Consolas,"Courier New",monospace +} +code{ + padding:2px 4px; + font-size:90%; + color:#c7254e; + background-color:#f9f2f4; + border-radius:4px +} +kbd{ + padding:2px 4px; + font-size:90%; + color:#fff; + background-color:#333; + border-radius:3px; + -webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25); + box-shadow:inset 0 -1px 0 rgba(0,0,0,.25) +} +kbd kbd{ + padding:0; + font-size:100%; + font-weight:700; + -webkit-box-shadow:none; + box-shadow:none +} +pre{ + display:block; + padding:9.5px; + margin:0 0 10px; + font-size:13px; + line-height:1.42857143; + color:#333; + word-break:break-all; + word-wrap:break-word; + background-color:#f5f5f5; + border:1px solid #ccc; + border-radius:4px +} +pre code{ + padding:0; + font-size:inherit; + color:inherit; + white-space:pre-wrap; + background-color:transparent; + border-radius:0 +} +.pre-scrollable{ + max-height:340px; + overflow-y:scroll +} +.container{ + padding-right:15px; + padding-left:15px; + margin-right:auto; + margin-left:auto +} +@media (min-width:768px){ + .container{ + width:750px + } +} +@media (min-width:992px){ + .container{ + width:970px + } +} +@media (min-width:1200px){ + .container{ + width:1170px + } +} +.container-fluid{ + padding-right:15px; + padding-left:15px; + margin-right:auto; + margin-left:auto +} +.row{ + margin-right:-15px; + margin-left:-15px +} +.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{ + position:relative; + min-height:1px; + padding-right:15px; + padding-left:15px +} +.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{ + float:left +} +.col-xs-12{ + width:100% +} +.col-xs-11{ + width:91.66666667% +} +.col-xs-10{ + width:83.33333333% +} +.col-xs-9{ + width:75% +} +.col-xs-8{ + width:66.66666667% +} +.col-xs-7{ + width:58.33333333% +} +.col-xs-6{ + width:50% +} +.col-xs-5{ + width:41.66666667% +} +.col-xs-4{ + width:33.33333333% +} +.col-xs-3{ + width:25% +} +.col-xs-2{ + width:16.66666667% +} +.col-xs-1{ + width:8.33333333% +} +.col-xs-pull-12{ + right:100% +} +.col-xs-pull-11{ + right:91.66666667% +} +.col-xs-pull-10{ + right:83.33333333% +} +.col-xs-pull-9{ + right:75% +} +.col-xs-pull-8{ + right:66.66666667% +} +.col-xs-pull-7{ + right:58.33333333% +} +.col-xs-pull-6{ + right:50% +} +.col-xs-pull-5{ + right:41.66666667% +} +.col-xs-pull-4{ + right:33.33333333% +} +.col-xs-pull-3{ + right:25% +} +.col-xs-pull-2{ + right:16.66666667% +} +.col-xs-pull-1{ + right:8.33333333% +} +.col-xs-pull-0{ + right:auto +} +.col-xs-push-12{ + left:100% +} +.col-xs-push-11{ + left:91.66666667% +} +.col-xs-push-10{ + left:83.33333333% +} +.col-xs-push-9{ + left:75% +} +.col-xs-push-8{ + left:66.66666667% +} +.col-xs-push-7{ + left:58.33333333% +} +.col-xs-push-6{ + left:50% +} +.col-xs-push-5{ + left:41.66666667% +} +.col-xs-push-4{ + left:33.33333333% +} +.col-xs-push-3{ + left:25% +} +.col-xs-push-2{ + left:16.66666667% +} +.col-xs-push-1{ + left:8.33333333% +} +.col-xs-push-0{ + left:auto +} +.col-xs-offset-12{ + margin-left:100% +} +.col-xs-offset-11{ + margin-left:91.66666667% +} +.col-xs-offset-10{ + margin-left:83.33333333% +} +.col-xs-offset-9{ + margin-left:75% +} +.col-xs-offset-8{ + margin-left:66.66666667% +} +.col-xs-offset-7{ + margin-left:58.33333333% +} +.col-xs-offset-6{ + margin-left:50% +} +.col-xs-offset-5{ + margin-left:41.66666667% +} +.col-xs-offset-4{ + margin-left:33.33333333% +} +.col-xs-offset-3{ + margin-left:25% +} +.col-xs-offset-2{ + margin-left:16.66666667% +} +.col-xs-offset-1{ + margin-left:8.33333333% +} +.col-xs-offset-0{ + margin-left:0 +} +@media (min-width:768px){ + .col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{ + float:left + } + .col-sm-12{ + width:100% + } + .col-sm-11{ + width:91.66666667% + } + .col-sm-10{ + width:83.33333333% + } + .col-sm-9{ + width:75% + } + .col-sm-8{ + width:66.66666667% + } + .col-sm-7{ + width:58.33333333% + } + .col-sm-6{ + width:50% + } + .col-sm-5{ + width:41.66666667% + } + .col-sm-4{ + width:33.33333333% + } + .col-sm-3{ + width:25% + } + .col-sm-2{ + width:16.66666667% + } + .col-sm-1{ + width:8.33333333% + } + .col-sm-pull-12{ + right:100% + } + .col-sm-pull-11{ + right:91.66666667% + } + .col-sm-pull-10{ + right:83.33333333% + } + .col-sm-pull-9{ + right:75% + } + .col-sm-pull-8{ + right:66.66666667% + } + .col-sm-pull-7{ + right:58.33333333% + } + .col-sm-pull-6{ + right:50% + } + .col-sm-pull-5{ + right:41.66666667% + } + .col-sm-pull-4{ + right:33.33333333% + } + .col-sm-pull-3{ + right:25% + } + .col-sm-pull-2{ + right:16.66666667% + } + .col-sm-pull-1{ + right:8.33333333% + } + .col-sm-pull-0{ + right:auto + } + .col-sm-push-12{ + left:100% + } + .col-sm-push-11{ + left:91.66666667% + } + .col-sm-push-10{ + left:83.33333333% + } + .col-sm-push-9{ + left:75% + } + .col-sm-push-8{ + left:66.66666667% + } + .col-sm-push-7{ + left:58.33333333% + } + .col-sm-push-6{ + left:50% + } + .col-sm-push-5{ + left:41.66666667% + } + .col-sm-push-4{ + left:33.33333333% + } + .col-sm-push-3{ + left:25% + } + .col-sm-push-2{ + left:16.66666667% + } + .col-sm-push-1{ + left:8.33333333% + } + .col-sm-push-0{ + left:auto + } + .col-sm-offset-12{ + margin-left:100% + } + .col-sm-offset-11{ + margin-left:91.66666667% + } + .col-sm-offset-10{ + margin-left:83.33333333% + } + .col-sm-offset-9{ + margin-left:75% + } + .col-sm-offset-8{ + margin-left:66.66666667% + } + .col-sm-offset-7{ + margin-left:58.33333333% + } + .col-sm-offset-6{ + margin-left:50% + } + .col-sm-offset-5{ + margin-left:41.66666667% + } + .col-sm-offset-4{ + margin-left:33.33333333% + } + .col-sm-offset-3{ + margin-left:25% + } + .col-sm-offset-2{ + margin-left:16.66666667% + } + .col-sm-offset-1{ + margin-left:8.33333333% + } + .col-sm-offset-0{ + margin-left:0 + } +} +@media (min-width:992px){ + .col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{ + float:left + } + .col-md-12{ + width:100% + } + .col-md-11{ + width:91.66666667% + } + .col-md-10{ + width:83.33333333% + } + .col-md-9{ + width:75% + } + .col-md-8{ + width:66.66666667% + } + .col-md-7{ + width:58.33333333% + } + .col-md-6{ + width:50% + } + .col-md-5{ + width:41.66666667% + } + .col-md-4{ + width:33.33333333% + } + .col-md-3{ + width:25% + } + .col-md-2{ + width:16.66666667% + } + .col-md-1{ + width:8.33333333% + } + .col-md-pull-12{ + right:100% + } + .col-md-pull-11{ + right:91.66666667% + } + .col-md-pull-10{ + right:83.33333333% + } + .col-md-pull-9{ + right:75% + } + .col-md-pull-8{ + right:66.66666667% + } + .col-md-pull-7{ + right:58.33333333% + } + .col-md-pull-6{ + right:50% + } + .col-md-pull-5{ + right:41.66666667% + } + .col-md-pull-4{ + right:33.33333333% + } + .col-md-pull-3{ + right:25% + } + .col-md-pull-2{ + right:16.66666667% + } + .col-md-pull-1{ + right:8.33333333% + } + .col-md-pull-0{ + right:auto + } + .col-md-push-12{ + left:100% + } + .col-md-push-11{ + left:91.66666667% + } + .col-md-push-10{ + left:83.33333333% + } + .col-md-push-9{ + left:75% + } + .col-md-push-8{ + left:66.66666667% + } + .col-md-push-7{ + left:58.33333333% + } + .col-md-push-6{ + left:50% + } + .col-md-push-5{ + left:41.66666667% + } + .col-md-push-4{ + left:33.33333333% + } + .col-md-push-3{ + left:25% + } + .col-md-push-2{ + left:16.66666667% + } + .col-md-push-1{ + left:8.33333333% + } + .col-md-push-0{ + left:auto + } + .col-md-offset-12{ + margin-left:100% + } + .col-md-offset-11{ + margin-left:91.66666667% + } + .col-md-offset-10{ + margin-left:83.33333333% + } + .col-md-offset-9{ + margin-left:75% + } + .col-md-offset-8{ + margin-left:66.66666667% + } + .col-md-offset-7{ + margin-left:58.33333333% + } + .col-md-offset-6{ + margin-left:50% + } + .col-md-offset-5{ + margin-left:41.66666667% + } + .col-md-offset-4{ + margin-left:33.33333333% + } + .col-md-offset-3{ + margin-left:25% + } + .col-md-offset-2{ + margin-left:16.66666667% + } + .col-md-offset-1{ + margin-left:8.33333333% + } + .col-md-offset-0{ + margin-left:0 + } +} +@media (min-width:1200px){ + .col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{ + float:left + } + .col-lg-12{ + width:100% + } + .col-lg-11{ + width:91.66666667% + } + .col-lg-10{ + width:83.33333333% + } + .col-lg-9{ + width:75% + } + .col-lg-8{ + width:66.66666667% + } + .col-lg-7{ + width:58.33333333% + } + .col-lg-6{ + width:50% + } + .col-lg-5{ + width:41.66666667% + } + .col-lg-4{ + width:33.33333333% + } + .col-lg-3{ + width:25% + } + .col-lg-2{ + width:16.66666667% + } + .col-lg-1{ + width:8.33333333% + } + .col-lg-pull-12{ + right:100% + } + .col-lg-pull-11{ + right:91.66666667% + } + .col-lg-pull-10{ + right:83.33333333% + } + .col-lg-pull-9{ + right:75% + } + .col-lg-pull-8{ + right:66.66666667% + } + .col-lg-pull-7{ + right:58.33333333% + } + .col-lg-pull-6{ + right:50% + } + .col-lg-pull-5{ + right:41.66666667% + } + .col-lg-pull-4{ + right:33.33333333% + } + .col-lg-pull-3{ + right:25% + } + .col-lg-pull-2{ + right:16.66666667% + } + .col-lg-pull-1{ + right:8.33333333% + } + .col-lg-pull-0{ + right:auto + } + .col-lg-push-12{ + left:100% + } + .col-lg-push-11{ + left:91.66666667% + } + .col-lg-push-10{ + left:83.33333333% + } + .col-lg-push-9{ + left:75% + } + .col-lg-push-8{ + left:66.66666667% + } + .col-lg-push-7{ + left:58.33333333% + } + .col-lg-push-6{ + left:50% + } + .col-lg-push-5{ + left:41.66666667% + } + .col-lg-push-4{ + left:33.33333333% + } + .col-lg-push-3{ + left:25% + } + .col-lg-push-2{ + left:16.66666667% + } + .col-lg-push-1{ + left:8.33333333% + } + .col-lg-push-0{ + left:auto + } + .col-lg-offset-12{ + margin-left:100% + } + .col-lg-offset-11{ + margin-left:91.66666667% + } + .col-lg-offset-10{ + margin-left:83.33333333% + } + .col-lg-offset-9{ + margin-left:75% + } + .col-lg-offset-8{ + margin-left:66.66666667% + } + .col-lg-offset-7{ + margin-left:58.33333333% + } + .col-lg-offset-6{ + margin-left:50% + } + .col-lg-offset-5{ + margin-left:41.66666667% + } + .col-lg-offset-4{ + margin-left:33.33333333% + } + .col-lg-offset-3{ + margin-left:25% + } + .col-lg-offset-2{ + margin-left:16.66666667% + } + .col-lg-offset-1{ + margin-left:8.33333333% + } + .col-lg-offset-0{ + margin-left:0 + } +} +table{ + background-color:transparent +} +caption{ + padding-top:8px; + padding-bottom:8px; + color:#777; + text-align:left +} +th{ + text-align:left +} +.table{ + width:100%; + max-width:100%; + margin-bottom:20px +} +.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{ + padding:8px; + line-height:1.42857143; + vertical-align:top; + border-top:1px solid #ddd +} +.table>thead>tr>th{ + vertical-align:bottom; + border-bottom:2px solid #ddd +} +.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{ + border-top:0 +} +.table>tbody+tbody{ + border-top:2px solid #ddd +} +.table .table{ + background-color:#fff +} +.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{ + padding:5px +} +.table-bordered{ + border:1px solid #ddd +} +.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{ + border:1px solid #ddd +} +.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{ + border-bottom-width:2px +} +.table-striped>tbody>tr:nth-of-type(odd){ + background-color:#f9f9f9 +} +.table-hover>tbody>tr:hover{ + background-color:#f5f5f5 +} +table col[class*=col-]{ + position:static; + display:table-column; + float:none +} +table td[class*=col-],table th[class*=col-]{ + position:static; + display:table-cell; + float:none +} +.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{ + background-color:#f5f5f5 +} +.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{ + background-color:#e8e8e8 +} +.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{ + background-color:#dff0d8 +} +.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{ + background-color:#d0e9c6 +} +.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{ + background-color:#d9edf7 +} +.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{ + background-color:#c4e3f3 +} +.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{ + background-color:#fcf8e3 +} +.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{ + background-color:#faf2cc +} +.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{ + background-color:#f2dede +} +.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{ + background-color:#ebcccc +} +.table-responsive{ + min-height:.01%; + overflow-x:auto +} +@media screen and (max-width:767px){ + .table-responsive{ + width:100%; + margin-bottom:15px; + overflow-y:hidden; + -ms-overflow-style:-ms-autohiding-scrollbar; + border:1px solid #ddd + } + .table-responsive>.table{ + margin-bottom:0 + } + .table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{ + white-space:nowrap + } + .table-responsive>.table-bordered{ + border:0 + } + .table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{ + border-left:0 + } + .table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{ + border-right:0 + } + .table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{ + border-bottom:0 + } +} +fieldset{ + min-width:0; + padding:0; + margin:0; + border:0 +} +legend{ + display:block; + width:100%; + padding:0; + margin-bottom:20px; + font-size:21px; + line-height:inherit; + color:#333; + border:0; + border-bottom:1px solid #e5e5e5 +} +label{ + display:inline-block; + max-width:100%; + margin-bottom:5px; + font-weight:700 +} +input[type=search]{ + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box +} +input[type=checkbox],input[type=radio]{ + margin:4px 0 0; + margin-top:1px\9; + line-height:normal +} +input[type=file]{ + display:block +} +input[type=range]{ + display:block; + width:100% +} +select[multiple],select[size]{ + height:auto +} +input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{ + outline:5px auto -webkit-focus-ring-color; + outline-offset:-2px +} +output{ + display:block; + padding-top:7px; + font-size:14px; + line-height:1.42857143; + color:#555 +} +.form-control{ + display:block; + width:100%; + height:34px; + padding:6px 12px; + font-size:14px; + line-height:1.42857143; + color:#555; + background-color:#fff; + background-image:none; + border:1px solid #ccc; + border-radius:4px; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075); + box-shadow:inset 0 1px 1px rgba(0,0,0,.075); + -webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s; + -o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s; + transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s +} +.form-control:focus{ + border-color:#66afe9; + outline:0; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6); + box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6) +} +.form-control::-moz-placeholder{ + color:#999; + opacity:1 +} +.form-control:-ms-input-placeholder{ + color:#999 +} +.form-control::-webkit-input-placeholder{ + color:#999 +} +.form-control::-ms-expand{ + background-color:transparent; + border:0 +} +.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{ + background-color:#eee; + opacity:1 +} +.form-control[disabled],fieldset[disabled] .form-control{ + cursor:not-allowed +} +textarea.form-control{ + height:auto +} +input[type=search]{ + -webkit-appearance:none +} +@media screen and (-webkit-min-device-pixel-ratio:0){ + input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{ + line-height:34px + } + .input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{ + line-height:30px + } + .input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{ + line-height:46px + } +} +.form-group{ + margin-bottom:15px +} +.checkbox,.radio{ + position:relative; + display:block; + margin-top:10px; + margin-bottom:10px +} +.checkbox label,.radio label{ + min-height:20px; + padding-left:20px; + margin-bottom:0; + font-weight:400; + cursor:pointer +} +.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{ + position:absolute; + margin-top:4px\9; + margin-left:-20px +} +.checkbox+.checkbox,.radio+.radio{ + margin-top:-5px +} +.checkbox-inline,.radio-inline{ + position:relative; + display:inline-block; + padding-left:20px; + margin-bottom:0; + font-weight:400; + vertical-align:middle; + cursor:pointer +} +.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{ + margin-top:0; + margin-left:10px +} +fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{ + cursor:not-allowed +} +.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{ + cursor:not-allowed +} +.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{ + cursor:not-allowed +} +.form-control-static{ + min-height:34px; + padding-top:7px; + padding-bottom:7px; + margin-bottom:0 +} +.form-control-static.input-lg,.form-control-static.input-sm{ + padding-right:0; + padding-left:0 +} +.input-sm{ + height:30px; + padding:5px 10px; + font-size:12px; + line-height:1.5; + border-radius:3px +} +select.input-sm{ + height:30px; + line-height:30px +} +select[multiple].input-sm,textarea.input-sm{ + height:auto +} +.form-group-sm .form-control{ + height:30px; + padding:5px 10px; + font-size:12px; + line-height:1.5; + border-radius:3px +} +.form-group-sm select.form-control{ + height:30px; + line-height:30px +} +.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{ + height:auto +} +.form-group-sm .form-control-static{ + height:30px; + min-height:32px; + padding:6px 10px; + font-size:12px; + line-height:1.5 +} +.input-lg{ + height:46px; + padding:10px 16px; + font-size:18px; + line-height:1.3333333; + border-radius:6px +} +select.input-lg{ + height:46px; + line-height:46px +} +select[multiple].input-lg,textarea.input-lg{ + height:auto +} +.form-group-lg .form-control{ + height:46px; + padding:10px 16px; + font-size:18px; + line-height:1.3333333; + border-radius:6px +} +.form-group-lg select.form-control{ + height:46px; + line-height:46px +} +.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{ + height:auto +} +.form-group-lg .form-control-static{ + height:46px; + min-height:38px; + padding:11px 16px; + font-size:18px; + line-height:1.3333333 +} +.has-feedback{ + position:relative +} +.has-feedback .form-control{ + padding-right:42.5px +} +.form-control-feedback{ + position:absolute; + top:0; + right:0; + z-index:2; + display:block; + width:34px; + height:34px; + line-height:34px; + text-align:center; + pointer-events:none +} +.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{ + width:46px; + height:46px; + line-height:46px +} +.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{ + width:30px; + height:30px; + line-height:30px +} +.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{ + color:#3c763d +} +.has-success .form-control{ + border-color:#3c763d; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075); + box-shadow:inset 0 1px 1px rgba(0,0,0,.075) +} +.has-success .form-control:focus{ + border-color:#2b542c; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168; + box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168 +} +.has-success .input-group-addon{ + color:#3c763d; + background-color:#dff0d8; + border-color:#3c763d +} +.has-success .form-control-feedback{ + color:#3c763d +} +.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{ + color:#8a6d3b +} +.has-warning .form-control{ + border-color:#8a6d3b; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075); + box-shadow:inset 0 1px 1px rgba(0,0,0,.075) +} +.has-warning .form-control:focus{ + border-color:#66512c; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b; + box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b +} +.has-warning .input-group-addon{ + color:#8a6d3b; + background-color:#fcf8e3; + border-color:#8a6d3b +} +.has-warning .form-control-feedback{ + color:#8a6d3b +} +.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{ + color:#a94442 +} +.has-error .form-control{ + border-color:#a94442; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075); + box-shadow:inset 0 1px 1px rgba(0,0,0,.075) +} +.has-error .form-control:focus{ + border-color:#843534; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483; + box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483 +} +.has-error .input-group-addon{ + color:#a94442; + background-color:#f2dede; + border-color:#a94442 +} +.has-error .form-control-feedback{ + color:#a94442 +} +.has-feedback label~.form-control-feedback{ + top:25px +} +.has-feedback label.sr-only~.form-control-feedback{ + top:0 +} +.help-block{ + display:block; + margin-top:5px; + margin-bottom:10px; + color:#737373 +} +@media (min-width:768px){ + .form-inline .form-group{ + display:inline-block; + margin-bottom:0; + vertical-align:middle + } + .form-inline .form-control{ + display:inline-block; + width:auto; + vertical-align:middle + } + .form-inline .form-control-static{ + display:inline-block + } + .form-inline .input-group{ + display:inline-table; + vertical-align:middle + } + .form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{ + width:auto + } + .form-inline .input-group>.form-control{ + width:100% + } + .form-inline .control-label{ + margin-bottom:0; + vertical-align:middle + } + .form-inline .checkbox,.form-inline .radio{ + display:inline-block; + margin-top:0; + margin-bottom:0; + vertical-align:middle + } + .form-inline .checkbox label,.form-inline .radio label{ + padding-left:0 + } + .form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{ + position:relative; + margin-left:0 + } + .form-inline .has-feedback .form-control-feedback{ + top:0 + } +} +.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{ + padding-top:7px; + margin-top:0; + margin-bottom:0 +} +.form-horizontal .checkbox,.form-horizontal .radio{ + min-height:27px +} +.form-horizontal .form-group{ + margin-right:-15px; + margin-left:-15px +} +@media (min-width:768px){ + .form-horizontal .control-label{ + padding-top:7px; + margin-bottom:0; + text-align:right + } +} +.form-horizontal .has-feedback .form-control-feedback{ + right:15px +} +@media (min-width:768px){ + .form-horizontal .form-group-lg .control-label{ + padding-top:11px; + font-size:18px + } +} +@media (min-width:768px){ + .form-horizontal .form-group-sm .control-label{ + padding-top:6px; + font-size:12px + } +} +.btn{ + display:inline-block; + padding:6px 12px; + margin-bottom:0; + font-size:14px; + font-weight:400; + line-height:1.42857143; + text-align:center; + white-space:nowrap; + vertical-align:middle; + -ms-touch-action:manipulation; + touch-action:manipulation; + cursor:pointer; + -webkit-user-select:none; + -moz-user-select:none; + -ms-user-select:none; + user-select:none; + background-image:none; + border:1px solid transparent; + border-radius:4px +} +.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{ + outline:5px auto -webkit-focus-ring-color; + outline-offset:-2px +} +.btn.focus,.btn:focus,.btn:hover{ + color:#333; + text-decoration:none +} +.btn.active,.btn:active{ + background-image:none; + outline:0; + -webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125); + box-shadow:inset 0 3px 5px rgba(0,0,0,.125) +} +.btn.disabled,.btn[disabled],fieldset[disabled] .btn{ + cursor:not-allowed; + filter:alpha(opacity=65); + -webkit-box-shadow:none; + box-shadow:none; + opacity:.65 +} +a.btn.disabled,fieldset[disabled] a.btn{ + pointer-events:none +} +.btn-default{ + color:#333; + background-color:#fff; + border-color:#ccc +} +.btn-default.focus,.btn-default:focus{ + color:#333; + background-color:#e6e6e6; + border-color:#8c8c8c +} +.btn-default:hover{ + color:#333; + background-color:#e6e6e6; + border-color:#adadad +} +.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{ + color:#333; + background-color:#e6e6e6; + border-color:#adadad +} +.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{ + color:#333; + background-color:#d4d4d4; + border-color:#8c8c8c +} +.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{ + background-image:none +} +.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{ + background-color:#fff; + border-color:#ccc +} +.btn-default .badge{ + color:#fff; + background-color:#333 +} +.btn-primary{ + color:#fff; + background-color:#d11010; + border-color:#c40f0f +} +.btn-primary.focus,.btn-primary:focus{ + color:#fff; + background-color:#b20c0c; + border-color:#c40f0f +} +.btn-primary:hover{ + color:#fff; + background-color:#b20c0c; + border-color:#c40f0f +} +.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{ + color:#fff; + background-color:#b20c0c; + border-color:#c40f0f +} +.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{ + color:#fff; + background-color:#b20c0c; + border-color:#c40f0f +} +.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{ + background-image:none +} +.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{ + background-color:#b20c0c; + border-color:#c40f0f +} +.btn-primary .badge{ + color:#337ab7; + background-color:#fff +} +.btn-success{ + color:#fff; + background-color:#5cb85c; + border-color:#4cae4c +} +.btn-success.focus,.btn-success:focus{ + color:#fff; + background-color:#449d44; + border-color:#255625 +} +.btn-success:hover{ + color:#fff; + background-color:#449d44; + border-color:#398439 +} +.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{ + color:#fff; + background-color:#449d44; + border-color:#398439 +} +.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{ + color:#fff; + background-color:#398439; + border-color:#255625 +} +.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{ + background-image:none +} +.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{ + background-color:#5cb85c; + border-color:#4cae4c +} +.btn-success .badge{ + color:#5cb85c; + background-color:#fff +} +.btn-info{ + color:#fff; + background-color:#5bc0de; + border-color:#46b8da +} +.btn-info.focus,.btn-info:focus{ + color:#fff; + background-color:#31b0d5; + border-color:#1b6d85 +} +.btn-info:hover{ + color:#fff; + background-color:#31b0d5; + border-color:#269abc +} +.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{ + color:#fff; + background-color:#31b0d5; + border-color:#269abc +} +.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{ + color:#fff; + background-color:#269abc; + border-color:#1b6d85 +} +.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{ + background-image:none +} +.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{ + background-color:#5bc0de; + border-color:#46b8da +} +.btn-info .badge{ + color:#5bc0de; + background-color:#fff +} +.btn-warning{ + color:#fff; + background-color:#f0ad4e; + border-color:#eea236 +} +.btn-warning.focus,.btn-warning:focus{ + color:#fff; + background-color:#ec971f; + border-color:#985f0d +} +.btn-warning:hover{ + color:#fff; + background-color:#ec971f; + border-color:#d58512 +} +.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{ + color:#fff; + background-color:#ec971f; + border-color:#d58512 +} +.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{ + color:#fff; + background-color:#d58512; + border-color:#985f0d +} +.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{ + background-image:none +} +.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{ + background-color:#f0ad4e; + border-color:#eea236 +} +.btn-warning .badge{ + color:#f0ad4e; + background-color:#fff +} +.btn-danger{ + color:#fff; + background-color:#d9534f; + border-color:#d43f3a +} +.btn-danger.focus,.btn-danger:focus{ + color:#fff; + background-color:#c9302c; + border-color:#761c19 +} +.btn-danger:hover{ + color:#fff; + background-color:#c9302c; + border-color:#ac2925 +} +.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{ + color:#fff; + background-color:#c9302c; + border-color:#ac2925 +} +.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{ + color:#fff; + background-color:#ac2925; + border-color:#761c19 +} +.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{ + background-image:none +} +.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{ + background-color:#d9534f; + border-color:#d43f3a +} +.btn-danger .badge{ + color:#d9534f; + background-color:#fff +} +.btn-link{ + font-weight:400; + color:#337ab7; + border-radius:0 +} +.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{ + background-color:transparent; + -webkit-box-shadow:none; + box-shadow:none +} +.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{ + border-color:transparent +} +.btn-link:focus,.btn-link:hover{ + color:#23527c; + text-decoration:underline; + background-color:transparent +} +.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{ + color:#777; + text-decoration:none +} +.btn-group-lg>.btn,.btn-lg{ + padding:10px 16px; + font-size:18px; + line-height:1.3333333; + border-radius:6px +} +.btn-group-sm>.btn,.btn-sm{ + padding:5px 10px; + font-size:12px; + line-height:1.5; + border-radius:3px +} +.btn-group-xs>.btn,.btn-xs{ + padding:1px 5px; + font-size:12px; + line-height:1.5; + border-radius:3px +} +.btn-block{ + display:block; + width:100% +} +.btn-block+.btn-block{ + margin-top:5px +} +input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{ + width:100% +} +.fade{ + opacity:0; + -webkit-transition:opacity .15s linear; + -o-transition:opacity .15s linear; + transition:opacity .15s linear +} +.fade.in{ + opacity:1 +} +.collapse{ + display:none +} +.collapse.in{ + display:block +} +tr.collapse.in{ + display:table-row +} +tbody.collapse.in{ + display:table-row-group +} +.collapsing{ + position:relative; + height:0; + overflow:hidden; + -webkit-transition-timing-function:ease; + -o-transition-timing-function:ease; + transition-timing-function:ease; + -webkit-transition-duration:.35s; + -o-transition-duration:.35s; + transition-duration:.35s; + -webkit-transition-property:height,visibility; + -o-transition-property:height,visibility; + transition-property:height,visibility +} +.caret{ + display:inline-block; + width:0; + height:0; + margin-left:2px; + vertical-align:middle; + border-top:4px dashed; + border-top:4px solid\9; + border-right:4px solid transparent; + border-left:4px solid transparent +} +.dropdown,.dropup{ + position:relative +} +.dropdown-toggle:focus{ + outline:0 +} +.dropdown-menu{ + position:absolute; + top:100%; + left:0; + z-index:1000; + display:none; + float:left; + min-width:160px; + padding:5px 0; + margin:2px 0 0; + font-size:14px; + text-align:left; + list-style:none; + background-color:#fff; + -webkit-background-clip:padding-box; + background-clip:padding-box; + border:1px solid #ccc; + border:1px solid rgba(0,0,0,.15); + border-radius:4px; + -webkit-box-shadow:0 6px 12px rgba(0,0,0,.175); + box-shadow:0 6px 12px rgba(0,0,0,.175) +} +.dropdown-menu.pull-right{ + right:0; + left:auto +} +.dropdown-menu .divider{ + height:1px; + margin:9px 0; + overflow:hidden; + background-color:#e5e5e5 +} +.dropdown-menu>li>a{ + display:block; + padding:3px 20px; + clear:both; + font-weight:400; + line-height:1.42857143; + color:#333; + white-space:nowrap +} +.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{ + color:#262626; + text-decoration:none; + background-color:#f5f5f5 +} +.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{ + color:#fff; + text-decoration:none; + background-color:#337ab7; + outline:0 +} +.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{ + color:#777 +} +.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{ + text-decoration:none; + cursor:not-allowed; + background-color:transparent; + background-image:none; + filter:progid:DXImageTransform.Microsoft.gradient(enabled=false) +} +.open>.dropdown-menu{ + display:block +} +.open>a{ + outline:0 +} +.dropdown-menu-right{ + right:0; + left:auto +} +.dropdown-menu-left{ + right:auto; + left:0 +} +.dropdown-header{ + display:block; + padding:3px 20px; + font-size:12px; + line-height:1.42857143; + color:#777; + white-space:nowrap +} +.dropdown-backdrop{ + position:fixed; + top:0; + right:0; + bottom:0; + left:0; + z-index:990 +} +.pull-right>.dropdown-menu{ + right:0; + left:auto +} +.dropup .caret,.navbar-fixed-bottom .dropdown .caret{ + content:""; + border-top:0; + border-bottom:4px dashed; + border-bottom:4px solid\9 +} +.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{ + top:auto; + bottom:100%; + margin-bottom:2px +} +@media (min-width:768px){ + .navbar-right .dropdown-menu{ + right:0; + left:auto + } + .navbar-right .dropdown-menu-left{ + right:auto; + left:0 + } +} +.btn-group,.btn-group-vertical{ + position:relative; + display:inline-block; + vertical-align:middle +} +.btn-group-vertical>.btn,.btn-group>.btn{ + position:relative; + float:left +} +.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{ + z-index:2 +} +.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{ + margin-left:-1px +} +.btn-toolbar{ + margin-left:-5px +} +.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{ + float:left +} +.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{ + margin-left:5px +} +.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){ + border-radius:0 +} +.btn-group>.btn:first-child{ + margin-left:0 +} +.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){ + border-top-right-radius:0; + border-bottom-right-radius:0 +} +.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){ + border-top-left-radius:0; + border-bottom-left-radius:0 +} +.btn-group>.btn-group{ + float:left +} +.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{ + border-radius:0 +} +.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{ + border-top-right-radius:0; + border-bottom-right-radius:0 +} +.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{ + border-top-left-radius:0; + border-bottom-left-radius:0 +} +.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{ + outline:0 +} +.btn-group>.btn+.dropdown-toggle{ + padding-right:8px; + padding-left:8px +} +.btn-group>.btn-lg+.dropdown-toggle{ + padding-right:12px; + padding-left:12px +} +.btn-group.open .dropdown-toggle{ + -webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125); + box-shadow:inset 0 3px 5px rgba(0,0,0,.125) +} +.btn-group.open .dropdown-toggle.btn-link{ + -webkit-box-shadow:none; + box-shadow:none +} +.btn .caret{ + margin-left:0 +} +.btn-lg .caret{ + border-width:5px 5px 0; + border-bottom-width:0 +} +.dropup .btn-lg .caret{ + border-width:0 5px 5px +} +.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{ + display:block; + float:none; + width:100%; + max-width:100% +} +.btn-group-vertical>.btn-group>.btn{ + float:none +} +.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{ + margin-top:-1px; + margin-left:0 +} +.btn-group-vertical>.btn:not(:first-child):not(:last-child){ + border-radius:0 +} +.btn-group-vertical>.btn:first-child:not(:last-child){ + border-top-left-radius:4px; + border-top-right-radius:4px; + border-bottom-right-radius:0; + border-bottom-left-radius:0 +} +.btn-group-vertical>.btn:last-child:not(:first-child){ + border-top-left-radius:0; + border-top-right-radius:0; + border-bottom-right-radius:4px; + border-bottom-left-radius:4px +} +.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{ + border-radius:0 +} +.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{ + border-bottom-right-radius:0; + border-bottom-left-radius:0 +} +.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{ + border-top-left-radius:0; + border-top-right-radius:0 +} +.btn-group-justified{ + display:table; + width:100%; + table-layout:fixed; + border-collapse:separate +} +.btn-group-justified>.btn,.btn-group-justified>.btn-group{ + display:table-cell; + float:none; + width:1% +} +.btn-group-justified>.btn-group .btn{ + width:100% +} +.btn-group-justified>.btn-group .dropdown-menu{ + left:auto +} +[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{ + position:absolute; + clip:rect(0,0,0,0); + pointer-events:none +} +.input-group{ + position:relative; + display:table; + border-collapse:separate +} +.input-group[class*=col-]{ + float:none; + padding-right:0; + padding-left:0 +} +.input-group .form-control{ + position:relative; + z-index:2; + float:left; + width:100%; + margin-bottom:0 +} +.input-group .form-control:focus{ + z-index:3 +} +.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{ + height:46px; + padding:10px 16px; + font-size:18px; + line-height:1.3333333; + border-radius:6px +} +select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{ + height:46px; + line-height:46px +} +select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{ + height:auto +} +.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{ + height:30px; + padding:5px 10px; + font-size:12px; + line-height:1.5; + border-radius:3px +} +select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{ + height:30px; + line-height:30px +} +select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{ + height:auto +} +.input-group .form-control,.input-group-addon,.input-group-btn{ + display:table-cell +} +.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){ + border-radius:0 +} +.input-group-addon,.input-group-btn{ + width:1%; + white-space:nowrap; + vertical-align:middle +} +.input-group-addon{ + padding:6px 12px; + font-size:14px; + font-weight:400; + line-height:1; + color:#555; + text-align:center; + background-color:#eee; + border:1px solid #ccc; + border-radius:4px +} +.input-group-addon.input-sm{ + padding:5px 10px; + font-size:12px; + border-radius:3px +} +.input-group-addon.input-lg{ + padding:10px 16px; + font-size:18px; + border-radius:6px +} +.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{ + margin-top:0 +} +.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){ + border-top-right-radius:0; + border-bottom-right-radius:0 +} +.input-group-addon:first-child{ + border-right:0 +} +.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{ + border-top-left-radius:0; + border-bottom-left-radius:0 +} +.input-group-addon:last-child{ + border-left:0 +} +.input-group-btn{ + position:relative; + font-size:0; + white-space:nowrap +} +.input-group-btn>.btn{ + position:relative +} +.input-group-btn>.btn+.btn{ + margin-left:-1px +} +.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{ + z-index:2 +} +.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{ + margin-right:-1px +} +.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{ + z-index:2; + margin-left:-1px +} +.nav{ + padding-left:0; + margin-bottom:0; + list-style:none +} +.nav>li{ + position:relative; + display:block +} +.nav>li>a{ + position:relative; + display:block; + padding:10px 15px +} +.nav>li>a:focus,.nav>li>a:hover{ + text-decoration:none; + background-color:#eee +} +.nav>li.disabled>a{ + color:#777 +} +.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{ + color:#777; + text-decoration:none; + cursor:not-allowed; + background-color:transparent +} +.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{ + background-color:#eee; + border-color:#337ab7 +} +.nav .nav-divider{ + height:1px; + margin:9px 0; + overflow:hidden; + background-color:#e5e5e5 +} +.nav>li>a>img{ + max-width:none +} +.nav-tabs{ + border-bottom:1px solid #ddd +} +.nav-tabs>li{ + float:left; + margin-bottom:-1px +} +.nav-tabs>li>a{ + margin-right:2px; + line-height:1.42857143; + border:1px solid transparent; + border-radius:4px 4px 0 0 +} +.nav-tabs>li>a:hover{ + border-color:#eee #eee #ddd +} +.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{ + color:#555; + cursor:default; + background-color:#fff; + border:1px solid #ddd; + border-bottom-color:transparent +} +.nav-tabs.nav-justified{ + width:100%; + border-bottom:0 +} +.nav-tabs.nav-justified>li{ + float:none +} +.nav-tabs.nav-justified>li>a{ + margin-bottom:5px; + text-align:center +} +.nav-tabs.nav-justified>.dropdown .dropdown-menu{ + top:auto; + left:auto +} +@media (min-width:768px){ + .nav-tabs.nav-justified>li{ + display:table-cell; + width:1% + } + .nav-tabs.nav-justified>li>a{ + margin-bottom:0 + } +} +.nav-tabs.nav-justified>li>a{ + margin-right:0; + border-radius:4px +} +.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{ + border:1px solid #ddd +} +@media (min-width:768px){ + .nav-tabs.nav-justified>li>a{ + border-bottom:1px solid #ddd; + border-radius:4px 4px 0 0 + } + .nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{ + border-bottom-color:#fff + } +} +.nav-pills>li{ + float:left +} +.nav-pills>li>a{ + border-radius:4px +} +.nav-pills>li+li{ + margin-left:2px +} +.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{ + color:#fff; + background-color:#337ab7 +} +.nav-stacked>li{ + float:none +} +.nav-stacked>li+li{ + margin-top:2px; + margin-left:0 +} +.nav-justified{ + width:100% +} +.nav-justified>li{ + float:none +} +.nav-justified>li>a{ + margin-bottom:5px; + text-align:center +} +.nav-justified>.dropdown .dropdown-menu{ + top:auto; + left:auto +} +@media (min-width:768px){ + .nav-justified>li{ + display:table-cell; + width:1% + } + .nav-justified>li>a{ + margin-bottom:0 + } +} +.nav-tabs-justified{ + border-bottom:0 +} +.nav-tabs-justified>li>a{ + margin-right:0; + border-radius:4px +} +.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{ + border:1px solid #ddd +} +@media (min-width:768px){ + .nav-tabs-justified>li>a{ + border-bottom:1px solid #ddd; + border-radius:4px 4px 0 0 + } + .nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{ + border-bottom-color:#fff + } +} +.tab-content>.tab-pane{ + display:none +} +.tab-content>.active{ + display:block +} +.nav-tabs .dropdown-menu{ + margin-top:-1px; + border-top-left-radius:0; + border-top-right-radius:0 +} +.navbar{ + position:relative; + min-height:50px; + margin-bottom:20px; + border:1px solid transparent +} +@media (min-width:768px){ + .navbar{ + border-radius:4px + } +} +@media (min-width:768px){ + .navbar-header{ + float:left + } +} +.navbar-collapse{ + padding-right:15px; + padding-left:15px; + overflow-x:visible; + -webkit-overflow-scrolling:touch; + border-top:1px solid transparent; + -webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1); + box-shadow:inset 0 1px 0 rgba(255,255,255,.1) +} +.navbar-collapse.in{ + overflow-y:auto +} +@media (min-width:768px){ + .navbar-collapse{ + width:auto; + border-top:0; + -webkit-box-shadow:none; + box-shadow:none + } + .navbar-collapse.collapse{ + display:block!important; + height:auto!important; + padding-bottom:0; + overflow:visible!important + } + .navbar-collapse.in{ + overflow-y:visible + } + .navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{ + padding-right:0; + padding-left:0 + } +} +.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{ + max-height:340px +} +@media (max-device-width:480px) and (orientation:landscape){ + .navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{ + max-height:200px + } +} +.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{ + margin-right:-15px; + margin-left:-15px +} +@media (min-width:768px){ + .container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{ + margin-right:0; + margin-left:0 + } +} +.navbar-static-top{ + z-index:1000; + border-width:0 0 1px +} +@media (min-width:768px){ + .navbar-static-top{ + border-radius:0 + } +} +.navbar-fixed-bottom,.navbar-fixed-top{ + position:fixed; + right:0; + left:0; + z-index:1030 +} +@media (min-width:768px){ + .navbar-fixed-bottom,.navbar-fixed-top{ + border-radius:0 + } +} +.navbar-fixed-top{ + top:0; + border-width:0 0 1px +} +.navbar-fixed-bottom{ + bottom:0; + margin-bottom:0; + border-width:1px 0 0 +} +.navbar-brand{ + float:left; + height:50px; + padding:15px 15px; + font-size:18px; + line-height:20px +} +.navbar-brand:focus,.navbar-brand:hover{ + text-decoration:none +} +.navbar-brand>img{ + display:block +} +@media (min-width:768px){ + .navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{ + margin-left:-15px + } +} +.navbar-toggle{ + position:relative; + float:right; + padding:9px 10px; + margin-top:8px; + margin-right:15px; + margin-bottom:8px; + background-color:transparent; + background-image:none; + border:1px solid transparent; + border-radius:4px +} +.navbar-toggle:focus{ + outline:0 +} +.navbar-toggle .icon-bar{ + display:block; + width:22px; + height:2px; + border-radius:1px +} +.navbar-toggle .icon-bar+.icon-bar{ + margin-top:4px +} +@media (min-width:768px){ + .navbar-toggle{ + display:none + } +} +.navbar-nav{ + margin:7.5px -15px +} +.navbar-nav>li>a{ + padding-top:10px; + padding-bottom:10px; + line-height:20px +} +@media (max-width:767px){ + .navbar-nav .open .dropdown-menu{ + position:static; + float:none; + width:auto; + margin-top:0; + background-color:transparent; + border:0; + -webkit-box-shadow:none; + box-shadow:none + } + .navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{ + padding:5px 15px 5px 25px + } + .navbar-nav .open .dropdown-menu>li>a{ + line-height:20px + } + .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{ + background-image:none + } +} +@media (min-width:768px){ + .navbar-nav{ + float:left; + margin:0 + } + .navbar-nav>li{ + float:left + } + .navbar-nav>li>a{ + padding-top:15px; + padding-bottom:15px + } +} +.navbar-form{ + padding:10px 15px; + margin-top:8px; + margin-right:-15px; + margin-bottom:8px; + margin-left:-15px; + border-top:1px solid transparent; + border-bottom:1px solid transparent; + -webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1); + box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1) +} +@media (min-width:768px){ + .navbar-form .form-group{ + display:inline-block; + margin-bottom:0; + vertical-align:middle + } + .navbar-form .form-control{ + display:inline-block; + width:auto; + vertical-align:middle + } + .navbar-form .form-control-static{ + display:inline-block + } + .navbar-form .input-group{ + display:inline-table; + vertical-align:middle + } + .navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{ + width:auto + } + .navbar-form .input-group>.form-control{ + width:100% + } + .navbar-form .control-label{ + margin-bottom:0; + vertical-align:middle + } + .navbar-form .checkbox,.navbar-form .radio{ + display:inline-block; + margin-top:0; + margin-bottom:0; + vertical-align:middle + } + .navbar-form .checkbox label,.navbar-form .radio label{ + padding-left:0 + } + .navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{ + position:relative; + margin-left:0 + } + .navbar-form .has-feedback .form-control-feedback{ + top:0 + } +} +@media (max-width:767px){ + .navbar-form .form-group{ + margin-bottom:5px + } + .navbar-form .form-group:last-child{ + margin-bottom:0 + } +} +@media (min-width:768px){ + .navbar-form{ + width:auto; + padding-top:0; + padding-bottom:0; + margin-right:0; + margin-left:0; + border:0; + -webkit-box-shadow:none; + box-shadow:none + } +} +.navbar-nav>li>.dropdown-menu{ + margin-top:0; + border-top-left-radius:0; + border-top-right-radius:0 +} +.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{ + margin-bottom:0; + border-top-left-radius:4px; + border-top-right-radius:4px; + border-bottom-right-radius:0; + border-bottom-left-radius:0 +} +.navbar-btn{ + margin-top:8px; + margin-bottom:8px +} +.navbar-btn.btn-sm{ + margin-top:10px; + margin-bottom:10px +} +.navbar-btn.btn-xs{ + margin-top:14px; + margin-bottom:14px +} +.navbar-text{ + margin-top:15px; + margin-bottom:15px +} +@media (min-width:768px){ + .navbar-text{ + float:left; + margin-right:15px; + margin-left:15px + } +} +@media (min-width:768px){ + .navbar-left{ + float:left!important + } + .navbar-right{ + float:right!important; + margin-right:-15px + } + .navbar-right~.navbar-right{ + margin-right:0 + } +} +.navbar-default{ + background-color:#f8f8f8; + border-color:#e7e7e7 +} +.navbar-default .navbar-brand{ + color:#777 +} +.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{ + color:#5e5e5e; + background-color:transparent +} +.navbar-default .navbar-text{ + color:#777 +} +.navbar-default .navbar-nav>li>a{ + color:#777 +} +.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{ + color:#333; + background-color:transparent +} +.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{ + color:#555; + background-color:#e7e7e7 +} +.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{ + color:#ccc; + background-color:transparent +} +.navbar-default .navbar-toggle{ + border-color:#ddd +} +.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{ + background-color:#ddd +} +.navbar-default .navbar-toggle .icon-bar{ + background-color:#888 +} +.navbar-default .navbar-collapse,.navbar-default .navbar-form{ + border-color:#e7e7e7 +} +.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{ + color:#555; + background-color:#e7e7e7 +} +@media (max-width:767px){ + .navbar-default .navbar-nav .open .dropdown-menu>li>a{ + color:#777 + } + .navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{ + color:#333; + background-color:transparent + } + .navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{ + color:#555; + background-color:#e7e7e7 + } + .navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{ + color:#ccc; + background-color:transparent + } +} +.navbar-default .navbar-link{ + color:#777 +} +.navbar-default .navbar-link:hover{ + color:#333 +} +.navbar-default .btn-link{ + color:#777 +} +.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{ + color:#333 +} +.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{ + color:#ccc +} +.navbar-inverse{ + background-color:#222; + border-color:#080808 +} +.navbar-inverse .navbar-brand{ + color:#9d9d9d +} +.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{ + color:#fff; + background-color:transparent +} +.navbar-inverse .navbar-text{ + color:#9d9d9d +} +.navbar-inverse .navbar-nav>li>a{ + color:#9d9d9d +} +.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{ + color:#fff; + background-color:transparent +} +.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{ + color:#fff; + background-color:#080808 +} +.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{ + color:#444; + background-color:transparent +} +.navbar-inverse .navbar-toggle{ + border-color:#333 +} +.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{ + background-color:#333 +} +.navbar-inverse .navbar-toggle .icon-bar{ + background-color:#fff +} +.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{ + border-color:#101010 +} +.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{ + color:#fff; + background-color:#080808 +} +@media (max-width:767px){ + .navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{ + border-color:#080808 + } + .navbar-inverse .navbar-nav .open .dropdown-menu .divider{ + background-color:#080808 + } + .navbar-inverse .navbar-nav .open .dropdown-menu>li>a{ + color:#9d9d9d + } + .navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{ + color:#fff; + background-color:transparent + } + .navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{ + color:#fff; + background-color:#080808 + } + .navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{ + color:#444; + background-color:transparent + } +} +.navbar-inverse .navbar-link{ + color:#9d9d9d +} +.navbar-inverse .navbar-link:hover{ + color:#fff +} +.navbar-inverse .btn-link{ + color:#9d9d9d +} +.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{ + color:#fff +} +.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{ + color:#444 +} +.breadcrumb{ + padding:8px 15px; + margin-bottom:20px; + list-style:none; + background-color:#f5f5f5; + border-radius:4px +} +.breadcrumb>li{ + display:inline-block +} +.breadcrumb>li+li:before{ + padding:0 5px; + color:#ccc; + content:"/\00a0" +} +.breadcrumb>.active{ + color:#777 +} +.pagination{ + display:inline-block; + padding-left:0; + margin:20px 0; + border-radius:4px +} +.pagination>li{ + display:inline +} +.pagination>li>a,.pagination>li>span{ + position:relative; + float:left; + padding:6px 12px; + margin-left:-1px; + line-height:1.42857143; + color:#337ab7; + text-decoration:none; + background-color:#fff; + border:1px solid #ddd +} +.pagination>li:first-child>a,.pagination>li:first-child>span{ + margin-left:0; + border-top-left-radius:4px; + border-bottom-left-radius:4px +} +.pagination>li:last-child>a,.pagination>li:last-child>span{ + border-top-right-radius:4px; + border-bottom-right-radius:4px +} +.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{ + z-index:2; + color:#23527c; + background-color:#eee; + border-color:#ddd +} +.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{ + z-index:3; + color:#fff; + cursor:default; + background-color:#337ab7; + border-color:#337ab7 +} +.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{ + color:#777; + cursor:not-allowed; + background-color:#fff; + border-color:#ddd +} +.pagination-lg>li>a,.pagination-lg>li>span{ + padding:10px 16px; + font-size:18px; + line-height:1.3333333 +} +.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{ + border-top-left-radius:6px; + border-bottom-left-radius:6px +} +.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{ + border-top-right-radius:6px; + border-bottom-right-radius:6px +} +.pagination-sm>li>a,.pagination-sm>li>span{ + padding:5px 10px; + font-size:12px; + line-height:1.5 +} +.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{ + border-top-left-radius:3px; + border-bottom-left-radius:3px +} +.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{ + border-top-right-radius:3px; + border-bottom-right-radius:3px +} +.pager{ + padding-left:0; + margin:20px 0; + text-align:center; + list-style:none +} +.pager li{ + display:inline +} +.pager li>a,.pager li>span{ + display:inline-block; + padding:5px 14px; + background-color:#fff; + border:1px solid #ddd; + border-radius:15px +} +.pager li>a:focus,.pager li>a:hover{ + text-decoration:none; + background-color:#eee +} +.pager .next>a,.pager .next>span{ + float:right +} +.pager .previous>a,.pager .previous>span{ + float:left +} +.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{ + color:#777; + cursor:not-allowed; + background-color:#fff +} +.label{ + display:inline; + padding:.2em .6em .3em; + font-size:75%; + font-weight:700; + line-height:1; + color:#fff; + text-align:center; + white-space:nowrap; + vertical-align:baseline; + border-radius:.25em +} +a.label:focus,a.label:hover{ + color:#fff; + text-decoration:none; + cursor:pointer +} +.label:empty{ + display:none +} +.btn .label{ + position:relative; + top:-1px +} +.label-default{ + background-color:#777 +} +.label-default[href]:focus,.label-default[href]:hover{ + background-color:#5e5e5e +} +.label-primary{ + background-color:#337ab7 +} +.label-primary[href]:focus,.label-primary[href]:hover{ + background-color:#286090 +} +.label-success{ + background-color:#5cb85c +} +.label-success[href]:focus,.label-success[href]:hover{ + background-color:#449d44 +} +.label-info{ + background-color:#5bc0de +} +.label-info[href]:focus,.label-info[href]:hover{ + background-color:#31b0d5 +} +.label-warning{ + background-color:#f0ad4e +} +.label-warning[href]:focus,.label-warning[href]:hover{ + background-color:#ec971f +} +.label-danger{ + background-color:#d9534f +} +.label-danger[href]:focus,.label-danger[href]:hover{ + background-color:#c9302c +} +.badge{ + display:inline-block; + min-width:10px; + padding:3px 7px; + font-size:12px; + font-weight:700; + line-height:1; + color:#fff; + text-align:center; + white-space:nowrap; + vertical-align:middle; + background-color:#777; + border-radius:10px +} +.badge:empty{ + display:none +} +.btn .badge{ + position:relative; + top:-1px +} +.btn-group-xs>.btn .badge,.btn-xs .badge{ + top:0; + padding:1px 5px +} +a.badge:focus,a.badge:hover{ + color:#fff; + text-decoration:none; + cursor:pointer +} +.list-group-item.active>.badge,.nav-pills>.active>a>.badge{ + color:#337ab7; + background-color:#fff +} +.list-group-item>.badge{ + float:right +} +.list-group-item>.badge+.badge{ + margin-right:5px +} +.nav-pills>li>a>.badge{ + margin-left:3px +} +.jumbotron{ + padding-top:30px; + padding-bottom:30px; + margin-bottom:30px; + color:inherit; + background-color:#eee +} +.jumbotron .h1,.jumbotron h1{ + color:inherit +} +.jumbotron p{ + margin-bottom:15px; + font-size:21px; + font-weight:200 +} +.jumbotron>hr{ + border-top-color:#d5d5d5 +} +.container .jumbotron,.container-fluid .jumbotron{ + padding-right:15px; + padding-left:15px; + border-radius:6px +} +.jumbotron .container{ + max-width:100% +} +@media screen and (min-width:768px){ + .jumbotron{ + padding-top:48px; + padding-bottom:48px + } + .container .jumbotron,.container-fluid .jumbotron{ + padding-right:60px; + padding-left:60px + } + .jumbotron .h1,.jumbotron h1{ + font-size:63px + } +} +.thumbnail{ + display:block; + padding:4px; + margin-bottom:20px; + line-height:1.42857143; + background-color:#fff; + border:1px solid #ddd; + border-radius:4px; + -webkit-transition:border .2s ease-in-out; + -o-transition:border .2s ease-in-out; + transition:border .2s ease-in-out +} +.thumbnail a>img,.thumbnail>img{ + margin-right:auto; + margin-left:auto +} +a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{ + border-color:#337ab7 +} +.thumbnail .caption{ + padding:9px; + color:#333 +} +.alert{ + padding:15px; + margin-bottom:20px; + border:1px solid transparent; + border-radius:4px +} +.alert h4{ + margin-top:0; + color:inherit +} +.alert .alert-link{ + font-weight:700 +} +.alert>p,.alert>ul{ + margin-bottom:0 +} +.alert>p+p{ + margin-top:5px +} +.alert-dismissable,.alert-dismissible{ + padding-right:35px +} +.alert-dismissable .close,.alert-dismissible .close{ + position:relative; + top:-2px; + right:-21px; + color:inherit +} +.alert-success{ + color:#3c763d; + background-color:#dff0d8; + border-color:#d6e9c6 +} +.alert-success hr{ + border-top-color:#c9e2b3 +} +.alert-success .alert-link{ + color:#2b542c +} +.alert-info{ + color:#31708f; + background-color:#d9edf7; + border-color:#bce8f1 +} +.alert-info hr{ + border-top-color:#a6e1ec +} +.alert-info .alert-link{ + color:#245269 +} +.alert-warning{ + color:#8a6d3b; + background-color:#fcf8e3; + border-color:#faebcc +} +.alert-warning hr{ + border-top-color:#f7e1b5 +} +.alert-warning .alert-link{ + color:#66512c +} +.alert-danger{ + color:#a94442; + background-color:#f2dede; + border-color:#ebccd1 +} +.alert-danger hr{ + border-top-color:#e4b9c0 +} +.alert-danger .alert-link{ + color:#843534 +} +@-webkit-keyframes progress-bar-stripes{ + from{ + background-position:40px 0 + } + to{ + background-position:0 0 + } +} +@-o-keyframes progress-bar-stripes{ + from{ + background-position:40px 0 + } + to{ + background-position:0 0 + } +} +@keyframes progress-bar-stripes{ + from{ + background-position:40px 0 + } + to{ + background-position:0 0 + } +} +.progress{ + height:20px; + margin-bottom:20px; + overflow:hidden; + background-color:#f5f5f5; + border-radius:4px; + -webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1); + box-shadow:inset 0 1px 2px rgba(0,0,0,.1) +} +.progress-bar{ + float:left; + width:0; + height:100%; + font-size:12px; + line-height:20px; + color:#fff; + text-align:center; + background-color:#337ab7; + -webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15); + box-shadow:inset 0 -1px 0 rgba(0,0,0,.15); + -webkit-transition:width .6s ease; + -o-transition:width .6s ease; + transition:width .6s ease +} +.progress-bar-striped,.progress-striped .progress-bar{ + background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + -webkit-background-size:40px 40px; + background-size:40px 40px +} +.progress-bar.active,.progress.active .progress-bar{ + -webkit-animation:progress-bar-stripes 2s linear infinite; + -o-animation:progress-bar-stripes 2s linear infinite; + animation:progress-bar-stripes 2s linear infinite +} +.progress-bar-success{ + background-color:#5cb85c +} +.progress-striped .progress-bar-success{ + background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent) +} +.progress-bar-info{ + background-color:#5bc0de +} +.progress-striped .progress-bar-info{ + background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent) +} +.progress-bar-warning{ + background-color:#f0ad4e +} +.progress-striped .progress-bar-warning{ + background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent) +} +.progress-bar-danger{ + background-color:#d9534f +} +.progress-striped .progress-bar-danger{ + background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent) +} +.media{ + margin-top:15px +} +.media:first-child{ + margin-top:0 +} +.media,.media-body{ + overflow:hidden; + zoom:1 +} +.media-body{ + width:10000px +} +.media-object{ + display:block +} +.media-object.img-thumbnail{ + max-width:none +} +.media-right,.media>.pull-right{ + padding-left:10px +} +.media-left,.media>.pull-left{ + padding-right:10px +} +.media-body,.media-left,.media-right{ + display:table-cell; + vertical-align:top +} +.media-middle{ + vertical-align:middle +} +.media-bottom{ + vertical-align:bottom +} +.media-heading{ + margin-top:0; + margin-bottom:5px +} +.media-list{ + padding-left:0; + list-style:none +} +.list-group{ + padding-left:0; + margin-bottom:20px +} +.list-group-item{ + position:relative; + display:block; + padding:10px 15px; + margin-bottom:-1px; + background-color:#fff; + border:1px solid #ddd +} +.list-group-item:first-child{ + border-top-left-radius:4px; + border-top-right-radius:4px +} +.list-group-item:last-child{ + margin-bottom:0; + border-bottom-right-radius:4px; + border-bottom-left-radius:4px +} +a.list-group-item,button.list-group-item{ + color:#555 +} +a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{ + color:#333 +} +a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{ + color:#555; + text-decoration:none; + background-color:#f5f5f5 +} +button.list-group-item{ + width:100%; + text-align:left +} +.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{ + color:#777; + cursor:not-allowed; + background-color:#eee +} +.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{ + color:inherit +} +.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{ + color:#777 +} +.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{ + z-index:2; + color:#fff; + background-color:#337ab7; + border-color:#337ab7 +} +.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{ + color:inherit +} +.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{ + color:#c7ddef +} +.list-group-item-success{ + color:#3c763d; + background-color:#dff0d8 +} +a.list-group-item-success,button.list-group-item-success{ + color:#3c763d +} +a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{ + color:inherit +} +a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{ + color:#3c763d; + background-color:#d0e9c6 +} +a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{ + color:#fff; + background-color:#3c763d; + border-color:#3c763d +} +.list-group-item-info{ + color:#31708f; + background-color:#d9edf7 +} +a.list-group-item-info,button.list-group-item-info{ + color:#31708f +} +a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{ + color:inherit +} +a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{ + color:#31708f; + background-color:#c4e3f3 +} +a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{ + color:#fff; + background-color:#31708f; + border-color:#31708f +} +.list-group-item-warning{ + color:#8a6d3b; + background-color:#fcf8e3 +} +a.list-group-item-warning,button.list-group-item-warning{ + color:#8a6d3b +} +a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{ + color:inherit +} +a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{ + color:#8a6d3b; + background-color:#faf2cc +} +a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{ + color:#fff; + background-color:#8a6d3b; + border-color:#8a6d3b +} +.list-group-item-danger{ + color:#a94442; + background-color:#f2dede +} +a.list-group-item-danger,button.list-group-item-danger{ + color:#a94442 +} +a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{ + color:inherit +} +a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{ + color:#a94442; + background-color:#ebcccc +} +a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{ + color:#fff; + background-color:#a94442; + border-color:#a94442 +} +.list-group-item-heading{ + margin-top:0; + margin-bottom:5px +} +.list-group-item-text{ + margin-bottom:0; + line-height:1.3 +} +.panel{ + margin-bottom:20px; + background-color:#fff; + border:1px solid transparent; + border-radius:4px; + -webkit-box-shadow:0 1px 1px rgba(0,0,0,.05); + box-shadow:0 1px 1px rgba(0,0,0,.05) +} +.panel-body{ + padding:15px +} +.panel-heading{ + padding:10px 15px; + border-bottom:1px solid transparent; + border-top-left-radius:3px; + border-top-right-radius:3px +} +.panel-heading>.dropdown .dropdown-toggle{ + color:inherit +} +.panel-title{ + margin-top:0; + margin-bottom:0; + font-size:16px; + color:inherit +} +.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{ + color:inherit +} +.panel-footer{ + padding:10px 15px; + background-color:#f5f5f5; + border-top:1px solid #ddd; + border-bottom-right-radius:3px; + border-bottom-left-radius:3px +} +.panel>.list-group,.panel>.panel-collapse>.list-group{ + margin-bottom:0 +} +.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{ + border-width:1px 0; + border-radius:0 +} +.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{ + border-top:0; + border-top-left-radius:3px; + border-top-right-radius:3px +} +.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{ + border-bottom:0; + border-bottom-right-radius:3px; + border-bottom-left-radius:3px +} +.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{ + border-top-left-radius:0; + border-top-right-radius:0 +} +.panel-heading+.list-group .list-group-item:first-child{ + border-top-width:0 +} +.list-group+.panel-footer{ + border-top-width:0 +} +.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{ + margin-bottom:0 +} +.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{ + padding-right:15px; + padding-left:15px +} +.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{ + border-top-left-radius:3px; + border-top-right-radius:3px +} +.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{ + border-top-left-radius:3px; + border-top-right-radius:3px +} +.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{ + border-top-left-radius:3px +} +.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{ + border-top-right-radius:3px +} +.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{ + border-bottom-right-radius:3px; + border-bottom-left-radius:3px +} +.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{ + border-bottom-right-radius:3px; + border-bottom-left-radius:3px +} +.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{ + border-bottom-left-radius:3px +} +.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{ + border-bottom-right-radius:3px +} +.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{ + border-top:1px solid #ddd +} +.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{ + border-top:0 +} +.panel>.table-bordered,.panel>.table-responsive>.table-bordered{ + border:0 +} +.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{ + border-left:0 +} +.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{ + border-right:0 +} +.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{ + border-bottom:0 +} +.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{ + border-bottom:0 +} +.panel>.table-responsive{ + margin-bottom:0; + border:0 +} +.panel-group{ + margin-bottom:20px +} +.panel-group .panel{ + margin-bottom:0; + border-radius:4px +} +.panel-group .panel+.panel{ + margin-top:5px +} +.panel-group .panel-heading{ + border-bottom:0 +} +.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{ + border-top:1px solid #ddd +} +.panel-group .panel-footer{ + border-top:0 +} +.panel-group .panel-footer+.panel-collapse .panel-body{ + border-bottom:1px solid #ddd +} +.panel-default{ + border-color:#ddd +} +.panel-default>.panel-heading{ + color:#333; + background-color:#f5f5f5; + border-color:#ddd +} +.panel-default>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#ddd +} +.panel-default>.panel-heading .badge{ + color:#f5f5f5; + background-color:#333 +} +.panel-default>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#ddd +} +.panel-primary{ + border-color:#337ab7 +} +.panel-primary>.panel-heading{ + color:#fff; + background-color:#337ab7; + border-color:#337ab7 +} +.panel-primary>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#337ab7 +} +.panel-primary>.panel-heading .badge{ + color:#337ab7; + background-color:#fff +} +.panel-primary>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#337ab7 +} +.panel-success{ + border-color:#d6e9c6 +} +.panel-success>.panel-heading{ + color:#3c763d; + background-color:#dff0d8; + border-color:#d6e9c6 +} +.panel-success>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#d6e9c6 +} +.panel-success>.panel-heading .badge{ + color:#dff0d8; + background-color:#3c763d +} +.panel-success>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#d6e9c6 +} +.panel-info{ + border-color:#bce8f1 +} +.panel-info>.panel-heading{ + color:#31708f; + background-color:#d9edf7; + border-color:#bce8f1 +} +.panel-info>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#bce8f1 +} +.panel-info>.panel-heading .badge{ + color:#d9edf7; + background-color:#31708f +} +.panel-info>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#bce8f1 +} +.panel-warning{ + border-color:#faebcc +} +.panel-warning>.panel-heading{ + color:#8a6d3b; + background-color:#fcf8e3; + border-color:#faebcc +} +.panel-warning>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#faebcc +} +.panel-warning>.panel-heading .badge{ + color:#fcf8e3; + background-color:#8a6d3b +} +.panel-warning>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#faebcc +} +.panel-danger{ + border-color:#ebccd1 +} +.panel-danger>.panel-heading{ + color:#a94442; + background-color:#f2dede; + border-color:#ebccd1 +} +.panel-danger>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#ebccd1 +} +.panel-danger>.panel-heading .badge{ + color:#f2dede; + background-color:#a94442 +} +.panel-danger>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#ebccd1 +} +.embed-responsive{ + position:relative; + display:block; + height:0; + padding:0; + overflow:hidden +} +.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{ + position:absolute; + top:0; + bottom:0; + left:0; + width:100%; + height:100%; + border:0 +} +.embed-responsive-16by9{ + padding-bottom:56.25% +} +.embed-responsive-4by3{ + padding-bottom:75% +} +.well{ + min-height:20px; + padding:19px; + margin-bottom:20px; + background-color:#f5f5f5; + border:1px solid #e3e3e3; + border-radius:4px; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05); + box-shadow:inset 0 1px 1px rgba(0,0,0,.05) +} +.well blockquote{ + border-color:#ddd; + border-color:rgba(0,0,0,.15) +} +.well-lg{ + padding:24px; + border-radius:6px +} +.well-sm{ + padding:9px; + border-radius:3px +} +.close{ + float:right; + font-size:21px; + font-weight:700; + line-height:1; + color:#000; + text-shadow:0 1px 0 #fff; + filter:alpha(opacity=20); + opacity:.2 +} +.close:focus,.close:hover{ + color:#000; + text-decoration:none; + cursor:pointer; + filter:alpha(opacity=50); + opacity:.5 +} +button.close{ + -webkit-appearance:none; + padding:0; + cursor:pointer; + background:0 0; + border:0 +} +.modal-open{ + overflow:hidden +} +.modal{ + position:fixed; + top:0; + right:0; + bottom:0; + left:0; + z-index:1050; + display:none; + overflow:hidden; + -webkit-overflow-scrolling:touch; + outline:0 +} +.modal.fade .modal-dialog{ + -webkit-transition:-webkit-transform .3s ease-out; + -o-transition:-o-transform .3s ease-out; + transition:transform .3s ease-out; + -webkit-transform:translate(0,-25%); + -ms-transform:translate(0,-25%); + -o-transform:translate(0,-25%); + transform:translate(0,-25%) +} +.modal.in .modal-dialog{ + -webkit-transform:translate(0,0); + -ms-transform:translate(0,0); + -o-transform:translate(0,0); + transform:translate(0,0) +} +.modal-open .modal{ + overflow-x:hidden; + overflow-y:auto +} +.modal-dialog{ + position:relative; + width:auto; + margin:10px +} +.modal-content{ + position:relative; + background-color:#fff; + -webkit-background-clip:padding-box; + background-clip:padding-box; + border:1px solid #999; + border:1px solid rgba(0,0,0,.2); + border-radius:6px; + outline:0; + -webkit-box-shadow:0 3px 9px rgba(0,0,0,.5); + box-shadow:0 3px 9px rgba(0,0,0,.5) +} +.modal-backdrop{ + position:fixed; + top:0; + right:0; + bottom:0; + left:0; + z-index:1040; + background-color:#000 +} +.modal-backdrop.fade{ + filter:alpha(opacity=0); + opacity:0 +} +.modal-backdrop.in{ + filter:alpha(opacity=50); + opacity:.5 +} +.modal-header{ + padding:15px; + border-bottom:1px solid #e5e5e5 +} +.modal-header .close{ + margin-top:-2px +} +.modal-title{ + margin:0; + line-height:1.42857143 +} +.modal-body{ + position:relative; + padding:15px +} +.modal-footer{ + padding:15px; + text-align:right; + border-top:1px solid #e5e5e5 +} +.modal-footer .btn+.btn{ + margin-bottom:0; + margin-left:5px +} +.modal-footer .btn-group .btn+.btn{ + margin-left:-1px +} +.modal-footer .btn-block+.btn-block{ + margin-left:0 +} +.modal-scrollbar-measure{ + position:absolute; + top:-9999px; + width:50px; + height:50px; + overflow:scroll +} +@media (min-width:768px){ + .modal-dialog{ + width:600px; + margin:30px auto + } + .modal-content{ + -webkit-box-shadow:0 5px 15px rgba(0,0,0,.5); + box-shadow:0 5px 15px rgba(0,0,0,.5) + } + .modal-sm{ + width:300px + } +} +@media (min-width:992px){ + .modal-lg{ + width:900px + } +} +.tooltip{ + position:absolute; + z-index:1070; + display:block; + font-family:"Helvetica Neue",Helvetica,Arial,sans-serif; + font-size:12px; + font-style:normal; + font-weight:400; + line-height:1.42857143; + text-align:left; + text-align:start; + text-decoration:none; + text-shadow:none; + text-transform:none; + letter-spacing:normal; + word-break:normal; + word-spacing:normal; + word-wrap:normal; + white-space:normal; + filter:alpha(opacity=0); + opacity:0; + line-break:auto +} +.tooltip.in{ + filter:alpha(opacity=90); + opacity:.9 +} +.tooltip.top{ + padding:5px 0; + margin-top:-3px +} +.tooltip.right{ + padding:0 5px; + margin-left:3px +} +.tooltip.bottom{ + padding:5px 0; + margin-top:3px +} +.tooltip.left{ + padding:0 5px; + margin-left:-3px +} +.tooltip-inner{ + max-width:200px; + padding:3px 8px; + color:#fff; + text-align:center; + background-color:#000; + border-radius:4px +} +.tooltip-arrow{ + position:absolute; + width:0; + height:0; + border-color:transparent; + border-style:solid +} +.tooltip.top .tooltip-arrow{ + bottom:0; + left:50%; + margin-left:-5px; + border-width:5px 5px 0; + border-top-color:#000 +} +.tooltip.top-left .tooltip-arrow{ + right:5px; + bottom:0; + margin-bottom:-5px; + border-width:5px 5px 0; + border-top-color:#000 +} +.tooltip.top-right .tooltip-arrow{ + bottom:0; + left:5px; + margin-bottom:-5px; + border-width:5px 5px 0; + border-top-color:#000 +} +.tooltip.right .tooltip-arrow{ + top:50%; + left:0; + margin-top:-5px; + border-width:5px 5px 5px 0; + border-right-color:#000 +} +.tooltip.left .tooltip-arrow{ + top:50%; + right:0; + margin-top:-5px; + border-width:5px 0 5px 5px; + border-left-color:#000 +} +.tooltip.bottom .tooltip-arrow{ + top:0; + left:50%; + margin-left:-5px; + border-width:0 5px 5px; + border-bottom-color:#000 +} +.tooltip.bottom-left .tooltip-arrow{ + top:0; + right:5px; + margin-top:-5px; + border-width:0 5px 5px; + border-bottom-color:#000 +} +.tooltip.bottom-right .tooltip-arrow{ + top:0; + left:5px; + margin-top:-5px; + border-width:0 5px 5px; + border-bottom-color:#000 +} +.popover{ + position:absolute; + top:0; + left:0; + z-index:1060; + display:none; + max-width:276px; + padding:1px; + font-family:"Helvetica Neue",Helvetica,Arial,sans-serif; + font-size:14px; + font-style:normal; + font-weight:400; + line-height:1.42857143; + text-align:left; + text-align:start; + text-decoration:none; + text-shadow:none; + text-transform:none; + letter-spacing:normal; + word-break:normal; + word-spacing:normal; + word-wrap:normal; + white-space:normal; + background-color:#fff; + -webkit-background-clip:padding-box; + background-clip:padding-box; + border:1px solid #ccc; + border:1px solid rgba(0,0,0,.2); + border-radius:6px; + -webkit-box-shadow:0 5px 10px rgba(0,0,0,.2); + box-shadow:0 5px 10px rgba(0,0,0,.2); + line-break:auto +} +.popover.top{ + margin-top:-10px +} +.popover.right{ + margin-left:10px +} +.popover.bottom{ + margin-top:10px +} +.popover.left{ + margin-left:-10px +} +.popover-title{ + padding:8px 14px; + margin:0; + font-size:14px; + background-color:#f7f7f7; + border-bottom:1px solid #ebebeb; + border-radius:5px 5px 0 0 +} +.popover-content{ + padding:9px 14px +} +.popover>.arrow,.popover>.arrow:after{ + position:absolute; + display:block; + width:0; + height:0; + border-color:transparent; + border-style:solid +} +.popover>.arrow{ + border-width:11px +} +.popover>.arrow:after{ + content:""; + border-width:10px +} +.popover.top>.arrow{ + bottom:-11px; + left:50%; + margin-left:-11px; + border-top-color:#999; + border-top-color:rgba(0,0,0,.25); + border-bottom-width:0 +} +.popover.top>.arrow:after{ + bottom:1px; + margin-left:-10px; + content:" "; + border-top-color:#fff; + border-bottom-width:0 +} +.popover.right>.arrow{ + top:50%; + left:-11px; + margin-top:-11px; + border-right-color:#999; + border-right-color:rgba(0,0,0,.25); + border-left-width:0 +} +.popover.right>.arrow:after{ + bottom:-10px; + left:1px; + content:" "; + border-right-color:#fff; + border-left-width:0 +} +.popover.bottom>.arrow{ + top:-11px; + left:50%; + margin-left:-11px; + border-top-width:0; + border-bottom-color:#999; + border-bottom-color:rgba(0,0,0,.25) +} +.popover.bottom>.arrow:after{ + top:1px; + margin-left:-10px; + content:" "; + border-top-width:0; + border-bottom-color:#fff +} +.popover.left>.arrow{ + top:50%; + right:-11px; + margin-top:-11px; + border-right-width:0; + border-left-color:#999; + border-left-color:rgba(0,0,0,.25) +} +.popover.left>.arrow:after{ + right:1px; + bottom:-10px; + content:" "; + border-right-width:0; + border-left-color:#fff +} +.carousel{ + position:relative +} +.carousel-inner{ + position:relative; + width:100%; + overflow:hidden +} +.carousel-inner>.item{ + position:relative; + display:none; + -webkit-transition:.6s ease-in-out left; + -o-transition:.6s ease-in-out left; + transition:.6s ease-in-out left +} +.carousel-inner>.item>a>img,.carousel-inner>.item>img{ + line-height:1 +} +@media all and (transform-3d),(-webkit-transform-3d){ + .carousel-inner>.item{ + -webkit-transition:-webkit-transform .6s ease-in-out; + -o-transition:-o-transform .6s ease-in-out; + transition:transform .6s ease-in-out; + -webkit-backface-visibility:hidden; + backface-visibility:hidden; + -webkit-perspective:1000px; + perspective:1000px + } + .carousel-inner>.item.active.right,.carousel-inner>.item.next{ + left:0; + -webkit-transform:translate3d(100%,0,0); + transform:translate3d(100%,0,0) + } + .carousel-inner>.item.active.left,.carousel-inner>.item.prev{ + left:0; + -webkit-transform:translate3d(-100%,0,0); + transform:translate3d(-100%,0,0) + } + .carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{ + left:0; + -webkit-transform:translate3d(0,0,0); + transform:translate3d(0,0,0) + } +} +.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{ + display:block +} +.carousel-inner>.active{ + left:0 +} +.carousel-inner>.next,.carousel-inner>.prev{ + position:absolute; + top:0; + width:100% +} +.carousel-inner>.next{ + left:100% +} +.carousel-inner>.prev{ + left:-100% +} +.carousel-inner>.next.left,.carousel-inner>.prev.right{ + left:0 +} +.carousel-inner>.active.left{ + left:-100% +} +.carousel-inner>.active.right{ + left:100% +} +.carousel-control{ + position:absolute; + top:0; + bottom:0; + left:0; + width:15%; + font-size:20px; + color:#fff; + text-align:center; + text-shadow:0 1px 2px rgba(0,0,0,.6); + background-color:rgba(0,0,0,0); + filter:alpha(opacity=50); + opacity:.5 +} +.carousel-control.left{ + background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%); + background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%); + background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001))); + background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); + background-repeat:repeat-x +} +.carousel-control.right{ + right:0; + left:auto; + background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%); + background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%); + background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5))); + background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); + background-repeat:repeat-x +} +.carousel-control:focus,.carousel-control:hover{ + color:#fff; + text-decoration:none; + filter:alpha(opacity=90); + outline:0; + opacity:.9 +} +.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{ + position:absolute; + top:50%; + z-index:5; + display:inline-block; + margin-top:-10px +} +.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{ + left:50%; + margin-left:-10px +} +.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{ + right:50%; + margin-right:-10px +} +.carousel-control .icon-next,.carousel-control .icon-prev{ + width:20px; + height:20px; + font-family:serif; + line-height:1 +} +.carousel-control .icon-prev:before{ + content:'\2039' +} +.carousel-control .icon-next:before{ + content:'\203a' +} +.carousel-indicators{ + position:absolute; + bottom:10px; + left:50%; + z-index:15; + width:60%; + padding-left:0; + margin-left:-30%; + text-align:center; + list-style:none +} +.carousel-indicators li{ + display:inline-block; + width:10px; + height:10px; + margin:1px; + text-indent:-999px; + cursor:pointer; + background-color:#000\9; + background-color:rgba(0,0,0,0); + border:1px solid #fff; + border-radius:10px +} +.carousel-indicators .active{ + width:12px; + height:12px; + margin:0; + background-color:#fff +} +.carousel-caption{ + position:absolute; + right:15%; + bottom:20px; + left:15%; + z-index:10; + padding-top:20px; + padding-bottom:20px; + color:#fff; + text-align:center; + text-shadow:0 1px 2px rgba(0,0,0,.6) +} +.carousel-caption .btn{ + text-shadow:none +} +@media screen and (min-width:768px){ + .carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{ + width:30px; + height:30px; + margin-top:-10px; + font-size:30px + } + .carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{ + margin-left:-10px + } + .carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{ + margin-right:-10px + } + .carousel-caption{ + right:20%; + left:20%; + padding-bottom:30px + } + .carousel-indicators{ + bottom:20px + } +} +.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{ + display:table; + content:" " +} +.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{ + clear:both +} +.center-block{ + display:block; + margin-right:auto; + margin-left:auto +} +.pull-right{ + float:right!important +} +.pull-left{ + float:left!important +} +.hide{ + display:none!important +} +.show{ + display:block!important +} +.invisible{ + visibility:hidden +} +.text-hide{ + font:0/0 a; + color:transparent; + text-shadow:none; + background-color:transparent; + border:0 +} +.hidden{ + display:none!important +} +.affix{ + position:fixed +} +@-ms-viewport{ + width:device-width +} +.visible-lg,.visible-md,.visible-sm,.visible-xs{ + display:none!important +} +.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{ + display:none!important +} +@media (max-width:767px){ + .visible-xs{ + display:block!important + } + table.visible-xs{ + display:table!important + } + tr.visible-xs{ + display:table-row!important + } + td.visible-xs,th.visible-xs{ + display:table-cell!important + } +} +@media (max-width:767px){ + .visible-xs-block{ + display:block!important + } +} +@media (max-width:767px){ + .visible-xs-inline{ + display:inline!important + } +} +@media (max-width:767px){ + .visible-xs-inline-block{ + display:inline-block!important + } +} +@media (min-width:768px) and (max-width:991px){ + .visible-sm{ + display:block!important + } + table.visible-sm{ + display:table!important + } + tr.visible-sm{ + display:table-row!important + } + td.visible-sm,th.visible-sm{ + display:table-cell!important + } +} +@media (min-width:768px) and (max-width:991px){ + .visible-sm-block{ + display:block!important + } +} +@media (min-width:768px) and (max-width:991px){ + .visible-sm-inline{ + display:inline!important + } +} +@media (min-width:768px) and (max-width:991px){ + .visible-sm-inline-block{ + display:inline-block!important + } +} +@media (min-width:992px) and (max-width:1199px){ + .visible-md{ + display:block!important + } + table.visible-md{ + display:table!important + } + tr.visible-md{ + display:table-row!important + } + td.visible-md,th.visible-md{ + display:table-cell!important + } +} +@media (min-width:992px) and (max-width:1199px){ + .visible-md-block{ + display:block!important + } +} +@media (min-width:992px) and (max-width:1199px){ + .visible-md-inline{ + display:inline!important + } +} +@media (min-width:992px) and (max-width:1199px){ + .visible-md-inline-block{ + display:inline-block!important + } +} +@media (min-width:1200px){ + .visible-lg{ + display:block!important + } + table.visible-lg{ + display:table!important + } + tr.visible-lg{ + display:table-row!important + } + td.visible-lg,th.visible-lg{ + display:table-cell!important + } +} +@media (min-width:1200px){ + .visible-lg-block{ + display:block!important + } +} +@media (min-width:1200px){ + .visible-lg-inline{ + display:inline!important + } +} +@media (min-width:1200px){ + .visible-lg-inline-block{ + display:inline-block!important + } +} +@media (max-width:767px){ + .hidden-xs{ + display:none!important + } +} +@media (min-width:768px) and (max-width:991px){ + .hidden-sm{ + display:none!important + } +} +@media (min-width:992px) and (max-width:1199px){ + .hidden-md{ + display:none!important + } +} +@media (min-width:1200px){ + .hidden-lg{ + display:none!important + } +} +.visible-print{ + display:none!important +} +@media print{ + .visible-print{ + display:block!important + } + table.visible-print{ + display:table!important + } + tr.visible-print{ + display:table-row!important + } + td.visible-print,th.visible-print{ + display:table-cell!important + } +} +.visible-print-block{ + display:none!important +} +@media print{ + .visible-print-block{ + display:block!important + } +} +.visible-print-inline{ + display:none!important +} +@media print{ + .visible-print-inline{ + display:inline!important + } +} +.visible-print-inline-block{ + display:none!important +} +@media print{ + .visible-print-inline-block{ + display:inline-block!important + } +} +@media print{ + .hidden-print{ + display:none!important + } +} +/*# sourceMappingURL=bootstrap.min.css.map */ + diff --git a/themes/squares/client/src/css/01-main.css b/themes/squares/client/src/css/01-main.css new file mode 100644 index 000000000..be80c2227 --- /dev/null +++ b/themes/squares/client/src/css/01-main.css @@ -0,0 +1,77 @@ +body { + /*background-image: url("//*img//*LargeTriangles.svg");*/ + /*background-image: url("//*img//*RandomizedPattern.svg");*/ + background-image: url("/img/background.svg"); + /*background-color:#000000;*/ +} +canvas{ + position:absolute; + top:0; + left:0; +} +.authelia-brand { + font-weight: bold; + font-style: italic; + color: #ffffff +} +.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: white; +} +/* 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; + color: white; +} +.header { + background-color: #000000; + color: white; + margin: 0px; +} +.body { + padding: 10px; +} +h1 { + font-size: 25px; +} diff --git a/themes/squares/client/src/css/02-login.css b/themes/squares/client/src/css/02-login.css new file mode 100644 index 000000000..a6984267b --- /dev/null +++ b/themes/squares/client/src/css/02-login.css @@ -0,0 +1,136 @@ +.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 #000; + margin-top: 20px; + padding-bottom: 20px; + background-color: #000000; + -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 1); + -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 1); + box-shadow: 0px 2px 2px rgba(0, 0, 0, 1); +} +.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; + color: white; +} +.account-wall .form-inputs +{ + margin-bottom: 10px; + border-color: #b20c0c; +} +.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; + color: white; +} + +.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; + color: white; +} + +.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 */ +} diff --git a/themes/squares/client/src/css/03-errors.css b/themes/squares/client/src/css/03-errors.css new file mode 100644 index 000000000..e9f97f33b --- /dev/null +++ b/themes/squares/client/src/css/03-errors.css @@ -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%; +} \ No newline at end of file diff --git a/themes/squares/client/src/css/03-password-reset-form.css b/themes/squares/client/src/css/03-password-reset-form.css new file mode 100644 index 000000000..34066bc24 --- /dev/null +++ b/themes/squares/client/src/css/03-password-reset-form.css @@ -0,0 +1,4 @@ + +.password-reset-form .header-img { + border-radius: 0%; +} diff --git a/themes/squares/client/src/css/03-password-reset-request.css b/themes/squares/client/src/css/03-password-reset-request.css new file mode 100644 index 000000000..1a2ad4df8 --- /dev/null +++ b/themes/squares/client/src/css/03-password-reset-request.css @@ -0,0 +1,4 @@ + +.password-reset-request .header-img { + border-radius: 0%; +} diff --git a/themes/squares/client/src/css/03-totp-register.css b/themes/squares/client/src/css/03-totp-register.css new file mode 100644 index 000000000..cb76720ad --- /dev/null +++ b/themes/squares/client/src/css/03-totp-register.css @@ -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; +} \ No newline at end of file diff --git a/themes/squares/client/src/css/03-u2f-register.css b/themes/squares/client/src/css/03-u2f-register.css new file mode 100644 index 000000000..e54cddf89 --- /dev/null +++ b/themes/squares/client/src/css/03-u2f-register.css @@ -0,0 +1,5 @@ + +.u2f-register img { + display: block; + margin: 20px auto; +} \ No newline at end of file diff --git a/themes/squares/client/src/img/LargeTriangles.svg b/themes/squares/client/src/img/LargeTriangles.svg new file mode 100644 index 000000000..0988bcb39 --- /dev/null +++ b/themes/squares/client/src/img/LargeTriangles.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/themes/squares/client/src/img/RandomizedPattern.svg b/themes/squares/client/src/img/RandomizedPattern.svg new file mode 100644 index 000000000..51afee6d6 --- /dev/null +++ b/themes/squares/client/src/img/RandomizedPattern.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/themes/squares/client/src/img/background.jpg b/themes/squares/client/src/img/background.jpg new file mode 100644 index 0000000000000000000000000000000000000000..974ea273fa87adccec4b4433fc4d097c04ae4c31 GIT binary patch literal 587 zcmb7za3BG;m3jxdn?P_E@h=1|8$YR;59+9 zW`I5}XRiI4&WC@yzle_&cQR_(0pU8Ji5j{Gs2||wia~Q)aTB2CVR=3aT5jO?)niY+ P^@QfW&?n$dCIR+GQO8ew literal 0 HcmV?d00001 diff --git a/themes/squares/client/src/img/background.svg b/themes/squares/client/src/img/background.svg new file mode 100644 index 000000000..668312f9f --- /dev/null +++ b/themes/squares/client/src/img/background.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/themes/squares/client/src/img/icon.png b/themes/squares/client/src/img/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..040d10c1ab5feaf6720fa8e8c0a0aa336402894e GIT binary patch literal 1461 zcmV;m1xosfP)e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00kXML_t(o!_`+?Y*bYgUHhE- zn3=ZI7qzh{%0mzcVu-|44KcBlMg%4JNF)*ye@ry-m;Ooo@C(p{@FNm6K%#tzfV8Dp z(gHC?u?eAyEkY@6v9v&$K4#|5J!kuI=XL2!XD|}uPVUd$_w2p)Ui)z_@E;5PlS6Y? zzD$W&;aqBgCCWIgr3%#B17TU=LfaFniBfPfGHk@o-q0}>pSg3elo_&jXcox?pxuirC( z+64_w<1wcpt~Ct9s1v|4#r%X~rnEyMkPy>M!hxWQY!F}#Xodt_W_^yC&$ChylYpwS z&=(^YTQB4VP`#+(neoZ!X+^?|Fo&2@Mx>OVR#uekyFNbs3W#>4vjG6i{zq#-D?lmO zgwb%&er)to`<;|MBis1xiRpQoq=1lspman6NDy#26twqNR#fgA_~q+c*8$*>20$i( zX43)A%7y?m0U*TPM9rNfXHy+Yt03Y#2NpCw4d56E zr8CNk8ARxwwFZ)MCdHT&Pdu5eCDZpn@^ zg!ri-09%OTsSF5HzMiccym0J>)_N^~%UQkOi}{T!LDU98Rq07el%z1z(&37?5ok7xHr~3h{1!EAv9uU=n1bkaDm3plKtq}^CpAB|@>l6g=Th{?L zO><|;%(Uw^S3mw@#S9R`U6(-E0l-S{0N(|L(*w#PM1vt?cfmoFzLeC?=_s+x9)RJv zx_T-a+gbFrCOycqq7T~N44pf2vuFUnckNe}Et`c$GGYn$Em5~-bAIoXqv}29Cef4~ z2yM@YUF1_#SCxNnnX)rE+6@p6Qr(*6e&3X1gC-+a8c+&BL+%*qK6K@t0rdTNNQVOU z`=%5Qz|c$<11C_oX0Z=|Z|P*x4y9laU$!iJR{F$j%3FW5_5>uoM=4&3>Oj<9vSgh% zpy=+qV9L1*!8#Ez-`$zbhb**wobmo&McKSC1NF&>%U#7=0}m|ebs1q0HD!v9 zy$h(TT*3Ty{Ax#AA^WnJjZ3BTP6{EanRzux5Qt1y^Q$1#gAm97P#~o>v3^Gnb?z_5 zc2NL$a)2xAmbJ20*BIQu3@cgwp7UhUS%QW_rq~p}dg}0f`!}4kbp4`eG`2hHsz#kW zZxFdbQ|=801KV!&9J_Phe@-7=xryR#eBI=9yqTE?O9J-xk&7+8_&>_uokeL63{>@u P00000NkvXXu0mjfXm6c7 literal 0 HcmV?d00001 diff --git a/themes/squares/client/src/img/mail.png b/themes/squares/client/src/img/mail.png new file mode 100644 index 0000000000000000000000000000000000000000..834bfce9107a94be10da4c011fb78e37a192888f GIT binary patch literal 3545 zcmWkxc{CJ$5dZF4tXtNx*0s3`#S$s!5_0FZ2?r0sk5=BUk&_Rfm`^vTD z%#kBkY*-}m^PBf(=FQA|pU;~&f6Pq0iSbnyMie6efJIMN%k0Dx{#Q8NiEUML>OFC= zAWc0B_=((uyT+Zw3^#Rcf&ici|6d_HOJ!{*N#0;>T(G&ndvJ(Tpc@DY36b{n^A2)x zy6GnEAL#LPLj`rhV5O&}VG%m<>)Dz)r!}gh`sIE>yJEBH0oyW)_DS6=t)VW~UUAj$ zm+hiLcfH+H{fex~j_(uClb=^=A>;#1uWMUy{+4Tb4*!BdqZuD{5fW-KrVz0}Qw(L} za@q)nnRz60-@;pCi~Hhm>IUq&0(@ai{WjCs-tr zACjZUSIC+XRz?w>hDYM}b7}IWaF45pAB@ivor$T$1Y)i(IxTkrrMHkX&kWKPRp1hg zo=mAMrPeyF;iZ{Vt?q%>H0ZzDPZKJ|(U#Jl;f z06i6&8@wn?^&SkpMos(hkQeM4T*f2MgzTaNS1mA$!umiOb5LgSwFu%u(u zxl5l!lfKwD2^vQ@s^A+-wh}8pN2y1)F~TmnSLe`z?}x~jT2_L8%H|axd=D5X`;&{> zP>X5~|M;ND8k#(ZG=gC1d}?gqKl-%2@dGXVHzKB%M(%~U@vF1V8GNgTYJRv*V-%RB zE?7#4CY`tCW6Rh=X>>G{9qOo_vpc4hVxP@Q_CSH)2MD614Seip{M{ETb1p+mL1AN4 zP=*G!s#NrkF=IRj2zEi(Am6vfJ0yS9t@)iN$CIJClYjK3=j^;zF+d;xVL2xn_C8sB zbt&j|^EUgp$dr>75LtlGxGnY7c;~jzrPG&&5M|^-kJE=CYq(H>N7h^*^;cOA<@|L7 zo3*HDm~%9f-e&kdFH^h|Q4$a%B0dOms8)v!9&#R~<$lrR33!A8q6wc?Y!XMKRwfOH z4*XY5U)3mL|0YU{TbM`kq>j85d(Y6R1%<^YK(Rdy|IFlW%_GBRvvkXm9gFbA>2zT4%~wgv zAa{%nEl}FuVcZ8qkO%y;u}g;FXX8CA0(V zHo$2?uxjL&5mzp7SH^_L2C;OiCX>rmMyoy;vTG$k2nW{n0cDEV02>9Bh=VB)tdf$W zrorSuy@_@0rUOW3$ZL-KDckegvIY&h_WoG-|1ck^9o(Uuv9{`?2a#*EvstqrcU;OkT%&O z&?2q|bdqN?hn4Vf7m}CLABr98mRx8-;?q+v5XK08(*u@sx_(3j* zwBweN+hiAHpXTP~5Ms>Mi#JY+7E~4JK=DjQpvuacJBq6F3_{QaKJQs^b}qae=K8>g zmjUqkf+}w1a*BP5n{MVHzz2hy<-_{Dc)dbjoQebl&j$rIfuF2d!KJx{Xan~57;aKY z*cp0oD@2eKqn|i>$qf2D({4472CcKI7Y@N>p{=|McnE1GMUYE&cW91q>S4?h5CYdc zD>jt_+|+-^so@x@KH>st-l0mkN2n*Xf7vo~s4j$KQNxqukdo#tn*4rG<1cq8nW0=t~jx@i5$Yzh}-9R7YT}D7A6DYU5 zB}Sommbv#GGl+$48vKt(LSc7%-B7@|WNpltJ)j^na_{1~R9U)o$SYR#Q*m!7C{I!- zq6B&H1TBRWX!)D}Qy8DT#!Dy$H9;TT!7N8x&)vOj*_r9}hi)PIT%O8qz!uuIyOIMX zmf!B#>W3K9xc`td+{ymLd&R7@QovuOEPBz^2?*eD1G^Svw3mY z--Ts5TPh5EQ{L4ab8gTT(`bvI&U?)fx@UC?;hR>-?)nV@{H=LNX&YboU+{G`mW3mT4zkU=M{34>Ue(w*XT0L=|s)EEY=NJQB zyq{(qvcLA3&T++b|4(_N8|OlznVJ6QlY% zd}XHHJ#=tl^)=sY=cnyIL1U&?XP5cYh7^8gE_ewLFyi9T36^c`=bVb~+_)H4GN63; ze!01GwqHW2(hWpeKI4|we%qg&+E-E<7bRi~BbBbko-X)$BihDeI7>Ei*3(%H9CW6?+!kwP)K}$%-iX&Fx-dO8OayE0>WtE?}Ceu*lDoW3u)c+rZ+>D*wT{+UNT4v z7y!ad%qetWFY*OwQ>w&{#d9_*L_QC`=Oxtk0(TaG=JPJtgT10hh7L^Ni=L+Dk-}j_Zb{DeRbsUtukh*#)(O2maD^ebRpzH&k zc1-{#JRWW@uk?3xYia$^u=P^Uf$z}^$Eya-26HZIyOKl{fJMO_#eP&R3VmL0WNyWL z>|hHtJ009@t0p!BZ))12BVh4c+K$V^w=fsnlpnfpM6JX!51A~t*kfGwF1j9})Q*Uo=fMgwFn^O>KYtsCNIrx$_O6?4zks$w|$aiP)S7HbUnC?4#0( z?9RyvUybw@hfr{p?rAz#dHK3NU30$NVMv4d%yC=7jQgzx)^|a$^*x7mKbVyrWFU8GbZ8()Nlj2>E@cM*03ZNKL_t(|+U&e(l%>gC z=J$(;cfD)gn|oGfW$k-+S8vs=mb%qy)M!C1w9vw!BMAv%jIr5)Rn=8ldsSvuZg;=$ebfw`r{k`&r+VuEGcPz}!EZ+9YuRr_7;!<*7zA5(l_kuH1 zCc6?7)+@yA0ge3=^e%QWHHnB4%vDfGntIovK$RmRqMc*Z+^bxK%+uu-{bYDZW`Y`jJyV!Y6P0Hvqg~i~2u({5yZ_ z^0}q&U;5O!dh^({S8Qr&Hb=x4M#wuuCcm|Uks+PW53sUNaCe>hRGIYIgsszE%D0w@ zpB-Ww8nlPA4*9S^T1#+il`PLO*AsNBKv;vd5@$?N3@wf?F?Gk-R+8CFw3QN{(1ZLJ zjAii6_Mqf9^5(wp@=yQu=O;HGKXKygZ~a$0KDqzQ2S5LYu2T#D+2C3{Z8r$Kfs~a! z9`P{7`VQr4Nb3(eM5hMS54-qFCNT0bJ^V8v6sjtf7bmWK(+nm(U2y)?WxT{N zOnNN5d6C*=owcv7;0Q_cduPdBj1dikTlX1`QbtQ1)LBh>#o*Kga?)Yq)&+WMfLZDi z$TB%8gVR043KTaJY{_8S2H{(bV{vOCw#adt4q00hB}4qo!DI%l3^G+y*&uqo((Xrt zLzN$_yl(nR=114;MV$jUM|{kt{znG*o7O+={4$@v`;&a{`Tud@*kkv8uI|@w43pmP z$E{(Lfna!J#PIoR1d|hFrI@^z;dEWv!#17u9we4vM};g+8J!toB1wK_#Eu7d5X_b6 z42Cq{SjLnEg{K*D#LXov|MR1e}y5rhtAXi>&uj6iCMT5!NBl9d<=ONo%& z0Ky_e5A6!vp@G66JUe;YBX|9Q*(h!}bI#|OQ9FvW=|3{S|9QFZD`U>&0Gw=n{gyZV z;(LDS{PN4ck%;t=o4DbvyHD`^D^F3{QK9|#2Cgcz{*|kES0&kI0X0SQ;c1dVN_>6* zBT2p-W80e29W|U-(tfOkiUs*hhLw`q9TSjb?0#?`dfuhjikLh!$tcbdagNC>7DLbo zDK>LN4itjm^&WYY5*(|NzZRqW38iB-3Y`-?FhOx?fDI%Vh2ts;lQI|#Lpt6eRp!Uk zm~%js>}5N!YyZdqz^zZdiTu+Ml^>s}-1+z;FI7tZDK*=C|0s;r~=gaSyewH`YY5+5j9{m3w z0Cs)p@V3T(ajib~zwG$6{ksngK0N=CWzLHYkGtK4R z64^~bc*w)u?NNW*1Y0+HR0b7v+kh5K+%rqi2^c=zX6)_|bEHa9$_e(kXAzXAzW0gT@NqRjejWTj$ zkadZ41u_Uwp<(HK79a(3%uff{bA7%{j@Kf*P%mnS4)ZGs2zkC&?9kO1Gk%pqH5wT@(vWxB8 z?KIF;RNqx)^y+}HTqfJdKx>q8NiQcz$Hf&cS(GCT5DSdf6ulhf3vA2a%sbewgl>)r z1$i&R4L$Z8IrP5S#rb#lA5fpZ@>5U7{~!Ty`4i{4_r-UA&}l|b1{1;b>g5}_6II3* zny8v&>E>(PeEb4)-?5KyA;4`a#BzuYQ{=eI+>u4vPqY|2HckIj2fLL(P0@L-g7mbN&9(wmXxUhbjJklsf;WQ+9p|ODL2$Gc?Th39d4!yHOw9Jr>LoqT8 zpY0pEB<|pb4(>F$!x-b2M0l?HdW{8%Cl-?SC zEs6&}Y*S5S60)l?_nzj(PR%kRPDJC>=9^)@N=%k>3s!!TZIDhgC)uRofueZ^og8Y)^_Mf?j_F#kN zz0=etN{mVxc|?&u)kQ5j_=kP6QzLBKF#nF7v`@ECTYwahL+u@PVmYL|Yl7s32#LmN zy5xO>Yz`6Dr z6?=1P&Ai*7fV`QSltwKN{9ucD()wV>4BfAfY)w zO?k4yfw{YQ!amN8PoH7p-Mc6{8GhTtd0mB~zg?GKeR&lfS!lQnp6@a0k*^q zhEMj8(nAkZWKH2z73uX6V$z^j`dGkar|@EysynoLw&I zC_x53$}o;bL4DUG?bkLLT_2LK#5C^TLF`1> zvO&3k23dn_DMMB~%w&ql1V&488xzhnF-b;P^=H?vcRyTdH=h_??5qa&H7Rby-#kH} zzgUB$KzQ<9I*XsGH5)V9#Ec!EBMtgQGDUC3Shs{MD>hn}dEw8VX6$X_m=Z8$sNOP8 ze0qzU_1C!sGJk|c@$pV}yhxA$>HSYZi*8EY5NB?XGr{R%! zF=Ajb{hTx}5d9&#pR*+gI9bl{g^13VZZI)XqwkDJ<`QmPxI*WJ4U(5TwEnV->bQ)) z-lhG?Wv=|r8A^*4L|72DV!#qz9I^EuZelu;)jxifuq1Ijhy12x-=TvzmjxT2U4xz_ z?iZ*#TpABf5a$Ww@0!88+oOC}Nae&B^*ic_TV1RRfeILZU=n9n2uffih{7zvhIVl5Lf4+*`U8a7pOz-7_eAGvd2MnGcaQg#yv+ww=tR@%eeYK0z^l%p)yd5=6 zKOtMmQISGu@aF?`X0Ta-nhz-u=vI!%ECbOcz7nB(hG372URLCTl;}%+^w5%C&&bcj z6cGdqWmF`1`?r55XTJObQYs=bKn#J{ps!~b9D3Kc&|2Un5_Qa_y|RUFXJj40xBkF8 zId$qOkm+!9mltdBp3{A}r_%Xsh8`28khgYJ_*(MR@|X&&O}Ls7Zx)yiBQDqL2yY-66IPj<2x20mijB z`#hWm7|(KT=`tdh=*-{!-j@))DzHo8gFiz6x<3%N;E5Nn% z&-by*g7``YlUuTWf$YYRL#}i3HBD)Mlk!BF3m-d0Fj2 zUy4x+{x=>2q+f}t+*3c8RS2AEz!zMexNa0)?TQp$HvqNWu|He~YnB- z?ygd_GD?T4MCTGjtN@EswS;%ouqZ~)w5cpq$XyB906BCpQA+D(IvCrtG5u8_S zd}0;d6DS>^wIxps?N44u=Q%dF2)m6@J1o$i07u|WmdUe>;Yy5jEJybqhH((%4%8gP zP9HOfNiU_0o*3ewFfzd=nu%L?kgpEOmq)nMRZO*@JXeFsNs25c-%4;s29xG@(b@my z&)xTHR!+PBWdR_)Z30Q%{8^#p(c#zoxVb=R$m1L?Mx@o}74$cG~&9wn)m+O><} z<`#A!IQPm6q@7LbhZ@{`w#}R0`gUIbwugzfG}eMp3Tr%!jhTFCAC-63@%K0!{?I+N zFRtOJ0O3n?D`%`fK{2qAu*BR;>8QZt9=3|V6@fe&;I47 z(9a6;r$p?r4;MAoO19 z=`)18%S7ijFZ|+HsUI367YQPd z!N?h5uNwwu`;ZsNaY<=DK+XH8hDKEj{1cPt^#nDkPz@Jn%poDcse4rC#t8R^WUE8M zg$l!AjOTiYP-4>@r9!MPA8fpL$EOFMU1Q;scm4eWKzafJD98P{aqas>E|3+Akr_f8 zgcYDI!WwV{&Rhv+zD5}MbmA6g!xp@p7S9De8)rmMRU!bf{Kt6{$|~XFRebp=dzqh(eSUe&FGH5<4^$nWAW?SZnar_7N3P3fFRMOJh13YbC-! zX*>X5ZL{69gM+4ecpMHml<#REQh{EF@OT|H)Z`nMVznS&O)%DyuWMvikZoj`k%h?$ zZ~2)IGq}(t)DB6e2^tDpm7ohu=6He8c{Oj<~xG82!w(9(I)XI!<#N6V}UI>h&hS3!=3akVr5eGbEpuk!%2BaWQ3QVeTw1ZPv3RjX`>?5`yyD`8N1;TadKe35%!9k%G zEpD}p8g~$xAH??;30_r3qWoqmMxeD(YOegL3K&M^ePZ=^=1a=Qgu2`C&(k!XaYFxnu5!j=@yf={_# zCVygp*^E(@GB2f1vvq2N-ji*_sLbV!OPDJOYvS<`>4jjRb|BgfkpqVcd3E^{B)i5T-h3)DPVB~XrwMxawe=g&9cMN8|qSFwFf z@>&E}3r1@x`Lzh$EwHZ@6fYWzL?ELKVRA$w=zXSx-2^=-Kq!O(n`neJ=)jV{CU9ct zzS_cWXxy2Q@~w5$enDltj5`q!zCJ|B03lUQlfG zD9ujbh5^$Ql}c9TarqtzIHca3no_S=U)acuGTZ~6FtUI5(k7yrB2b%w8li?pBt zoC3Q!Bt1FAnGLZHh}42EkP|*C2pC@NP&+b#8D@-%J}1WBNZ!}@NUl6_j{I~Vr|wcd z60&@Kg<>NjiwlOfjrA1_G%5t$a4q)g@F`DKaK;^&aR{mkAq}GBf%Py_r+4Mc#87u#HWWeK~HV?9CR-MgqX#=#cl{&TIP_Ga;E#udc&@0I~Zzt|%D;pU_L zrNP@O^%D2}{F^xN`TLQF;Z48!?d-gFA9*Lmj@j)1$qLkji>QIq@ELybCgrIy>dkS2 za)qp+VM4%26P+0%rxo0=w4T3;TT?8ru2J7LM&u50s)o^plz7cBe7r^Hl{VonpZIJ< z@^}O@AyPwnIbm{l14;^G4Nl#`j%k!-$6_g~#fNDA@&qL^(@w=x9 zXIwahyfTYUdgo*OoET5IT}%t&b{TaJ`2v1TGFmO@dJD ztWOX&ONc8uGS%-9P4!_~8YbPpI{>&3RfPK%H9j@l{FJD`*pYEg{`!j?DjlNvzA08N zt#JGY?&O~T`5_kHuoqbeltmPzWwz&4k zix8HguW=59=!zmUIpqTZGyh-QH(NoYDW-<bgmQ7P z6vj~f;22`ofpG_G48~Y&A+er7;ZgA_EZ)7JpZwjQLzX0tP!xWS9SX9Ql;lcEygJ0V z8ao16R@lOz2Lk!44qPmdZjIj3!zcA`^*~(Ac|+=xUdh z$FE?OiwND1(QvD5=6`n*V6J4y>F{%&k^4I@^zj{+)|2ZL-hkb2y`A&F`z&&gz;D)> zx^t4vY!y$ASrBDvOK{@w#@y}V3(+EBf(MqW#Ba|h7_+emru z5B%4x_68(ZHpnjwh@+U`t~zC}0m|SVc1g|+Xg)fDR1Vq61TiCMylITl*$5pQym1#C zgBV(r28|@YIzkRS245T?>I$t3vUP)Ml*nrym9ijdr4+B`WWyX4Sac?lQebn96#^*~ zwqIa{+|CHB!AeP6Z8B>0`Pv_Sjck1cF4;YE73jo+wadl{AEH3l#CkP=u> zP*s$URWLU+%NJJ=yBvmJ+#<~iJf#q!C0|ZafKEY9JM4VNBK6}t=)AhY#$Q}RT8orI z+PruRiT#+UD9G&pHwF+@fnQc1w(G^Wjy*C*weB)H*~gADu6*Vk?K5lK_G7PStUAg0 zufE87Yl~f9eH_R}-9GPIS6QJ)ENd*aoVu z@TclL`R9MZmES+f))N~PQs9&nNn{zc`Xn1E#TR;%?wX+YmmTaVL^}rC)3knP3pxfR z1-4@lbKo5FaBnLycBGE38Kh^iK>;GiMuxPdv0E7|4T-xsp7ao&M0zeVx7flW3xgAS zh*pNw4#u@80gfk;fkIl%H9qPXW2FO5+dtaas<;tFbt&3Ysyl#f*=@^6{HA9yF@)E5ZEuwRMvda)Tzn@e>QIf9x{Z znnoCfzLBCgEjxBiVU8KZO;CGWgc8`IK(8hg7ZRK?pWs-T{MvRAM^qG*H`meY87lY4 z;so8wQ8k~+9Tnm@W&GhOvNR$+ouZ7zJ?h~!16g0tFGUz3P!&afHbVw3G8K%{1pQiy1%eZGvgHv3K7Lgo8j7Oq zk+%li`)zM#Qw+$?bt$g)Vb)>hj@={|2h2UZpYE%hJ%hGc0$ zwK|D(OQZvf@(d;wSgSCOVe@PoHOyh?(fi{K!VlCaPgIG57~?6_UdidOUKY}d*R$yd?M1rG8Hbz(r zdI;9B2uGunlp@a&TC=fqop7cBb`7fyQoC4b@EQ({{bO`GYXonoki0rT#3^RrApI1z z=(GFLL#!2RIKv5E`r1iG-8Q36jITh>PLMYxvSPXNYcCQ``V36MjyLRO>Dnne=i3y< z;249o0V-5r5MIsw;qKYq&;7rW0J+gj7t-Mf?T`JVp{Jscpk zWa>v|2oC$W8?gRME9^MBfO|-hy9zaCnW~i-#tEae!-4tZxT3`HN`h$_q>~bi1xVxJ z$T~$!lh;!E*H>}SsKpY_9)Twd`xr4o zs2sURPFX`?z(5-5Das9MrVW*nLezSs((1 zY`9nq;q7JQv?SPD#z=$pT-s+^=#>J2%Y)zcD6*Rzlyf^hZnBt&F^#PU>#}03ZNKL_t*5=g|HG|h^>U-^K0l< zLFtwzc3+9mc-(yQ0;NK1#}CI#*qVoTK%oMOyCcATNA+hXp*ity7yh41fbD9`jQ6q3 zq_-1GgmQ4^VKhp3?delQ=MvneKyN0bTbAfz2Pd(}eIe;+2t9%6S>nwR#ZpSN9@Cj! zW9`BQ_1QB1q)%GPC>^Qc&MNAACaLW3(0$4D;VCwsTSqorvNJJh3j2O&4_ZQDGiE+? z5K*(F%Og}npxYYLHpnrMuEcaTIs)qm%o)4w;olI%?LM}zQR5D(CrOKh@_`1?7rPj# zY1}u1E^CTGgxm5+ha>Dz;_epMfkGYHE(@D5<$HhSCwS$tryy~#0vuym-hGX=CsxQh z385d-m>Q>Zv5hz3l6NwsGK6(OHYg~cs1cuy@w7l!3`LStfBiT%^EmUXCu!a?Pm;wT zT)eP?Gwu;B_o$V}@#bCP^&aCVcJs<7zl^t7CpZz1UK`=~Aw`n1>*w|oP6eb#om;kk za`kuL^x}KC`tM)++X0|_e+6^U9FckWG0!hBg@Y+HI)sUXO$L{GXq{|3XO7niELh`W zBg5c@O?JFX)6ZZ4l}pzVX(eQDX22|T9@khK$% zt2vGPs$?n$G%f^&* zBkI+Z(!L5?f4Pq7YE&Q?Jl;VWLsI0-e#b2F&4|)HO`N)qzL62`F42E!NVb*Zjrq8H z9CX(qC@K6C0nToZJ&zuxwYiSdmH5ZYG|i$ZVXXIVO+sK z``Mr8(v1tW&ur2?yULZXT!a=B{ha(_3d&NN_Q?leJcHTFF@-|ZEUM}d>?`32kLa;Y z+zvNl9*`WTao zjn(D5I)AqDJAW$x*o!Ii_S;qWC(r(2{lo;h$*~FOE(qVS__lqNk2lzH`yPhZJ9uyN zDPGnDJ8GnZ2sx&3gk*FjK{dhKYHgb}y3BGH|Q;ky(h*u)mOrhoyzY>w1FA%kY=u{8g z(&T8IwkC@O=CVOtv%K``lMK%fx0_)VKpLKY<{6?J1BzaTjJMss7J>34GRrAkMR>T5 z6$NA8KFR3$5ezlXv?Mnf6K~y#TX7j)=n^)|)Nh@l{lX2*gimlpP`bZ_no`{H=DWxv zO;Gc>`K1-aO-tcfI40QnhJB>*kkUJvyyaWp!eAz1e&?=zSIoKJ{EI< zIX1l7L$4W>19oIU2wGoRr~S2c+Fw~CUX3{P_FGBkG{Q|V1BqErsP3JhJma(Y{{3uz zdIRbO?xLh}R~fUGAy6bYdhB@T0{O6D=R-3j{S;?D#4AY__a5ZpU%WzX=Oo!ypZNR; zD_q83UuFH{Ya~~Pq^&l?r+egQQ-tuy%Q<1)W$vMQHvVK4k%PY&;M594ASf@^DBKj$ zu+-l?x!rqDbE>yih=(acD_lRMNK?W?bsGDc^j3PvuE1IFD7JFETYU6TBOHqr4z`zJ z5^!riMq5NbC)YV5HsCmDt(khy6vNXa8Xw+)PIH{HB0igto*7|!8YplUtJsl7g^J2d zgTc!iXlu!1OL4tV^=&nrodGj)p54wKUi<86Mk{@$_AIcmx=y7WQa;gO@CPkAUs%VK zB-vl~dHD-3P;b;poCsF@?&!ttJlIQxBmmfm)0s1^u4z{5%iNV(AWfQ0b@s|=$;y) zFeK0P(91c=jXp??2*viSfCf7#kg39*uaG-2);Guj?0MrZrha@U&ABn;$pUvnasR&i zX&$dI``{vJ5z`p25xEJ)(*t(CZ!dPzq1en|(xoz9r;`m>Us)#Z4gbY|QUZF9w|LTg z<)Ne@zb`u-VYS89CB@C{AC|5pU{fVsb%?^o-lP{mV7ElNN~yl<)c-yGcguJ>1dr~YsmP~8goYu5_u6xtR*arZ@M{jY{`a@j zjXLOyny~3J@xV;0f1`KO_RZfc0do%@eC){KS%@9L7&011B3%b#eu}26OBLS(CW9T&o^+>0CPPd?PnB|5Ix;Tr3&4ONX{tw zFW-c#3F+m8WVOrLm#!c(i;WFJL+_b3`2|gUy$_o?`NbS3RzyjL>nn;P!BoIr2P<-l z$l^cLM1Q4=2^7tT#z=4EIC}(k#gb(yjuePSh~IQ^#sjp@sl8#0=?CZOU2mh4oW|Y; zX)D2hQ;A|D$7;)-2M*D@wuMOy!gmmbLwdD~8wQYDbPXB@#!1{h=0b{|5saQoP%9EG zQmz*xMy z1AJM+mSe*Gbxcq|swpZiZpUIO`F7f-1XgB{DI8-M{^14;Ei#d8{qY9Hnnk{rAw7W{ z8Jzt-!{>UWM&MQzHixa>US~L)AqQZ4F6|dv2rE(2MU+xJB$q$?8iE{9*fxTSdjdt4BPDS4DK z?6z2Yas?qIW9}sH-}her_47Z+mW?pSn>f!|@<|AGd&Jk|_MM0fr&=KD2IGUQ`pC@z zjx3*W>SdLlS=av;2Cx@1Cf`2)J<+K3uvk~vVGgcD%$1PRVp0vMMF3Vxqyg(296Wko zYoi)2qNT{XDQahkkr|E`;!gP_C;L=CI!9>`GQ1RHvu%ThYC2eBA=5b368TDkD!@cJ zp6ipf3qW9o26KJH=z4;_k(0MG^5+$DL4#{4?WmA#L=@Umu9V378D^B@&WHHpAR7Do&0B-;(9F@f$C80{mC z-L{7X8lgZoJ?LwU6x0t+ply!c(u{xCEOyL6g$^>b;8^0Vq)9S^U+11OR7TVPcE7FZ@{i^=Qh53Pu`)zzH18sDNPs z!i7R?+n~n++`5mBMp!8^dp*3=B7~yo#5m(s*osNkz&~0+UyD$u3gYe%>nNbWcoty< zA~jHVFp=K=i#5l=YS3E+c2FRMhc+N{1HMHrl*q3S(H#R@iflDPCjuvvz4l&feZ%v< z-)N3kz)T^LVe#)TQJpVvTMn&MeJ#cc63||={=#v3$GVKq4^U~${K}9JX;k5n+ZDyu zoZ@PRp9R!RD9aLsLiH@|!!4qA!upvFzHLuw)hcMi~2qoxj7)Ku`Oxz_wOPcA>m0^GhQ zUREoEFYY{~sO#zgsubL4hU5;6MTsL?)ToUJSC$laW~~40X_}{7{LO#*_h~-d#Xi@7 zRfAHF_~{;@U9#}QEvjwL{`MG@BJQN9NYDkytXB1i7?jeO08)X@z}ni;1Mg8rP~8}f z#c$3b1yg9GERngO)-2Lg=&r>&i}V7vV+k9A?OW8Lgy?umv(v!a2sO;`xvI$_t&u!H z7SJPM@@FGuow-nzr)8xz7oP8z37-*FU^ z43&c$l}x{v;V|d}!JJKDNi%wLOx%o+-9Yl52JL6MG@t4at#*i~u<*XM)PLA*bpPJk z|33|M*8GxG0SSNoR|QRt>Zj0&AQqH?XsLl1kIEFft8lGAc`N5z|I`1&{AN!1tvMz{ zG>^6sFQ_2QZ{}=#_I^|XddZ;s4kr}ID$vk*f^VLA%ki3nSs$O>_3S@_UVq>-bPsyl%(*s&#hXTs#% z1g#7@2^@d+UMMT#D1}{*Z5#A{MMzVMZ{DWbDeB*|eTtXoOtvztZE<~c3$vRs8$*93 zhMpnY%xNwgn*9_V8KjY*$3mn6*}zeKZ_F?K&Oar6sKMD^ew=iz!__~y$dN~mQ@l0h z-k&&27CC0GZQJ#eYtM{-WBcD<0jocEMCJ3@|0w%VyFi7%Q&C-*6SupR!x_5YAPh^~sKCt}ssh0iJVYH!H4T*8b7t3P zBvVT@5>N_CN96;}LoKr13DHUi=_}5D@+q!<^BmG@V07(n5{rgqO?6P91hloxFO3O> zFqq!rYIGBu3%B09MU+R#?SkS~ML2+bTGH8AW%~UgX7ihw2qMd5-PCA8~90yi7aaB@Djiolow=H)%1%~i1Vl_K+s58piM-z zH>SE=B5i|Nj!`CX;^&Vsxma-ILr3wCMufRXSw(szL9fQRafT{|*69_B;gsab2COPt zD_!)72)8?jT%pE+GIJ;s5D{?op{ZA6XhcX<5=Mc|?_P(4fbs#!>w=OOU1w9HfOtdu z=_UGSdMrG#$aFEMnCA>`jJfBBPLN+5Gk#Z)Q;zQA3oM-JvcGpgW8LC>gk6Wm^F2(a zX`E^>`qlwjE3#X2C_T!?l+!sl&*d(a-IJ@@^>gB+aOgCb$Jb-=%B5 z9O0&hxNj)6BWbq?mB740v|t1zD3sLe$6Z8Fd;Jv&a@_#@$ba>jU%2w!w?D2kLx?;& zO`)tvj<@hfG-fa0wmoLWVttBgQ3o_8VBgmvDk6+in2nfnn$e`g))%i6W*%cI(#Kb5 z{hdB?afGYth&*h2jMD^D#{m)>BxqFSP_adMC`NO%70&*>53u!>OW-vj3Q*|Uztb3W z91*++rBMb*1vNEPp+E-%K~PnwE#Sr#fk5-YHlb~aPb6fwGW@8-?-lsmqjZXzIF!;* z22{5}d43F8OuQCRjY@PBkt{dyrK`=Sj0RV;5mIWx&YWPuHdCxsNY5fJ(0jZ`W248$ zW5*d>+r#CK$A0BGWD=OZazHgN;7E&GU%G%DdX|3SG%vjHL3B6a!G*KDbL$P%*fP76 z(R$d{=-phQl&8E|A%({sIQ*zWQO`I)q*hpqs$agSI=)l#ZiM=p0hr0;f1ci&9l=Sx zXsrmTN}_hcdq43kJJ)X`-a%#YILf^Y<%H%B^>G)bxRIxt7r07LLq{bLs4z`~Sxs5| zxkVNimQV`~vKvFxtr#a2#%qET@Bw2iDg~_)q!9=+k4`N#gtbSGvi;gkboAGdW`IZ` zRjAV_ALP{j~z)<5~p_ZZHJR~)`?z4b26f!E&aZIr}Chc_@-rOa> zHb-eqIhiwf?IzNV5IoJpO^RD%64m6yr|x6Kh}+{UeE-V#nZGlm*-x45%@CzfeS=(4 z=(Z+|LR~R+p}wX{!5&KR&ei|h-BU{k0W>-=azTDi0~~yB`+d65Xs^Hs(UM?{!3WPn z&pb}FTHldM2fQoLu_Z)`;>{@`vxxWTm4v9JsIrRAeJxJ@)B`;J4?f7kXI5|rIjywM z?cY65dv%G*Ofe0O51|%PL>d|vKd%VdqZ#H6No5og zDH`|oYn*FVfsM(pjL?y-?b(!|79-qBgij$}Rul-A{`edRU*DtjQ_@FTq>F7_q6jvi zGNIfr@gtA&fKpTwhiHXLg>*T^SV3XID3qy(uPCFS#tDi0y53OU(mdWmKeEK&%`GOE zN9b0QkU+LMqx2fxPEkuS5C6zhU}C_NsDwzRG&_BUZ*S3futno=o6gCYPzmkj7OLwA zKA_tgY#=Cu7>5^7UTD2<5#jDNsTCouTi0IevvnG{*gYg(VVp-Tg4jTqJ5)C!X&HX= zSN{iI__3?RC6F6;!Vea1%%GiFMi! zHAveLu~h`CeqZ9X~+#>2yB&< zqUf*oIQPoyq(?faB~9bO7U53a6EF8?xBwEMv93tOKrWaLRGo_3Z(L>e%`u{cs7h!r zrbM(^dSMkm@<>?`sscTSa8OWA0xALbcFExKfJg;&1kz5hiG_}$cc{U_`;Xx!Bfuic zVPi|2rUX~eerN?z9u_SmCDq;Hl5W>kO-U;BEFhzTrRCGK?p-8`VC}t2v>xc7k2g5= zUz{PizYo5^dySbub#q84U9AQR0j0n+BBXM4Ft`}kSCCVL%Axy~;6N`X5E+ht@+?W) zf`;k&Au_8d_XEKTeqR1Sf#fp>g`qf9vT^t%&E+=lzJ8HN41O}F+6+vu8>Q4yU0ldk8%G=H(=mD@s&|zr z{F0&jaEsy1DZ8)SMh!fE&y!!uAf~S8XPS7$LJIqTw$Cifc=~gnLiUBxAMI0oZ;siE zn0{@K>E<3As}JJxg7Jktl0&Wfm@*MWLfkSn^*ahorU$qz$F-WEzz@q>l@nm;p<}Gw zw_e8%p`vqk5$CHKAO6P0} z=RHwsIrGyG@$lh?spc745=2+%G^QFASXZlpRHcz3FdI$Lag6R+qE55 z^(N)5DTKiFKX`+D+aYP78kQ8-rbyorwPQ?T@U9{xn$U*O4?O-$&vB~tFrjd4UA%?2 zAkhLV&n)2jLi?UJo#z&?u1PhkMJTK&q^~jB60BfXEM5zG*&;1npN>xM9uJ_De!pp* zY=2~UW%M_G=2w1^{he**2Qy3uw71N!jZn9s|2J1qi<5&#J0b|4A zPo8A<&XE1Dy-T@MkQ|M0bs2^3&#yAdcA0JN;xo_w*AIv*O>8asG{>Ydz9m?rF>4W4 z#kkEJyaOYcIL7u3_TfIpIaJp$erp%;274$%6XW-DRG|n(029^yg)-DNgE&DQ2^{*! zDM$*=e&k`6m+$4F{`;9Wci6qShq8fu=qaZK4?p|_Z-3?c_`D=imU2{~ts*_$rP`Y# z-lGyr+-cTMBq8ATbL?td`y^`UKE6tMYXWgpr(oTbu%Dshl#pknN80#p@Og>y1`{c~ zcEk&o?gL#qk1w*5-)1x$v$(Xx&X=yke#Q949@VDc_Y2HUK|U?e!-{GYV5~4!QD(I{ zmx(o!g6Sws71e@~QX?v$6=;!LI@>Hx#Vy5`e*X{IzqW%4wb`TJ$x$fL=uN_LRrmlrN(v)(MC$L-ePtwgWOVG@tBq(dR(*moRAN4P#^x;JJ2>X=|P_=@EC5>ixk+GZ7s zI+YX`P)-ji;;(*{%#7KN_UV6oh0<62)!Sc& z69H8S<;LZT?MjOM)^+&CxcHu0MiP@pT% z;=^nF?ce%kLa#s{X=2wEBEssE>u8;F^gpD>dV2)VwI}_gfL+>X_>NF^Lp_-R4 zGqfJ>Gu)oC^k|Q0rN#cWAqx+7NKdwj8Zj=_0Bb;$zo^8JRG^#n(e;mh^;c+Ybn61k zfe-|6F{a(2n&(W;?V;uY(Fvn3T%#P+4@U?{5JZFk(A1D>k|$#9=|$qDn58p^snRK< zDyQ1B_{EZNMv5 zd-D#5KD|LU_au!r%biuKc}}#_frW^zuU+EG`HP5#q+=L=@qpy19;PiQ+khCNBMlUR zPzLlu3)*1sZxD>2B8^y&#?)7U$h|tu8XQlN9oa5_V6XEc`k^5+#Zqt2knP@S>bQRKo>1@dE z>zmAHQxHd#q*U1$N=uw32zL!IhJos!MCJigc~tIcKe&L640ff(^x7Vwj8J*_!ONE- z&|B!U(LGYz*z^))Ac)Ud{pll=%K;hYxIxBfzDv}O+5gHdc-Qgz|NaWa%X8$CWAfS* zDj{xD!zgKiREDrsAVy=ALb;4ERrs$~-~~PNXf#3q@ra_SRZo_66XN9vy=1AH3fs`w zZUm)5d_wlk0e)DaOJH8Raw{K*ri$*FKHVo1^l-+kERlxblqFei)E&fq&G6+RJF?HM z-@Z)IE9gAi#`i7pN(U)CDsq^a;BL>6{fZZV{R`xq87c+S5HOnH0w_bW*g!P`&4*WN zV+KGMbr)0WwEzSk7c@WFM;})lymXUnA!95DjLJE?=XTh=be%8$>winnwHduTq_{H1 z-*R*x=rf;`_@u;b7f7jSRhm#i$c3XndLMLEebN*jae^{JsRAN3dmwX7ertl82UJrb z4Goc?JlH5^`@7fq+<)>fF|&xuJCapRe5T34%li~>jmTX|Sp-xnjH`@hyNP5TnN&2| zDe8e9z4vyA+D-C{V^mYH%P}f-*o6cY>Qh*m7#z5UCyF#KR_IQxp3-&8H`bDvg|MVC z{RlM&i2@RVk3ft==^USG(r!wyfzXELJzboI{!bmEY-qxnh(TFklR*0MK6VXUsOW!a zjoF{gP@9O1P=SD;dh3NtkAH*SP27 z4>N7&*xVzt29vjjR9giLM;6$-IK;-#`pIRcdpV|K5bxM}^%go)=!PX~7^novEtEnK zMQeSDs>n!>_s~7qc;*CRG%D!&5vl?`i7*2}jKM4#&V2H{$gG69(7bPf?qZiHHW(VT zR7~L8UW+;;|)Z;YtM0k!JzH*1T!a^GQE3FT%E$`Ewjd=l@G0aPWZZs5QF^1r6_ zKp$l_C?fsP-+i*Bvj}$(@R6c_F@W^8yVD##M(|`9dS`AQ6;jdSivZi=dE>w3W zxP}lv)La{^dNRP?P)BhZ2jSF#;@;E?h9ROP=_pj-ys7JUc=oc zn4`XWv=4}_E#P)uyo50comK-$4XqQ)#7aRD&|)x&AWe@$h>ttkpI+i)KmOws7OI;< zd25J75!D^f{`fZCrA2}`oUMBflVQN`I(n!2C={iyPz^)xM^@?}@UA_t4vOkdQLkfJ zLR`~JfBEKLKu-uZfME57-+DXEua0rs8BB%AOJmCKSD+!gSwJ_?e$NU^KX#bopL!TM zGTG5r?f?;db#=WSL$a*~}luQJ-3 zk(@}WbPhdBG%8Wjtc@TtPjzmBU|AN~yTqGc{w9)XWlw}_e%6017svkKF;*mjIm0#V?D@B0YxdWw_+BHHwx?EN#P zvO<6XrEqY$t-Z>y(hWxrOQ@p~0|Q>a3P8&s$`!Za-AxJC_u6`^x-5qEP)QFt0B8u+4p>si z00@e&e)%uR&rj;g>C7vWKHvXuC)WomX{7idsD_ZN#}rpG7%0qxhP`V8#+UB!)xUU= z&Ps&_pgX6tJgsbWKPGD5CZh^zSo@pXCc*mxIak)Q@ebs-D*8`!YHF#B zY5BmysTCHEZm@G<3%o{KOSn^^Lfv*1AJCD;J7J#980_yceDql~x_;pc(k zN`~?ZX(>!*QQqU`iqvSDOI`AAKqqz3Rl9Q~{Q1B59d5pL4fCKPd8$crs)crrKpZUuNd?qNDQ%yaIekqcRO%2Bcuf?QrJRcHf!0o zp9xW>kORm@j>geG_<(Z-y-zP;VuMK(_VH$I16u}UDA+h9K4u9fV6A15Wr}N_>8PZ* zSrT%Mi4EoL5+jo=H%Z!}QX6a%ij=zq-r*cWz=A z70hb{y#G)W>kVqnGJk1Gc78^DvVj2EEl;%XK?PJC5!xl)hZi9hcK>RVpgqbsR9CV3 zfel!!YY|dcy3tLA_8zxi5w-)WTZ?#-l}4Q%tS3n9(1U>Y1+Mfge)KT*kP1d^sQ;X@ zYD%QBu_Ek)UNxu;)OtjEq`^HOe1IEYe1~dRc+Y2_W}|tOH^1--D)SI((nNMDyejcy zjovNEHgn<=4OAjTm7%xNXLEQ5Rlv^IZ;-!TU~G(b0k`95o@%3$g!WIY;q(lD(IXOQ zoaqvB4-OEAG&Mf+NFh|tp%7F7I$9$iHgTQlSUn0vf;RF^Z7^%U7qoO<#!c`@bi zbL%WFta57UKCbUwt@Q?FMY`6+xq^752|dB08EqEqjyAdQ?gfgi5gqfwY%M0KY`FBn+y*k(d{y2aAz4YYE&dCvYf zN4RN)$_253>8$~s^)8)9`+WExe2RCz_6FgO!#py#p=$?q#)hT2`d@3R2&{}M<^5iiNc6WI2zQ-8n6Gkr`kXTDGuRuJjA3et8 z`~dfzUHq=XWe!~`%B_-kIYGw)Ct~bS(7lLgBO;0#l+zq-HO2}=5fTSlrx&O;Gn6&N zM-!@nudQ9hviJHe^1XujBTolU z8+x?U1SN6Wn}+ohq^mV}gU$@eZv=mne3|%(pUz!###y-z8mZ(R_A^XaNW@ zWn93Xruj@8uRY5@y2SK*6SUE=Y>;_i?Z?&_&WE{Hnc8*Z3|UMl-Uu9j_8xQuAu{M$ z#e5VHt5GeD?#9SGV7)=Ssz+w12FlfV>G<-1D2I5O;-`)zYT`F@s$R+7=55}0`Z-qZ z8YhmQAbvE(cLaOVpbsZ({n0h5y_^z*Pc*jD1Qk#*_(_hNdDzSGGef-4;HhVSg!y<( z+>UCsZLf*WYZ)0yhH1ypsy+PDqX|CIsQV0Nqs{Vr4zqlJpM0(%RD=L34m7$c(ftlP ziO8?zbRKJ=8U_;!&EGe`rCQK=aWy)ig)40LIp3bOoXV(p7|9 zQS8q#iAGm}y>IOyg`=1W>CuFEU6U7}OGtddVr2 z+ER@&x-MnS9w8Jt6oOebOuscib_2un87ft18=;IxbwRdc+N6c{#@#WorE_mgbPWF1 zKmX6r5@RZbIw3SaxWqj_af)gfP}>DKp^-LFML-vTuW$;iS4{tE$gMxVj&uv6Q*93q14OB7lZQ5qrE(SQC3 zVWUMHm2^7|jy!sVq}ia%9Z9=^pXYdSNLi7+rIAU2pE%rRNqId--xQ*OqS%{~yAje& z=q@Po%CY|ZGW{bRnkOyyJhRT>A8Qi#W6GdmIda6N*-!IWK!B1-% z*_1-|${6JgO4rn~%RHe{sH~ojW_*C-5m-g=710W)Zi+EN<7kJF2;C<-G?J94rI~(v z7qJmiIvOVz8P8|Tw?>@#htJS^yiNQ-O6W)UQgi1E*Ex3dB>HedbSz@E(ZJ_|M^U+g z$bng_Xgt+NAG37MG@13Q+$5)yXWXWHx@=SLo_i{8e)BTQTijlSDgu5Evzr4Djj~Ww zj@hLl-div=+sA@JrIz*w7ntXB8Xbeoz;q1Vw1e4*2~)V?-sOA4mrzs1?DCYP)uftc zs8S(TBZG?Sb_PQTE)WV&C^T+6Lua1S!}P|CSHJNxEALyPGzDS{3t!{loe7iejApM%F)9f@ zP~CPl6||aC9Tat1r)#HgtU@Y5Dk1%37d_IrS%pq4#%Kr*iJ`I9qB;<4q6qsQrG@TC z4%69alkH@LBXHvV58{oZv$RTHRBZhpm+Ae`3fblqH!cXJ$Hp3I343qvGkt50>}5z* zuhXTM&e2$HQQgj%ECgE8KrMKhPqoP|IrO8JfA;JD3*SEXRetGX|AfDK<@2u_yg7~Y zA4`*jd@rMQWQpBx+(tza-jp;?cjzDQbMW>6JqYw3U1IzDS72&Up@Qh{b}`!$_&a@cqA~3zlidM+IOX(1k8|mriL5Tc_)(5AG3A?c%DG236lT@p#}0G6MNwuP ze(ogGOZznMZ7{w#X1Fs)#RW^N3EzL^Jflg@bT>m8MPnnz#t{eK8*=!g8x(0pd9gr? z!ZxGYEj=|btGUBkxH3nyg-+c73=<87)FRldo+}_jjWh&FDi%(zGIeA6PcP8vE_3gp zGpt32*)iK3{F~e4TRFxF%4$^0;8f0ge)3tusN&$2+u#%?hma_u4NG*QNpW?GDI(l7 z$K@b<6)vwCfKe}J_u3Y(UH&HXtM2nU9QIdCqNr93q{7|V%!d1n&|K>H4tA_EnGu*aferLq^;x>D)?ef$8zr}M8 ze3(2HOt9qhvNlZiRULaAIOdn#Ys?>7p>+e!-jtDKyeUCf@>x$kJJ@lEF z+_-v|s~pp5;l@Jt+602aJ1A!b2-sjy5>QzDUO+c36doo4<^fbdTdD8;j-ma-i)iK1 zr6O7~Y+l)B>6t^|ET`A+XEfbss0L)aW7ItfA?d2?~lI1wcmdW zF9z)c-Y9Uw$zOVm`S-{5)bqZhv5=75ACbJ!!YoBph3C05&yfVh>`F%U((Jp&%%h$x z>0i`AkS&e01LM~=@eY~~^eAtS@Yf1hiec`+YSelIQ+dKX)cC6eXj(`#N<54MdI6$# zM0IV9-BXBIHh%OT7CMXE&TlhF_Hb7Um@B+dBuzy%1KY7A%L&y^j+BBib#X-%9yS&U z2N_%gnbkk-t)N?)l}Fc@+!_+*f#`G_+00NqL;t6n=%W#lGq^#43xXRLm`YLZmPn;3 zH%tEHbDw8+yQ-bLZ&z?5pc)aOGK8HeoJt9!5?2LmLu0xLVrsExm{$KLa=l_z~yJK84M^%Cp z73srmSX3nMiDEt8lQ%jqKS%?!64n7uKs5iU`r%JRsuCn2_oD)9}AHU`l` zK*+ZXLg^s2sMu3dp=Wja?n3Q%+{hZ}C5s;f-0l3TcC*Rt#Sd>#-70B)bO|d8J@QCy zkwL-mrF~Rlp%2DtO7GF5oKW6XoP@f@?57nn4z;h4Q7F;i6(I;A7h0q|@z@VjjU3%$ zU7}Qz9Io$sv)W?xwIOQg37eXOZ;q(4n#b1o;sc){8njt@W(mD)p&4PYXc1H+t$Pq@?f*~H)b1(DphVD0{U9AA(ln1Y zP;o$@chI7^jGK|>(e;Vc=reL@bc2^H9soJ zU!9SipHdbTHaOz5DP|)^Eem?b5C#=;b;k5hZ{fdxa94Iz`}e2{n3VWD)Kf!|T9BX= zk{wB1E7@4nKCFJa%FjpPwNk zK@8*v73DZ1Ozs*&Xpgd?1*Un1HO1~~`2d%XP0tN3{> z?og>lx8k~0E((Ie4J(?X29p;jl+zqPDj0rsk1AxCTbkeeO?diYmi^^sGW+9kDn&jC0c7_>hU)wFcA=iwjjD@;#f~mixwT# zVo#-N{=CGqeBTP&^8=c_1hd+pe|!NoDey&!T8nWz0e#7#qln#a-N26=D2;wd)3~<@ zN+S!3#``*$F0i0s!4OG3ph|{yDb~+C>RN&Jiut7(zxI#+8GrH5e~;}yyp6e;plreN z2bO63aGT_)CWs(jqbo2A8g(jxrz6tGVpgA7A`gy6tk~V%p&<#AEU($R+`(Ltk4hv_ zL=yArZ+(-&&Y%WVMe0#+#uByT+G7ZfUDUKb)Ihp1$$NYBeqx>B+uKyRV&}pRq7B2r zEvDZY;cib+Dn`6O8bCw z2G#8eey%a|=+z&50K5O{^8alHp3#@~h#E0bGiAOLkiiUD2&88gAqfO^H|PsB#VSgn z6e#K$u}bNBm~x#J{d+62$gumZP2RuqAvQj7n)^TY5ZC|o9A>3M@A-9-PK22Td=khm z&j%2JHOvK=Wg%bdpFQ%fB*=9 zAVE^3NRgDdXrbg-nNhPs@R^f9oHxx zYp1nQD%LL2IeQ@R=?_5zyUUW%~Fcoqf|hKKm|2Glo>qdc2943s}7OAREst z^Wy*hJVFIHVQ|(!VR41$OFA6xtmVuve~a+dF}1k{>VRNtWki^l*f8hN8}A{SlW05q z@@@c_nQ&7y)V7*WV8am12ZS9-G?X+RTjH*}?}UK{r02uqCw0O&ObN~!#0c#546`=I z>58|Fz)G-A;)KTPl18&eH`_+EVv3VJHa@XJo~M)zL3A{saesr(u^J|lIN_LkcmcJf zKuXH4p*UAokZ-U(utqc9DiFgQ)(pkfA?fEv=)7R-pKT+q3;JL0p;rvfN+#}Hpd4q6 zwV`p}B%&!$3lX(DLd0}H{Vi=sE$w?}s4XT$_chSBY8L*(Bk0DME5C9EyPV_34pQ$( zMyNvjKxrvnln@JoM!;yjAUY8f>}wIvCItIqW***;YBd;aZ(v2i z3zwck)Cy#*$S!3NhFtveIf67mWCEFaT>_!Piu@w|ejpIeQ}?r#lw&`04<@wOWry37 z;Ifk8<|---a4YUhy8;ol#SZy+_`cTNjkyPY@ojX|4YD(1%3(qMt~wWf?-b=m;WYv) zW{E&nDw!4II0U(;h7uB+X)wa)2SOksftphM$oqeaOX?iAPrRD57oH~%Q#ywyDVB3s z2YQ11+Q^G13=9}YJ~9-`IrYUl$?*i41n9JYLSQ-}p|ym!Odw)|2`tS+4bqJ5iRLYnt?&oB|+U`Mh+PQ&M^7-UY7pe5>Xkl|IWiqEY6Vi z3-(MevT=04QDPjyZv#6PX(i%h#W>tIWyAxhb1y|AEy~(!p5^e}lQd^i{ zd|?Z7mt)WCm*_vafzAy-`=9+I`s2YjuYT^_Z|?>`c_TyJ9v=uAlMfG8HyHQExErZA zA~;s3|6I?jX%u*9k*n^U07R&~cb)>~B2T6q`SGm)obnXvs8i#g{M?86=*K_Gwc;Wh zCzqK%x=3DblAkxQtYJbTXJSlfa08!11kC`O8f*#a^8@UP$H2M3A=46*I@rvR^ARp} z6w>k^|Mo9)@%(xECpR&Pz>N(^ftrdvNHSK;ylpS?a1A#yn3hD$Ko|v-y&PLO%!)x; zfgL&e+qq}_v|@Bt5>MAKRwI)DGxCH{TNvVYg0b57M-<432!x~a)=5myQ0k1{nUvvL zm+oet8|yC-H=A^(W*CSqx?j6Nc`+yZRv(dq5sq*m(W@G_TH;FloIxsIfrLS2Xz24X zAw8{6*qCq+^v?A$R|<50j9K;)wojdWlB?^>ziiH?&wYnT)Wl-@BgN)`|KRWZM@;US zA#5uM001BWNkl2GyYA7pZfvmI1!hp9gd)F|G5GX0 zwyQC#W9pq2!hlnWNIJ~pcM7R266)Q^^#&MMLI=sV~k5fRPG6? zl};L_@SFo_LDr-5GXZoZa@wMnVv<7*Y;K8{nmFq# zg!=x7pe@mq1d(L0UQj&Wr@U5x!)yAO9YF_CIf7N%ik12Y(jfxhdk{(>gg`n;|LILg z1?7bi!98{KW`RQCS}Ln8HUEpj)7y6U1lazgb$<%nI{(pZl%cPWsU2yeT|v-@k@E`K zl+}H25!l_bLr8~>Aee6wOe8qrvwnigSV?;DgtQW&ONvs`|H>vWe)Ai=boQG#G&B=L zC@`TUn30_L*+&Q$Bq)J8&_V@@Y-NCO5~F?G$x|cL(ChP%VhUHM^-jNkW%EPrNB59^ql;cGF;fy@zzz+r^wEbCH)~G;0)Z_{ zpZpbmNVL;rkq(>-5k})`5qJI4UDRINpf)!};S6rek$q!?xmG}FX&$X3nu^|2ee%tm z`iYn_v?PgFD4`Q#HwxrTjB5u-1L3T~&1qa1_=p~ssEHVdKnDgghZ`8=Kr-4GczSl3 z(Nv1@#WBV@%;g-03cHnI3Qtn?11akPpMmpZ4=0e$VZ06w!UhmqTxn4!TwZ2!ejjR2#KOW}F1>gey_%6eGs30@cAm2n z2%-F|>MEQhqDltwnt_EwlvTG8gDe89@^Zk*+dBlWZzCxvmn|kMA0tQqYkmRdi6K{h z|J=7JQ>9@}91^P3cxMmRFmk;J2F8zJen5!z(P$dF&Rs7*)q(um@s!(K^IN1NC=hblFq zVTq5``Nd!RZjbJS`9ML(fm_{CV*BP9`ee~m@aN0Lw|{u@#q6^FNqc&Tbs!p%@Ae5< zeF$;WRa6LE;*cW1N$WSgdQk2aA72Fu6FOWe*^X9O)5~m}-$G<1``>(&;f-zdnK5GM z;DH9Nfa2v5dQi}Md>*&xa91oQ&k(f+c6EqLz5cDuysF02AA5%JdLKJ1D0>Cvl?>6< zjIZScxp(afdxrgQKEc+7>jY5^OW;Kzw@XCiTdE@Pf=j|0#A1xSUQ&Ns3v*iIf(nx_ zoTsZBOId<&7CW*CXMJ*5+Ul7&n#XE%M`Id$W3tthxwkHH;=%j5k=!7Ziefoqv^Au@ zRHJ`pM0s|IwXSNU$=!|KIcX&hnh_$Fh|*uNs7m$2Qpvyop`eU3ulujx!lhFeu*)Uo zhW=m4-B(R&|ILme6CYY&`*&79oj$caz$#x@Ho!F%0*7>!KAyOF3Q& zfJ9&kwWL{V@Yo&il43lNd- ztyobF-a-Yjm(;a^Z^ODoAV!X-fA>lB$(-_BiTg+9fBv^RhGgFw0vH*ohpgk6G!{`; zPzu~Y`yK#3(-hz<0%yTUMBGpKAh2DHla6w0M~TxCHWJEh zgQ`nfx3?jXURt(hdH1h>kjq!k<0cFo35j(~v7w13K(-W(x6e>KKZH&|bf|_(1pnRd z|0ZAjm!HD)G$Im2vrTkf5Hv#cpdem|y)$VEQ9H(z1&u=;#w!^cm#>qEI!Pm8<-%2Z z;gB@%QaUK}0$E-6+aJG%y{x@P5Wckb@0Fku^ualwOxwaDM%6cJuTVl+J$EBP`2HX+ zB<&5c-4eSkKkPnfp7^euLuDv_(kG#nV|Gz#ioW7sdzl$iwIY2#8dEiYUO5O5UZv7m zfzX2Ti#eyi_zYh?^Ji$4a^_p##FajIv|WR}TrzunFR~qzryg?l?*IG)*tWse161Ar zTp3mpp;CB-4(l+vA?ORz%@jL=a;v2OjV{H7A!g(V3OM)arx~9clMHHF-P-L?w|Xs|2M|rmTc1RpOHxhebF+=lCSIzWF%T3SNKzJ2^i%&%~WA`d{m!da!w6 zmBBOXPzpruIQb`ELl%-ygPm4{6ES9>Q5|mrF>zvo(Z-l~I>B@-^0qo6_m1DK{cV^I zd`GhGxb=;9a{bgY&DSK1pBwv8Yw3FxqAh4>ML+Y`fT~Gk!;xQ3E54-9{=>-tGd3g> zq5u7w5;Hagdm`*qglj134>k#dI=!?@xUWT?4!HL0RYXuyY>x?ACAGyCTNgH&J2KDO z$8O-p!n1>%AFO@ndnEo1UyJH3_Hz@d6hb&$Ln7+FLc%)otLaxpAK&_4zB}K*%=*!8 zDJ}@d-$xpRR@lq~G@P$C5zgKGfX0H79t&^mcLac}`o_-rAW%4hBN4UxCg{Gj#WSCM zg8b?Tx2I0=l`*RA7))ePkuXd>Kyg_XRPxT;<30jiS2=tai zL=Mr2a6^Mq0XB13rHPv{c`qlnA>rW`#ibEZ0WK7bH^zv*qbv;dy)~F}l-&WU(V;Wl zfk@Gun`U$^WAK@^s?>8h{ffK4x<-W%yBq(?U|_d0bOLGHDno&NsYIj->oj7RB8?;nL(FuEOB`lh5Ff9j;sDhSsqe3O@Mshv zoWK+c7ge924Cp?;jq6&{4bACqJkR(-$@uGQ3|9Kc=>$niv?rl7n!#r`J-#8;IHSlj z14<$a8CmSdtavz zf}(F2e&G_!U%$jwcMGEhHXXvOq$k%{UEN^v@HAx(wOWKrb8h_FC4`6&LL)-upPj9g zy`86*{eRzjs2E9j**78avb)Y#3H8VJA?8Blwb3I&ih)@+-&+7wrAJC4Zxd47=}PDQ ze5`Nr?4C~6B86{GNC(#XKZWqv0VxF1R0oZaUJE!5=sY+@bht%&ZbD(QIG5 zGm3pbb_?yj(@de@Xn?#eqBu3AT-Mk!Ln%w+?s???1QmiCSg0vvO;WqRLF4Wzk4Kzx zC=q+ru(9(v!cur+i^4#nAP6XRPHnb9ZN7%+q%>w5OgE=dO+|e+L?jB41kf-r8K82Y zgt%9rMiOH+@qq-{7RaTDyf;SOR!1I&JAU;MIuB0+hW7Cq&6#QL|2vO@5o|VljGi0v z;+b#KYE1H~gAa1~<@27@Asq(e&vIAcE0iBFIpJyGPTJi>+leX<6UqZ_l=CNtlvP#a zaH~1)*y8_Gd-v?Wb$`{rD*$K<)O0u`6mJ#I4_K%YJfuS=s+zJ%-_n{< zU;~`hsHKQ|KJ)-*K6RRf)*cS-zngD;?ek16PLN*BNgkRaYFLUbP<74hY`s7 zLuL@t%gmciO}U+d_9Iz8i}3yc&Qwq}=WtRWML@Kmn0{c1?wJiXpI*Vqgx=?uIroKg zFtiMx?c;Lc`F|I4%qF;QjsO7(0gywlf? z(UjQ{BCf=g1gKErWC%ftBtQg?&G`{~jx93E`z+sh5i{9fwmFNPgV59{Z{*~w8TbCe z>oJWX^Y`y%`?(b;6nbn>MMUtLgm|Gzxjkm;wR5DS(s#i!>4VT#$Q6ZG&x`|fKf`R5 zh|FLMgBm+r=v&;igLR7SF|PDFK}5dOhN-GvtXKT15I8|oqI(9bEFw3^c8qLF%C!QK zNC+f_7^7wd^YpU(}EdS~$vgcF(%&MQE@Nj#q^X_e8r@2=32ed!> zcO6R{Ua8E0XjLabVP0^l`-Nc2+X4RN0ss)RO1QrMxOCzdusc(TYSM={e2}3}2GN%4 z^Hp6#AQlK~QG}p0)D_bQTSQ67%fEM;SAX!0+%fSgp8TCp!i44EUAL3J)MxNgmn_fN zI-$@bS4W7kKsaB{dgn8VlOPhw#ACB$ms6yZm^8;V1kFQJ%Jf3)&89ISFMaaZUgOZ$CCcLHvhZI0RY^(ab&ExB>WJ8sN6EW9IKy4D-3Se zp2XpHJ5xA^D?NwMSc}skq9fS%BgaS&NddOVj@UnNEsX8XI%-p>X zQF_HMGc1v#>N`T^r=J#ITMLIPVDQws7h<*oTRL19M$h+A)-w6VCFGRF2#~dqXf~#A z2K01B>p((27c=v~1Pe>Icp3aHFD^+n-k0`fnZ46(^+~kzL|Nl=EI|xx_rq21cukfHns;^b#?=~et1|@FY zu>R}~E`QpuACg?voY?nf z4wMI2{jJMh_Qr~xjtSOTtTnrO$NmuWva~yfPp+z_c0$E*vtc9b2W0*O%SW|iQo`?ms52yHEd}f?QGapsfAND%F*VYak)FSF2H=@X6>=F$i`u#{3EP&nr> zsvtSg#5Dz?86u|>%z8$i79^b*=`2xLGJW?PwqeO*i<*t;{OBzn*WVC`p(kNZ9Bg45 z7H0)vQ{Y+>Hx&_Qg5KF{-fC7G@?cE3zfOsubt0jC-y+xk>Rp52!B%j8K4+?G$ zWW-DWvA~WD!n&Ob);E+w;T^052<33o3UxH3`QRj@?ttcjNm@YzF&UC9b$I`t_i@c! zW&6T4Oy3e7O-NcXrZ5EaHO$zeS959)%wX43oB=CygqGN`mv)!h`bJ6BR+UD09pBty zhQ{j*3Q4*?_H?#PGQ2jzjRm5##7Rtgy^HM{Kity3spXubTrLr%Lo0!l7CknEnW1)g zmhP~JUCW3csna-ICtRvCciRE-e8luai(EMQ0{PQDY!c$OGI#~ZhKf<-ccy}Z30F-4 z#m!);UVW!`j|3b%M8UF6?f0~$lE1)m#(fjShYQnhGQL#uz+I2hX-{DDlBe?*h!04@y$MDOWG)%JFvb;<;(UoKEFy$* zIHq-C0z*l?(WDGC@t!)iX>m1g87D=6QxX??Ag8sCs2;+kK<cP4VPXxg&UUydm{oZF|`0Q%2~gBo{4>X z8C+UHc47M6iv-5(bkc9$`p)fIySd8gYi9=FUeOG4RjzgONxPpzyJv;`-nUczMFK!b zg|p^qiMq{UPdF=JYlx5vJ1B2r*{WYpnu;AHK{|`9!Tfs;aN+Z(nR&w^y{k8P_Jwb9 z;R|OdiV>}SQyfYUaARQ^V>F{@`$WwcV>P1g#hJyxBBFp|WsKZZK@ zN8ipJ?|e1qpMC*5kVKQ-kt+-&Qb=rU2o5ILbpy44Fj5#*xroRRF(-YkTv!hxU8-RR z8aroEYhWXR4lJ%H(JNI$NmtVjXAmj?tx<7^QVNZ!W*d%hI>Jl|Txw~)euCiE8r$Dk zqu9zw4krXbgSpq8?B%>2+E((9UdE+9WS@a3M6zL#IY>}60*U6muKhn@py zoCHh zl*I{wOal5(uOL)Rabdvt6C>)cN@(5RVP>ug6N>g!6I#? zp;iQLPNBOQloHc}pmdlii46@u{^1XD>WQam%-4C>ue^^lr=CS@8C($}Vz3j2sdpSe zUmMfBcghn^u8j~gY}D`Tu0lmFj6hRb8zw)Lam-LkowU|LW}|HcdFJqhg2RZtDqoOs$5 z_K{Jv{5>Uo-`@bx)kz@vEn)d7q?q)4KhdC?|ae$f!T^BT_yl-!|W~d!Y z(2I`YCkI5X&i1)&qQ#ilHF#|Q!_*Eoxp3|h@k~X78VJ;_5#@T}wS_XzNK(cpoSWA+ z7+vmSwdTt6SICx!NDa=&N>IU}&x|MrC9ZEUs~R`3n2|%&@%}nPi!j2gfm!ESIa46= z0EZ^%!~|_csRUx2A%XyI5lp{!f!6JFEZn`9?TrEE#WCc9^kN@-B|y(f8pqq@XNSAb zaQFD<6{xD69M4pUYf*Jv1NjV@c^#oJl3la{3+qwwEGS)1E|2e zaXKk*f#%ikeGE0Bn3$Po`tUs4XRnjLl#|?EBRmw~>K4_MWS0xBJ#&UD)7QBAwet+W zu}(H}M3Xh_4TBzxAqCWG6_qWH;=GEu z&xJujX8zJ!_k9TfA(TJH4!PfAD1H*lv_fH>H*)Y6GXg0UqA6L^tJt=rySmNxsTR zzbf5?rn!HD>_$o;Biwe0(H5D7h|*&65-}D8iveoep-YL&4QevLt{AMg$XF3{LR{To z3WwAoQftgWU`~%HyO!~aBMdY;&X{}f7H+(JmDz){)F#?Ia`i957rSDd`|kMZDiuu_oGMI zxNwc;eM>Apet`McFHtsg(v1#5)sT=G z7+0g!x`jew(*iffAN?aQQzrs}h%BNVA?hHOOI)IGdqQL`5eFo3QKECl!W#~?XA&cmF0`bAoeDK38Od7Kc0?K)yX;aU;;Vh=lHcX|p{o89?4-^1v{4lwxR`}W`W z$gsC!c9ZNMoB(*m3fG`?zbB+P2JX%sl8aNGXJDtjV!LGabrvPXa{?7 zo8n4Nc%se3(TLGkGIUL{dF3k2=^6BMeYT!jW&PvVu>w@7$hJ$I1ra#JwD4HM!duFU zvBfS38q;;kyzo=MMvUGY;u@aTEu|xPw2c^O)ItrjT@c+8V^=k*DRB|V`51Mufw?p! zd3A^4Qi>boOuT6p*@`GPHO(V!#^(m~&i5G)QlwPKp(IUHw!gTHGa*rK$z>> z+DSw~R1R5J)L+*mJQiZxl4I|^mtk{Av?s<*DQszJ>}wOhCS>%rQ7YOL?kr~86paZmm)@|`;;R~d3lID zkzlqAR>G@Z`w-_p`5eJijUpY>xMzy^aEt76A5qBN-FNI#fczL3fl%`2U1|T}cXZc& zpaY;PV?-cu+C3uz`E}Xk9l>ln$*=Tq4a?*2`!UXa<{OwziOovvTH*DAHxu+qpVAiS zt&-wghF$YI`D#8Qiekd05G7;APi-*!mSfBwoTRK-lA|#$cTf`qOMw@eib0NyH)xEU zXYk|})$pR0eqOF{u0+g+h|-WuG_cNiCUjwutpFD()D+Z@HOSU9(d{+LzG33_N$PV= z8V^n3#)9#)15{?wy2N!$7WXZ(b^01Ql=NP_?nlsLhwhfx6@UM`e23Xv;U{I)1VU^` z#5-MS{*W-JP`=;)CHX5_PCG;_ak>4p9-4*n`R$Wp7*m^{p)AMf`6jh{n#ghLn;3CI z?cR{&RnyFW4}FFvsE z@y`Bf*4#S6YSImj7(=`mVdH?}tJ{Rik-jjXxR@gYg;|5P39dbJp8PP>It{XJiqi@PC8{H^eL?5JX$CjC)Q+?{^qwR1 z&-IY|4TEp4p|cS30(l5r4+1GM9YZvec+0wmL_}WnDYijfI&m~!djdBj*^E@ZfFjvITvU)mcY`WCZlJ;u^s{nwq~xYO zwg7OOhM)Y@Kj8m*)Bo7}#`~Z9$mF%?e0*Z+-RZ?1bOrU}G4(ghlRh^fcvFX@6H=^< z5mKRMW8_?j>tokT5PS&72QaQnSV_VB z|1AIjPW|rJ0O;t*c7JE%+vQaH8(ekAoZvRoHI(O4%tlGl3CONxko$Yn2%oLHDFN3F z>9b?Rs$(Nv=HLfkL$*2M)}u!l6e-1GLD;BaohF=(P?Ew@<0n`$0YL|^m<0o*UZ290K=CyaebJ7&mEK( zHqluQx#h$=A7=I3CCsTY!Bm}cJE!axNFf`@;9)0 zAKj+?GYjN@Fx)6!NZ*yrgnMKuUY2(!)DP5o=$GDxnvmEu#iar_c0{ug;jsvLq(*o= zzzj6mw+-L?>HmkR+B{pjM|*Ax))jJ7;37@AVGzcU-xy)?lF7r<*meNQP(RwF{+bCy zOXFGr;X#3#50K3O>kRRub+3GqhsZk>?s`NrbbRPHK1}vv52qkV1nW;;#)ct*k_^{Z z2$})J4Qf%7Z~f6HkflXL5yizVR8b;Qfp*!M#lL^Yq7(LAWNZuN8P0zbOYt|f@~4LY z?Ei^d*!axa(Dco(H{ZAS$?RL(b1R>|c%;lMLF8S))MP->D^QuDyq4mYEv7K22}w8` zl1$cUE!0>)d!6z55u%o2ZwRaus4Y#_GdMK)R*vi|Y_1WpAb)9$%Rr1BW<^0uz)A^5 z5jI1Ls|6yG=u0U=S@NOb`q$30adw4(2)&gv{pdl)%WIfzs4X-oOHEWbibl!w8;?L~ zNiTE}GYyPTpHyS{^XaqQf3W?j<-sm)&+#|56o133_d@q3;_TV(MSZ#aJv}tPUx z=Vl5iLN%(`Mk9e3ILbjuSO9Hr%3voYdtZNqm;dkuns>Fa-H`U(b)tO<+1GlArXsnw zgWDSujx@pHklFX&fxWtkna8Vrj0@_Iv=9q5N>Ucza|@#vy3`+@Laz*|KeCsJ`4$^5 zu9CEBWW6C+u){IQv1#mJ%>GAC5KlE&d-fXb*#*{4USM)+ay&fOe?}~)Z_u0hM@(A0 z%n#6#KS&?Ou3O)?AD8F!wtYz5Q9tY^-|o5eFK`!eUPs=;sUa)L}=G;9Gsh{Q|B+5JouZs1IwTFK&`d%yQ$C zD-g$|&kPA#b%uX@jg>#W$kF?bdkMNiAVdgDErw^euw%>e#mfw?T*nGSy0JBoq5RqC z&WT%XuY6EDcRpwfe$bZuLHj^vaI@)QuB0pWh3q%Ym9jCukiEs7FaHIW%buenA#o&& z3FFhNT>Z=iTyAI{ZZq}J0isz+IPLJR6CurmO;kIlIp4ud32L*mG!~nP#6qYDq6nKr z$hib*GHfp+3Kavt#e0^}!va$(#O3YFM}G9-e_o%^KQuno zZQGOCZ!Q19sT;fSiN5=R3joCr@N!*?nvJ~HZ)#qaHTlO3#Xk)fbzy~jsvf|_0`Y)>gQi4oqUbmDBI2` z3rXu^7cjjealbTN8h5Jn@L5xSdKAPh^muZ}GsSWtvxMd$EbfAy=E zo^lQOm0-I5G`T&MetY}k?r}OFVONX4RnPNpT>yMH>xBD2eB&2C_PXsqNnc}E-Cd$A zZ?E0np3=jtX4h;|JKl)0&<33o^K$h0OBm6%c4MGzsf!?ynRZD^Hr}k=-Z^XL>))%BO3pDW&@U?SP)W=! zDc?Ui-aR+*QAzPrN%B%k(lRg6F)Yt8EY2<}@KQE7hu-4Yr*R{3Rwzk*2yw|}er{!*H?d|RE?(XmJ@9|Pe z@l#6iRZa2L)$&_X^xod}eRuVKcle{8_^zq=uB!R9vH9)o`n|dOzPkFrz5Mt0{o&yK z=jQ(M@&5Su|N8m=|NkefO3nZP04a1*PE!B}2pAb5A}ud5Nl;l_gqN7AtiQp*$ji&x z+SOM_51t${QmxXRol}500C!7L_t(I%XO1yLxV67h6AluQ2_@iwQ8v|?ok!@ z-YYn2>#nW&|35*)Th&V_rjo9T ze#S;A*_l5-mq!xibJ6!wKOoo|s*V1UP3zFvtN+N2dmiZIe60TG@1IwnMAb&U!p;(& z)}V2vi81~RiCPdeuW8l@`p5cH$Duxa;$!@DOgp9kjcwZ?dHnuDpI=KyB(DLJ)l%Oiwu0v z+v3DX>XcwFHCNP3#`xu`i0+p@$j}v zvupdO%QTv77cWeCo)rccc+eBSa%GrMK}}rR1TofuJ(!M(-Rha$>YCo`p5N@L;_j;B?zHCcxaje_>G8bj z^1kZw$L#db@AlE~_S5k8)$#Y&^7q~J_~G^W`|Nj5~ z|Ns9nkp&R|000qmQchC<2M8D$ElGrzn8C=}`_N%@rX)a0Z|ZgN7D!E%EzO4BF|T#Lrx_z7o7 zd?!cSUYQljo-O%8iN{bX7`_+_qS82+VrJ&JnEfw#z64H(U-54aTjihr0J@_p!#+G! zy3)t@`pEEzh@4#i^|Mv;7Key@{s%shl_<~B(S5fjCT5W<>1t;HU|V=x3WPOt0|0}0 z;R+};#==wo_zLLBoFPTVXzf+G2XtgWM5*d=5kONq(p*3b_?p!LkqWp59$A+NrCECi zE?74ypUGPQc2z*22i4BEz^)$kGw`7J#8S(ueFE+LvgEqg05q!2ySiGp5X?S+^Vhc> zR)P_dI{|=~6(ejrC)y2Qa66arabZmMn}^G$K_mOghco%~CLizL=l=r{;7UBdz#9|* O0000yM8sXh&8)@HnE5{yMH&b zi8!)}IJ|&4vWh&ugFU{3KE8xOzlA}+hC{%IMZkzm!;DhJkyFKyRK=22#*$UWlUBZ) zR>qT8#*|sVoM_0SZO)>6)2n{dtbo+6fz_^p&$W-)xRcttr{c%C=hM&c;Lz{k((vNc z@Z;k4?d0|EQchC<2M7rb5f~XBAS*37Npg6EmzbTd zzP`c1#K_v+#*M~}1NYv0PuyGb-`g~4NNM?g=#$?2rOjRL0O0aSsuqeyLz*gifKz@Yg191z zKL>l|7_uoQdqwWkkwdyK%Y$L$qHvJ!cjbnIeZ#AxJkseoWH&ZpK4mk%m=iCbf1Qqz zTu8Qxldn8tjL$}lm15K{;^3>#m}rMo%mX9`Uwbh*KI%1IB%rcG?JhFD&iHE|>={5R z(9G!4E&e^A{EhKx2w`_Z10fQfzcWIaePgxxPWjU$B|M90H0=dj*NGj+-Ny})4&FI4Kv;zI(5W!X~C zUYq=9P~`C<;j-pFNvaznhqborUbw;E+cw@2us2MiAKF!!+j5C68*eOdG_B3iQsUC~ zf}^?Ab#S<&p*YXzQAc}KVcy|29BtJBm-*)7Ur{cM|38x literal 0 HcmV?d00001 diff --git a/themes/squares/client/src/img/notifications/warning.png b/themes/squares/client/src/img/notifications/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..ab8b54ffc0ea43e214c5b0a9ca13a8b590ded293 GIT binary patch literal 580 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dy%*9TgAsieWw;%dH0CG7CJR*yM z^m`CyyfQK10+1nD;u=vBoS#-wo>-L1P+nfHmzkGcoSayYs+V7sKKq@G6j0H$0G|-o z|NsBro}_YrzQU{d+OKD7zL>B3e6H?`c{%Ew#`y9xfr~3@Z1+ro2RJG>m8qL$S znrCV?&(>?6YtTC1sCB+c>jDrpYh7&Cy4w(iq8y3c@UF36%l#cL8k7YddH`2{nu zb4W<)dbUiTaqz^g$Dclby}H6Mnt_3l+0(@_#Nu>o@X4}620T-m3x$N|_$<_Zxi6UO z#sB(s?ca`0x@1-??YV86;6(=US2wog3cr4k!4t14H$f}^j_b{hPT^vg?4v($bVq(W zKm8B)w9gM~zwM7*D_4BvynO4A|3VhRk^5e2Y)<88Xg}kvq@@4Kp5cJsmq(o|Cbcpi zSpGnCqPI~3>jRC7B9V~nvt(klsYhDhGT*w&)XQlP>IKiN(4Ay_`qsC<)~{1nmVR!PD}COZ z(3kxnC7)%nN;&_XJ{5J>)4TcQI?JNIU2Zq7t-5^t_xGRmjOKwIbEe*~^8khwgQu&X J%Q~loCIDF$0tNs8 literal 0 HcmV?d00001 diff --git a/themes/squares/client/src/img/padlock.png b/themes/squares/client/src/img/padlock.png new file mode 100644 index 0000000000000000000000000000000000000000..31abbaeefcbc507ac59450a76b222d0f4e2ef298 GIT binary patch literal 3265 zcmX9>2{aV`6aVgk-7GeD>sK}rA(Ry*>^gE3Epmj|b<4VQ&fYA4`JLL~x$GyxygiPF4+|0N z>#b^bO(w)_&M)^CaxMsCWK&r#aHm&lGhQsda2g)u)YZ+nA*$Y=^SD?-yyum$c%Fo+ zu$ZJykg9=Rm)m9Enp(-yxPTR<#xXC42HEHB_4DZg4O#W`_q$H2B2I8p9;H-yavPzH zq>PRmoiMumG_oM7?b?9OKx=_Dkk+mM`wBnshtOJ>eU=7mz?U3Q~$Mj}SCJX1OL zyqf1?ymH|9kdKx=DX?)8JOf!9^UJ%poV#?*v99*RQ2i$@z$~vES|EhDv3k?p4arGu`0~c! zB)j#@7}HKFW*v0jh^7s5seY+tT~QL61vGi3^n&;$V?sqb^m$mcJ2D+&AnT5%LkI5`dy*61((J>5;-?QSiAbncNza z=_&RvR%{Lm(ptkSN4tVOmF{n3?6j5{zR&sjwxo5xQ|}d}l%r)b#65Pmb-wHy%d{L5 zwPQ+6{)w~M9AWrc-NtIJ$ENRvF)A{Jcr?4>y9&B4!~eLW4p`>> zhOsvg0z;G;#Cd?1bqhHtaA;*Rw-MY>E-4btGR_itUOXXU;(CuwOo62{(s@)KAuvtf z94FvGsk{$ylrmS~owI-KK@53|%_`yM&Ga8O>wLP){j(dkJHc<&j@laS3U@w@+G6&w zEevGlg6D?SCiHJ!F%BgKr*L>vX>8T2Yz8_^rTT41_xEvRdH3@!{Y6nDd=hKj!2QFr~YXu#zGc(XeTQN|WFn zQL_-O7SLo_0!Z!Uw1xG0znZF6-!;c4eL1j}u0$=vD0_f6MZe}${`8%K>BVi{5{i64 z8|m|u%LIcFxg8~xj5`zYvL?g-%vn?irwONS$jMqAk3f>u{z3D7%BMV>yDEvMtTO}d zihwHDkS-^BS)X^O)qI+wO7Cc(2D*}y<0#8~eDLL6y<=>IF)Ap{9VFsh4qS?SFhufl z(BAyj_Xz^v|A{MSeU=T;7-%0JP+Fk$q2Jf_V0xC09?A!2-q?Aq)ofD99`8HUCp^82<@{syV+N=MB$R(;352@)bTYSwwhc~HL>YeOhXS2 z7`6_89MT2B;X|FdD&I|?onT7-6g&9z@)L`P1X{VjD@l`xFt`dSWnu-|L(T!BuDGCO zoNZT=%_xMbko*hlYL}Lr*taSNRW1F@1&kj9i@TIdC=fj&0N@eyB`7fWtiKMWbATr7 zg2A&7Cg4T5(+4hN01qd{VyFg4sxm;p3HcbRj)$mUv5*IoG^Zgs!BAG-W%*=6t-UX1 zU4QHH#qWz97EB%r^T_V%r{|qM{wSWG<4a_D^Ew7a$i-0Yk9WjL_=N`g?oYJ(W=ebM z|GFOF|0d|pVM$+nV+MucRs3I=LV87{<7gYRlcx$_ud;|+Mjjfc2%;Bv>51`;2WwAO z{c#Xh5c{q-09~!XFT$9OGxf4;JiF(&p>h>ky0Ibgx8JJ};Iy=w>Y;6~e>xfhJR=#) z?X7~{(N*kOUXV%aks!{hW#9|2?9tcRK#m>F`IY_s5%sSFibr9p&|^RqvfLL<+0hfZ zQ2#zFFl5GcWlkcj8mkvA&?_^zI}S)_xCX$VFb1XnngbM-h5}-I!jWl0to`{s>4a!>rl_SvhYH` z#z%W=pEP5bHRfR-e>gHKrz_RgoAR0_lc8 z1wgm>6)?zy0O_Y4nZOkhU=5=m57R>ePM|>OJ{JSjfjgDPqai|s(%GPC-5q!&@I~B> zSVkm*1!$!dLJMNhmHgZ1#hYDkem|p7SVlnEVB!7mzhr7(ZDwave%rx4k2Hgd2yY zu*oy(^w{+B6AuMNhcISd%EV%^s3qi>3E{XaHr<;vT zLLrQgWh590jnIhuyr>83`CRZp^K;+SWri)#8qXn&eeR2GDV6$hV3 zC;|yg5QWXs$In0}S(;{3+Q)f=6U2}}MMHFMPv@82v>(iUde7*h*H5ztB$Znr8fO>y z!y`qZhxbu(c4#2?(|Dsu>wHrr5L8Mm7v}nG-NOl%x7?KV{2zXuG5c zINn@IL5?joMBh>QMAAEjtiT~pmik?HjwA;M0d{)zA}fX%gcBdHORe)+lk^9Zr_Sa` zqG{W1mf=ek9)Nz+@^^gU;jecyJU>{AcJz_GD5nPjG8kaCTN}IbF=6ffiih+nz7Y=} zg(Wv$gUXyf=OEH{vqj2}e8wO2W(9OkmWPm8m*O-uDNz}#qhmDS>q!LhLM6GcT7m~%ocUNs)9JZz20hA~qKL^0J>S_J*M z)nRkU{h6XFfO=E#6g^4IIX9ymv=`Tp0?dRuu?g;HXq3gz7`?Bzmy1d)VRWn*uU)mN zYWD;+?tYeZ=I>*4nFQYCc=YDtY9sq84;X$x+%XA_Fj)!tqt%uuX0^Y0;;2xn*cJp` zv=$LqqjK7*r)2+UuQcF?iwJcHvD+aWYZ@o41Yr=8*nku=w5P9*7f0p$0HdYq1mjgn zqcU#ytbI(erQh)M!$DN=c zob)aNpOrt>_~Jc-Tol-c0s~2SUY7LD?svNDNk)x9cNst4vPu@PS+VaFOyBnbK1YM?0W`%*k-$2d-mZTsWZ}_GVrOLYb{)xfkykLOnSth0t7LBH#Q4w#lnEDYfbqtK!^A6}Be(TLV z()-%kH^MOnA$DAl#6;0q;(8P`@0<463$}+n65fZVc3~e`j;{ez1xa zR7M72GnlI3{adw^n53_0n$*%)F3AZFNit>LsO49xGM6sRglr-`t*zImB_($s9aRZ^ z&0Sep_H>jCoh9EvJK07LYM$|kb zpMKcaMs8yX1!vnOQ3S1L+uNGR(kMc|g8X>h+?Bjl?4OzwFoO6T89rbg>H_FjBFwpG6+EE zN%ri5OinH4dsL@S;9}~71zhj3Y|b1D%iMp!(}3tW*_M4A1FjC`Qt+|9(rrsBs=M=0 z=P+YB005w8wBBy`qm+sncXSHNI0pDs9p%c?uqAMYf5Av zN{`(i^Dk2lRfB2kc(bs=xZ_}>O$S@>*IfzJ9Y@A!D>DLplnl#flvd9#dW19lhV@6I zg7(;x6Y1R@esAAx9~EFH;#6!88D3Eo6OPeuc-V6VT`DY^)HSJcR#NXt4T&eU4}~d; z0Fn1_5&O5cWUe_WJTSb1w%n5aNVNNEyPOYMZwL;2F;NPSZX9Zi3L)HWibmQLj+Q(y z0|vJdKi|%eB4*b1?5dt^k4}gfY_i!2PY>}oezLHK-qSK`o|;jK9wVpcG5}UeqKKKV zwAkJnxh$?{^^vB_=9Cg$q-j=1o0^9CBc7Z^iN${{7~#b*!f)!zH+Xc-o)P}&iG3)h zkER+>l~ovZnn`*;i)yw=^L|J6U>XclESv1t-E7u9rt|iD(?qErP8e0iHuK3F21!Gg zhSkOKt|Ciw5UxfwXHkKcRXrWPvM8IVz|T`6$E^b`eY&tCdfJh>$AhoHLQwt+$EpO! z6qjCaD;a!aRH(InPN0EbSzqnEh(f9~=_5zh?iXvux`#GflkhLWzmsNU2WbVYg=#98 zR!{FzrukEN33nzgVQe%2RGq^S`$UjYG&sDS3At2f#P-eX#vVUSAf)_QU z3AI#$B!`)QqMm}ttH8SbL16sA2YJYMtCP)Da0RI2KJmKf zk?bV%RwA>;1o3;{QWk*Yswqj~UyA@R)(pI><)rc0%1zMg`1JYp+ADycv0%6eUnjOD zpjRCuzug>@^hTa0cD17lC_BcE5mulOf^dB%HKV3>ZL_UJ!y`6fZM~6IY6LTnDTSE6ZpA->X_J`@mnH*#(6w=9ulM zF&}ZjdH}YkBZz5=8aNVG!>SYZx{?&xY{Y5SBIzQ8Jn5Nse8K9>=9DBj9!N*te{{wzHdE1LHD0SFMO8uS$w15z|ff$5W~VZ?dA~#x&f7)3^VDFqjU?~x3iTCKPY)su;gfIrWzWt zmY;zU#mjxz_8jIeVK~BRY(i+UNsTK?pd8>KX~aDYM~A@lfk1sd8INvqz#p1*VP|sv z6{M(1n_N`2!2u@_8gOib+#6(VAGC)e3I=RW zy}poa@S*3i@zaZFH*z?V0 zZB}+k$~M2@GE?x}J0CZ#NKAbymHTw~w|u}b{vJJEAO7WH_=tFXT7iR0Tr8Szk{iak z$#SA6*7Sa6t;@q0y>b)E5d$d|Y@c|VrT;+LRFZB%&-YNz(ku(a|0Mrqk-fk>JIhVR zRW*>HMqwwFSuf@BH?oBqO0u%qq?+xL zi?KbyZN#e+D$Y>mt9S!g^7{m&_nicl1+?{!5skjq?T+645K*SH6W7{CS5^(RRnWnJ zu-*}(r+t`J;^h+-mC7F7R1k1?_bN&-EFkF>ubQO5tN%<1+ttb(nP5_1*QDPi2e`EL zEY70`;CH@E1Q`l?p#JQ!Dizgg)l~aAB63}`WX4QuUXBYWWK@=qOm)-gp!NSrUD>wb z`&5h{PU$g14}q&Z#goj779|-*)vx2-ytB*DAF56zyo3Lg!}3V-x3njX{{-`3ElULW z4KL!Oa3qmcYDl_gZ`0rUNg!6{^QlIS(&$J;khg@ueo|3ere~{xQ{{SF0z)o108z*m z{!O|{d1(bNr;2gj>s)J*%~)y|+NiC2HG*A#Q$rRplR)6YbyXt`LXG3a1fSHnhzB|oJlDT;8qDTuHM*ux? zEq^i5^@aY6=L$#|Y;4LWE}vd!^UJ;K3~TgGOWfauWzGz;@6&Wh^s$OJ^To6|yG`g% zHcEO>7uZsP*xQp7Bi&H%n@8`6cRABq`2ZhDBQbqWlr(m{j%lrB#Lk0=~ac5t)t|Z>Q4jH30;8kH$qkz)OmxP^z%n# zHVReRmb=7d0aq%{>^6wnX?C|2k+Z8VMX6f`ns9SSpP2GrWZN74=-djRJqLuJ zdd_1KD)m+WOq5k&&9B}mo1DH7NnghlGHbT~Hfc>?%q6t4LcQ&Hs zGz;n#p~(4*d4EDN0TT#v2`HsD;(#aLZ@Ao=;K*I~*WgWr2^$b{uxMu7VqfUn7muy= zlPs##Z`6iXy`8JsFe~?!K$A2LjiiX$X<`?&_7kCKLkUNvE4|y;1MwnUBn-lBClYYL`wT;ySc6LAr+HJ0qIS$BW zW+9q1)lG1I;~@K@BKI2vV_^^yK1#fVh>(pEETGjZ0h5 zhbF0{A1<-AOhG4(N%nV*a)MmydUw{{(@K}+fhrB^Tt2tp_UBh5gL}Gh$ zWNynbo30F7KSq=my*=9TWcV*rHl+TnD~T(}88f8Z5<}^xx^e(b=GagHMkWG-mf4F( z;=Zn(lqU&U4zZ#AMOyo~*yZ$30NTH8=#T?=0adgYV$f+GWnEMOcz;bfA=rVQD>qZg yfK{G;*sMIT^q}!}G=q*C=Lfs;R>;VR z$X=mHWZd7o&-e4k@4jC5^*qlx=Q+gJU`;0Wjzk14Kr2 zu=aZFj1G8jZ37D;`U@gDMxbk^E62`y1F$Uo{bPtXm}F7lP9NPfKIWI5ef;dboWRe| zPsZ(%ySIb=6(^a?UM^`1YJw^rtlsTWbmwuD!;DB%b?}U>BPr*-HV6K#5krRGRm-9 z$5_1ZA<|iRr?ly@qMTxS>fxa+^Is{+QLIMv;nu^Cs@f0Sh(FC4!V~br!kfBEZX+*{ zHOSF!r@C}H-*t$0jdvz?rRP)_dEd^zHNn17;yph+{<1yNp8ny&YUrP$!f&m$&faQ- zAc7`7BkRo?vc6c z0<-nnd#{u&*2GL*ml!r6UHJCpQ)_R~1i&ocb>33(%f-Sy%Dq-rJL0 z%@@6di!&emRP?wh&j9{X!HRK`*zEHZLr1WJJNeDrs=LCzEX`f>yHz_vejtT^zF*1RtrrIo!F_j4Ef`}>UWp5OND z%&DkR;*N>KSKkW|;8xV4j<=vLmqxSq{0(^y$l#;1`Hm7N;tw=?Z!xr_Q!{J!c!R9kXI2heg$7akcGqZIWikBl#xb5=fi-~bD7xU63(VmHW@61}0@z^5Y`d16 zBInALloRu$7ARjPII^BJ*y$eRb@E^U_|8YaN@Zm=eRtgK{lFYhgeJ;9oU)FHE!|r} zfYO>$5hL^4^$~qiS3OvjaPV20UZB!)$8~;xeF4w{l$#E|&pJv@J}-PZs(Gw)=s2sA zPR^6@i;?ms6N?dZ8$`)vh9g+G+ASBjX`4Z01G@W`zM9BtUsz|_0Ybmz_%Bg z=-&eGg^sBfIJf^!QW$D$Wdic^O&KzW)3u4U$=<+sR1wX=N`n_e<(GQW*S3YYy(%gE z_{8ElEVLe|+wsvM_3M#lp!_JanT4r&~g{&YS{q zoh-`(%lKCcZ#fcz#0ezBU1vxjW>WtG=haEQjXe%SF$6>DFzU1DzlGE~4( zhMMsykhrj~KbP;wKD5voof-m^e?VNDGSlX+hC}AIfF=sZZcvz0po6dwei8?USd?DF z8bcAea+)fbC7bFc3Mv1O3bfLKYzIy`R?J_naT0r;zKiwfp@Gay9Sf*+V7c* zFzh}-vJ3Hgj)g7lU{lAgk~ItYowK!JeRr&%r26<#O}>VFuri-fzQmQvE5wFxH574& z_*>d({ACwCO>Dlswg`ES&B{&75%ILC6eix!AV*^T+b)+ z+%)H219+3$`~Bphr&tR8ZE4>`@2kE$mP}xA zymwFSeM!4NGH)*S;Auj6N*b6Cac;GnYM3m?&DHM|+AIG8CJIB;#h`qTK@PBJ3#@mS zbLe-Z`A#?5`60`T(P%@U>z1*J(_S# zYgCtd=6Vgx`gd}#HC+PIUhMG?Rbm-7k=V4*%db{Np7C;ilZvDb= zM*NJzAU~sQx8`u2efkcd){^(Mz_C6`9Vol-Sd<|4=T8ob-h$it`P;=HRS`1A7^$Al z0q-azOA(NelC1-k7-k6buuY}g$@RIp zxu&Fo-lin?-o{@dm+s%7HOaQ&pq8ur*DhG$u*@mn#9qlAG0Zt0x;RA_TbFF(smm*N z?C2|+n#7<1LJ{?Ql+e69EO?VvKduclW$g2-nCaW2KbdH=jnDL==|%Z*2U!i}0z^$| zKV45=t@+`N-BW-XzK*!k#X#e0nBYLd9JCFLFP;mZpwEt8Rp@q#ZBoQ+93jL%Y_Ij; z414O{YoM$z0yIhNi%NK=?YWL4sXzLrxP3Qs^3b*YML+iscO1T6bK46bllZKYfyC4> z!GdJ-^v3Jo5f(ZadbUBg=QDO!lV(3nHB9JT+hI;vx=pz!11@IPwGd&rPx;LDuyA+0 zmctth9HnC3uVR*}g-=Yl4TvhfO#_2`$1K5LmUGIBJMkoymH62Lj7#esIe#hBYulnQ z!hy+K{>TRdIj@2+a{hx)C3}DT3J%)(&K50AkWspF(ljjlgQ+IaM6loaAl=Ne7Kp?5 z?3a2%TQaPpFYwI+DcYD}K}y;d#B(zhh_t?$H0jDc5!r8%-STGv(ZmlH zv&h{B-W1yotL@6l5B$Si)165L0hnVSWf)*{&qxjw3FM)oE)P%Ju6R#&N{K~Nb8QZN-qYz|vO-mLsyEF+fxTnYxL*hnRK;E<^ZiEGB zV%WRPATK42hRR7%&K!u3>BOROB7tpUhKm2d@daQS_aAtI8Kz7vQR-Zsk#sE03C-I8 z`?$)V!4*Ot+WAn9;}@i$JL?q+J{Cz~LkFPP;4fSu(zswT#`dVQOq8S>!|>hmoZ)-8 zvpX_dYFAjqMOR;aI$Qi22|9mX>H7gmEB5pNnJ=RU8owsWHtH{zCfF=8U?GIFJUjtO z2RZd==uh>J(~$_yvwV|tBEI8+?$f;^doMdS+(?yZY#^xd&ohAF$7c?=UpO4)WWyDC z1v32x{PQ*d!C05C-wKN*?%%=?hs~PRp?>ZqAbMl}#0PPwUH@xlx9aGVckHaLphgmp zU5tzo7!<&xa|1f9D^sM-66_tD_>qy*9k{+BKW#)5JZ@ zSk%{1(0J3zGu-RrzcVrMH_%!Oq#p7};J-07ff-2%&AsPoK6x+ha*gp3-&yU-Rbfj0 z#&aBSJNce?(_0DU56it^q|8TkW+;>s?WQP4s(Vsjg)TprT+giAdcZ2Ax*Lw&t5`RP ztb~=Lo(%-M?=P^OU$))WZSf;#of$7O0=MJWAH1RG08J~SIJuABO^XxCcuJktTMWVr zx!d3UKmc22YEz8y4^6kqfz2AL%Rt)ZLq4`oH2h{j=|ek`N7E@XzzPyb>F za`=~KcBnb=Ux_QOo9aVtbwmm$^WWN3o|=$yW`Gma&FqRXeX0JRM=VArth_PEv^m0Z zZ%YVE=@%N2Ieu{UeAZnaH5+VJU?%1at9ZRIpiZ;x!3no0%*PKZf#GLi(O?4WHFo5Q zO*X2JP}pEd@!}t#$<%Yt6N;3cB7JOvdW>vB`q($4MIs0*55jDIG8U7icszzp3lM|{ zIpG+0ffsJkjQ=f^o}-@QBVxhlP;0CGuZp_R=}yd0V&;e{a)U~*8{&n5Pior#)?a6q zr8qqtjq|^9Jo zkA?3=_dTW*qx%9Al;Y1BOjY1W9;caR==IuQSK?0pQ771S(E+ohhms)NWyqFe?>Fy3 zD7tW3ey6f~$pw^A{)a~@(kF~zptCz`uoOdS>WG|c{GHu9bcua z{wKB<>n3XrgTl%T;F5SQ)LNsT4J_ius^t=gme@3}KWB%dBV=Z}mvz}LJnVPzZ#sxIxN~XFm=e zHO34vL006>;`3t7bf2`v3-p)ICaDBuPX}KWv1A=`0O?Pm-&(upwy%79F}cH5N8>3a+)j#c~M@;4^; zel2p;mu926lT>dB?fg!N{onTUckx@K|Hm5j4}$C;8E|VoSA5#oXshz=Kh1<3DM+ znhU?B4KnR#LM(ToSY&Q`z=q%Yu|sIAo8ViMiwf&|;{7topfa z4aZU83gzvkeJ7Snoh6?$qiUR)^uve+l>t2a(-k^s8=vm}U1I=?I4iVsm^u^KGs*>_ zI^JfHU{Eu6$0g*Rm;a!VKPpXOCvQs><=8c@opO75p~y#M)0`9_*VIyk;5z=_xN# z0`rF>TNg^_vy9Qe@yV@_{%%meZk5Ymw=|#hb73}Zr`4j>J&nQ7TP3JRhS`vNI@`F@ zEA@`RpIDB&0n|c2vjHP%9}^y!9(0<{PCU)nmcsLBW-b>)Ym>NPWc$3!@-t`Z3(?Uc~|8Bkcn?o$f%jiPGZuY^-4TJSAHUDY;9WdQ0P%W;CJ^udUzzN36 z8*YrqSFs)O#c85>vKm#vy<%txeX3rG5(1yk>=xLtM>|D|AEc5+T0ICh@|QYd+H87 zjlmioUFGWVML!nJ_6-iTR_3ngm-6Eh|tbr$7LQ8W2Lkt z`-_$g-k>v$M-(nx#CvAze%M+ws&KJ=zsG8RTudEgJ;;^}c+R;O6PjnHY^f7%fuZFD zic8B6RuXc?g{APQ=AQ?R^CWG*8!&QHm9qzndFPepCoGd&r?ewaQ5%!xE&Ilh0%q5| z+Q%vw45D@BqOYLNmZZW!B<-)6sGImFezP><92Yn2CI-1)K;ZmjLG8=?Q(29>xdhRO zI89X`A@}bcu88ytKQY0|S0nNUm!;@J3!juEA>>jk+2Ec#qw512AjhgDBskIZoPdqU zP+hTpII$>hE}4s^5U{FLMGDv-Seu>X7ni{jkOeEWJjs6DjOc3*_7h9HbqPLknhlngQJ`( zU5QxCx4p}o3U%`ln(tLWj{kZ+xq^Z$wl>BUPap7J)kS@R>X(CT!*`VYVjv zWIH}tg9jL^D!TOP|+c&KR|ifYSVbz>1=5IeJrRsbi_F;8LNFXCfQvvi+lO~bKPXh?#Q(@ zk0x7rUqBv{e4MNJ_I*hnO5(#z_=ik>T2sxqc6)h>FTXtMRsUY}J9I((E-q>({gFu%7jo0_>I8H0YfRO8S#q;8YL$e+j(RPv7q;Gw3T&=i! zpzd(R`zonJfnR@gi~qS$dw6NQxMI&EmZT@2RGg>O=p0T9x)1t)$FZZ`3K-~~(0QS4 Hcm4kWRQYbi literal 0 HcmV?d00001 diff --git a/themes/squares/client/src/img/sharingan.png b/themes/squares/client/src/img/sharingan.png new file mode 100644 index 0000000000000000000000000000000000000000..526787d3be6a78d936b6a21941291994d0eca38e GIT binary patch literal 9213 zcmW++Wk3{98$U`)y1NcIN~F8<=n#-b8bl-|rMtUB8tLvhq`OlZq(MqL-u=HH_Gb6P z%|5d;^E|(pa5WV<3^Yyph0zz*L4P3+5axMWH};j z5QrKCm6rJ6o^_h-;SILPKJV80o_h#h6JSjI95ZHqMxv7Zh-;v>h(LgpB!jc46N7ae zQ3d(2%^Z+ljZ)XP9dX9;V;dBK@^%R3Fs8JpWSPd2Sf6-yo!xcxTrk6NSasB%=;tk# z`!ZHo)>hV*V=OZH_udezWBhl=xY)a2-W%eS zVv{j~=TYhNpxyHX;~Go1uiO1U_fnzT4&qDZmk~CGK*MN&_cf3NiT0 z`FZ&fDF~<+eYA$3LN!45#w}&ujU~{>}wAp%2}NZ$r$W5kCUg+XxFrb_JxiG2vMDtI{6$xsWgvd zMj=o8>A@OMIDRU3;$?zNiAt5pRH3#OTXiAA%BFzk+H+u4W$EjyCie7w>rNdm%r}&e zJUQV+m{g;7?VV-vbNtT+v9DJiTVdD z)}8qGp}!pSo_woHdJh`Dd-;FSX18vRB`fWHe_$yb2TO0qm=-N38 z(y#GyOr$=3+X*kfxykgu)hOQjVbFbo0IQB8NY!QK&1iMLCne0}ocE{O!k&zYy`1Ue z|NXqKVOUM@DSY`CeHFty2VS+J(Ss#sNn;2Ms+A_!ThITk+GdKng>aL4;vRC=gft1lPRU~ zkRl=?YJ3vLUH=&6Ka7$%EWN;3Cb3B|-)OMBI-n3)nIAAkg?e${DRiJi6hIdD%q*ih zvX&YDeI;|ojhiHDwcQB;m|#j0l1-0TFO~LNhQN6H@@xV^Aqq=$3{Se~H+03q+|ND3 z?+d0O?eKWlcZ^E}=S-eAh?`@iL^|w8KYFutP^8q!BZKa)&lVj9@x30Qn@S2b#Dpv$ zcn5Z;rG*M-Vww2cd7gS#(^JJ>o#Q?2W!YKVrFp|9%$eIMabV04c;REm ztCKrOXCAELnG|t-$9D55CFU@%>c33YVmWynlw^e*>aPizaZ@|de84})!Erazrh066B6H+R1#)yhWfjdSWIu54cmmj%^ zQ+VeWS%c$f<(+n&{?sU0YFgujv1DJ1^W+g#Kt3T^r3pmADB^tQ&U-b-i)ipVyv-Ml z^lJ@iW9VgDvmCbEz*PLno>#d+evTT*ci-mVjm6BzL0niV8*zGlquz@a89jbRQ*>xc zhb<8!JP|30xS2WxO?8&6?-^P%Ja%s`-%=)E+?*q8X{kburmmS4lTb=n+PkV@+1vrk zDeHy=2?oYna}Ixdm40{!=RxdFOgy@8aKAnt3>zO)Uwl=OV{@ziI7nNN&*#xYhXPM_ zGM}|ux;2l=-X)#&GW=dO8l!$iPbOP}hxy%CZQXMB^)2gERZ(~c0;d6${*9M%qQ3 zIqJCbb`Z`xtpxCDEre8J^CP&>oOuC60rO4lWlcOjk~N{45_C3;R#B}~Q8mucs=%+Z zw*8Zl5`$EEkQ@Mp3UhM8d?}BzRp{Gcx|56*xQYzn3eXri+Mq;Kpn}P};Ht7J zNG6~#;JsCr4FluJ<0>gYc?zbk>HXd4aF&mAJn!2*`Bm;;D&R1S+?_r_HUzZef3@OP#Do$`im7WoI8B$V_LCZ z`Ofsel8KB_%C|@@`ASj}qQG7Q_Irt-t%pvdv(ff#Ksdi@Z1iP|S6KE-ctjslN<7pA zb*j2S^K`h8ri5)W!oMy5XY%B`voZcVA_Td+p6NjPHH%sRTxA(*4(jr}n@{U?s%78Y z*@}nDAthso6REc{dLKcH^M~jn5=NmQG>5qPZ8rd8)*8wG)zFJ`Tvl_Sr`9~-^G(OZ zgm{RBt%?m#M(fZ#?0(`6>8Obv+=(I*NE z@Q$&+iE)BF?Umwq)RxCfZDEU~v*!mH$Z^m)$q!>UP~N%C1ntzbWxJ$RmJzufTpu9C zr!owncmAzQLc>jS>Qj$@8xNKwln4(varG081 z$=8cPO->Cu;4jY4YRxW*k zjd|FaUo2gGl)Do0xz{e~&}^ZXfgY6XQ6k1?+%13JvhJKca^?PI8o|TI%s$@l^aZu) zk-dP=v4cQ^vS@8++gxdbvB?0>P^qMa>>tt|a&L*m7UY{kfI zXhi3lYHDHVSy((jU1J==J7_<^0b60IF}O%NJODOMVMx63YhIL)_9w5~;WaDY?1m)X1?n-< z0d}{_?}@W@et(A+GUuZO!1TNb+5<@#Jkjg0g+-pTNrZ?B)K=Y%uyxWShfttLZTtR~ZZ%9b?y3mz1nnCEi7>@wpZK8&MUm08S&&a-UQ;v`S19mK@;5981)NJ^iH#gz;=A;t=(R9RQA?P{F-vu zh=e5LtF?AK#@2>q#cEoJaLfA|V?0(qB@$l?6xgjIj566Zsfqz1i~xS}pLxYSgktbHzi zmCCxzV4j+Y`tXttAGfu~LXW*84tY=U;8Ir*Ql9aa6b7^?~996AtKDL19Or!L-D+{ zh#aCFZ7G<149ug!LQBhm^PVIfiuccg$Pi8scukZ581fO3qjq$8#lCVL0b#)({vdk9GY7P+nr31yVI%oZHox22 zA;1l|`g|-<%e-Tigfu?aS-!L!Da#(_?R?1m&WUGYW&Kmr52hcJ9ATy*owCEOVluK= zo6S{w7#`vDA>I>(cI>TMYvBGkU%&?Y&# zJwt{zLaUd*`P20$R^aSR3a+2vOtBwnP!tEfF&Jv<&3xG?BY}n_0g$RwCt`PXogwqY z&WwJ72{10Azv58_nkUzM>B3P<$!7{iZHo_!>BR*giVCey9o5rha|<(>-)@R-tX6~7 z=?F!hq3}6Oa1KXFUO1naFRqGj{#fXq|BWJD%-Y3oAQ`tE(OL&k=u?ZA)C4x~XE=J7 zWKU2ab-t!bUAq0>x%Ic|T#h$gHN7al~ z_*uD+%(6i6#f9@TcYTFPa`DpPg%Ek;YQ3pQqsNi9tg;V#uo1Ry88t{xLXD+cY;FB# zCrMzZEVcsa;fuZK_+Qq~Uog)`ug2vE%jWl0>U%L9F^}IMUtBUcF>W zOomRC(0Bc69)!+FZ)222&085Glv24pvbWHY+$+y1P(ouZp#7LO0FWtwV74^W7D14B zHVD-+q4Gyx_@5CpCi&^~9XZokBSH9dvU(D`87V4Vjdnau!bgaAI~)Pd#G`yka$Osy z1>ukFe0u~+<+7Zya#n;qWePS131LiDY|1)aN~q$pn_du_5(rTsY(qpdUiw1xAD6Ac zllMP{w#u7|L1l(S@|=SFB+z5POU0vV^|#Ar8M%^4D05c&(R{S~!ThT*$3+HcYIX8TS_>|9~AX?TOXBcwm=1 z$oY@tkah_>;ZBrm89VSa+D5?7Mkja{7aYp<(KA((sYwRDr@;6SW2YNfy7kax;mtJ~ zcbD>H*(I`ww@AWW$#t{%xr_JOMVN{gXkdm zIK<(98lP<51W+sSeGr%}o2YQS80BHC8_qdA2T1P)pQ=%CZ3F`o^vU);-B^SxFEUn@ zsK2HH@!#2t!5xA61mF)Ar#7;4i)3k4-08R=EHqDX(UBNZCe;tE=(2poQBNlQf-kO) zj1Syg`>GFE9J+TiXC7BGeRmakvRr@s9=0M}Eq*@nB=&BZOrRbAt)4vJoTOEJR93uD zqJM9O%~sSVGxE`i`eT=%BolW4Y%o*L!(?7o&x+sAQwq+<=PAteNav@!8Aq3Mi(8wb zvVe1y&0)pRljvebuOXqn%9UHn%_z1XzZ2R8D`x7{_hx0DF%xdiQxaCbQC42j1#Y3H z&3ynE9lM7pXyQ(rCsb3da(&JlL^s5+t57?EgMA~A;p(@Dj&wdEW$vx+i=scaim;ar z4qZ3-anQ_U!LBNgE0RvYHY(fpO3eUWb#lO(jEYSuVg%%y|38#itd=Qvjz|cn@ABlf zfn3s9v(o7Vp$TBmWUmcS-30O&p~p-sx^}e-VdmH@!>mGa6icfm-7uK%_H>&l zP%bA$MClu$gg+b!NiyS*{6fB>Wwa_Of~>+*u>^b;CD$-}Y{d)pz9h zm_yRlMmWbKnJeuDdH;G$ew^kFHULmS{`dc)biSs}U3(P#{e3;aGthyyClEodrNfG> z`ln0(yo~3&tF6NeC4a@*72Ex+l~o!2M@cxItGkCSSo9QEyKcZY%`(;bm!$Yj|p$y zAP66m{6W2SQ0Xkvn3ju|maH9w)4lJ^`=V>)6HMt!ub~C%5XW)~j_am6y;VsKW=Y~{ zDm(1Q$Z^2Zkp@Xiz>2kqKyi&Zz&pO(F=EH}mKX?ky zFM5pcYw3SFA=&-pv(RdFNsC6;3}1y;d`qjyL84fk>;$Rby|U4mU~Ub-2Ss^ z^`}WILzcBuiQXUd45!0b*n#}CD+>VfzrZgdlcyiSW0Nua1``ZlxdVQr zk!qo4U<|zFbE!qdbF>$zVeB=%tsUt1{y|fqr>gQF5hyo!VRiUm6S~5F&l8siefoCi zuFhasu07gnvo~t|mb{2h&d!j=C1Y-MDJAinrcIw3@6}cj?u~191om3C!=qiez z#v7G9K+fJC-A=xeBv#_Q#ESMU_TPg2--`CLCtAio&o_)oVadRfGix3xXlnRLYRFks% z-9t}jeE<);3$MfDjFt?$P`8JRJlnNBygT)8H)q2OGKe|G+M;im1J1YS*|O{ifxm8) zXDrL&g*ZX^ohkR*GslW`0(Q3^tv}uNS!GTOqS?G#>&p)YZU@bt?8LS5d_W%BB??vL zUB9C81DMuPknas~zE*1Ew;JTB%@ro?7C(!euyQz+rj6(Y*R%!UU#E8L7&QA1Hq1f; zVai`di$|si#LC^ROLHauEOk>m-R346=%o1RtCjNI84qIZXk!U!|M9l!+!hv0QgT@9 zqBt4CK~IfVKo{q*aHfa;r^+u*T6?$4j=Hrk;PcF94vT}w*pczA+^E^Ste+fBBA{#2_^Hgq|434?xxWZ}dqcJT z8Tm#j)SaC}zr|O}Mj()SP|xSlp*mynE278U1XNU2QPN02xFGlbYAH;^@FA;XR(*7s zS-Z-FwkP`0?fm@wA|QCQV!EzaveQqqgnbB6m@?97=Y^7z-^ab}v7>K(=y{E7v<`c3 zZ<^*MrIlMLLdd^k;J|y{lHEz&pKHbq_~ahV<#|6Mv;(N8EE+E!<9iNG6S$zDNv}cT zXl1|xFjQOZ}!?EN`uOIN5y@GcawB!TvTru z>7MkBV^`n}MPT#dMjqdT$1;r&-XW4&+P|g8dn-qGSw@RQ{JzZ9(=@>|pC0yN;%7A= zcUZRs)PzgHZgqT*mbbGbE}| z1NAQY%7>h6_QL2N%SCr42IzM%umi}0WJY(xi;ldnc%HC=zPf4o{y5F^Ex8{brCGff zim&hl^Jjt(@~>*i+Jk^-9*JqCvjf2HvQ|*yFk&~U@WA-@_7crOG7Q;RkQ?v( zBP|o0Ukpx5u##eA>!w5!Ao&B5#N+6J)kpiLOCYC^h^T?%H>cy_?9YCW&+ZgR7l8qD zizV}Siz}2bn;!qSwrS+xxLfS?Pmyw;-Q*rLg~N9PE;qmCf-Nz^P{A*I6g#Uc4*|56 z#~8A>35BG-Vu=o%Jy+`-FT0eoj1dhPV-aSy8VPsx3f8V11{#apqddQXBNOJ8Jwx{~q1S*i3QY-XYx(d9IuG*OM&;NPgb} zJRz@kKKzCWHP-aqmCow=PI0L?06x;{ApCXYnX#`^LbeUTb$sXk@4l|`?qc>V8W51| z{>5CSdBst@gxmOrN#J|LmdD5PG}V+!=GmA{Lv&7k=zE>{gM&JzO14*Et&k$$2kdh( z$TcmQ9p>4QUSzFbde4-3(5P3_7E>KWrhRyfWd0qmmR9{Q)BSSfn;IDb-YRP8-CyI# zlJ&_|df7RV0KriE?!4~$8qq@)d({ersq7`a!`$3v7R zgMn2o7L3K$P&1;fRqImY;(bBPceXKHeiY$|()xQtd2$3-XFlKtyrvdKa;Kv(?7oyg~8>*Q?d>I8{rAN z2mH9(geFezLXF?c~&7U^Mu@4*bi28)O|^7P;Gt@nv)*2jNEu76PK$F~z9t6N=P(K4#rL40GLbwsWK<7X*+jL%jL;9FO zdrLj7fZrid#qiEP{kQI09#|o8Pb}v88_)6xFEi_=*CeLsA-JOt^)4jTfDKI3jxR3ys?^jbM?*lwmlxSEffKLU0l7P;71CGsstmPpx1pm48eoyM&OR8baDAYmwg6@}yq z)&jn`3<|Bp+W}lR{(49lFhpnDxGY#$+Cg;fT>hnCK=seo8SMakR>1=(I$P?5~Q3r4ReR0XxEDLNG?|m+i}W zfodpWkcBdOWI$=8wz3~m}iSk;j)t3|QV z&L5}Yo-X)VDtNC0(x$Nig4{q#1o$X1XmeCg3j1iJqaQ$$31Fbq-I9w8HDjKZu;ULk zdP-uq5VjBm^N=(57@&fN2+F7sD~4D;vKR?5Ku_K(ooD9xz=tD7MisxE%!i<4ebnlu z4GbshG+S<)32;v>NKxfZ?z$@kL&>9qTB={)HD57DP}uP*_fmu?u5-DA+h z+}sb4&#zo<@BM%Kx2M%63>t74!-Gta?LqeNmz>Q!WJ-YLIOs9qDQJ23+F487+*zBt z9}%XeaxuKeD>|ECOi+~$rVn5Lk;^z3;sMaBRBf^2@*#SCd-1Aie)h2vP~m|%<<&d- z?)R=9_n#Ak6Ulk9j85}MO?x2`PnM;*RFpy=9jN6qhsS)YMd8eH&elw5CvyB^_l517 zJO>3xQ3*IvphNLOeAHy)d&$AnAnJ|5Vc23EhCI?C1A}@mXJoRLwh(YrK!@P*8=V{F z$>EP6FN9T+28x>}ZCW;C zhNBCHur9`L!fCzaH+okmn%14t{8ednI54Ys+N zyt~LmBC=v!$!fv{h?_=+C1>H#@<{oS_=)%_K8~*nd!x1<{36_4K~+TvRhQ56t@!15HTxsKi=o^Lvr7`NT z^6ihlviy+Kp3qnWP6ynIhGZH4m8G;K>~lH&vR+L4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/themes/squares/client/src/img/stores/googleplay-badge.svg b/themes/squares/client/src/img/stores/googleplay-badge.svg new file mode 100644 index 000000000..9e33e3aa2 --- /dev/null +++ b/themes/squares/client/src/img/stores/googleplay-badge.svg @@ -0,0 +1,429 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/themes/squares/client/src/img/success.png b/themes/squares/client/src/img/success.png new file mode 100644 index 0000000000000000000000000000000000000000..ee9d6841bbb7208220a831c1c42917eb8facffa5 GIT binary patch literal 3147 zcmV-R47Br!P)@~8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H13&=@CK~#9!?VWj0Ro5NIWpwOJQDRbwNDwr}T5D`fYExS^ZNS!! z({?)3WhP^$({{SdG*eL?n@DR!L!g~W+NQSuv}t7VDXU9R+=7CjxFCufZipa@@1Y=Q z0fk@B56^R+j|cDGckj99-22XPzT=Dw+=`0CLjfNNgnC0Sw=jZrT^)0zv?CqKirDB=}*$K3Qy8 z0m#XXB}uo)ojuDZ2JEA;mJ)zo>nkK+#i=mr-oOa>%SoIPs_I~$EjO(IrB0#^a(-M+lx<9lN`o{IqVIbS~NiSdXS z#6y4#+4;gG!%zX}@U9%;6Klk9xgi43i5)q%FXr4q1XBU%l&+lB6MGhMEK32>WhV=t zbS47Ol|1sgZ;tzc#kvYW*YL?@T;^I{Hcn3gY_d~L7n?2u(7S%QdRK>hyX-yq-N}z3 znAFu7~3~!grS-_H?$ML&Q-^1W1)o)xkf}aUEDS zXKy#WS(dKX@un&fK(6jp0srJ3cJN)TsPU9;tBFSyfR$Ka zd)I0$W;x!Q0MQcg=vhqQ*W9@QJld}-vWEn~JeRu~UMbZiL9{mkHcH5&!#{nO4}5H| z@5p!}0A^CoDwww?#ozPU=uLo133>$hpKXBJyN%r7clAAhXR=m5B>*NQI~L~d_Dh1w zrv#uS*ZU=G@!(^7r^-GIzAF>6VqwmwYy7El9_%i_G6~x|{4VmS0-$Z zh1t83)t=uncL6eabTBT7di&VFV8i+CU~jB~e_YuQf2hfVm_zS#hrdJs`1PG_Fk@S) zPXRE&n^(fD9Z71=G1FauGNU{At)rXZ(!Cb1Cyq;{{(89-7O-72H!A$IoZ(+0@TYHE zs|db(UMx;jdY)zO0<;*}!JEiWI!<)>YUjF|;d_VwY$$wOpI6t}#1;Obs;}|&gAf2c_BYu$vKA@y7)sad{=9{?}TNC zGnpRSw-7%0#NpX45cpABwZnHMY?}>U^|43<1y~_6d{?`1-vQrcBmu%d&l&#tyDhrH zcjfuf=MPq(0BNGbe#g z>Rjl$#TEX!JGYp^cjejK)$o>w3e-M>eC{6(T{j!bOv~xH8b;KQ(l`Gr0`~ zaOf?-%9?CLJN0sRtIw6Q`1keqTDihMBMAH_5|E$(eR>Pt7 zZATNlU{4kXzAH=wz#T1ICQ!)7J6_{G;p1gO!uLLn%;XaA!-^01&7!$UfH#P|;h%Ok zLHHK&;M2##0VX$rU)}ddXnt^q-pY4~L!-c(xknO=02F!J%GekAc5E`5&K^ zF<7?Vfqn_!M<(0jx|j_FVG5d#Lm;+O&PJHJB}r)bbfT!8=}_?353Gao+m`|g{z->9 z;A5ud*{(643_=Mi9DyOh3C9fxGYNdUXr#uF>0qpxbHq_E;R}|Cg7_VVEeKItHOoxx zxC!B7rWGXo#n2^T{#MTtxmDk8fGN3&rh`u}0Y5iMu|$jr`|&+n1%V%$6K@&#bO2~Oh*~5{gss_DhL8E;&g7qh z$YtVC0^VD+%iFG468@x|Rgb)ui37n3MZUptw{Cgs6_{*6_;d|G9hjw5ERT(whg&aO z7Ct6CFaGynree`X?%~6+eJjI%HfL2Yn6X?uO2B$`Z$r1!((vj0e*u{Fg%K#hEk-Mi zdmi>en29HLlnwq26_tDm%>2q2-1*b`zq`7^fAFXuUM@_tG<-Vue;%yx(x~Vm2|9Iw zfB#`$K)}b1ptE-+$ADE{8;3i7+B8al@V{KR-tzDt5B=Jjz-lj!T&h6=gg>X?-GG3P znO>CmHdyi1u}jM`pyffQgg<(Ls5@Zgmq%m4s2e>_Mev=yJ@BOff=_S$;=$^#kAHpt zI{4_~K^PeHz84YqDCS(PhDly(?pryb_PCp%<@NDYfC!Rq(IvoQh>Lb-K+^Fd*wuIz z>bu%ufAa-ccWM{RFMcnez|%#agENW~=V=yzN?f)qcl}Oe#7qFUrvj&xK)k#DsG@}VYI&*_fJ(63EcXL}B0+dw{Ca;_`XvCB zAj3?rfU~j$5e4zP{pF}dfFKgPlz{tCFIIUAGF$++rR4jh1l)=|-Ed#xZMXnbf(axi zqy&b~@jG5AT{}@LKg|MA2|`6%O>AWe$m3r3%ezyi>cLJI0jLC#QUdmQE_d~VIiIeX zr588d1aSK#ke35?+q&-0C{CQICo^3IaJwgvH-`Gx^=+S4kPxOfFWm)jdpeMhi26P1 zogvr!&#_>|L;$yOf($ugsBLEN#p1-RTAuT{3E(koEawi?*YbZjYiH6gSo2{nfZGI; zoJCY#!ljMe>CZHRjUfWKO(mzxMJ1!};33!hJ!+?PO(B5C$|AYEZ20qiPh?@jFOBA5 zxBzZz3~gpvZOl>v@@zjqz<;cC4WA_h@VGa~4@3#@4xc>NpPsiTWjap=Tm|shfm9v{ z65w?{JTJa!ZpoS-h|u>!2;i|HwbZH-gxcd;UfPlL6OsB{C;>d`6BLQ~#GqCJ{HUL@ z&327^+`lDaUkfRKN4c6*(PkNaNr10pCC~ErrWYkQ%Z2PeMs@COMQUL#i2&V5l z$fR#RVfp%-7WzFh>Gupad;ghUfDxn!p*NB%=z9@!=^Iij=)01y)3>Jg(KzUz)zW`c lLjO-1{TleSLaf+V{|9F)WMK{Ekgos$002ovPDHLkV1l3q_6h(1 literal 0 HcmV?d00001 diff --git a/themes/squares/client/src/img/user.png b/themes/squares/client/src/img/user.png new file mode 100644 index 0000000000000000000000000000000000000000..00941399d34f0f2d85d323f61daa67c2c00c494d GIT binary patch literal 2933 zcmV-*3ySoKP)>+Y_1buC zheF*1yf*GR)m>@otnGcw%sqGZe)vfU+q>t^|F^R<_ujcTYIJlo%4jV9WAaR39I1IcZ1yCpS zIY0`~LOAsCXaEv{=cNF_2S1lxgn0!F-rv{6d0@U2VDjK!2d)?(^zpb(O#~{z<1L%h zz`>Ihfp_q;fNbDV1ck2z&vq|HoB+`1Xdnp~jL_hfe8nynpNI$nprH$ZHW~ak_zd`^ zVFiH3O##fnhzxsNhFJ&KlZFxi8uS9|F_R$&2KI2yF@ylnkj21V8EVk<1HThm0M?5q z!wen#x-@Ay#-LU3Q-KpQK-IVm#0Vh(6gr)CbjaY;1?MwNCoKRJI2)*zVQI!~;5m{4 zKw;knT4hL@Q-{ZzlmKWYD~#$v`_Iw@bH~becUG-S%q-brIc!hQ%CTE>@`@1rH~t^~ z41WiI7ypKEd+>ucn}h)DJO4JN0|WtfTGFky((<~-_O71$J^cftp@4sjuYs?HuSraT zYBjD2!VN!$U7T<~AWPy*p3UYD9S&zzZK#1y2EI1F2fi1fQN^iT1YmRXE_461$D3Uj zt`?WMJ^jO41wR&iFMLmYZvqmW;3a^GcjNIpyUo>oPp{y|g71wr5Rf26R{^k#lJlU> z;E|J`7V-*yELaPHq3y-G2*4(4_H+EMA1JSBYNZms53C7m<0t@*x4fXK060v^Myhx< z^3f-GCAX-B?*nV&Ji7@;T&HL$KpM}}+_t|Sw^I$@2iC~>tH7*@061HPO<3U4u(#Ll zsCIk$ywnB28nIT+BZv!B6#(a}u_;U3%(j=$=Do z#TDQ{E@c6dl>%I)Smctph;09_P|%>BeJ>}(((xfktJ z7s0;&KLKzvB#H$t|I_wH(ZlzFZ6PYb%qX@a7P;cyPd?-@iU8Oaq5{l|V%wsTEAFGy z*NTiH0Jepw04t){-gxASJA0|%rcngIwottjh+=#5NC6^K0BaQ6B~uCznF16>v8__2 z0Ffy`B??e41&CMy8c~2ADL}*u(1`*(ASuA*0;d!}b9zyLhw=;%u>#yj0luVI+|iR) z3XLLwHTR!XJA#H#02jsLHvDC;VR{11e>-6#D!>C2powB}vGJxJqX>XY&=VEFjRI6q zowRWAT5*}^;a|S)yiN7^;*#@GRF-=mZ=;7B#TC_}hF@9V(it0Xb`w6nHnOvU6tjZv zQCu5%GVtsb8)v#heEXR*D8MP|`{X294%5Wjv`g+KzVLjis&La`#O6b=`CF3!uv2g`gE;3*M` z#;x4QFBQM@Sb;#{W9HL+)51GMDB4+3-$eJ~&*NPZXX+P7fL8JkhK%LUCb~0)Po2vz z6DWM-oDh$Ojp1QkbGw`1CJ`gcS6NyF6W{}-0KXTd@WgEg$(;rE9XVelSop{vlmbkn zIxoQA5N1tN8<~EArgql=;phJ*x&WLxc7cc+qBP*Wtyu+B1lX9gzgWOezti6eA)5ti zfVZT)u7UgBK&6$ng5CG~*y2xspNrIpH&+Myu$D9>mOOMdV9Tq;UhoxC%|Js zpGXZc?K;kFCGqsE9J`?5w@)Hw&vy)6A®tx`|Cc!K{R~B2jc1N|ng#YPY z01+^g*}>)+v;a(kg*ZQ7)0sYS5YJw62)S4!2N7^)@}f|(3`qbcL4r0*!k9bJT`jgp}PPI5fXrZ%isracGF5;IdBtzN$?_Y z46k-0FnAp9&IZ2EGXt&yC`6b8>;=AxfZ)FdvVb2@IQAq2P>3)S*uhSVMm*K;1fHjK z%t;BL5Mc(81b7S(JhovA@HE9^O;P}b2rkn{6#(u#g!>KLW&ICB?B8ANXZSm~9Q{dlI$z1o fINvtfu|oa}IMcn@FFyTa00000NkvXXu0mjfV5=n> literal 0 HcmV?d00001 diff --git a/themes/squares/client/src/img/warning.png b/themes/squares/client/src/img/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..c6acd953b142a8e43e293c353a60e6313a8d39eb GIT binary patch literal 4038 zcmZ8kc{CK>7k@Km%nZhmB)g(al4J@g%*Ybi@{R0UV<~HPGGh#)vXiopt-hj2MImcp zY$;2&EGf;GWSK0JWq#AYzjw}k@4R>Jx$oW2UEdvZQv)=Q7!LpdZFo`Fg0oWo5|W!U zw`;lda28mgj^Py~XG9{237kF3|DsJG09+w|2|^mC_HqtIgY>L}{_%4U3Uvu^1EHaz zO4oh80%A^>WZHKCL(h;uQN z$(4fz2^gkydwnSstYvq?z;jH?4dHpjOIsi?F~5mv-=X~B1+IfT=c@TV0XW+s38jp3 zIm`8ol6U1Y3*FZ4G|uCdEr5XCymsumB+=8f9+kg$8eP;^u9f!rnogW5jie4Qx&UZq%;ioEF43zF#7yy~zM-eI zO?nd_=$d}*1Zdj89sj6I?h<22KC>b!%CwF;CBL2iff7f&(SHL%Y3gOm9%rm>z`BQ< zags$B!oM7@imA=`0F#zKMyK{>K{K;0WBK$OE(4l07$MAiJTzu}?T}$Eu6~)JEgc2l zf=SEgU@ShC_l!E7E{fG`pTiY)xdsO#!EcwQcM9VpAIi>eEYTaUlkwl)(ssK%=%={( zyLCh_G^+Jtx;(pcl>ozzx!j?_lkK#SDH7&caoLxQxBIRw{7*%VTL*&Jr`VX~UcGAs z20}q%Os|>1VApPeKz|^ki=-gYS z9Y@17L~Ki*qd&(F9GL7rg&nPk`xaYE{WG~yqtOG|AGd;=yeICQ3?P8Wl(?P zFZ7V8e_`3g%Z~$IWK8`8B+2Uaw1D@FC+7 z-$J8b@&2r&R`X87_vAo=1n`SqVsgn1)n)~(GkKZ^+y9#v%9lDe!PdCl<}d?4l&Z)= zLn&Pk*;lOwuF)@Xq&>Lrv$yIxxEBk9P*f6q;C&Gxctr z>UH@f5#<~SKVIH7{8TvYp}h5)?t8@rj)0$ z=(|UP)`a4gb}Nfh$}-Aot~qRv-2NG&Pw?!YJ_L{-cI0;gx6C17f#<(JU-shN4s}Xm zsr`bk<+y&nAzmdTfd%Fm{E6d?o6#b_8AlL0OZR6X0~$FN0^=Va&iV>~OZ62FfY!fr z+G@7W*EE5ULNUF6+k=dc=OuP(y~w^QFmG}IU~z1DYm-#g5&k$r%25&3Dt&RF(P6Qy z!hED53NQMhpoEN40!T@U!U(PZ!f|Zebkh%KFy7&Ljg0S%@80Y9{`L*_p0Gq(-Xa_n zWwVhZa3tlb{&BvDJc*;YmC03wmleLmlfhCIJloH!f053eR9|}uD3}(DuE;rORKDF~ z8OF`gF;dSjm&vc;j;H-8dLiCxQey{Hz=`iQ)BzERRTZji7F7reYFTV&VRD5#mM;P( zOnzY7S=<_1pZRu#%rT>*GCwrZtj>RI@Q?16O+_|h&X4@_qT z{7RG|7N$Y~RTfv^0XLX@uwvGl4+Up|W7er#Ssc0MwmZo`pUyz>8<(%}N)*8iLT<*$ zO7nn-T=u0({;o{JGM*TzB-T(t)5aqsCE#OXne$tyGP|&oKl$KN3NP@5DkD^g0|1p@ z4(AS`US%VnTz+8Z^%`KGKzv~a`sOjR&m!IZiOqmEvGch4*@fGRIo{5gpM0we7|#}f z^CTPH)gw)SBkBRwS>h6OBTukHNYUs@{TU=wUvp<|w38svkaGfXF17 zWRj^r4BA@-?+x!da`Cm2p9d}nF!q(p6NOeC5{5={Kr7n#qM8v0M{@o_vGlW5W}(rbH_w)a22X{DsMW>q`VKlT zXk-Y086nM7%Po?soy))lmj}z#H&QEp9}4F_KI_@4j-*^)ygEts*RkiKn62eo$G03- zOi`)M-~=((Ir@vJgpNwW2eeFB3y_uX|F=QcfJs>&llMuD- z;!BoDIu2Ioq}!HTPb2l-x0v3yVVB%8h?y?A85*|%wy3iFW1CtV+P>P+YD!=Qp&6nh zpIe}j|SXo%pJ_yAdzPv0Hd-05QBz!d!eVX!e_ z-js2+CCJYh%8^@Wg9~fkF6Qu>bT&Ro^dIN&*5>tgan0OP%w$V%fA-%-^*%>}*H>=U z!jTaxwGN=$~V1I2jb9T`^h|OQK?o+N?D|4 z%z=m!488|@gJ9jym%RV<;yy5CvE=1Zp4VzOXMit87iM+GU@MG@F&tH|m(TSZJC+>> zVV7QsOjvSjJ`@G(vuKq+ovB&m7i#;On+EK^sDZq)O`%>Aot~S$T6tk$6miZ z37nON_ttN@aBChO!q9G=2`91_S^1OMbP-+Qzb~7M7k%ZmY*tzbI>2DcboU%unRGe_ z^g6>eZQIZhx<$yz=BE-TqN+ya*KDr>e5)V=r(T0o09y}kAUswax2W8N6l5)zy{FXT zBCBMbJKg8c8{RJV4?lJfI=NJj3;36BD84{BOqIIaih|ifAHPMW@E@HB`s-w(SAV(o z8BYmb{tLZdgx&WRQ_Flp++DL9J%{!bU5L_O@OV^m-WN_@GfqN+t#6a>ZJ)fIEc54H z_Ahf?jW>B!W)Hfw>LIikDG8h3(g;8IH78vtMT}9pZk9gqaafF_b|zx9h71z{H7U<{ z+UCstV>w|G^N5qtm{_m!4V!Cc_#$GTw0CkB!PSFBD(8Zm(v9+Jmfk6V{CR2+G|9x>_Jus|!Ap5u#`Lp(Fwz*3I*OOGUT(q-M!Afy{tE@U zJ2F=Cu$IgfDXw+ZF2+tPekHA`bg@1(%I2vkB|8wwf zRU;Ls;@*5M8xL*Febm=t`gFhF?0_;=b~>tWNj@wC1yr}RMl6=> zX6-I}2_osksR_lEmhYexY@erD5yZS_@mPt{mq!Rag+PIruP1DNV3J?1b5O|b#ua+1 zKK*Qlxf{Zjnf!_bXYF=)Kg>LK0X;cl;x&6Us;m43gbnAydy`^s!rJ|y34o}Mlo51w#*DkxKu*ZSO^rjzY4`S%iTMPk?%#D9Hq0u zXO7WS9c7da`CWXmSEK4H`Zo6u$TGFji_X0wF zYHis}o|pf`w?RCV<8AhzFDtAF=BVX(@&7)kpvZHP-ER?mQZc1eo%81ghI*#D&vcxr F{{xrIJYfI; literal 0 HcmV?d00001 diff --git a/themes/squares/client/src/index.ts b/themes/squares/client/src/index.ts new file mode 100644 index 000000000..802004a8f --- /dev/null +++ b/themes/squares/client/src/index.ts @@ -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 () { + (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, (global as any).u2f); + 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, (global as any).u2f); + 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); +})(); diff --git a/themes/squares/client/src/lib/GetPromised.ts b/themes/squares/client/src/lib/GetPromised.ts new file mode 100644 index 000000000..779139654 --- /dev/null +++ b/themes/squares/client/src/lib/GetPromised.ts @@ -0,0 +1,14 @@ +import BluebirdPromise = require("bluebird"); + +export default function ($: JQueryStatic, url: string, data: Object, fn: any, + dataType: string): BluebirdPromise { + return new BluebirdPromise((resolve, reject) => { + $.get(url, {}, undefined, dataType) + .done((data: any) => { + resolve(data); + }) + .fail((xhr: JQueryXHR, textStatus: string) => { + reject(textStatus); + }); + }); +} \ No newline at end of file diff --git a/themes/squares/client/src/lib/INotifier.ts b/themes/squares/client/src/lib/INotifier.ts new file mode 100644 index 000000000..df947538f --- /dev/null +++ b/themes/squares/client/src/lib/INotifier.ts @@ -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; +} \ No newline at end of file diff --git a/themes/squares/client/src/lib/Notifier.ts b/themes/squares/client/src/lib/Notifier.ts new file mode 100644 index 000000000..c0252b9b9 --- /dev/null +++ b/themes/squares/client/src/lib/Notifier.ts @@ -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('status %s\ + %s', 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); + } +} \ No newline at end of file diff --git a/themes/squares/client/src/lib/QueryParametersRetriever.ts b/themes/squares/client/src/lib/QueryParametersRetriever.ts new file mode 100644 index 000000000..a529adb6e --- /dev/null +++ b/themes/squares/client/src/lib/QueryParametersRetriever.ts @@ -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, " ")); + } +} \ No newline at end of file diff --git a/themes/squares/client/src/lib/SafeRedirect.ts b/themes/squares/client/src/lib/SafeRedirect.ts new file mode 100644 index 000000000..7e7684b8a --- /dev/null +++ b/themes/squares/client/src/lib/SafeRedirect.ts @@ -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(); +} \ No newline at end of file diff --git a/themes/squares/client/src/lib/firstfactor/FirstFactorValidator.ts b/themes/squares/client/src/lib/firstfactor/FirstFactorValidator.ts new file mode 100644 index 000000000..eaa496fdd --- /dev/null +++ b/themes/squares/client/src/lib/firstfactor/FirstFactorValidator.ts @@ -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 { + return new BluebirdPromise(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)); + }); + }); +} diff --git a/themes/squares/client/src/lib/firstfactor/UISelectors.ts b/themes/squares/client/src/lib/firstfactor/UISelectors.ts new file mode 100644 index 000000000..0e971b3c3 --- /dev/null +++ b/themes/squares/client/src/lib/firstfactor/UISelectors.ts @@ -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"; diff --git a/themes/squares/client/src/lib/firstfactor/index.ts b/themes/squares/client/src/lib/firstfactor/index.ts new file mode 100644 index 000000000..24affee2d --- /dev/null +++ b/themes/squares/client/src/lib/firstfactor/index.ts @@ -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); + }); +} + diff --git a/themes/squares/client/src/lib/reset-password/constants.ts b/themes/squares/client/src/lib/reset-password/constants.ts new file mode 100644 index 000000000..d48d4e67d --- /dev/null +++ b/themes/squares/client/src/lib/reset-password/constants.ts @@ -0,0 +1,2 @@ + +export const FORM_SELECTOR = ".form-signin"; \ No newline at end of file diff --git a/themes/squares/client/src/lib/reset-password/reset-password-form.ts b/themes/squares/client/src/lib/reset-password/reset-password-form.ts new file mode 100644 index 000000000..b94279cde --- /dev/null +++ b/themes/squares/client/src/lib/reset-password/reset-password-form.ts @@ -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); + }); +} diff --git a/themes/squares/client/src/lib/reset-password/reset-password-request.ts b/themes/squares/client/src/lib/reset-password/reset-password-request.ts new file mode 100644 index 000000000..846226d75 --- /dev/null +++ b/themes/squares/client/src/lib/reset-password/reset-password-request.ts @@ -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); + }); +} + diff --git a/themes/squares/client/src/lib/secondfactor/TOTPValidator.ts b/themes/squares/client/src/lib/secondfactor/TOTPValidator.ts new file mode 100644 index 000000000..5394139a6 --- /dev/null +++ b/themes/squares/client/src/lib/secondfactor/TOTPValidator.ts @@ -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 { + return new BluebirdPromise(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)); + }); + }); +} \ No newline at end of file diff --git a/themes/squares/client/src/lib/secondfactor/U2FValidator.ts b/themes/squares/client/src/lib/secondfactor/U2FValidator.ts new file mode 100644 index 000000000..5812922f7 --- /dev/null +++ b/themes/squares/client/src/lib/secondfactor/U2FValidator.ts @@ -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 { + return new BluebirdPromise(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 { + 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, $); + }); +} diff --git a/themes/squares/client/src/lib/secondfactor/constants.ts b/themes/squares/client/src/lib/secondfactor/constants.ts new file mode 100644 index 000000000..50bba7571 --- /dev/null +++ b/themes/squares/client/src/lib/secondfactor/constants.ts @@ -0,0 +1,3 @@ + +export const TOTP_FORM_SELECTOR = ".form-signin.totp"; +export const TOTP_TOKEN_SELECTOR = ".form-signin #token"; diff --git a/themes/squares/client/src/lib/secondfactor/index.ts b/themes/squares/client/src/lib/secondfactor/index.ts new file mode 100644 index 000000000..279723dce --- /dev/null +++ b/themes/squares/client/src/lib/secondfactor/index.ts @@ -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); + }); +} \ No newline at end of file diff --git a/themes/squares/client/src/lib/totp-register/totp-register.ts b/themes/squares/client/src/lib/totp-register/totp-register.ts new file mode 100644 index 000000000..6a9aa7ee0 --- /dev/null +++ b/themes/squares/client/src/lib/totp-register/totp-register.ts @@ -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); +} diff --git a/themes/squares/client/src/lib/totp-register/ui-selector.ts b/themes/squares/client/src/lib/totp-register/ui-selector.ts new file mode 100644 index 000000000..9d43fabea --- /dev/null +++ b/themes/squares/client/src/lib/totp-register/ui-selector.ts @@ -0,0 +1,2 @@ + +export const QRCODE_ID_SELECTOR = "#qrcode"; \ No newline at end of file diff --git a/themes/squares/client/src/lib/u2f-register/u2f-register.ts b/themes/squares/client/src/lib/u2f-register/u2f-register.ts new file mode 100644 index 000000000..abf40ee05 --- /dev/null +++ b/themes/squares/client/src/lib/u2f-register/u2f-register.ts @@ -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 { + return new BluebirdPromise(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 { + 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); + }); + }); +} diff --git a/themes/squares/client/src/thirdparties/qrcode.min.js b/themes/squares/client/src/thirdparties/qrcode.min.js new file mode 100644 index 000000000..993e88f39 --- /dev/null +++ b/themes/squares/client/src/thirdparties/qrcode.min.js @@ -0,0 +1 @@ +var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); \ No newline at end of file diff --git a/themes/squares/client/src/thirdparties/u2f-api.js b/themes/squares/client/src/thirdparties/u2f-api.js new file mode 100644 index 000000000..8c7801e3b --- /dev/null +++ b/themes/squares/client/src/thirdparties/u2f-api.js @@ -0,0 +1,749 @@ +//Copyright 2014-2015 Google Inc. All rights reserved. + +//Use of this source code is governed by a BSD-style +//license that can be found in the LICENSE file or at +//https://developers.google.com/open-source/licenses/bsd + +/** + * @fileoverview The U2F api. + */ +'use strict'; + + +/** + * Namespace for the U2F api. + * @type {Object} + */ +var u2f = u2f || {}; + +/** + * FIDO U2F Javascript API Version + * @number + */ +var js_api_version; + +/** + * The U2F extension id + * @const {string} + */ +// The Chrome packaged app extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the package Chrome app and does not require installing the U2F Chrome extension. + u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; +// The U2F Chrome extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the U2F Chrome extension to authenticate. +// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; + + +/** + * Message types for messsages to/from the extension + * @const + * @enum {string} + */ +u2f.MessageTypes = { + 'U2F_REGISTER_REQUEST': 'u2f_register_request', + 'U2F_REGISTER_RESPONSE': 'u2f_register_response', + 'U2F_SIGN_REQUEST': 'u2f_sign_request', + 'U2F_SIGN_RESPONSE': 'u2f_sign_response', + 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', + 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' +}; + + +/** + * Response status codes + * @const + * @enum {number} + */ +u2f.ErrorCodes = { + 'OK': 0, + 'OTHER_ERROR': 1, + 'BAD_REQUEST': 2, + 'CONFIGURATION_UNSUPPORTED': 3, + 'DEVICE_INELIGIBLE': 4, + 'TIMEOUT': 5 +}; + + +/** + * A message for registration requests + * @typedef {{ + * type: u2f.MessageTypes, + * appId: ?string, + * timeoutSeconds: ?number, + * requestId: ?number + * }} + */ +u2f.U2fRequest; + + +/** + * A message for registration responses + * @typedef {{ + * type: u2f.MessageTypes, + * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), + * requestId: ?number + * }} + */ +u2f.U2fResponse; + + +/** + * An error object for responses + * @typedef {{ + * errorCode: u2f.ErrorCodes, + * errorMessage: ?string + * }} + */ +u2f.Error; + +/** + * Data object for a single sign request. + * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} + */ +u2f.Transport; + + +/** + * Data object for a single sign request. + * @typedef {Array} + */ +u2f.Transports; + +/** + * Data object for a single sign request. + * @typedef {{ + * version: string, + * challenge: string, + * keyHandle: string, + * appId: string + * }} + */ +u2f.SignRequest; + + +/** + * Data object for a sign response. + * @typedef {{ + * keyHandle: string, + * signatureData: string, + * clientData: string + * }} + */ +u2f.SignResponse; + + +/** + * Data object for a registration request. + * @typedef {{ + * version: string, + * challenge: string + * }} + */ +u2f.RegisterRequest; + + +/** + * Data object for a registration response. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: Transports, + * appId: string + * }} + */ +u2f.RegisterResponse; + + +/** + * Data object for a registered key. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: ?Transports, + * appId: ?string + * }} + */ +u2f.RegisteredKey; + + +/** + * Data object for a get API register response. + * @typedef {{ + * js_api_version: number + * }} + */ +u2f.GetJsApiVersionResponse; + + +//Low level MessagePort API support + +/** + * Sets up a MessagePort to the U2F extension using the + * available mechanisms. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + */ +u2f.getMessagePort = function(callback) { + if (typeof chrome != 'undefined' && chrome.runtime) { + // The actual message here does not matter, but we need to get a reply + // for the callback to run. Thus, send an empty signature request + // in order to get a failure response. + var msg = { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: [] + }; + chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { + if (!chrome.runtime.lastError) { + // We are on a whitelisted origin and can talk directly + // with the extension. + u2f.getChromeRuntimePort_(callback); + } else { + // chrome.runtime was available, but we couldn't message + // the extension directly, use iframe + u2f.getIframePort_(callback); + } + }); + } else if (u2f.isAndroidChrome_()) { + u2f.getAuthenticatorPort_(callback); + } else if (u2f.isIosChrome_()) { + u2f.getIosPort_(callback); + } else { + // chrome.runtime was not available at all, which is normal + // when this origin doesn't have access to any extensions. + u2f.getIframePort_(callback); + } +}; + +/** + * Detect chrome running on android based on the browser's useragent. + * @private + */ +u2f.isAndroidChrome_ = function() { + var userAgent = navigator.userAgent; + return userAgent.indexOf('Chrome') != -1 && + userAgent.indexOf('Android') != -1; +}; + +/** + * Detect chrome running on iOS based on the browser's platform. + * @private + */ +u2f.isIosChrome_ = function() { + return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; +}; + +/** + * Connects directly to the extension via chrome.runtime.connect. + * @param {function(u2f.WrappedChromeRuntimePort_)} callback + * @private + */ +u2f.getChromeRuntimePort_ = function(callback) { + var port = chrome.runtime.connect(u2f.EXTENSION_ID, + {'includeTlsChannelId': true}); + setTimeout(function() { + callback(new u2f.WrappedChromeRuntimePort_(port)); + }, 0); +}; + +/** + * Return a 'port' abstraction to the Authenticator app. + * @param {function(u2f.WrappedAuthenticatorPort_)} callback + * @private + */ +u2f.getAuthenticatorPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedAuthenticatorPort_()); + }, 0); +}; + +/** + * Return a 'port' abstraction to the iOS client app. + * @param {function(u2f.WrappedIosPort_)} callback + * @private + */ +u2f.getIosPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedIosPort_()); + }, 0); +}; + +/** + * A wrapper for chrome.runtime.Port that is compatible with MessagePort. + * @param {Port} port + * @constructor + * @private + */ +u2f.WrappedChromeRuntimePort_ = function(port) { + this.port_ = port; +}; + +/** + * Format and return a sign request compliant with the JS API version supported by the extension. + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatSignRequest_ = + function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: challenge, + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: signRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + appId: appId, + challenge: challenge, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; +}; + +/** + * Format and return a register request compliant with the JS API version supported by the extension.. + * @param {Array} signRequests + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatRegisterRequest_ = + function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + for (var i = 0; i < registerRequests.length; i++) { + registerRequests[i].appId = appId; + } + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: registerRequests[0], + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + signRequests: signRequests, + registerRequests: registerRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + appId: appId, + registerRequests: registerRequests, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; +}; + + +/** + * Posts a message on the underlying channel. + * @param {Object} message + */ +u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { + this.port_.postMessage(message); +}; + + +/** + * Emulates the HTML 5 addEventListener interface. Works only for the + * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedChromeRuntimePort_.prototype.addEventListener = + function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message' || name == 'onmessage') { + this.port_.onMessage.addListener(function(message) { + // Emulate a minimal MessageEvent object + handler({'data': message}); + }); + } else { + console.error('WrappedChromeRuntimePort only supports onMessage'); + } +}; + +/** + * Wrap the Authenticator app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedAuthenticatorPort_ = function() { + this.requestId_ = -1; + this.requestObject_ = null; +} + +/** + * Launch the Authenticator intent. + * @param {Object} message + */ +u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { + var intentUrl = + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + + ';S.request=' + encodeURIComponent(JSON.stringify(message)) + + ';end'; + document.location = intentUrl; +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { + return "WrappedAuthenticatorPort_"; +}; + + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message') { + var self = this; + /* Register a callback to that executes when + * chrome injects the response. */ + window.addEventListener( + 'message', self.onRequestUpdate_.bind(self, handler), false); + } else { + console.error('WrappedAuthenticatorPort only supports message'); + } +}; + +/** + * Callback invoked when a response is received from the Authenticator. + * @param function({data: Object}) callback + * @param {Object} message message Object + */ +u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = + function(callback, message) { + var messageObject = JSON.parse(message.data); + var intentUrl = messageObject['intentURL']; + + var errorCode = messageObject['errorCode']; + var responseObject = null; + if (messageObject.hasOwnProperty('data')) { + responseObject = /** @type {Object} */ ( + JSON.parse(messageObject['data'])); + } + + callback({'data': responseObject}); +}; + +/** + * Base URL for intents to Authenticator. + * @const + * @private + */ +u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = + 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; + +/** + * Wrap the iOS client app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedIosPort_ = function() {}; + +/** + * Launch the iOS client app request + * @param {Object} message + */ +u2f.WrappedIosPort_.prototype.postMessage = function(message) { + var str = JSON.stringify(message); + var url = "u2f://auth?" + encodeURI(str); + location.replace(url); +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedIosPort_.prototype.getPortType = function() { + return "WrappedIosPort_"; +}; + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name !== 'message') { + console.error('WrappedIosPort only supports message'); + } +}; + +/** + * Sets up an embedded trampoline iframe, sourced from the extension. + * @param {function(MessagePort)} callback + * @private + */ +u2f.getIframePort_ = function(callback) { + // Create the iframe + var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; + var iframe = document.createElement('iframe'); + iframe.src = iframeOrigin + '/u2f-comms.html'; + iframe.setAttribute('style', 'display:none'); + document.body.appendChild(iframe); + + var channel = new MessageChannel(); + var ready = function(message) { + if (message.data == 'ready') { + channel.port1.removeEventListener('message', ready); + callback(channel.port1); + } else { + console.error('First event on iframe port was not "ready"'); + } + }; + channel.port1.addEventListener('message', ready); + channel.port1.start(); + + iframe.addEventListener('load', function() { + // Deliver the port to the iframe and initialize + iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); + }); +}; + + +//High-level JS API + +/** + * Default extension response timeout in seconds. + * @const + */ +u2f.EXTENSION_TIMEOUT_SEC = 30; + +/** + * A singleton instance for a MessagePort to the extension. + * @type {MessagePort|u2f.WrappedChromeRuntimePort_} + * @private + */ +u2f.port_ = null; + +/** + * Callbacks waiting for a port + * @type {Array} + * @private + */ +u2f.waitingForPort_ = []; + +/** + * A counter for requestIds. + * @type {number} + * @private + */ +u2f.reqCounter_ = 0; + +/** + * A map from requestIds to client callbacks + * @type {Object.} + * @private + */ +u2f.callbackMap_ = {}; + +/** + * Creates or retrieves the MessagePort singleton to use. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + * @private + */ +u2f.getPortSingleton_ = function(callback) { + if (u2f.port_) { + callback(u2f.port_); + } else { + if (u2f.waitingForPort_.length == 0) { + u2f.getMessagePort(function(port) { + u2f.port_ = port; + u2f.port_.addEventListener('message', + /** @type {function(Event)} */ (u2f.responseHandler_)); + + // Careful, here be async callbacks. Maybe. + while (u2f.waitingForPort_.length) + u2f.waitingForPort_.shift()(u2f.port_); + }); + } + u2f.waitingForPort_.push(callback); + } +}; + +/** + * Handles response messages from the extension. + * @param {MessageEvent.} message + * @private + */ +u2f.responseHandler_ = function(message) { + var response = message.data; + var reqId = response['requestId']; + if (!reqId || !u2f.callbackMap_[reqId]) { + console.error('Unknown or missing requestId in response.'); + return; + } + var cb = u2f.callbackMap_[reqId]; + delete u2f.callbackMap_[reqId]; + cb(response['responseData']); +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the sign request. + * @param {string=} appId + * @param {string=} challenge + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual sign request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual sign request in the supported API version. + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * @param {string=} appId + * @param {string=} challenge + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the register request. + * @param {string=} appId + * @param {Array} registerRequests + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual register request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual register request in the supported API version. + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * @param {string=} appId + * @param {Array} registerRequests + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatRegisterRequest_( + appId, registeredKeys, registerRequests, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + + +/** + * Dispatches a message to the extension to find out the supported + * JS API version. + * If the user is on a mobile phone and is thus using Google Authenticator instead + * of the Chrome extension, don't send the request and simply return 0. + * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.getApiVersion = function(callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + // If we are using Android Google Authenticator or iOS client app, + // do not fire an intent to ask which JS API version to use. + if (port.getPortType) { + var apiVersion; + switch (port.getPortType()) { + case 'WrappedIosPort_': + case 'WrappedAuthenticatorPort_': + apiVersion = 1.1; + break; + + default: + apiVersion = 0; + break; + } + callback({ 'js_api_version': apiVersion }); + return; + } + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var req = { + type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, + timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), + requestId: reqId + }; + port.postMessage(req); + }); +}; + diff --git a/themes/squares/client/test/Notifier.test.ts b/themes/squares/client/test/Notifier.test.ts new file mode 100644 index 000000000..70bfea146 --- /dev/null +++ b/themes/squares/client/test/Notifier.test.ts @@ -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"); + }); +}); \ No newline at end of file diff --git a/themes/squares/client/test/firstfactor/FirstFactorValidator.test.ts b/themes/squares/client/test/firstfactor/FirstFactorValidator.test.ts new file mode 100644 index 000000000..ac8353278 --- /dev/null +++ b/themes/squares/client/test/firstfactor/FirstFactorValidator.test.ts @@ -0,0 +1,44 @@ + +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", "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", "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."); + }); + }); +}); \ No newline at end of file diff --git a/themes/squares/client/test/mocks/NotifierStub.ts b/themes/squares/client/test/mocks/NotifierStub.ts new file mode 100644 index 000000000..9c268d66d --- /dev/null +++ b/themes/squares/client/test/mocks/NotifierStub.ts @@ -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(); + } +} \ No newline at end of file diff --git a/themes/squares/client/test/mocks/jquery.ts b/themes/squares/client/test/mocks/jquery.ts new file mode 100644 index 000000000..273f90861 --- /dev/null +++ b/themes/squares/client/test/mocks/jquery.ts @@ -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() + }; +} diff --git a/themes/squares/client/test/mocks/u2f-api.ts b/themes/squares/client/test/mocks/u2f-api.ts new file mode 100644 index 000000000..d123f6a95 --- /dev/null +++ b/themes/squares/client/test/mocks/u2f-api.ts @@ -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() + }; +} \ No newline at end of file diff --git a/themes/squares/client/test/secondfactor/TOTPValidator.test.ts b/themes/squares/client/test/secondfactor/TOTPValidator.test.ts new file mode 100644 index 000000000..5dd6f15c3 --- /dev/null +++ b/themes/squares/client/test/secondfactor/TOTPValidator.test.ts @@ -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(); + }); + }); +}); \ No newline at end of file diff --git a/themes/squares/client/test/totp-register/totp-register.test.ts b/themes/squares/client/test/totp-register/totp-register.test.ts new file mode 100644 index 000000000..86fc455a1 --- /dev/null +++ b/themes/squares/client/test/totp-register/totp-register.test.ts @@ -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); + }); +}); \ No newline at end of file diff --git a/themes/squares/client/tsconfig.json b/themes/squares/client/tsconfig.json new file mode 100644 index 000000000..0bb4d62ff --- /dev/null +++ b/themes/squares/client/tsconfig.json @@ -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/**/*" + ] +} diff --git a/themes/squares/client/tslint.json b/themes/squares/client/tslint.json new file mode 100644 index 000000000..c2c1b7501 --- /dev/null +++ b/themes/squares/client/tslint.json @@ -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 + } +} diff --git a/themes/squares/server/.directory b/themes/squares/server/.directory new file mode 100644 index 000000000..b7754766c --- /dev/null +++ b/themes/squares/server/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,58,20 +Version=3 +ViewMode=1 diff --git a/themes/squares/server/src/index.ts b/themes/squares/server/src/index.ts new file mode 100755 index 000000000..fcbf4d022 --- /dev/null +++ b/themes/squares/server/src/index.ts @@ -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 "); + 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); diff --git a/themes/squares/server/src/lib/.directory b/themes/squares/server/src/lib/.directory new file mode 100644 index 000000000..006b379ad --- /dev/null +++ b/themes/squares/server/src/lib/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,59,13 +Version=3 +ViewMode=1 diff --git a/themes/squares/server/src/lib/AuthenticationSessionHandler.ts b/themes/squares/server/src/lib/AuthenticationSessionHandler.ts new file mode 100644 index 000000000..57361bf8a --- /dev/null +++ b/themes/squares/server/src/lib/AuthenticationSessionHandler.ts @@ -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; + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/ErrorReplies.ts b/themes/squares/server/src/lib/ErrorReplies.ts new file mode 100644 index 000000000..f1c5f4fd1 --- /dev/null +++ b/themes/squares/server/src/lib/ErrorReplies.ts @@ -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 }); +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/Exceptions.ts b/themes/squares/server/src/lib/Exceptions.ts new file mode 100644 index 000000000..83fa4eb6b --- /dev/null +++ b/themes/squares/server/src/lib/Exceptions.ts @@ -0,0 +1,88 @@ + +export class LdapSearchError extends Error { + constructor(message?: string) { + super(message); + this.name = "LdapSearchError"; + (Object).setPrototypeOf(this, LdapSearchError.prototype); + } +} + +export class LdapBindError extends Error { + constructor(message?: string) { + super(message); + this.name = "LdapBindError"; + (Object).setPrototypeOf(this, LdapBindError.prototype); + } +} + +export class LdapError extends Error { + constructor(message?: string) { + super(message); + this.name = "LdapError"; + (Object).setPrototypeOf(this, LdapError.prototype); + } +} + +export class IdentityError extends Error { + constructor(message?: string) { + super(message); + this.name = "IdentityError"; + (Object).setPrototypeOf(this, IdentityError.prototype); + } +} + +export class AccessDeniedError extends Error { + constructor(message?: string) { + super(message); + this.name = "AccessDeniedError"; + (Object).setPrototypeOf(this, AccessDeniedError.prototype); + } +} + +export class AuthenticationRegulationError extends Error { + constructor(message?: string) { + super(message); + this.name = "AuthenticationRegulationError"; + (Object).setPrototypeOf(this, AuthenticationRegulationError.prototype); + } +} + +export class InvalidTOTPError extends Error { + constructor(message?: string) { + super(message); + this.name = "InvalidTOTPError"; + (Object).setPrototypeOf(this, InvalidTOTPError.prototype); + } +} + +export class NotAuthenticatedError extends Error { + constructor(message?: string) { + super(message); + this.name = "NotAuthenticatedError"; + (Object).setPrototypeOf(this, NotAuthenticatedError.prototype); + } +} + +export class NotAuthorizedError extends Error { + constructor(message?: string) { + super(message); + this.name = "NotAuthanticatedError"; + (Object).setPrototypeOf(this, NotAuthorizedError.prototype); + } +} + +export class FirstFactorValidationError extends Error { + constructor(message?: string) { + super(message); + this.name = "FirstFactorValidationError"; + (Object).setPrototypeOf(this, FirstFactorValidationError.prototype); + } +} + +export class SecondFactorValidationError extends Error { + constructor(message?: string) { + super(message); + this.name = "SecondFactorValidationError"; + (Object).setPrototypeOf(this, FirstFactorValidationError.prototype); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/FirstFactorValidator.ts b/themes/squares/server/src/lib/FirstFactorValidator.ts new file mode 100644 index 000000000..231060002 --- /dev/null +++ b/themes/squares/server/src/lib/FirstFactorValidator.ts @@ -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 { + 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(); + }); +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/IdentityCheckMiddleware.spec.ts b/themes/squares/server/src/lib/IdentityCheckMiddleware.spec.ts new file mode 100644 index 000000000..842ed6bcb --- /dev/null +++ b/themes/squares/server/src/lib/IdentityCheckMiddleware.spec.ts @@ -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")); + }); + }); + }); +}); diff --git a/themes/squares/server/src/lib/IdentityCheckMiddleware.ts b/themes/squares/server/src/lib/IdentityCheckMiddleware.ts new file mode 100644 index 000000000..e72ea4db1 --- /dev/null +++ b/themes/squares/server/src/lib/IdentityCheckMiddleware.ts @@ -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 { + + 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 { + 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 { + 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 { + + let authSession: AuthenticationSession; + const identityToken = objectPath.get( + 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 { + 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)); + }; +} diff --git a/themes/squares/server/src/lib/IdentityCheckPreValidationTemplate.ts b/themes/squares/server/src/lib/IdentityCheckPreValidationTemplate.ts new file mode 100644 index 000000000..0161ce406 --- /dev/null +++ b/themes/squares/server/src/lib/IdentityCheckPreValidationTemplate.ts @@ -0,0 +1,3 @@ + + +export const PRE_VALIDATION_TEMPLATE = "need-identity-validation"; \ No newline at end of file diff --git a/themes/squares/server/src/lib/IdentityValidable.ts b/themes/squares/server/src/lib/IdentityValidable.ts new file mode 100644 index 000000000..075580c9e --- /dev/null +++ b/themes/squares/server/src/lib/IdentityValidable.ts @@ -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; + postValidationInit(req: Express.Request): Bluebird; + + // 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; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/IdentityValidableStub.spec.ts b/themes/squares/server/src/lib/IdentityValidableStub.spec.ts new file mode 100644 index 000000000..20a977140 --- /dev/null +++ b/themes/squares/server/src/lib/IdentityValidableStub.spec.ts @@ -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 { + return this.preValidationInitStub(req); + } + + postValidationInit(req: Express.Request): Bluebird { + 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(); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/Server.spec.ts b/themes/squares/server/src/lib/Server.spec.ts new file mode 100644 index 000000000..365163254 --- /dev/null +++ b/themes/squares/server/src/lib/Server.spec.ts @@ -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(); + }); + }); +}); diff --git a/themes/squares/server/src/lib/Server.ts b/themes/squares/server/src/lib/Server.ts new file mode 100644 index 000000000..4090f6294 --- /dev/null +++ b/themes/squares/server/src/lib/Server.ts @@ -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 { + 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((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 { + 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(); + } +} + diff --git a/themes/squares/server/src/lib/ServerVariables.ts b/themes/squares/server/src/lib/ServerVariables.ts new file mode 100644 index 000000000..cd3dd6dc8 --- /dev/null +++ b/themes/squares/server/src/lib/ServerVariables.ts @@ -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; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/ServerVariablesInitializer.ts b/themes/squares/server/src/lib/ServerVariablesInitializer.ts new file mode 100644 index 000000000..df79238cc --- /dev/null +++ b/themes/squares/server/src/lib/ServerVariablesInitializer.ts @@ -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 { + 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 { + + 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); + }); + } +} diff --git a/themes/squares/server/src/lib/ServerVariablesMockBuilder.spec.ts b/themes/squares/server/src/lib/ServerVariablesMockBuilder.spec.ts new file mode 100644 index 000000000..7874702a0 --- /dev/null +++ b/themes/squares/server/src/lib/ServerVariablesMockBuilder.spec.ts @@ -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 + }; + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/Level.ts b/themes/squares/server/src/lib/authentication/Level.ts new file mode 100644 index 000000000..57b6a2346 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/Level.ts @@ -0,0 +1,5 @@ +export enum Level { + NOT_AUTHENTICATED = 0, + ONE_FACTOR = 1, + TWO_FACTOR = 2 +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/GroupsAndEmails.ts b/themes/squares/server/src/lib/authentication/backends/GroupsAndEmails.ts new file mode 100644 index 000000000..3434ba66f --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/GroupsAndEmails.ts @@ -0,0 +1,5 @@ + +export interface GroupsAndEmails { + groups: string[]; + emails: string[]; +} diff --git a/themes/squares/server/src/lib/authentication/backends/IUsersDatabase.ts b/themes/squares/server/src/lib/authentication/backends/IUsersDatabase.ts new file mode 100644 index 000000000..d7fa13b7a --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/IUsersDatabase.ts @@ -0,0 +1,10 @@ +import Bluebird = require("bluebird"); + +import { GroupsAndEmails } from "./GroupsAndEmails"; + +export interface IUsersDatabase { + checkUserPassword(username: string, password: string): Bluebird; + getEmails(username: string): Bluebird; + getGroups(username: string): Bluebird; + updatePassword(username: string, newPassword: string): Bluebird; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts b/themes/squares/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts new file mode 100644 index 000000000..19341a5dd --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts @@ -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 { + return this.checkUserPasswordStub(username, password); + } + + getEmails(username: string): Bluebird { + return this.getEmailsStub(username); + } + + getGroups(username: string): Bluebird { + return this.getGroupsStub(username); + } + + updatePassword(username: string, newPassword: string): Bluebird { + return this.updatePasswordStub(username, newPassword); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts b/themes/squares/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts new file mode 100644 index 000000000..a258a78f7 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts @@ -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()); + }); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/file/FileUsersDatabase.ts b/themes/squares/server/src/lib/authentication/backends/file/FileUsersDatabase.ts new file mode 100644 index 000000000..d34dde21e --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/file/FileUsersDatabase.ts @@ -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 { + return new Bluebird((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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + return this.readDatabase() + .then((database) => { + return this.checkUserExists(database, username) + .then(() => this.retrieveEmails(database, username)); + }); + } + + getGroups(username: string): Bluebird { + return this.readDatabase() + .then((database) => { + return this.checkUserExists(database, username) + .then(() => this.retrieveGroups(database, username)); + }); + } + + updatePassword(username: string, newPassword: string): Bluebird { + return this.readDatabase() + .then((database) => { + return this.checkUserExists(database, username) + .then(() => this.replacePassword(database, username, newPassword)); + }); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/file/ReadWriteQueue.ts b/themes/squares/server/src/lib/authentication/backends/file/ReadWriteQueue.ts new file mode 100644 index 000000000..957ddaec5 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/file/ReadWriteQueue.ts @@ -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(); + } + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/ISession.ts b/themes/squares/server/src/lib/authentication/backends/ldap/ISession.ts new file mode 100644 index 000000000..da2c74433 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/ldap/ISession.ts @@ -0,0 +1,12 @@ + +import BluebirdPromise = require("bluebird"); + +export interface ISession { + open(): BluebirdPromise; + close(): BluebirdPromise; + + searchUserDn(username: string): BluebirdPromise; + searchEmails(username: string): BluebirdPromise; + searchGroups(username: string): BluebirdPromise; + modifyPassword(username: string, newPassword: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/ISessionFactory.ts b/themes/squares/server/src/lib/authentication/backends/ldap/ISessionFactory.ts new file mode 100644 index 000000000..014d1eea5 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/ldap/ISessionFactory.ts @@ -0,0 +1,6 @@ + +import { ISession } from "./ISession"; + +export interface ISessionFactory { + create(userDN: string, password: string): ISession; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts b/themes/squares/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts new file mode 100644 index 000000000..f4a6e6306 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts @@ -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); + }) + }); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts b/themes/squares/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts new file mode 100644 index 000000000..edda62ec6 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts @@ -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 = (session: ISession) => Bluebird; + +export class LdapUsersDatabase implements IUsersDatabase { + private sessionFactory: ISessionFactory; + private configuration: LdapConfiguration; + + constructor( + sessionFactory: ISessionFactory, + configuration: LdapConfiguration) { + this.sessionFactory = sessionFactory; + this.configuration = configuration; + } + + private withSession( + username: string, + password: string, + cb: SessionCallback): Bluebird { + const session = this.sessionFactory.create(username, password); + return session.open() + .then(() => cb(session)) + .finally(() => session.close()); + } + + checkUserPassword(username: string, password: string): Bluebird { + const that = this; + function verifyUserPassword(userDN: string) { + return that.withSession( + 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 { + 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 { + 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 { + 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)); + }); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts b/themes/squares/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts new file mode 100644 index 000000000..9dedfcb7c --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts @@ -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"); + }); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/SafeSession.ts b/themes/squares/server/src/lib/authentication/backends/ldap/SafeSession.ts new file mode 100644 index 000000000..572209066 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/ldap/SafeSession.ts @@ -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 { + return this.sesion.open(); + } + + close(): BluebirdPromise { + return this.sesion.close(); + } + + searchGroups(username: string): BluebirdPromise { + 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 { + 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 { + 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 { + try { + const sanitizedUsername = Sanitizer.sanitize(username); + return this.sesion.modifyPassword(sanitizedUsername, newPassword); + } + catch (e) { + return BluebirdPromise.reject(new Error(SPECIAL_CHAR_USED_MESSAGE)); + } + } +} diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts b/themes/squares/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts new file mode 100644 index 000000000..9dd33fed4 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts @@ -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 { 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); + }); +}); diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/Sanitizer.ts b/themes/squares/server/src/lib/authentication/backends/ldap/Sanitizer.ts new file mode 100644 index 000000000..be74132ad --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/ldap/Sanitizer.ts @@ -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; + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/Session.spec.ts b/themes/squares/server/src/lib/authentication/backends/ldap/Session.spec.ts new file mode 100644 index 000000000..d55f6a805 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/ldap/Session.spec.ts @@ -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"]); + }); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/Session.ts b/themes/squares/server/src/lib/authentication/backends/ldap/Session.ts new file mode 100644 index 000000000..e0284b3c4 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/ldap/Session.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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(); + }); + } +} diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/SessionFactory.ts b/themes/squares/server/src/lib/authentication/backends/ldap/SessionFactory.ts new file mode 100644 index 000000000..0b6c4bff3 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/ldap/SessionFactory.ts @@ -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 + ) + ); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts b/themes/squares/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts new file mode 100644 index 000000000..face3930d --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts @@ -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); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts b/themes/squares/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts new file mode 100644 index 000000000..5faf2ba18 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts @@ -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 { + return this.openStub(); + } + + close(): Bluebird { + return this.closeStub(); + } + + searchUserDn(username: string): Bluebird { + return this.searchUserDnStub(username); + } + + searchEmails(username: string): Bluebird { + return this.searchEmailsStub(username); + } + + searchGroups(username: string): Bluebird { + return this.searchGroupsStub(username); + } + + modifyPassword(username: string, newPassword: string): Bluebird { + return this.modifyPasswordStub(username, newPassword); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/connector/Connector.ts b/themes/squares/server/src/lib/authentication/backends/ldap/connector/Connector.ts new file mode 100644 index 000000000..2542ea7f9 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/ldap/connector/Connector.ts @@ -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; + unbindAsync(): Bluebird; + searchAsync(base: string, query: LdapJs.SearchOptions): Bluebird; + modifyAsync(userdn: string, change: LdapJs.Change): Bluebird; +} + +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 { + return this.client.bindAsync(username, password); + } + + unbindAsync(): Bluebird { + return this.client.unbindAsync(); + } + + searchAsync(base: string, query: any): Bluebird { + const that = this; + return this.client.searchAsync(base, query) + .then(function (res: EventEmitter) { + const doc: SearchEntry[] = []; + return new Bluebird((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 { + return this.client.modifyAsync(dn, changeRequest); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts b/themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts new file mode 100644 index 000000000..61fef07a4 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts @@ -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); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts b/themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts new file mode 100644 index 000000000..d11fa6386 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts @@ -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(); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts b/themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts new file mode 100644 index 000000000..0b78225bf --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts @@ -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 { + return this.bindAsyncStub(username, password); + } + + unbindAsync(): BluebirdPromise { + return this.unbindAsyncStub(); + } + + searchAsync(base: string, query: any): BluebirdPromise { + return this.searchAsyncStub(base, query); + } + + modifyAsync(dn: string, changeRequest: any): BluebirdPromise { + return this.modifyAsyncStub(dn, changeRequest); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/connector/IConnector.ts b/themes/squares/server/src/lib/authentication/backends/ldap/connector/IConnector.ts new file mode 100644 index 000000000..1e63ab193 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/ldap/connector/IConnector.ts @@ -0,0 +1,9 @@ +import Bluebird = require("bluebird"); +import EventEmitter = require("events"); + +export interface IConnector { + bindAsync(username: string, password: string): Bluebird; + unbindAsync(): Bluebird; + searchAsync(base: string, query: any): Bluebird; + modifyAsync(dn: string, changeRequest: any): Bluebird; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts b/themes/squares/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts new file mode 100644 index 000000000..f9ed65eff --- /dev/null +++ b/themes/squares/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts @@ -0,0 +1,5 @@ +import { IConnector } from "./IConnector"; + +export interface IConnectorFactory { + create(): IConnector; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/totp/ITotpHandler.ts b/themes/squares/server/src/lib/authentication/totp/ITotpHandler.ts new file mode 100644 index 000000000..d600d31e4 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/totp/ITotpHandler.ts @@ -0,0 +1,6 @@ +import { TOTPSecret } from "../../../../types/TOTPSecret"; + +export interface ITotpHandler { + generate(label: string, issuer: string): TOTPSecret; + validate(token: string, secret: string): boolean; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/totp/TotpHandler.spec.ts b/themes/squares/server/src/lib/authentication/totp/TotpHandler.spec.ts new file mode 100644 index 000000000..67cffa638 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/totp/TotpHandler.spec.ts @@ -0,0 +1,39 @@ + +import { TotpHandler } from "./TotpHandler"; +import Sinon = require("sinon"); +import Speakeasy = require("speakeasy"); +import Assert = require("assert"); + +describe("authentication/totp/TotpHandler", function() { + let totpValidator: TotpHandler; + let validateStub: Sinon.SinonStub; + + beforeEach(() => { + validateStub = Sinon.stub(Speakeasy.totp, "verify"); + totpValidator = new TotpHandler(Speakeasy); + }); + + afterEach(function() { + validateStub.restore(); + }); + + it("should validate the TOTP token", function() { + const totp_secret = "NBD2ZV64R9UV1O7K"; + const token = "token"; + validateStub.withArgs({ + secret: totp_secret, + token: token, + encoding: "base32", + window: 1 + }).returns(true); + Assert(totpValidator.validate(token, totp_secret)); + }); + + it("should not validate a wrong TOTP token", function() { + const totp_secret = "NBD2ZV64R9UV1O7K"; + const token = "wrong token"; + validateStub.returns(false); + Assert(!totpValidator.validate(token, totp_secret)); + }); +}); + diff --git a/themes/squares/server/src/lib/authentication/totp/TotpHandler.ts b/themes/squares/server/src/lib/authentication/totp/TotpHandler.ts new file mode 100644 index 000000000..dfab502a7 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/totp/TotpHandler.ts @@ -0,0 +1,36 @@ +import { ITotpHandler } from "./ITotpHandler"; +import { TOTPSecret } from "../../../../types/TOTPSecret"; +import Speakeasy = require("speakeasy"); + +const TOTP_ENCODING = "base32"; +const WINDOW: number = 1; + +export class TotpHandler implements ITotpHandler { + private speakeasy: typeof Speakeasy; + + constructor(speakeasy: typeof Speakeasy) { + this.speakeasy = speakeasy; + } + + generate(label: string, issuer: string): TOTPSecret { + const secret = this.speakeasy.generateSecret({ + otpauth_url: false + }) as TOTPSecret; + + secret.otpauth_url = this.speakeasy.otpauthURL({ + secret: secret.ascii, + label: label, + issuer: issuer + }); + return secret; + } + + validate(token: string, secret: string): boolean { + return this.speakeasy.totp.verify({ + secret: secret, + encoding: TOTP_ENCODING, + token: token, + window: WINDOW + }); + } +} diff --git a/themes/squares/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts b/themes/squares/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts new file mode 100644 index 000000000..ea93330d9 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts @@ -0,0 +1,22 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import { ITotpHandler } from "./ITotpHandler"; +import { TOTPSecret } from "../../../../types/TOTPSecret"; + +export class TotpHandlerStub implements ITotpHandler { + generateStub: Sinon.SinonStub; + validateStub: Sinon.SinonStub; + + constructor() { + this.generateStub = Sinon.stub(); + this.validateStub = Sinon.stub(); + } + + generate(label: string, issuer: string): TOTPSecret { + return this.generateStub(label, issuer); + } + + validate(token: string, secret: string): boolean { + return this.validateStub(token, secret); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/u2f/IU2fHandler.ts b/themes/squares/server/src/lib/authentication/u2f/IU2fHandler.ts new file mode 100644 index 000000000..b9b7d6f26 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/u2f/IU2fHandler.ts @@ -0,0 +1,9 @@ +import U2f = require("u2f"); + +export interface IU2fHandler { + request(appId: string, keyHandle?: string): U2f.Request; + checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) + : U2f.RegistrationResult | U2f.Error; + checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) + : U2f.SignatureResult | U2f.Error; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/u2f/U2fHandler.ts b/themes/squares/server/src/lib/authentication/u2f/U2fHandler.ts new file mode 100644 index 000000000..bf3891e5b --- /dev/null +++ b/themes/squares/server/src/lib/authentication/u2f/U2fHandler.ts @@ -0,0 +1,24 @@ +import { IU2fHandler } from "./IU2fHandler"; +import U2f = require("u2f"); + +export class U2fHandler implements IU2fHandler { + private u2f: typeof U2f; + + constructor(u2f: typeof U2f) { + this.u2f = u2f; + } + + request(appId: string, keyHandle?: string): U2f.Request { + return this.u2f.request(appId, keyHandle); + } + + checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) + : U2f.RegistrationResult | U2f.Error { + return this.u2f.checkRegistration(registrationRequest, registrationResponse); + } + + checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) + : U2f.SignatureResult | U2f.Error { + return this.u2f.checkSignature(signatureRequest, signatureResponse, publicKey); + } +} diff --git a/themes/squares/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts b/themes/squares/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts new file mode 100644 index 000000000..135d7eb06 --- /dev/null +++ b/themes/squares/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts @@ -0,0 +1,31 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import U2f = require("u2f"); +import { IU2fHandler } from "./IU2fHandler"; + + +export class U2fHandlerStub implements IU2fHandler { + requestStub: Sinon.SinonStub; + checkRegistrationStub: Sinon.SinonStub; + checkSignatureStub: Sinon.SinonStub; + + constructor() { + this.requestStub = Sinon.stub(); + this.checkRegistrationStub = Sinon.stub(); + this.checkSignatureStub = Sinon.stub(); + } + + request(appId: string, keyHandle?: string): U2f.Request { + return this.requestStub(appId, keyHandle); + } + + checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) + : U2f.RegistrationResult | U2f.Error { + return this.checkRegistrationStub(registrationRequest, registrationResponse); + } + + checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) + : U2f.SignatureResult | U2f.Error { + return this.checkSignatureStub(signatureRequest, signatureResponse, publicKey); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authorization/Authorizer.spec.ts b/themes/squares/server/src/lib/authorization/Authorizer.spec.ts new file mode 100644 index 000000000..586814042 --- /dev/null +++ b/themes/squares/server/src/lib/authorization/Authorizer.spec.ts @@ -0,0 +1,372 @@ + +import Assert = require("assert"); +import winston = require("winston"); +import { Authorizer } from "./Authorizer"; +import { ACLConfiguration, ACLRule } from "../configuration/schema/AclConfiguration"; +import { Level } from "./Level"; + +describe("authorization/Authorizer", function () { + let authorizer: Authorizer; + let configuration: ACLConfiguration; + + describe("configuration is null", function() { + it("should allow access to anything, anywhere for anybody", function() { + configuration = undefined; + authorizer = new Authorizer(configuration, winston); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1", "group2"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "admin.example.com", resource: "/"}, {user: "user3", groups: ["group3"]}), Level.BYPASS); + }); + }); + + describe("configuration is not null", function () { + beforeEach(function () { + configuration = { + default_policy: "deny", + rules: [] + }; + authorizer = new Authorizer(configuration, winston); + }); + + describe("check access control with default policy to deny", function () { + beforeEach(function () { + configuration.default_policy = "deny"; + }); + + it("should deny access when no rule is provided", function () { + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should control access when multiple domain matcher is provided", function () { + configuration.rules = [{ + domain: "*.mail.example.com", + policy: "two_factor", + subject: "user:user1", + resources: [".*"] + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should allow access to all resources when resources is not provided", function () { + configuration.rules = [{ + domain: "*.mail.example.com", + policy: "two_factor", + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + describe("check user rules", function () { + it("should allow access when user has a matching allowing rule", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: [".*"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should deny to other users", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: [".*"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user2", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); + }); + + it("should allow user access only to specific resources", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: ["/private/.*", "^/begin", "/end$"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/middle/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/begin"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/not/begin"}, {user: "user1", groups: ["group1"]}), Level.DENY); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end/x"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should allow access to multiple domains", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: [".*"], + subject: "user:user1" + }, { + domain: "home1.example.com", + policy: "one_factor", + resources: [".*"], + subject: "user:user1" + }, { + domain: "home2.example.com", + policy: "deny", + resources: [".*"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home1.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); + Assert.equal(authorizer.authorization({domain: "home2.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home3.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should apply rules in order", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "one_factor", + resources: ["/my/private/resource"], + subject: "user:user1" + }, { + domain: "home.example.com", + policy: "deny", + resources: ["^/my/private/.*"], + subject: "user:user1" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/my/.*"], + subject: "user:user1" + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/poney"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/duck"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/resource"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); + }); + }); + + describe("check group rules", function () { + it("should allow access when user is in group having a matching allowing rule", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: ["^/$"], + subject: "group:group1" + }, { + domain: "home.example.com", + policy: "one_factor", + resources: ["^/test$"], + subject: "group:group2" + }, { + domain: "home.example.com", + policy: "deny", + resources: ["^/private$"], + subject: "group:group2" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.ONE_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); + }); + }); + }); + + describe("check any rules", function () { + it("should control access when any rules are defined", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "bypass", + resources: ["^/public$"] + }, { + domain: "home.example.com", + policy: "deny", + resources: ["^/private$"] + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, + {user: "user4", groups: ["group5"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, + {user: "user4", groups: ["group5"]}), Level.DENY); + }); + }); + + describe("check access control with default policy to allow", function () { + beforeEach(function () { + configuration.default_policy = "bypass"; + }); + + it("should allow access to anything when no rule is provided", function () { + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + }); + + it("should deny access to one resource when defined", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["/test"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + }); + }); + + describe("check access control with complete use case", function () { + beforeEach(function () { + configuration.default_policy = "deny"; + }); + + it("should control access of multiple user (real use case)", function () { + // Let say we have three users: admin, john, harry. + // admin is in groups ["admins"] + // john is in groups ["dev", "admin-private"] + // harry is in groups ["dev"] + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: ["^/public$", "^/$"] + }, { + domain: "home.example.com", + policy: "two_factor", + resources: [".*"], + subject: "group:admins" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/private/?.*"], + subject: "group:admin-private" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/private/john$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/private/harry"], + subject: "user:harry" + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); + }); + + it("should allow when allowed at group level and denied at user level", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"], + subject: "group:dev" + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); + }); + + it("should allow access when allowed at 'any' level and denied at user level", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"] + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); + }); + + it("should allow access when allowed at 'any' level and denied at group level", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "group:dev" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"] + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); + }); + + it("should respect rules precedence", function () { + // the priority from least to most is 'default_policy', 'all', 'group', 'user' + // and the first rules in each category as a lower priority than the latest. + // You can think of it that way: they override themselves inside each category. + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "group:dev" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"] + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + }); + }); + }); +}); diff --git a/themes/squares/server/src/lib/authorization/Authorizer.ts b/themes/squares/server/src/lib/authorization/Authorizer.ts new file mode 100644 index 000000000..889b7ec20 --- /dev/null +++ b/themes/squares/server/src/lib/authorization/Authorizer.ts @@ -0,0 +1,85 @@ + +import { ACLConfiguration, ACLPolicy, ACLRule } from "../configuration/schema/AclConfiguration"; +import { IAuthorizer } from "./IAuthorizer"; +import { Winston } from "../../../types/Dependencies"; +import { MultipleDomainMatcher } from "./MultipleDomainMatcher"; +import { Level } from "./Level"; +import { Object } from "./Object"; +import { Subject } from "./Subject"; + +function MatchDomain(actualDomain: string) { + return function (rule: ACLRule): boolean { + return MultipleDomainMatcher.match(actualDomain, rule.domain); + }; +} + +function MatchResource(actualResource: string) { + return function (rule: ACLRule): boolean { + // If resources key is not provided, the rule applies to all resources. + if (!rule.resources) return true; + + for (let i = 0; i < rule.resources.length; ++i) { + const regexp = new RegExp(rule.resources[i]); + if (regexp.test(actualResource)) return true; + } + return false; + }; +} + +function MatchSubject(subject: Subject) { + return (rule: ACLRule) => { + // If no subject, matches anybody + if (!rule.subject) return true; + + if (rule.subject.startsWith("user:")) { + const ruleUser = rule.subject.split(":")[1]; + if (subject.user == ruleUser) return true; + } + + if (rule.subject.startsWith("group:")) { + const ruleGroup = rule.subject.split(":")[1]; + if (subject.groups.indexOf(ruleGroup) > -1) return true; + } + return false; + }; +} + +export class Authorizer implements IAuthorizer { + private logger: Winston; + private readonly configuration: ACLConfiguration; + + constructor(configuration: ACLConfiguration, logger_: Winston) { + this.logger = logger_; + this.configuration = configuration; + } + + private getMatchingRules(object: Object, subject: Subject): ACLRule[] { + const rules = this.configuration.rules; + if (!rules) return []; + return rules + .filter(MatchDomain(object.domain)) + .filter(MatchResource(object.resource)) + .filter(MatchSubject(subject)); + } + + private ruleToLevel(policy: string): Level { + if (policy == "bypass") { + return Level.BYPASS; + } else if (policy == "one_factor") { + return Level.ONE_FACTOR; + } else if (policy == "two_factor") { + return Level.TWO_FACTOR; + } + return Level.DENY; + } + + authorization(object: Object, subject: Subject): Level { + if (!this.configuration) return Level.BYPASS; + + const rules = this.getMatchingRules(object, subject); + + return (rules.length > 0) + ? this.ruleToLevel(rules[0].policy) // extract the policy of the first matching rule + : this.ruleToLevel(this.configuration.default_policy); // otherwise use the default policy + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authorization/AuthorizerStub.spec.ts b/themes/squares/server/src/lib/authorization/AuthorizerStub.spec.ts new file mode 100644 index 000000000..9bd6f4a85 --- /dev/null +++ b/themes/squares/server/src/lib/authorization/AuthorizerStub.spec.ts @@ -0,0 +1,17 @@ +import Sinon = require("sinon"); +import { IAuthorizer } from "./IAuthorizer"; +import { Level } from "./Level"; +import { Object } from "./Object"; +import { Subject } from "./Subject"; + +export class AuthorizerStub implements IAuthorizer { + authorizationMock: Sinon.SinonStub; + + constructor() { + this.authorizationMock = Sinon.stub(); + } + + authorization(object: Object, subject: Subject): Level { + return this.authorizationMock(object, subject); + } +} diff --git a/themes/squares/server/src/lib/authorization/IAuthorizer.ts b/themes/squares/server/src/lib/authorization/IAuthorizer.ts new file mode 100644 index 000000000..fe7ba367a --- /dev/null +++ b/themes/squares/server/src/lib/authorization/IAuthorizer.ts @@ -0,0 +1,7 @@ +import { Level } from "./Level"; +import { Subject } from "./Subject"; +import { Object } from "./Object"; + +export interface IAuthorizer { + authorization(object: Object, subject: Subject): Level; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authorization/Level.ts b/themes/squares/server/src/lib/authorization/Level.ts new file mode 100644 index 000000000..d12802610 --- /dev/null +++ b/themes/squares/server/src/lib/authorization/Level.ts @@ -0,0 +1,6 @@ +export enum Level { + BYPASS = 0, + ONE_FACTOR = 1, + TWO_FACTOR = 2, + DENY = 3 +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authorization/MultipleDomainMatcher.ts b/themes/squares/server/src/lib/authorization/MultipleDomainMatcher.ts new file mode 100644 index 000000000..64c647a4a --- /dev/null +++ b/themes/squares/server/src/lib/authorization/MultipleDomainMatcher.ts @@ -0,0 +1,12 @@ + +export class MultipleDomainMatcher { + static match(domain: string, pattern: string): boolean { + if (pattern.startsWith("*") && + domain.endsWith(pattern.substr(1))) { + return true; + } + else if (domain == pattern) { + return true; + } + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authorization/Object.ts b/themes/squares/server/src/lib/authorization/Object.ts new file mode 100644 index 000000000..5411b0d24 --- /dev/null +++ b/themes/squares/server/src/lib/authorization/Object.ts @@ -0,0 +1,5 @@ + +export interface Object { + domain: string; + resource: string; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authorization/Subject.ts b/themes/squares/server/src/lib/authorization/Subject.ts new file mode 100644 index 000000000..310d6b4c0 --- /dev/null +++ b/themes/squares/server/src/lib/authorization/Subject.ts @@ -0,0 +1,5 @@ + +export interface Subject { + user: string; + groups: string[]; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/ConfigurationParser.spec.ts b/themes/squares/server/src/lib/configuration/ConfigurationParser.spec.ts new file mode 100644 index 000000000..60c0f6182 --- /dev/null +++ b/themes/squares/server/src/lib/configuration/ConfigurationParser.spec.ts @@ -0,0 +1,171 @@ +import * as Assert from "assert"; +import { Configuration } from "./schema/Configuration"; +import { ACLConfiguration } from "./schema/AclConfiguration"; +import { ConfigurationParser } from "./ConfigurationParser"; + +describe("configuration/ConfigurationParser", function () { + function buildYamlConfig(): Configuration { + const yaml_config: Configuration = { + port: 8080, + authentication_backend: { + ldap: { + url: "http://ldap", + base_dn: "dc=example,dc=com", + additional_users_dn: "ou=users", + additional_groups_dn: "ou=groups", + user: "user", + password: "pass" + }, + }, + session: { + domain: "example.com", + secret: "secret", + expiration: 40000 + }, + storage: { + local: { + path: "/mydirectory" + } + }, + regulation: { + max_retries: 3, + find_time: 5 * 60, + ban_time: 5 * 60 + }, + logs_level: "debug", + notifier: { + email: { + username: "user", + password: "password", + sender: "admin@example.com", + service: "gmail" + } + } + }; + return yaml_config; + } + + describe("port", function () { + it("should read the port from the yaml file", function () { + const yaml_config = buildYamlConfig(); + yaml_config.port = 7070; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.port, 7070); + }); + + it("should default the port to 8080 if not provided", function () { + const yaml_config = buildYamlConfig(); + delete yaml_config.port; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.port, 8080); + }); + }); + + describe("test session configuration", function() { + it("should get the session attributes", function () { + const yaml_config = buildYamlConfig(); + yaml_config.session = { + domain: "example.com", + secret: "secret", + expiration: 3600, + inactivity: 4000 + }; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.session.domain, "example.com"); + Assert.equal(config.session.secret, "secret"); + Assert.equal(config.session.expiration, 3600); + Assert.equal(config.session.inactivity, 4000); + }); + + it("should be ok not specifying inactivity", function () { + const yaml_config = buildYamlConfig(); + yaml_config.session = { + domain: "example.com", + secret: "secret", + expiration: 3600 + }; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.session.domain, "example.com"); + Assert.equal(config.session.secret, "secret"); + Assert.equal(config.session.expiration, 3600); + Assert.equal(config.session.inactivity, undefined); + }); + }); + + it("should get the log level", function () { + const yaml_config = buildYamlConfig(); + yaml_config.logs_level = "debug"; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.logs_level, "debug"); + }); + + it("should get the notifier config", function () { + const userConfig = buildYamlConfig(); + userConfig.notifier = { + email: { + username: "user", + password: "pass", + sender: "admin@example.com", + service: "gmail" + } + }; + const config = ConfigurationParser.parse(userConfig); + Assert.deepEqual(config.notifier, { + email: { + username: "user", + password: "pass", + sender: "admin@example.com", + service: "gmail" + } + }); + }); + + describe("access_control", function() { + it("should adapt access_control when it is already ok", function () { + const userConfig = buildYamlConfig(); + userConfig.access_control = { + default_policy: "deny", + rules: [{ + domain: "www.example.com", + policy: "two_factor", + subject: "user:user" + }, { + domain: "public.example.com", + policy: "two_factor" + }] + }; + const config = ConfigurationParser.parse(userConfig); + Assert.deepEqual(config.access_control, { + default_policy: "deny", + rules: [{ + domain: "www.example.com", + policy: "two_factor", + subject: "user:user" + }, { + domain: "public.example.com", + policy: "two_factor" + }] + } as ACLConfiguration); + }); + + + it("should adapt access_control when it is empty", function () { + const userConfig = buildYamlConfig(); + userConfig.access_control = {} as any; + const config = ConfigurationParser.parse(userConfig); + Assert.deepEqual(config.access_control, { + default_policy: "bypass", + rules: [] + }); + }); + }); + + describe("default_redirection_url", function() { + it("should parse default_redirection_url", function() { + const userConfig = buildYamlConfig(); + userConfig.default_redirection_url = "dummy_url"; + const config = ConfigurationParser.parse(userConfig); + Assert.deepEqual(config.default_redirection_url, "dummy_url"); + }); + }); +}); diff --git a/themes/squares/server/src/lib/configuration/ConfigurationParser.ts b/themes/squares/server/src/lib/configuration/ConfigurationParser.ts new file mode 100644 index 000000000..d92d163c4 --- /dev/null +++ b/themes/squares/server/src/lib/configuration/ConfigurationParser.ts @@ -0,0 +1,39 @@ + +import * as ObjectPath from "object-path"; +import { Configuration, complete } from "./schema/Configuration"; +import Ajv = require("ajv"); +import Path = require("path"); +import Util = require("util"); + +export class ConfigurationParser { + private static parseTypes(configuration: Configuration): string[] { + const schema = require(Path.resolve(__dirname, "./Configuration.schema.json")); + const ajv = new Ajv({ + allErrors: true, + missingRefs: "fail" + }); + ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json")); + const valid = ajv.validate(schema, configuration); + if (!valid) + return ajv.errors.map( + (e: Ajv.ErrorObject) => { return ajv.errorsText([e]); }); + return []; + } + + static parse(configuration: Configuration): Configuration { + const validationErrors = this.parseTypes(configuration); + if (validationErrors.length > 0) { + validationErrors.forEach((e: string) => { console.log(e); }); + throw new Error("Malformed configuration (schema). Please double-check your configuration file."); + } + + const [newConfiguration, completionErrors] = complete(configuration); + + if (completionErrors.length > 0) { + completionErrors.forEach((e: string) => { console.log(e); }); + throw new Error("Malformed configuration (validator). Please double-check your configuration file."); + } + return newConfiguration; + } +} + diff --git a/themes/squares/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts b/themes/squares/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts new file mode 100644 index 000000000..d4a3093ee --- /dev/null +++ b/themes/squares/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts @@ -0,0 +1,149 @@ +import { SessionConfigurationBuilder } from "./SessionConfigurationBuilder"; +import { Configuration } from "./schema/Configuration"; +import { GlobalDependencies } from "../../../types/Dependencies"; + +import ExpressSession = require("express-session"); +import ConnectRedis = require("connect-redis"); +import Sinon = require("sinon"); +import Assert = require("assert"); + +describe("configuration/SessionConfigurationBuilder", function () { + const configuration: Configuration = { + access_control: { + default_policy: "deny", + rules: [] + }, + totp: { + issuer: "authelia.com" + }, + authentication_backend: { + ldap: { + url: "ldap://ldap", + user: "user", + base_dn: "dc=example,dc=com", + password: "password", + additional_groups_dn: "ou=groups", + additional_users_dn: "ou=users", + group_name_attribute: "", + groups_filter: "", + mail_attribute: "", + users_filter: "" + }, + }, + logs_level: "debug", + notifier: { + filesystem: { + filename: "/test" + } + }, + port: 8080, + session: { + name: "authelia_session", + domain: "example.com", + expiration: 3600, + secret: "secret" + }, + regulation: { + max_retries: 3, + ban_time: 5 * 60, + find_time: 5 * 60 + }, + storage: { + local: { + in_memory: true + } + } + }; + + const deps: GlobalDependencies = { + ConnectRedis: Sinon.spy() as any, + ldapjs: Sinon.spy() as any, + nedb: Sinon.spy() as any, + session: Sinon.spy() as any, + speakeasy: Sinon.spy() as any, + u2f: Sinon.spy() as any, + winston: Sinon.spy() as any, + Redis: Sinon.spy() as any + }; + + it("should return session options without redis options", function () { + const options = SessionConfigurationBuilder.build(configuration, deps); + const expectedOptions = { + name: "authelia_session", + secret: "secret", + resave: false, + saveUninitialized: true, + cookie: { + secure: true, + httpOnly: true, + maxAge: 3600, + domain: "example.com" + } + }; + + Assert.deepEqual(expectedOptions, options); + }); + + it("should return session options with redis options", function () { + configuration.session["redis"] = { + host: "redis.example.com", + port: 6379 + }; + const RedisStoreMock = Sinon.spy(); + const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); + + deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; + deps.Redis = { + createClient: Sinon.mock().returns(redisClient) + } as any; + + const options = SessionConfigurationBuilder.build(configuration, deps); + + const expectedOptions: ExpressSession.SessionOptions = { + secret: "secret", + resave: false, + saveUninitialized: true, + name: "authelia_session", + cookie: { + secure: true, + httpOnly: true, + maxAge: 3600, + domain: "example.com" + }, + store: Sinon.match.object as any + }; + + Assert((deps.ConnectRedis as Sinon.SinonStub).calledWith(deps.session)); + Assert.equal(options.secret, expectedOptions.secret); + Assert.equal(options.resave, expectedOptions.resave); + Assert.equal(options.saveUninitialized, expectedOptions.saveUninitialized); + Assert.deepEqual(options.cookie, expectedOptions.cookie); + Assert(options.store != undefined); + }); + + it("should return session options with redis password", function () { + configuration.session["redis"] = { + host: "redis.example.com", + port: 6379, + password: "authelia_pass" + }; + const RedisStoreMock = Sinon.spy(); + const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); + const createClientStub = Sinon.stub(); + + deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; + deps.Redis = { + createClient: createClientStub + } as any; + + createClientStub.returns(redisClient); + + const options = SessionConfigurationBuilder.build(configuration, deps); + + Assert(createClientStub.calledWith({ + host: "redis.example.com", + port: 6379, + password: "authelia_pass" + })); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/SessionConfigurationBuilder.ts b/themes/squares/server/src/lib/configuration/SessionConfigurationBuilder.ts new file mode 100644 index 000000000..6ce643d9d --- /dev/null +++ b/themes/squares/server/src/lib/configuration/SessionConfigurationBuilder.ts @@ -0,0 +1,52 @@ +import ExpressSession = require("express-session"); +import Redis = require("redis"); + +import { Configuration } from "./schema/Configuration"; +import { GlobalDependencies } from "../../../types/Dependencies"; +import { RedisStoreOptions } from "connect-redis"; + +export class SessionConfigurationBuilder { + + static build(configuration: Configuration, deps: GlobalDependencies): ExpressSession.SessionOptions { + const sessionOptions: ExpressSession.SessionOptions = { + name: configuration.session.name, + secret: configuration.session.secret, + resave: false, + saveUninitialized: true, + cookie: { + secure: true, + httpOnly: true, + maxAge: configuration.session.expiration, + domain: configuration.session.domain + }, + }; + + if (configuration.session.redis) { + let redisOptions; + const options: Redis.ClientOpts = { + host: configuration.session.redis.host, + port: configuration.session.redis.port + }; + + if (configuration.session.redis.password) { + options["password"] = configuration.session.redis.password; + } + const client = deps.Redis.createClient(options); + + client.on("error", function (err: Error) { + console.error("Redis error:", err); + }); + + redisOptions = { + client: client, + logErrors: true + }; + + if (redisOptions) { + const RedisStore = deps.ConnectRedis(deps.session); + sessionOptions.store = new RedisStore(redisOptions); + } + } + return sessionOptions; + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/AclConfiguration.spec.ts b/themes/squares/server/src/lib/configuration/schema/AclConfiguration.spec.ts new file mode 100644 index 000000000..d1e2a03a2 --- /dev/null +++ b/themes/squares/server/src/lib/configuration/schema/AclConfiguration.spec.ts @@ -0,0 +1,34 @@ +import { ACLConfiguration, complete } from "./AclConfiguration"; +import Assert = require("assert"); + +describe("configuration/schema/AclConfiguration", function() { + it("should complete ACLConfiguration", function() { + const configuration: ACLConfiguration = {}; + const [newConfiguration, errors] = complete(configuration); + + Assert.deepEqual(newConfiguration.default_policy, "bypass"); + Assert.deepEqual(newConfiguration.rules, []); + }); + + it("should return errors when subject is not good", function() { + const configuration: ACLConfiguration = { + default_policy: "deny", + rules: [{ + domain: "dev.example.com", + subject: "user:abc", + policy: "bypass" + }, { + domain: "dev.example.com", + subject: "user:def", + policy: "bypass" + }, { + domain: "dev.example.com", + subject: "badkey:abc", + policy: "bypass" + }] + }; + const [newConfiguration, errors] = complete(configuration); + + Assert.deepEqual(errors, ["Rule 2 has wrong subject. It should be starting with user: or group:."]); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/AclConfiguration.ts b/themes/squares/server/src/lib/configuration/schema/AclConfiguration.ts new file mode 100644 index 000000000..40401dd64 --- /dev/null +++ b/themes/squares/server/src/lib/configuration/schema/AclConfiguration.ts @@ -0,0 +1,41 @@ + +export type ACLPolicy = "deny" | "bypass" | "one_factor" | "two_factor"; + +export type ACLRule = { + domain: string; + resources?: string[]; + subject?: string; + policy: ACLPolicy; +}; + +export interface ACLConfiguration { + default_policy?: ACLPolicy; + rules?: ACLRule[]; +} + +export function complete(configuration: ACLConfiguration): [ACLConfiguration, string[]] { + const newConfiguration: ACLConfiguration = (configuration) + ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.default_policy) { + newConfiguration.default_policy = "bypass"; + } + + if (!newConfiguration.rules) { + newConfiguration.rules = []; + } + + if (newConfiguration.rules.length > 0) { + const errors: string[] = []; + newConfiguration.rules.forEach((r, idx) => { + if (r.subject && !r.subject.match(/^(user|group):[a-zA-Z0-9]+$/)) { + errors.push(`Rule ${idx} has wrong subject. It should be starting with user: or group:.`); + } + }); + if (errors.length > 0) { + return [newConfiguration, errors]; + } + } + + return [newConfiguration, []]; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts b/themes/squares/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts new file mode 100644 index 000000000..3ca86381c --- /dev/null +++ b/themes/squares/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts @@ -0,0 +1,11 @@ +import { AuthenticationBackendConfiguration, complete } from "./AuthenticationBackendConfiguration"; +import Assert = require("assert"); + +describe("configuration/schema/AuthenticationBackendConfiguration", function() { + it("should ensure there is at least one key", function() { + const configuration: AuthenticationBackendConfiguration = {} as any; + const [newConfiguration, error] = complete(configuration); + + Assert.equal(error, "Authentication backend must have one of the following keys:`ldap` or `file`"); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts b/themes/squares/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts new file mode 100644 index 000000000..7f77f894f --- /dev/null +++ b/themes/squares/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts @@ -0,0 +1,25 @@ +import { LdapConfiguration } from "./LdapConfiguration"; +import { FileUsersDatabaseConfiguration } from "./FileUsersDatabaseConfiguration"; + +export interface AuthenticationBackendConfiguration { + ldap?: LdapConfiguration; + file?: FileUsersDatabaseConfiguration; +} + +export function complete( + configuration: AuthenticationBackendConfiguration) + : [AuthenticationBackendConfiguration, string] { + + const newConfiguration: AuthenticationBackendConfiguration = (configuration) + ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (Object.keys(newConfiguration).length != 1) { + return [ + newConfiguration, + "Authentication backend must have one of the following keys:" + + "`ldap` or `file`" + ]; + } + + return [newConfiguration, undefined]; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/Configuration.ts b/themes/squares/server/src/lib/configuration/schema/Configuration.ts new file mode 100644 index 000000000..8d16a5fb2 --- /dev/null +++ b/themes/squares/server/src/lib/configuration/schema/Configuration.ts @@ -0,0 +1,68 @@ +import { ACLConfiguration, complete as AclConfigurationComplete } from "./AclConfiguration"; +import { AuthenticationBackendConfiguration, complete as AuthenticationBackendComplete } from "./AuthenticationBackendConfiguration"; +import { NotifierConfiguration, complete as NotifierConfigurationComplete } from "./NotifierConfiguration"; +import { RegulationConfiguration, complete as RegulationConfigurationComplete } from "./RegulationConfiguration"; +import { SessionConfiguration, complete as SessionConfigurationComplete } from "./SessionConfiguration"; +import { StorageConfiguration, complete as StorageConfigurationComplete } from "./StorageConfiguration"; +import { TotpConfiguration, complete as TotpConfigurationComplete } from "./TotpConfiguration"; + +export interface Configuration { + access_control?: ACLConfiguration; + authentication_backend: AuthenticationBackendConfiguration; + default_redirection_url?: string; + logs_level?: string; + notifier?: NotifierConfiguration; + port?: number; + regulation?: RegulationConfiguration; + session?: SessionConfiguration; + storage?: StorageConfiguration; + totp?: TotpConfiguration; +} + +export function complete( + configuration: Configuration): + [Configuration, string[]] { + + const newConfiguration: Configuration = JSON.parse( + JSON.stringify(configuration)); + const errors: string[] = []; + + const [acls, aclsErrors] = AclConfigurationComplete( + newConfiguration.access_control); + + newConfiguration.access_control = acls; + if (aclsErrors.length > 0) { + errors.concat(aclsErrors); + } + + const [backend, error] = + AuthenticationBackendComplete( + newConfiguration.authentication_backend); + + if (error) errors.push(error); + newConfiguration.authentication_backend = backend; + + if (!newConfiguration.logs_level) { + newConfiguration.logs_level = "info"; + } + + const [notifier, notifierError] = NotifierConfigurationComplete( + newConfiguration.notifier); + newConfiguration.notifier = notifier; + if (notifierError) errors.push(notifierError); + + if (!newConfiguration.port) { + newConfiguration.port = 8080; + } + + newConfiguration.regulation = RegulationConfigurationComplete( + newConfiguration.regulation); + newConfiguration.session = SessionConfigurationComplete( + newConfiguration.session); + newConfiguration.storage = StorageConfigurationComplete( + newConfiguration.storage); + newConfiguration.totp = TotpConfigurationComplete( + newConfiguration.totp); + + return [newConfiguration, errors]; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts b/themes/squares/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts new file mode 100644 index 000000000..d19002ba9 --- /dev/null +++ b/themes/squares/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts @@ -0,0 +1,4 @@ + +export interface FileUsersDatabaseConfiguration { + path: string; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/LdapConfiguration.spec.ts b/themes/squares/server/src/lib/configuration/schema/LdapConfiguration.spec.ts new file mode 100644 index 000000000..cc73d1085 --- /dev/null +++ b/themes/squares/server/src/lib/configuration/schema/LdapConfiguration.spec.ts @@ -0,0 +1,25 @@ +import Assert = require("assert"); +import { LdapConfiguration, complete } from "./LdapConfiguration"; + +describe("configuration/schema/AuthenticationMethodsConfiguration", function() { + it("should ensure at least one key is provided", function() { + const configuration: LdapConfiguration = { + url: "ldap.example.com", + base_dn: "dc=example,dc=com", + user: "admin", + password: "password" + }; + const newConfiguration = complete(configuration); + + Assert.deepEqual(newConfiguration, { + url: "ldap.example.com", + base_dn: "dc=example,dc=com", + user: "admin", + password: "password", + users_filter: "cn={0}", + group_name_attribute: "cn", + groups_filter: "member={dn}", + mail_attribute: "mail" + }); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/LdapConfiguration.ts b/themes/squares/server/src/lib/configuration/schema/LdapConfiguration.ts new file mode 100644 index 000000000..5dacb9390 --- /dev/null +++ b/themes/squares/server/src/lib/configuration/schema/LdapConfiguration.ts @@ -0,0 +1,40 @@ +import Util = require("util"); + +export interface LdapConfiguration { + url: string; + base_dn: string; + + additional_users_dn?: string; + users_filter?: string; + + additional_groups_dn?: string; + groups_filter?: string; + + group_name_attribute?: string; + mail_attribute?: string; + + user: string; // admin username + password: string; // admin password +} + +export function complete(configuration: LdapConfiguration): LdapConfiguration { + const newConfiguration: LdapConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.users_filter) { + newConfiguration.users_filter = "cn={0}"; + } + + if (!newConfiguration.groups_filter) { + newConfiguration.groups_filter = "member={dn}"; + } + + if (!newConfiguration.group_name_attribute) { + newConfiguration.group_name_attribute = "cn"; + } + + if (!newConfiguration.mail_attribute) { + newConfiguration.mail_attribute = "mail"; + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts b/themes/squares/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts new file mode 100644 index 000000000..6c576e8e0 --- /dev/null +++ b/themes/squares/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts @@ -0,0 +1,40 @@ +import Assert = require("assert"); +import { NotifierConfiguration, complete } from "./NotifierConfiguration"; + +describe("configuration/schema/NotifierConfiguration", function() { + it("should use a default notifier when none is provided", function() { + const configuration: NotifierConfiguration = {}; + const [newConfiguration, error] = complete(configuration); + + Assert.deepEqual(newConfiguration.filesystem, {filename: "/tmp/authelia/notification.txt"}); + }); + + it("should ensure correct key is provided", function() { + const configuration = { + abc: "badvalue" + }; + const [newConfiguration, error] = complete(configuration as any); + + Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"); + }); + + it("should ensure there is no more than one key", function() { + const configuration: NotifierConfiguration = { + smtp: { + host: "smtp.example.com", + port: 25, + secure: false, + sender: "test@example.com" + }, + email: { + username: "test", + password: "test", + sender: "test@example.com", + service: "gmail" + } + }; + const [newConfiguration, error] = complete(configuration); + + Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/NotifierConfiguration.ts b/themes/squares/server/src/lib/configuration/schema/NotifierConfiguration.ts new file mode 100644 index 000000000..7bcce15c2 --- /dev/null +++ b/themes/squares/server/src/lib/configuration/schema/NotifierConfiguration.ts @@ -0,0 +1,45 @@ + +export interface EmailNotifierConfiguration { + username: string; + password: string; + sender: string; + service: string; +} + +export interface SmtpNotifierConfiguration { + username?: string; + password?: string; + host: string; + port: number; + secure: boolean; + sender: string; +} + +export interface FileSystemNotifierConfiguration { + filename: string; +} + +export interface NotifierConfiguration { + email?: EmailNotifierConfiguration; + smtp?: SmtpNotifierConfiguration; + filesystem?: FileSystemNotifierConfiguration; +} + +export function complete(configuration: NotifierConfiguration): [NotifierConfiguration, string] { + const newConfiguration: NotifierConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (Object.keys(newConfiguration).length == 0) + newConfiguration.filesystem = { filename: "/tmp/authelia/notification.txt" }; + + const ERROR = "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"; + + if (Object.keys(newConfiguration).length != 1) + return [newConfiguration, ERROR]; + + const key = Object.keys(newConfiguration)[0]; + + if (key != "filesystem" && key != "smtp" && key != "email") + return [newConfiguration, ERROR]; + + return [newConfiguration, undefined]; +} diff --git a/themes/squares/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts b/themes/squares/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts new file mode 100644 index 000000000..dce2caf4e --- /dev/null +++ b/themes/squares/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts @@ -0,0 +1,13 @@ +import Assert = require("assert"); +import { RegulationConfiguration, complete } from "./RegulationConfiguration"; + +describe("configuration/schema/RegulationConfiguration", function() { + it("should return default regulation configuration", function() { + const configuration: RegulationConfiguration = {}; + const newConfiguration = complete(configuration); + + Assert.equal(newConfiguration.ban_time, 300); + Assert.equal(newConfiguration.find_time, 120); + Assert.equal(newConfiguration.max_retries, 3); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/RegulationConfiguration.ts b/themes/squares/server/src/lib/configuration/schema/RegulationConfiguration.ts new file mode 100644 index 000000000..117463f43 --- /dev/null +++ b/themes/squares/server/src/lib/configuration/schema/RegulationConfiguration.ts @@ -0,0 +1,23 @@ +export interface RegulationConfiguration { + max_retries?: number; + find_time?: number; + ban_time?: number; +} + +export function complete(configuration: RegulationConfiguration): RegulationConfiguration { + const newConfiguration: RegulationConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.max_retries) { + newConfiguration.max_retries = 3; + } + + if (!newConfiguration.find_time) { + newConfiguration.find_time = 120; // seconds + } + + if (!newConfiguration.ban_time) { + newConfiguration.ban_time = 300; // seconds + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/SessionConfiguration.spec.ts b/themes/squares/server/src/lib/configuration/schema/SessionConfiguration.spec.ts new file mode 100644 index 000000000..e54010837 --- /dev/null +++ b/themes/squares/server/src/lib/configuration/schema/SessionConfiguration.spec.ts @@ -0,0 +1,16 @@ +import Assert = require("assert"); +import { SessionConfiguration, complete } from "./SessionConfiguration"; + +describe("configuration/schema/SessionConfiguration", function() { + it("should return default regulation configuration", function() { + const configuration: SessionConfiguration = { + domain: "example.com", + secret: "unsecure_secret" + }; + const newConfiguration = complete(configuration); + + Assert.equal(newConfiguration.name, 'authelia_session'); + Assert.equal(newConfiguration.expiration, 3600000); + Assert.equal(newConfiguration.inactivity, undefined); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/SessionConfiguration.ts b/themes/squares/server/src/lib/configuration/schema/SessionConfiguration.ts new file mode 100644 index 000000000..2c88bb215 --- /dev/null +++ b/themes/squares/server/src/lib/configuration/schema/SessionConfiguration.ts @@ -0,0 +1,32 @@ +export interface SessionRedisOptions { + host: string; + port: number; + password?: string; +} + +export interface SessionConfiguration { + name?: string; + domain: string; + secret: string; + expiration?: number; + inactivity?: number; + redis?: SessionRedisOptions; +} + +export function complete(configuration: SessionConfiguration): SessionConfiguration { + const newConfiguration: SessionConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.name) { + newConfiguration.name = "authelia_session"; + } + + if (!newConfiguration.expiration) { + newConfiguration.expiration = 3600000; // 1 hour + } + + if (!newConfiguration.inactivity) { + newConfiguration.inactivity = undefined; // disabled + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/StorageConfiguration.spec.ts b/themes/squares/server/src/lib/configuration/schema/StorageConfiguration.spec.ts new file mode 100644 index 000000000..9d02a11b6 --- /dev/null +++ b/themes/squares/server/src/lib/configuration/schema/StorageConfiguration.spec.ts @@ -0,0 +1,15 @@ +import Assert = require("assert"); +import { StorageConfiguration, complete } from "./StorageConfiguration"; + +describe("configuration/schema/StorageConfiguration", function() { + it("should return default regulation configuration", function() { + const configuration: StorageConfiguration = {}; + const newConfiguration = complete(configuration); + + Assert.deepEqual(newConfiguration, { + local: { + in_memory: true + } + }); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/StorageConfiguration.ts b/themes/squares/server/src/lib/configuration/schema/StorageConfiguration.ts new file mode 100644 index 000000000..47e356ef4 --- /dev/null +++ b/themes/squares/server/src/lib/configuration/schema/StorageConfiguration.ts @@ -0,0 +1,30 @@ +export interface MongoStorageConfiguration { + url: string; + database: string; + auth?: { + username: string; + password: string; + }; +} + +export interface LocalStorageConfiguration { + path?: string; + in_memory?: boolean; +} + +export interface StorageConfiguration { + local?: LocalStorageConfiguration; + mongo?: MongoStorageConfiguration; +} + +export function complete(configuration: StorageConfiguration): StorageConfiguration { + const newConfiguration: StorageConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.local && !newConfiguration.mongo) { + newConfiguration.local = { + in_memory: true + }; + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/TotpConfiguration.ts b/themes/squares/server/src/lib/configuration/schema/TotpConfiguration.ts new file mode 100644 index 000000000..683135639 --- /dev/null +++ b/themes/squares/server/src/lib/configuration/schema/TotpConfiguration.ts @@ -0,0 +1,13 @@ +export interface TotpConfiguration { + issuer: string; +} + +export function complete(configuration: TotpConfiguration): TotpConfiguration { + const newConfiguration: TotpConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.issuer) { + newConfiguration.issuer = "authelia.com"; + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts b/themes/squares/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts new file mode 100644 index 000000000..8008b4833 --- /dev/null +++ b/themes/squares/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts @@ -0,0 +1,9 @@ + +export interface UserInfo { + username: string; + password_hash: string; + email: string; + groups?: string[]; +} + +export type UserDatabaseConfiguration = UserInfo[]; \ No newline at end of file diff --git a/themes/squares/server/src/lib/connectors/mongo/IMongoClient.d.ts b/themes/squares/server/src/lib/connectors/mongo/IMongoClient.d.ts new file mode 100644 index 000000000..36cb4b8bf --- /dev/null +++ b/themes/squares/server/src/lib/connectors/mongo/IMongoClient.d.ts @@ -0,0 +1,6 @@ +import MongoDB = require("mongodb"); +import Bluebird = require("bluebird"); + +export interface IMongoClient { + collection(name: string): Bluebird +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/connectors/mongo/MongoClient.spec.ts b/themes/squares/server/src/lib/connectors/mongo/MongoClient.spec.ts new file mode 100644 index 000000000..ca0c68593 --- /dev/null +++ b/themes/squares/server/src/lib/connectors/mongo/MongoClient.spec.ts @@ -0,0 +1,119 @@ +import Assert = require("assert"); +import Bluebird = require("bluebird"); +import MongoDB = require("mongodb"); +import Sinon = require("sinon"); + +import { MongoClient } from "./MongoClient"; +import { GlobalLoggerStub } from "../../logging/GlobalLoggerStub.spec"; +import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration"; + +describe("connectors/mongo/MongoClient", function () { + let MongoClientStub: any; + let mongoClientStub: any; + let mongoDatabaseStub: any; + let logger: GlobalLoggerStub = new GlobalLoggerStub(); + + const configuration: MongoStorageConfiguration = { + url: "mongo://url", + database: "databasename" + }; + + describe("connection", () => { + before(() => { + mongoClientStub = { + db: Sinon.stub() + }; + mongoDatabaseStub = { + on: Sinon.stub(), + collection: Sinon.stub() + } + MongoClientStub = Sinon.stub( + MongoDB.MongoClient, "connect"); + MongoClientStub.yields( + undefined, mongoClientStub); + mongoClientStub.db.returns( + mongoDatabaseStub); + }); + + after(() => { + MongoClientStub.restore(); + }); + + it("should use credentials from configuration", () => { + configuration.auth = { + username: "authelia", + password: "authelia_pass" + }; + + const client = new MongoClient(configuration, logger); + return client.collection("test") + .then(() => { + Assert(MongoClientStub.calledWith("mongo://url", { + auth: { + user: "authelia", + password: "authelia_pass" + } + })) + }); + }); + }); + + describe("collection", () => { + before(function() { + mongoClientStub = { + db: Sinon.stub() + }; + mongoDatabaseStub = { + on: Sinon.stub(), + collection: Sinon.stub() + } + }); + + describe("Connection to mongo is ok", function() { + before(function () { + MongoClientStub = Sinon.stub( + MongoDB.MongoClient, "connect"); + MongoClientStub.yields( + undefined, mongoClientStub); + mongoClientStub.db.returns( + mongoDatabaseStub); + }); + + after(function () { + MongoClientStub.restore(); + }); + + it("should create a collection", function () { + const COLLECTION_NAME = "mycollection"; + const client = new MongoClient(configuration, logger); + + mongoDatabaseStub.collection.returns("COL"); + return client.collection(COLLECTION_NAME) + .then((collection) => mongoDatabaseStub.collection.calledWith(COLLECTION_NAME)); + }); + }); + + describe("Connection to mongo is broken", function() { + before(function () { + MongoClientStub = Sinon.stub( + MongoDB.MongoClient, "connect"); + MongoClientStub.yields( + new Error("Failed connection"), undefined); + }); + + after(function () { + MongoClientStub.restore(); + }); + + it("should fail creating the collection", function() { + const COLLECTION_NAME = "mycollection"; + const client = new MongoClient(configuration, logger); + + mongoDatabaseStub.collection.returns("COL"); + return client.collection(COLLECTION_NAME) + .then((collection) => Bluebird.reject(new Error("should not be here."))) + .catch((err) => Bluebird.resolve()); + }); + }) + }); +}); diff --git a/themes/squares/server/src/lib/connectors/mongo/MongoClient.ts b/themes/squares/server/src/lib/connectors/mongo/MongoClient.ts new file mode 100644 index 000000000..d15731e97 --- /dev/null +++ b/themes/squares/server/src/lib/connectors/mongo/MongoClient.ts @@ -0,0 +1,76 @@ + +import MongoDB = require("mongodb"); +import { IMongoClient } from "./IMongoClient"; +import Bluebird = require("bluebird"); +import { AUTHENTICATION_FAILED } from "../../../../../shared/UserMessages"; +import { IGlobalLogger } from "../../logging/IGlobalLogger"; +import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration"; + +export class MongoClient implements IMongoClient { + private configuration: MongoStorageConfiguration; + + private database: MongoDB.Db; + private client: MongoDB.MongoClient; + private logger: IGlobalLogger; + + constructor( + configuration: MongoStorageConfiguration, + logger: IGlobalLogger) { + + this.configuration = configuration; + this.logger = logger; + } + + connect(): Bluebird { + const that = this; + const options: MongoDB.MongoClientOptions = {}; + if (that.configuration.auth) { + options["auth"] = { + user: that.configuration.auth.username, + password: that.configuration.auth.password + }; + } + + return new Bluebird((resolve, reject) => { + MongoDB.MongoClient.connect( + this.configuration.url, + options, + function(err, client) { + if (err) { + reject(err); + return; + } + resolve(client); + }); + }) + .then(function (client: MongoDB.MongoClient) { + that.database = client.db(that.configuration.database); + that.database.on("close", () => { + that.logger.info("[MongoClient] Lost connection."); + }); + that.database.on("reconnect", () => { + that.logger.info("[MongoClient] Reconnected."); + }); + that.client = client; + }); + } + + close(): Bluebird { + if (this.client) { + this.client.close(); + this.database = undefined; + this.client = undefined; + } + return Bluebird.resolve(); + } + + collection(name: string): Bluebird { + if (!this.client) { + const that = this; + return this.connect() + .then(() => Bluebird.resolve(that.database.collection(name))); + } + + return Bluebird.resolve(this.database.collection(name)); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/connectors/mongo/MongoClientStub.spec.ts b/themes/squares/server/src/lib/connectors/mongo/MongoClientStub.spec.ts new file mode 100644 index 000000000..1cfd48e32 --- /dev/null +++ b/themes/squares/server/src/lib/connectors/mongo/MongoClientStub.spec.ts @@ -0,0 +1,16 @@ +import Sinon = require("sinon"); +import MongoDB = require("mongodb"); +import Bluebird = require("bluebird"); +import { IMongoClient } from "../../../../src/lib/connectors/mongo/IMongoClient"; + +export class MongoClientStub implements IMongoClient { + public collectionStub: Sinon.SinonStub; + + constructor() { + this.collectionStub = Sinon.stub(); + } + + collection(name: string): Bluebird { + return this.collectionStub(name); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/logging/GlobalLogger.ts b/themes/squares/server/src/lib/logging/GlobalLogger.ts new file mode 100644 index 000000000..4da7acf49 --- /dev/null +++ b/themes/squares/server/src/lib/logging/GlobalLogger.ts @@ -0,0 +1,34 @@ +import { IGlobalLogger } from "./IGlobalLogger"; +import Util = require("util"); +import Express = require("express"); +import Winston = require("winston"); + +declare module "express" { + interface Request { + id: string; + } +} + +export class GlobalLogger implements IGlobalLogger { + private winston: typeof Winston; + constructor(winston: typeof Winston) { + this.winston = winston; + } + + private buildMessage(message: string, ...args: any[]): string { + return Util.format("date='%s' message='%s'", new Date(), + Util.format(message, ...args)); + } + + info(message: string, ...args: any[]): void { + this.winston.info(this.buildMessage(message, ...args)); + } + + debug(message: string, ...args: any[]): void { + this.winston.debug(this.buildMessage(message, ...args)); + } + + error(message: string, ...args: any[]): void { + this.winston.debug(this.buildMessage(message, ...args)); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/logging/GlobalLoggerStub.spec.ts b/themes/squares/server/src/lib/logging/GlobalLoggerStub.spec.ts new file mode 100644 index 000000000..d4bb13710 --- /dev/null +++ b/themes/squares/server/src/lib/logging/GlobalLoggerStub.spec.ts @@ -0,0 +1,38 @@ +import Sinon = require("sinon"); +import { GlobalLogger } from "./GlobalLogger"; +import Winston = require("winston"); +import Express = require("express"); +import { IGlobalLogger } from "./IGlobalLogger"; + +export class GlobalLoggerStub implements IGlobalLogger { + infoStub: Sinon.SinonStub; + debugStub: Sinon.SinonStub; + errorStub: Sinon.SinonStub; + private globalLogger: IGlobalLogger; + + constructor(enableLogging?: boolean) { + this.infoStub = Sinon.stub(); + this.debugStub = Sinon.stub(); + this.errorStub = Sinon.stub(); + if (enableLogging) + this.globalLogger = new GlobalLogger(Winston); + } + + info(message: string, ...args: any[]): void { + if (this.globalLogger) + this.globalLogger.info(message, ...args); + this.infoStub(message, ...args); + } + + debug(message: string, ...args: any[]): void { + if (this.globalLogger) + this.globalLogger.info(message, ...args); + this.debugStub(message, ...args); + } + + error(message: string, ...args: any[]): void { + if (this.globalLogger) + this.globalLogger.info(message, ...args); + this.errorStub(message, ...args); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/logging/IGlobalLogger.ts b/themes/squares/server/src/lib/logging/IGlobalLogger.ts new file mode 100644 index 000000000..548515ec7 --- /dev/null +++ b/themes/squares/server/src/lib/logging/IGlobalLogger.ts @@ -0,0 +1,5 @@ +export interface IGlobalLogger { + info(message: string, ...args: any[]): void; + debug(message: string, ...args: any[]): void; + error(message: string, ...args: any[]): void; +} diff --git a/themes/squares/server/src/lib/logging/IRequestLogger.ts b/themes/squares/server/src/lib/logging/IRequestLogger.ts new file mode 100644 index 000000000..126a601fe --- /dev/null +++ b/themes/squares/server/src/lib/logging/IRequestLogger.ts @@ -0,0 +1,7 @@ +import Express = require("express"); + +export interface IRequestLogger { + info(req: Express.Request, message: string, ...args: any[]): void; + debug(req: Express.Request, message: string, ...args: any[]): void; + error(req: Express.Request, message: string, ...args: any[]): void; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/logging/RequestLogger.ts b/themes/squares/server/src/lib/logging/RequestLogger.ts new file mode 100644 index 000000000..c45c66018 --- /dev/null +++ b/themes/squares/server/src/lib/logging/RequestLogger.ts @@ -0,0 +1,45 @@ +import { IRequestLogger } from "./IRequestLogger"; +import Util = require("util"); +import Express = require("express"); +import Winston = require("winston"); + +declare module "express" { + interface Request { + id: string; + } +} + +export class RequestLogger implements IRequestLogger { + private winston: typeof Winston; + + constructor(winston: typeof Winston) { + this.winston = winston; + } + + private formatHeader(req: Express.Request) { + const clientIP = req.ip; // The IP of the original client going through the proxy chain. + return Util.format("date='%s' method='%s', path='%s' requestId='%s' sessionId='%s' ip='%s'", + new Date(), req.method, req.path, req.id, req.sessionID, clientIP); + } + + private formatBody(message: string) { + return Util.format("message='%s'", message); + } + + private formatMessage(req: Express.Request, message: string) { + return Util.format("%s %s", this.formatHeader(req), + this.formatBody(message)); + } + + info(req: Express.Request, message: string, ...args: any[]): void { + this.winston.info(this.formatMessage(req, message), ...args); + } + + debug(req: Express.Request, message: string, ...args: any[]): void { + this.winston.debug(this.formatMessage(req, message), ...args); + } + + error(req: Express.Request, message: string, ...args: any[]): void { + this.winston.error(this.formatMessage(req, message), ...args); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/logging/RequestLoggerStub.spec.ts b/themes/squares/server/src/lib/logging/RequestLoggerStub.spec.ts new file mode 100644 index 000000000..b0e375210 --- /dev/null +++ b/themes/squares/server/src/lib/logging/RequestLoggerStub.spec.ts @@ -0,0 +1,38 @@ +import { IRequestLogger } from "./IRequestLogger"; +import Sinon = require("sinon"); +import { RequestLogger } from "./RequestLogger"; +import Winston = require("winston"); +import Express = require("express"); + +export class RequestLoggerStub implements IRequestLogger { + infoStub: Sinon.SinonStub; + debugStub: Sinon.SinonStub; + errorStub: Sinon.SinonStub; + private requestLogger: RequestLogger; + + constructor(enableLogging?: boolean) { + this.infoStub = Sinon.stub(); + this.debugStub = Sinon.stub(); + this.errorStub = Sinon.stub(); + if (enableLogging) + this.requestLogger = new RequestLogger(Winston); + } + + info(req: Express.Request, message: string, ...args: any[]): void { + if (this.requestLogger) + this.requestLogger.info(req, message, ...args); + this.infoStub(req, message, ...args); + } + + debug(req: Express.Request, message: string, ...args: any[]): void { + if (this.requestLogger) + this.requestLogger.info(req, message, ...args); + this.debugStub(req, message, ...args); + } + + error(req: Express.Request, message: string, ...args: any[]): void { + if (this.requestLogger) + this.requestLogger.info(req, message, ...args); + this.errorStub(req, message, ...args); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/AbstractEmailNotifier.ts b/themes/squares/server/src/lib/notifiers/AbstractEmailNotifier.ts new file mode 100644 index 000000000..198e4e5db --- /dev/null +++ b/themes/squares/server/src/lib/notifiers/AbstractEmailNotifier.ts @@ -0,0 +1,23 @@ + +import { INotifier } from "../notifiers/INotifier"; +import { Identity } from "../../../types/Identity"; + +import Fs = require("fs"); +import Path = require("path"); +import Ejs = require("ejs"); +import BluebirdPromise = require("bluebird"); + +const email_template = Fs.readFileSync(Path.join(__dirname, "../../resources/email-template.ejs"), "UTF-8"); + +export abstract class AbstractEmailNotifier implements INotifier { + notify(to: string, subject: string, link: string): BluebirdPromise { + const d = { + url: link, + button_title: "Continue", + title: subject + }; + return this.sendEmail(to, subject, Ejs.render(email_template, d)); + } + + abstract sendEmail(to: string, subject: string, content: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/EmailNotifier.spec.ts b/themes/squares/server/src/lib/notifiers/EmailNotifier.spec.ts new file mode 100644 index 000000000..8211bbc02 --- /dev/null +++ b/themes/squares/server/src/lib/notifiers/EmailNotifier.spec.ts @@ -0,0 +1,54 @@ +import * as sinon from "sinon"; +import * as Assert from "assert"; +import BluebirdPromise = require("bluebird"); + +import { MailSenderStub } from "./MailSenderStub.spec"; +import EmailNotifier = require("./EmailNotifier"); + + +describe("notifiers/EmailNotifier", function () { + it("should send an email to given user", function () { + const mailSender = new MailSenderStub(); + const options = { + username: "user_gmail", + password: "pass_gmail", + sender: "admin@example.com", + service: "gmail" + }; + + mailSender.sendStub.returns(BluebirdPromise.resolve()); + const sender = new EmailNotifier.EmailNotifier(options, mailSender); + const subject = "subject"; + const url = "http://test.com"; + + return sender.notify("user@example.com", subject, url) + .then(function () { + Assert.equal(mailSender.sendStub.getCall(0).args[0].to, "user@example.com"); + Assert.equal(mailSender.sendStub.getCall(0).args[0].subject, "subject"); + return BluebirdPromise.resolve(); + }); + }); + + it("should fail while sending an email", function () { + const mailSender = new MailSenderStub(); + const options = { + username: "user_gmail", + password: "pass_gmail", + sender: "admin@example.com", + service: "gmail" + }; + + mailSender.sendStub.returns(BluebirdPromise.reject(new Error("Failed to send mail"))); + const sender = new EmailNotifier.EmailNotifier(options, mailSender); + const subject = "subject"; + const url = "http://test.com"; + + return sender.notify("user@example.com", subject, url) + .then(function () { + return BluebirdPromise.reject(new Error()); + }, function() { + Assert.equal(mailSender.sendStub.getCall(0).args[0].from, "admin@example.com"); + return BluebirdPromise.resolve(); + }); + }); +}); diff --git a/themes/squares/server/src/lib/notifiers/EmailNotifier.ts b/themes/squares/server/src/lib/notifiers/EmailNotifier.ts new file mode 100644 index 000000000..4df7c861e --- /dev/null +++ b/themes/squares/server/src/lib/notifiers/EmailNotifier.ts @@ -0,0 +1,27 @@ + +import * as BluebirdPromise from "bluebird"; + +import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; +import { EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; +import { IMailSender } from "./IMailSender"; + +export class EmailNotifier extends AbstractEmailNotifier { + private mailSender: IMailSender; + private sender: string; + + constructor(options: EmailNotifierConfiguration, mailSender: IMailSender) { + super(); + this.mailSender = mailSender; + this.sender = options.sender; + } + + sendEmail(to: string, subject: string, content: string) { + const mailOptions = { + from: this.sender, + to: to, + subject: subject, + html: content + }; + return this.mailSender.send(mailOptions); + } +} diff --git a/themes/squares/server/src/lib/notifiers/FileSystemNotifier.ts b/themes/squares/server/src/lib/notifiers/FileSystemNotifier.ts new file mode 100644 index 000000000..23f6242c4 --- /dev/null +++ b/themes/squares/server/src/lib/notifiers/FileSystemNotifier.ts @@ -0,0 +1,22 @@ +import * as BluebirdPromise from "bluebird"; +import * as util from "util"; +import * as Fs from "fs"; +import { INotifier } from "./INotifier"; +import { Identity } from "../../../types/Identity"; + +import { FileSystemNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; + +export class FileSystemNotifier implements INotifier { + private filename: string; + + constructor(options: FileSystemNotifierConfiguration) { + this.filename = options.filename; + } + + notify(to: string, subject: string, link: string): BluebirdPromise { + const content = util.format("Date: %s\nEmail: %s\nSubject: %s\nLink: %s", + new Date().toString(), to, subject, link); + const writeFilePromised: any = BluebirdPromise.promisify(Fs.writeFile); + return writeFilePromised(this.filename, content); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/IMailSender.ts b/themes/squares/server/src/lib/notifiers/IMailSender.ts new file mode 100644 index 000000000..34ac464a8 --- /dev/null +++ b/themes/squares/server/src/lib/notifiers/IMailSender.ts @@ -0,0 +1,6 @@ +import BluebirdPromise = require("bluebird"); +import Nodemailer = require("nodemailer"); + +export interface IMailSender { + send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/IMailSenderBuilder.ts b/themes/squares/server/src/lib/notifiers/IMailSenderBuilder.ts new file mode 100644 index 000000000..36d4dcdf7 --- /dev/null +++ b/themes/squares/server/src/lib/notifiers/IMailSenderBuilder.ts @@ -0,0 +1,7 @@ +import { IMailSender } from "./IMailSender"; +import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; + +export interface IMailSenderBuilder { + buildEmail(options: EmailNotifierConfiguration): IMailSender; + buildSmtp(options: SmtpNotifierConfiguration): IMailSender; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/INotifier.ts b/themes/squares/server/src/lib/notifiers/INotifier.ts new file mode 100644 index 000000000..b9a6b138c --- /dev/null +++ b/themes/squares/server/src/lib/notifiers/INotifier.ts @@ -0,0 +1,5 @@ +import * as BluebirdPromise from "bluebird"; + +export interface INotifier { + notify(to: string, subject: string, link: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/MailSender.ts b/themes/squares/server/src/lib/notifiers/MailSender.ts new file mode 100644 index 000000000..536a88e63 --- /dev/null +++ b/themes/squares/server/src/lib/notifiers/MailSender.ts @@ -0,0 +1,42 @@ +import { IMailSender } from "./IMailSender"; +import Nodemailer = require("nodemailer"); +import NodemailerDirectTransport = require("nodemailer-direct-transport"); +import NodemailerSmtpTransport = require("nodemailer-smtp-transport"); +import BluebirdPromise = require("bluebird"); + +export class MailSender implements IMailSender { + private transporter: Nodemailer.Transporter; + + constructor(options: NodemailerDirectTransport.DirectOptions | + NodemailerSmtpTransport.SmtpOptions, nodemailer: typeof Nodemailer) { + this.transporter = nodemailer.createTransport(options); + } + + verify(): BluebirdPromise { + const that = this; + return new BluebirdPromise(function (resolve, reject) { + that.transporter.verify(function (error: Error, success: any) { + if (error) { + reject(new Error("Unable to connect to SMTP server. \ + Please check the service is running and your credentials are correct.")); + return; + } + resolve(); + }); + }); + } + + send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise { + const that = this; + return new BluebirdPromise(function (resolve, reject) { + that.transporter.sendMail(mailOptions, (error: Error, + data: Nodemailer.SentMessageInfo) => { + if (error) { + reject(new Error("Error while sending email: " + error.message)); + return; + } + resolve(); + }); + }); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/MailSenderBuilder.spec.ts b/themes/squares/server/src/lib/notifiers/MailSenderBuilder.spec.ts new file mode 100644 index 000000000..41e0db426 --- /dev/null +++ b/themes/squares/server/src/lib/notifiers/MailSenderBuilder.spec.ts @@ -0,0 +1,67 @@ + +import { MailSenderBuilder } from ".//MailSenderBuilder"; +import Nodemailer = require("nodemailer"); +import Sinon = require("sinon"); +import Assert = require("assert"); + +describe("notifiers/MailSenderBuilder", function() { + let createTransportStub: Sinon.SinonStub; + beforeEach(function() { + createTransportStub = Sinon.stub(Nodemailer, "createTransport"); + }); + + afterEach(function() { + createTransportStub.restore(); + }); + + it("should create a email mail sender", function() { + const mailSenderBuilder = new MailSenderBuilder(Nodemailer); + mailSenderBuilder.buildEmail({ + username: "user_gmail", + password: "pass_gmail", + sender: "admin@example.com", + service: "gmail" + }); + Assert.equal(createTransportStub.getCall(0).args[0].auth.user, "user_gmail"); + Assert.equal(createTransportStub.getCall(0).args[0].auth.pass, "pass_gmail"); + Assert.equal(createTransportStub.getCall(0).args[0].service, "gmail"); + }); + + describe("build smtp mail sender", function() { + it("should create a smtp mail sender with authenticated user", function() { + const mailSenderBuilder = new MailSenderBuilder(Nodemailer); + mailSenderBuilder.buildSmtp({ + host: "mail.example.com", + password: "password", + port: 25, + secure: true, + username: "user", + sender: "admin@example.com" + }); + Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { + host: "mail.example.com", + auth: { + pass: "password", + user: "user" + }, + port: 25, + secure: true, + }); + }); + + it("should create a smtp mail sender with anonymous user", function() { + const mailSenderBuilder = new MailSenderBuilder(Nodemailer); + mailSenderBuilder.buildSmtp({ + host: "mail.example.com", + port: 25, + secure: true, + sender: "admin@example.com" + }); + Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { + host: "mail.example.com", + port: 25, + secure: true, + }); + }); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/MailSenderBuilder.ts b/themes/squares/server/src/lib/notifiers/MailSenderBuilder.ts new file mode 100644 index 000000000..1d06be52f --- /dev/null +++ b/themes/squares/server/src/lib/notifiers/MailSenderBuilder.ts @@ -0,0 +1,42 @@ +import { IMailSender } from "./IMailSender"; +import { IMailSenderBuilder } from "./IMailSenderBuilder"; +import { MailSender } from "./MailSender"; +import Nodemailer = require("nodemailer"); +import NodemailerSmtpTransport = require("nodemailer-smtp-transport"); +import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; + +export class MailSenderBuilder implements IMailSenderBuilder { + private nodemailer: typeof Nodemailer; + + constructor(nodemailer: typeof Nodemailer) { + this.nodemailer = nodemailer; + } + + buildEmail(options: EmailNotifierConfiguration): IMailSender { + const emailOptions = { + service: options.service, + auth: { + user: options.username, + pass: options.password + } + }; + return new MailSender(emailOptions, this.nodemailer); + } + + buildSmtp(options: SmtpNotifierConfiguration): IMailSender { + const smtpOptions: NodemailerSmtpTransport.SmtpOptions = { + host: options.host, + port: options.port, + secure: options.secure, // upgrade later with STARTTLS + }; + + if (options.username && options.password) { + smtpOptions.auth = { + user: options.username, + pass: options.password + }; + } + + return new MailSender(smtpOptions, this.nodemailer); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts b/themes/squares/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts new file mode 100644 index 000000000..5b76f6e56 --- /dev/null +++ b/themes/squares/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts @@ -0,0 +1,25 @@ +import { IMailSenderBuilder } from "../../../src/lib/notifiers/IMailSenderBuilder"; +import BluebirdPromise = require("bluebird"); +import Nodemailer = require("nodemailer"); +import Sinon = require("sinon"); +import { IMailSender } from "../../../src/lib/notifiers/IMailSender"; +import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../../../src/lib/configuration/schema/NotifierConfiguration"; + +export class MailSenderBuilderStub implements IMailSenderBuilder { + buildEmailStub: Sinon.SinonStub; + buildSmtpStub: Sinon.SinonStub; + + constructor() { + this.buildEmailStub = Sinon.stub(); + this.buildSmtpStub = Sinon.stub(); + } + + buildEmail(options: EmailNotifierConfiguration): IMailSender { + return this.buildEmailStub(options); + } + + buildSmtp(options: SmtpNotifierConfiguration): IMailSender { + return this.buildSmtpStub(options); + } + +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/MailSenderStub.spec.ts b/themes/squares/server/src/lib/notifiers/MailSenderStub.spec.ts new file mode 100644 index 000000000..d57c458fc --- /dev/null +++ b/themes/squares/server/src/lib/notifiers/MailSenderStub.spec.ts @@ -0,0 +1,16 @@ +import { IMailSender } from "../../../src/lib/notifiers/IMailSender"; +import BluebirdPromise = require("bluebird"); +import Nodemailer = require("nodemailer"); +import Sinon = require("sinon"); + +export class MailSenderStub implements IMailSender { + sendStub: Sinon.SinonStub; + + constructor() { + this.sendStub = Sinon.stub(); + } + + send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise { + return this.sendStub(mailOptions); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/NotifierFactory.spec.ts b/themes/squares/server/src/lib/notifiers/NotifierFactory.spec.ts new file mode 100644 index 000000000..f15e7667f --- /dev/null +++ b/themes/squares/server/src/lib/notifiers/NotifierFactory.spec.ts @@ -0,0 +1,42 @@ + +import * as sinon from "sinon"; +import * as BluebirdPromise from "bluebird"; +import * as assert from "assert"; + +import { NotifierFactory } from "./NotifierFactory"; +import { EmailNotifier } from "./EmailNotifier"; +import { SmtpNotifier } from "./SmtpNotifier"; +import { MailSenderBuilderStub } from "./MailSenderBuilderStub.spec"; + + +describe("notifiers/NotifierFactory", function () { + let mailSenderBuilderStub: MailSenderBuilderStub; + it("should build a Email Notifier", function () { + const options = { + email: { + username: "abc", + password: "password", + sender: "admin@example.com", + service: "gmail" + } + }; + mailSenderBuilderStub = new MailSenderBuilderStub(); + assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof EmailNotifier); + }); + + it("should build a SMTP Notifier", function () { + const options = { + smtp: { + username: "user", + password: "pass", + secure: true, + host: "localhost", + port: 25, + sender: "admin@example.com" + } + }; + + mailSenderBuilderStub = new MailSenderBuilderStub(); + assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof SmtpNotifier); + }); +}); diff --git a/themes/squares/server/src/lib/notifiers/NotifierFactory.ts b/themes/squares/server/src/lib/notifiers/NotifierFactory.ts new file mode 100644 index 000000000..a89155feb --- /dev/null +++ b/themes/squares/server/src/lib/notifiers/NotifierFactory.ts @@ -0,0 +1,33 @@ + +import { NotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; +import Nodemailer = require("nodemailer"); +import { INotifier } from "./INotifier"; + +import { FileSystemNotifier } from "./FileSystemNotifier"; +import { EmailNotifier } from "./EmailNotifier"; +import { SmtpNotifier } from "./SmtpNotifier"; +import { IMailSender } from "./IMailSender"; +import { IMailSenderBuilder } from "./IMailSenderBuilder"; + +export class NotifierFactory { + static build(options: NotifierConfiguration, mailSenderBuilder: IMailSenderBuilder): INotifier { + if ("email" in options) { + const mailSender = mailSenderBuilder.buildEmail(options.email); + return new EmailNotifier(options.email, mailSender); + } + else if ("smtp" in options) { + const mailSender = mailSenderBuilder.buildSmtp(options.smtp); + return new SmtpNotifier(options.smtp, mailSender); + } + else if ("filesystem" in options) { + return new FileSystemNotifier(options.filesystem); + } + else { + throw new Error("No available notifier option detected."); + } + } +} + + + + diff --git a/themes/squares/server/src/lib/notifiers/NotifierStub.spec.ts b/themes/squares/server/src/lib/notifiers/NotifierStub.spec.ts new file mode 100644 index 000000000..f99231b50 --- /dev/null +++ b/themes/squares/server/src/lib/notifiers/NotifierStub.spec.ts @@ -0,0 +1,16 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); + +import { INotifier } from "./INotifier"; + +export class NotifierStub implements INotifier { + notifyStub: Sinon.SinonStub; + + constructor() { + this.notifyStub = Sinon.stub(); + } + + notify(to: string, subject: string, link: string): BluebirdPromise { + return this.notifyStub(to, subject, link); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/SmtpNotifier.ts b/themes/squares/server/src/lib/notifiers/SmtpNotifier.ts new file mode 100644 index 000000000..f93a6d4a0 --- /dev/null +++ b/themes/squares/server/src/lib/notifiers/SmtpNotifier.ts @@ -0,0 +1,30 @@ + + +import * as BluebirdPromise from "bluebird"; + +import { IMailSender } from "./IMailSender"; +import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; +import { SmtpNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; + +export class SmtpNotifier extends AbstractEmailNotifier { + private mailSender: IMailSender; + private sender: string; + + constructor(options: SmtpNotifierConfiguration, + mailSender: IMailSender) { + super(); + this.mailSender = mailSender; + this.sender = options.sender; + } + + sendEmail(to: string, subject: string, content: string) { + const mailOptions = { + from: this.sender, + to: to, + subject: subject, + html: content + }; + const that = this; + return this.mailSender.send(mailOptions); + } +} diff --git a/themes/squares/server/src/lib/regulation/IRegulator.ts b/themes/squares/server/src/lib/regulation/IRegulator.ts new file mode 100644 index 000000000..c49425b24 --- /dev/null +++ b/themes/squares/server/src/lib/regulation/IRegulator.ts @@ -0,0 +1,6 @@ +import BluebirdPromise = require("bluebird"); + +export interface IRegulator { + mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; + regulate(userId: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/regulation/Regulator.spec.ts b/themes/squares/server/src/lib/regulation/Regulator.spec.ts new file mode 100644 index 000000000..f9c6e6086 --- /dev/null +++ b/themes/squares/server/src/lib/regulation/Regulator.spec.ts @@ -0,0 +1,186 @@ + +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import Assert = require("assert"); + +import { Regulator } from "./Regulator"; +import MockDate = require("mockdate"); +import exceptions = require("../Exceptions"); +import { UserDataStoreStub } from "../storage/UserDataStoreStub.spec"; + +describe("regulation/Regulator", function () { + const USER1 = "USER1"; + const USER2 = "USER2"; + let userDataStoreStub: UserDataStoreStub; + + beforeEach(function () { + userDataStoreStub = new UserDataStoreStub(); + const dataStore: { [userId: string]: { userId: string, date: Date, isAuthenticationSuccessful: boolean }[] } = { + [USER1]: [], + [USER2]: [] + }; + + userDataStoreStub.saveAuthenticationTraceStub.callsFake(function (userId, isAuthenticationSuccessful) { + dataStore[userId].unshift({ + userId: userId, + date: new Date(), + isAuthenticationSuccessful: isAuthenticationSuccessful, + }); + return BluebirdPromise.resolve(); + }); + + userDataStoreStub.retrieveLatestAuthenticationTracesStub.callsFake(function (userId, count) { + const ret = (dataStore[userId].length <= count) ? dataStore[userId] : dataStore[userId].slice(0, 3); + return BluebirdPromise.resolve(ret); + }); + }); + + afterEach(function () { + MockDate.reset(); + }); + + function markAuthenticationAt(regulator: Regulator, user: string, time: string, success: boolean) { + MockDate.set(time); + return regulator.mark(user, success); + } + + it("should mark 2 authentication and regulate (accept)", function () { + const regulator = new Regulator(userDataStoreStub, 3, 10, 10); + + return regulator.mark(USER1, false) + .then(function () { + return regulator.mark(USER1, true); + }) + .then(function () { + return regulator.regulate(USER1); + }); + }); + + it("should mark 3 authentications and regulate (reject)", function () { + const regulator = new Regulator(userDataStoreStub, 3, 10, 10); + + return regulator.mark(USER1, false) + .then(function () { + return regulator.mark(USER1, false); + }) + .then(function () { + return regulator.mark(USER1, false); + }) + .then(function () { + return regulator.regulate(USER1); + }) + .then(function () { return BluebirdPromise.reject(new Error("should not be here!")); }) + .catch(exceptions.AuthenticationRegulationError, function () { + return BluebirdPromise.resolve(); + }); + }); + + it("should mark 1 failed, 1 successful and 1 failed authentications within minimum time and regulate (accept)", function () { + const regulator = new Regulator(userDataStoreStub, 3, 60, 30); + + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", true); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:20", false); + }) + .then(function () { + return regulator.regulate(USER1); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:30", false); + }) + .then(function () { + return regulator.regulate(USER1); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:39", false); + }) + .then(function () { + return regulator.regulate(USER1); + }) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here!")); + }, + function () { + return BluebirdPromise.resolve(); + }); + }); + + it("should regulate user if number of failures is greater than 3 in allowed time lapse", function () { + function markAuthentications(regulator: Regulator, user: string) { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:45", false); + }) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:01:05", false); + }) + .then(function () { + return regulator.regulate(user); + }); + } + + const regulator1 = new Regulator(userDataStoreStub, 3, 60, 60); + const regulator2 = new Regulator(userDataStoreStub, 3, 2 * 60, 60); + + const p1 = markAuthentications(regulator1, USER1); + const p2 = markAuthentications(regulator2, USER2); + + return BluebirdPromise.join(p1, p2) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here...")); + }, function () { + Assert(p1.isFulfilled()); + Assert(p2.isRejected()); + }); + }); + + it("should user wait after regulation to authenticate again", function () { + function markAuthentications(regulator: Regulator, user: string) { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:10", false); + }) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:15", false); + }) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:25", false); + }) + .then(function () { + MockDate.set("1/2/2000 00:00:54"); + return regulator.regulate(user); + }) + .then(function () { + return BluebirdPromise.reject(new Error("should fail at this time")); + }, function () { + MockDate.set("1/2/2000 00:00:56"); + return regulator.regulate(user); + }); + } + + const regulator = new Regulator(userDataStoreStub, 4, 30, 30); + return markAuthentications(regulator, USER1); + }); + + it("should disable regulation when max_retries is set to 0", function () { + const maxRetries = 0; + const regulator = new Regulator(userDataStoreStub, maxRetries, 60, 30); + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", false); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:15", false); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:25", false); + }) + .then(function () { + MockDate.set("1/2/2000 00:00:26"); + return regulator.regulate(USER1); + }); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/regulation/Regulator.ts b/themes/squares/server/src/lib/regulation/Regulator.ts new file mode 100644 index 000000000..1037a6a17 --- /dev/null +++ b/themes/squares/server/src/lib/regulation/Regulator.ts @@ -0,0 +1,55 @@ + +import * as BluebirdPromise from "bluebird"; +import exceptions = require("../Exceptions"); +import { IUserDataStore } from "../storage/IUserDataStore"; +import { AuthenticationTraceDocument } from "../storage/AuthenticationTraceDocument"; +import { IRegulator } from "./IRegulator"; + +export class Regulator implements IRegulator { + private userDataStore: IUserDataStore; + private banTime: number; + private findTime: number; + private maxRetries: number; + + constructor(userDataStore: any, maxRetries: number, findTime: number, banTime: number) { + this.userDataStore = userDataStore; + this.banTime = banTime; + this.findTime = findTime; + this.maxRetries = maxRetries; + } + + // Mark authentication + mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { + return this.userDataStore.saveAuthenticationTrace(userId, isAuthenticationSuccessful); + } + + regulate(userId: string): BluebirdPromise { + const that = this; + + if (that.maxRetries <= 0) return BluebirdPromise.resolve(); + + return this.userDataStore.retrieveLatestAuthenticationTraces(userId, that.maxRetries) + .then((docs: AuthenticationTraceDocument[]) => { + // less than the max authorized number of authentication in time range, thus authorizing access + if (docs.length < that.maxRetries) return BluebirdPromise.resolve(); + + const numberOfFailedAuth = docs + .map(function (d: AuthenticationTraceDocument) { return d.isAuthenticationSuccessful == false ? 1 : 0; }) + .reduce(function (acc, v) { return acc + v; }, 0); + + if (numberOfFailedAuth < this.maxRetries) return BluebirdPromise.resolve(); + + const newestDocument = docs[0]; + const oldestDocument = docs[that.maxRetries - 1]; + + const authenticationsTimeRangeInSeconds = (newestDocument.date.getTime() - oldestDocument.date.getTime()) / 1000; + const tooManyAuthInTimelapse = (authenticationsTimeRangeInSeconds < this.findTime); + const stillInBannedTimeRange = (new Date(new Date().getTime() - this.banTime * 1000) < newestDocument.date); + + if (tooManyAuthInTimelapse && stillInBannedTimeRange) + throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes."); + + return BluebirdPromise.resolve(); + }); + } +} diff --git a/themes/squares/server/src/lib/regulation/RegulatorStub.spec.ts b/themes/squares/server/src/lib/regulation/RegulatorStub.spec.ts new file mode 100644 index 000000000..ca8a00fb1 --- /dev/null +++ b/themes/squares/server/src/lib/regulation/RegulatorStub.spec.ts @@ -0,0 +1,22 @@ +import Bluebird = require("bluebird"); +import Sinon = require("sinon"); +import { IRegulator } from "./IRegulator"; + + +export class RegulatorStub implements IRegulator { + markStub: Sinon.SinonStub; + regulateStub: Sinon.SinonStub; + + constructor() { + this.markStub = Sinon.stub(); + this.regulateStub = Sinon.stub(); + } + + mark(userId: string, isAuthenticationSuccessful: boolean): Bluebird { + return this.markStub(userId, isAuthenticationSuccessful); + } + + regulate(userId: string): Bluebird { + return this.regulateStub(userId); + } +} diff --git a/themes/squares/server/src/lib/routes/error/401/get.spec.ts b/themes/squares/server/src/lib/routes/error/401/get.spec.ts new file mode 100644 index 000000000..9fdac9c3c --- /dev/null +++ b/themes/squares/server/src/lib/routes/error/401/get.spec.ts @@ -0,0 +1,61 @@ +import Sinon = require("sinon"); +import Express = require("express"); +import Assert = require("assert"); +import Get401 from "./get"; +import { ServerVariables } from "../../../ServerVariables"; +import { ServerVariablesMockBuilder, ServerVariablesMock } + from "../../../ServerVariablesMockBuilder.spec"; + +describe("routes/error/401/get", function () { + let vars: ServerVariables; + let mocks: ServerVariablesMock; + let req: any; + let res: any; + let renderSpy: Sinon.SinonSpy; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + vars = s.variables; + mocks = s.mocks; + + renderSpy = Sinon.spy(); + req = { + headers: {} + }; + res = { + render: renderSpy + }; + }); + + it("should set redirection url to the default redirection url", function () { + vars.config.default_redirection_url = "http://default-redirection"; + return Get401(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/401", { + redirection_url: "http://default-redirection" + })); + }); + }); + + it("should set redirection url to the referer", function () { + req.headers["referer"] = "http://redirection"; + return Get401(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/401", { + redirection_url: "http://redirection" + })); + }); + }); + + it("should render without redirecting the user", function () { + return Get401(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/401", { + redirection_url: undefined + })); + }); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/error/401/get.ts b/themes/squares/server/src/lib/routes/error/401/get.ts new file mode 100644 index 000000000..ca4a3963d --- /dev/null +++ b/themes/squares/server/src/lib/routes/error/401/get.ts @@ -0,0 +1,15 @@ + +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import redirector from "../redirector"; +import { ServerVariables } from "../../../ServerVariables"; + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response): BluebirdPromise { + const redirectionUrl = redirector(req, vars); + res.render("errors/401", { + redirection_url: redirectionUrl + }); + return BluebirdPromise.resolve(); + }; +} diff --git a/themes/squares/server/src/lib/routes/error/403/get.spec.ts b/themes/squares/server/src/lib/routes/error/403/get.spec.ts new file mode 100644 index 000000000..22eb84853 --- /dev/null +++ b/themes/squares/server/src/lib/routes/error/403/get.spec.ts @@ -0,0 +1,61 @@ +import Sinon = require("sinon"); +import Express = require("express"); +import Assert = require("assert"); +import Get403 from "./get"; +import { ServerVariables } from "../../../ServerVariables"; +import { ServerVariablesMockBuilder, ServerVariablesMock } + from "../../../ServerVariablesMockBuilder.spec"; + +describe("routes/error/403/get", function () { + let vars: ServerVariables; + let mocks: ServerVariablesMock; + let req: any; + let res: any; + let renderSpy: Sinon.SinonSpy; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + vars = s.variables; + mocks = s.mocks; + + renderSpy = Sinon.spy(); + req = { + headers: {} + }; + res = { + render: renderSpy + }; + }); + + it("should set redirection url to the default redirection url", function () { + vars.config.default_redirection_url = "http://default-redirection"; + return Get403(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/403", { + redirection_url: "http://default-redirection" + })); + }); + }); + + it("should set redirection url to the referer", function () { + req.headers["referer"] = "http://redirection"; + return Get403(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/403", { + redirection_url: "http://redirection" + })); + }); + }); + + it("should render without redirecting the user", function () { + return Get403(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/403", { + redirection_url: undefined + })); + }); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/error/403/get.ts b/themes/squares/server/src/lib/routes/error/403/get.ts new file mode 100644 index 000000000..3ab0319e5 --- /dev/null +++ b/themes/squares/server/src/lib/routes/error/403/get.ts @@ -0,0 +1,15 @@ + +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import redirector from "../redirector"; +import { ServerVariables } from "../../../ServerVariables"; + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response): BluebirdPromise { + const redirectionUrl = redirector(req, vars); + res.render("errors/403", { + redirection_url: redirectionUrl + }); + return BluebirdPromise.resolve(); + }; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/error/404/get.spec.ts b/themes/squares/server/src/lib/routes/error/404/get.spec.ts new file mode 100644 index 000000000..73e4e6cef --- /dev/null +++ b/themes/squares/server/src/lib/routes/error/404/get.spec.ts @@ -0,0 +1,19 @@ +import Sinon = require("sinon"); +import Express = require("express"); +import Assert = require("assert"); +import Get404 from "./get"; + +describe("routes/error/404/get", function () { + it("should render the page", function () { + const req = {} as Express.Request; + const res = { + render: Sinon.stub() + }; + + return Get404(req, res as any) + .then(function () { + Assert(res.render.calledOnce); + Assert(res.render.calledWith("errors/404")); + }); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/error/404/get.ts b/themes/squares/server/src/lib/routes/error/404/get.ts new file mode 100644 index 000000000..6693b6fc8 --- /dev/null +++ b/themes/squares/server/src/lib/routes/error/404/get.ts @@ -0,0 +1,8 @@ + +import BluebirdPromise = require("bluebird"); +import express = require("express"); + +export default function (req: express.Request, res: express.Response): BluebirdPromise { + res.render("errors/404"); + return BluebirdPromise.resolve(); +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/error/redirector.ts b/themes/squares/server/src/lib/routes/error/redirector.ts new file mode 100644 index 000000000..b1a3ccc11 --- /dev/null +++ b/themes/squares/server/src/lib/routes/error/redirector.ts @@ -0,0 +1,13 @@ +import Express = require("express"); +import { ServerVariables } from "../../ServerVariables"; + +export default function (req: Express.Request, vars: ServerVariables): string { + let redirectionUrl: string; + + if (req.headers && req.headers["referer"]) + redirectionUrl = "" + req.headers["referer"]; + else if (vars.config.default_redirection_url) + redirectionUrl = vars.config.default_redirection_url; + + return redirectionUrl; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/firstfactor/get.ts b/themes/squares/server/src/lib/routes/firstfactor/get.ts new file mode 100644 index 000000000..d94f656c8 --- /dev/null +++ b/themes/squares/server/src/lib/routes/firstfactor/get.ts @@ -0,0 +1,72 @@ + +import express = require("express"); +import Endpoints = require("../../../../../shared/api"); +import BluebirdPromise = require("bluebird"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Constants = require("../../../../../shared/constants"); +import Util = require("util"); +import { ServerVariables } from "../../ServerVariables"; +import { SafeRedirector } from "../../utils/SafeRedirection"; +import { Level } from "../../authentication/Level"; + +function getRedirectParam( + req: express.Request) { + return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; +} + +function redirectToSecondFactorPage( + req: express.Request, + res: express.Response) { + + const redirectUrl = getRedirectParam(req); + if (!redirectUrl) + res.redirect(Endpoints.SECOND_FACTOR_GET); + else + res.redirect( + Util.format("%s?%s=%s", + Endpoints.SECOND_FACTOR_GET, + Constants.REDIRECT_QUERY_PARAM, + redirectUrl)); +} + +function redirectToService( + req: express.Request, + res: express.Response, + redirector: SafeRedirector) { + const redirectUrl = getRedirectParam(req); + if (!redirectUrl) { + res.redirect(Endpoints.LOGGED_IN); + } else { + redirector.redirectOrElse(res, redirectUrl, Endpoints.LOGGED_IN); + } +} + +function renderFirstFactor( + res: express.Response) { + + res.render("firstfactor", { + first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST, + reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET + }); +} + +export default function ( + vars: ServerVariables) { + + const redirector = new SafeRedirector(vars.config.session.domain); + return function (req: express.Request, res: express.Response): BluebirdPromise { + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (authSession.authentication_level == Level.ONE_FACTOR) { + redirectToSecondFactorPage(req, res); + } else if (authSession.authentication_level == Level.TWO_FACTOR) { + redirectToService(req, res, redirector); + } else { + renderFirstFactor(res); + } + resolve(); + }); + }; +} diff --git a/themes/squares/server/src/lib/routes/firstfactor/post.spec.ts b/themes/squares/server/src/lib/routes/firstfactor/post.spec.ts new file mode 100644 index 000000000..e1d078cdd --- /dev/null +++ b/themes/squares/server/src/lib/routes/firstfactor/post.spec.ts @@ -0,0 +1,136 @@ + +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import Assert = require("assert"); +import FirstFactorPost = require("./post"); +import exceptions = require("../../Exceptions"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../types/AuthenticationSession"; +import Endpoints = require("../../../../../shared/api"); +import AuthenticationRegulatorMock = require("../../regulation/RegulatorStub.spec"); +import ExpressMock = require("../../stubs/express.spec"); +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../ServerVariables"; + +describe("routes/firstfactor/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let emails: string[]; + let groups: string[]; + let vars: ServerVariables; + let mocks: ServerVariablesMock; + let authSession: AuthenticationSession; + + beforeEach(function () { + emails = ["test_ok@example.com"]; + groups = ["group1", "group2" ]; + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + mocks.authorizer.authorizationMock.returns(true); + mocks.regulator.regulateStub.returns(BluebirdPromise.resolve()); + mocks.regulator.markStub.returns(BluebirdPromise.resolve()); + + req = { + originalUrl: "/api/firstfactor", + body: { + username: "username", + password: "password" + }, + query: { + redirect: "http://redirect.url" + }, + session: { + cookie: {} + }, + headers: { + host: "home.example.com" + } + }; + + res = ExpressMock.ResponseMock(); + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + }); + + it("should reply with 204 if success", function () { + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.resolve({ + emails: emails, + groups: groups + })); + return FirstFactorPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal("username", authSession.userid); + Assert(res.send.calledOnce); + }); + }); + + describe("keep me logged in", () => { + beforeEach(() => { + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.resolve({ + emails: emails, + groups: groups + })); + req.body.keepMeLoggedIn = "true"; + return FirstFactorPost.default(vars)(req as any, res as any); + }); + + it("should set keep_me_logged_in session variable to true", function () { + Assert.equal(authSession.keep_me_logged_in, true); + }); + + it("should set cookie maxAge to one year", function () { + Assert.equal(req.session.cookie.maxAge, 365 * 24 * 60 * 60 * 1000); + }); + }); + + it("should retrieve email from LDAP", function () { + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.resolve([{ mail: ["test@example.com"] }])); + return FirstFactorPost.default(vars)(req as any, res as any); + }); + + it("should set first email address as user session variable", function () { + const emails = ["test_ok@example.com"]; + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.resolve({ + emails: emails, + groups: groups + })); + + return FirstFactorPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal("test_ok@example.com", authSession.email); + }); + }); + + it("should return error message when LDAP authenticator throws", function () { + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.reject(new exceptions.LdapBindError("Bad credentials"))); + + return FirstFactorPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.equal(mocks.regulator.markStub.getCall(0).args[0], "username"); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + }); + }); + + it("should return error message when regulator rejects authentication", function () { + const err = new exceptions.AuthenticationRegulationError("Authentication regulation..."); + mocks.regulator.regulateStub.returns(BluebirdPromise.reject(err)); + return FirstFactorPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + }); + }); +}); + + diff --git a/themes/squares/server/src/lib/routes/firstfactor/post.ts b/themes/squares/server/src/lib/routes/firstfactor/post.ts new file mode 100644 index 000000000..565681d6a --- /dev/null +++ b/themes/squares/server/src/lib/routes/firstfactor/post.ts @@ -0,0 +1,101 @@ + +import Exceptions = require("../../Exceptions"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import Endpoint = require("../../../../../shared/api"); +import ErrorReplies = require("../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Constants = require("../../../../../shared/constants"); +import UserMessages = require("../../../../../shared/UserMessages"); +import { ServerVariables } from "../../ServerVariables"; +import { AuthenticationSession } from "../../../../types/AuthenticationSession"; +import { GroupsAndEmails } from "../../authentication/backends/GroupsAndEmails"; +import { Level as AuthenticationLevel } from "../../authentication/Level"; +import { Level as AuthorizationLevel } from "../../authorization/Level"; +import { URLDecomposer } from "../../utils/URLDecomposer"; + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response) + : BluebirdPromise { + const username: string = req.body.username; + const password: string = req.body.password; + const keepMeLoggedIn: boolean = req.body.keepMeLoggedIn && + req.body.keepMeLoggedIn === "true"; + let authSession: AuthenticationSession; + + if (keepMeLoggedIn) { + // Stay connected for 1 year. + vars.logger.debug(req, "User requested to stay logged in for one year."); + req.session.cookie.maxAge = 365 * 24 * 60 * 60 * 1000; + } + + return BluebirdPromise.resolve() + .then(function () { + if (!username || !password) { + return BluebirdPromise.reject(new Error("No username or password.")); + } + vars.logger.info(req, "Starting authentication of user \"%s\"", username); + authSession = AuthenticationSessionHandler.get(req, vars.logger); + return vars.regulator.regulate(username); + }) + .then(function () { + vars.logger.info(req, "No regulation applied."); + return vars.usersDatabase.checkUserPassword(username, password); + }) + .then(function (groupsAndEmails: GroupsAndEmails) { + vars.logger.info(req, + "LDAP binding successful. Retrieved information about user are %s", + JSON.stringify(groupsAndEmails)); + authSession.userid = username; + authSession.keep_me_logged_in = keepMeLoggedIn; + authSession.authentication_level = AuthenticationLevel.ONE_FACTOR; + const redirectUrl: string = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined" + // Fuck, don't know why it is a string! + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : ""; + + const emails: string[] = groupsAndEmails.emails; + const groups: string[] = groupsAndEmails.groups; + const decomposition = URLDecomposer.fromUrl(redirectUrl); + const authorizationLevel = (decomposition) + ? vars.authorizer.authorization( + {domain: decomposition.domain, resource: decomposition.path}, + {user: username, groups: groups}) + : AuthorizationLevel.TWO_FACTOR; + + if (emails.length > 0) + authSession.email = emails[0]; + authSession.groups = groups; + + vars.logger.debug(req, "Mark successful authentication to regulator."); + vars.regulator.mark(username, true); + + if (authorizationLevel <= AuthorizationLevel.ONE_FACTOR) { + let newRedirectionUrl: string = redirectUrl; + if (!newRedirectionUrl) + newRedirectionUrl = Endpoint.LOGGED_IN; + res.send({ + redirect: newRedirectionUrl + }); + vars.logger.debug(req, "Redirect to '%s'", redirectUrl); + } + else { + let newRedirectUrl = Endpoint.SECOND_FACTOR_GET; + if (redirectUrl) { + newRedirectUrl += "?" + Constants.REDIRECT_QUERY_PARAM + "=" + + redirectUrl; + } + vars.logger.debug(req, "Redirect to '%s'", newRedirectUrl); + res.send({ + redirect: newRedirectUrl + }); + } + return BluebirdPromise.resolve(); + }) + .catch(Exceptions.LdapBindError, function (err: Error) { + vars.regulator.mark(username, false); + return ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)(err); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)); + }; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/loggedin/get.ts b/themes/squares/server/src/lib/routes/loggedin/get.ts new file mode 100644 index 000000000..283a041b1 --- /dev/null +++ b/themes/squares/server/src/lib/routes/loggedin/get.ts @@ -0,0 +1,23 @@ +import Express = require("express"); +import Endpoints = require("../../../../../shared/api"); +import BluebirdPromise = require("bluebird"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { ServerVariables } from "../../ServerVariables"; +import ErrorReplies = require("../../ErrorReplies"); + +export default function (vars: ServerVariables) { + function handler(req: Express.Request, res: Express.Response): BluebirdPromise { + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + res.render("already-logged-in", { + logout_endpoint: Endpoints.LOGOUT_GET, + username: authSession.userid, + redirection_url: vars.config.default_redirection_url + }); + resolve(); + }) + .catch(ErrorReplies.replyWithError401(req, res, vars.logger)); + } + + return handler; +} diff --git a/themes/squares/server/src/lib/routes/logout/get.ts b/themes/squares/server/src/lib/routes/logout/get.ts new file mode 100644 index 000000000..4d5112146 --- /dev/null +++ b/themes/squares/server/src/lib/routes/logout/get.ts @@ -0,0 +1,20 @@ + +import express = require("express"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Constants = require("../../../../../shared/constants"); +import { ServerVariables } from "../../ServerVariables"; + +function getRedirectParam(req: express.Request) { + return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; +} + +export default function (vars: ServerVariables) { + return function(req: express.Request, res: express.Response) { + const redirect_param = getRedirectParam(req); + const redirect_url = redirect_param || "/"; + AuthenticationSessionHandler.reset(req); + res.redirect(redirect_url); + }; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/password-reset/constants.ts b/themes/squares/server/src/lib/routes/password-reset/constants.ts new file mode 100644 index 000000000..5c639e92a --- /dev/null +++ b/themes/squares/server/src/lib/routes/password-reset/constants.ts @@ -0,0 +1,2 @@ + +export const CHALLENGE = "reset-password"; \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/password-reset/form/post.spec.ts b/themes/squares/server/src/lib/routes/password-reset/form/post.spec.ts new file mode 100644 index 000000000..ed029c906 --- /dev/null +++ b/themes/squares/server/src/lib/routes/password-reset/form/post.spec.ts @@ -0,0 +1,122 @@ + +import PasswordResetFormPost = require("./post"); +import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; +import { UserDataStore } from "../../../storage/UserDataStore"; +import Sinon = require("sinon"); +import Assert = require("assert"); +import BluebirdPromise = require("bluebird"); +import ExpressMock = require("../../../stubs/express.spec"); +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../ServerVariables"; +import { Level } from "../../../authentication/Level"; + +describe("routes/password-reset/form/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let vars: ServerVariables; + let mocks: ServerVariablesMock; + let authSession: AuthenticationSession; + + beforeEach(function () { + req = { + originalUrl: "/api/password-reset", + body: { + userid: "user" + }, + session: {}, + headers: { + host: "localhost" + } + }; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + + mocks.config.authentication_backend.ldap = { + url: "ldap://ldapjs", + mail_attribute: "mail", + user: "user", + password: "password", + additional_users_dn: "ou=users", + additional_groups_dn: "ou=groups", + base_dn: "dc=example,dc=com", + users_filter: "user", + group_name_attribute: "cn", + groups_filter: "groups" + }; + + res = ExpressMock.ResponseMock(); + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + authSession.userid = "user"; + authSession.email = "user@example.com"; + authSession.authentication_level = Level.ONE_FACTOR; + }); + + describe("test reset password post", () => { + it("should update the password and reset auth_session for reauthentication", function () { + req.body = {}; + req.body.password = "new-password"; + + mocks.usersDatabase.updatePasswordStub.returns(BluebirdPromise.resolve()); + + authSession.identity_check = { + userid: "user", + challenge: "reset-password" + }; + return PasswordResetFormPost.default(vars)(req as any, res as any) + .then(function () { + return AuthenticationSessionHandler.get(req as any, vars.logger); + }).then(function (_authSession) { + Assert.equal(res.status.getCall(0).args[0], 204); + Assert.equal(_authSession.authentication_level, Level.NOT_AUTHENTICATED); + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if identity_challenge does not exist", function () { + authSession.identity_check = { + userid: "user", + challenge: undefined + }; + return PasswordResetFormPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "An error occurred during password reset. Your password has not been changed." + }); + }); + }); + + it("should fail when ldap fails", function () { + req.body = {}; + req.body.password = "new-password"; + + mocks.usersDatabase.updatePasswordStub + .returns(BluebirdPromise.reject("Internal error with LDAP")); + + authSession.identity_check = { + challenge: "reset-password", + userid: "user" + }; + return PasswordResetFormPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "An error occurred during password reset. Your password has not been changed." + }); + return BluebirdPromise.resolve(); + }); + }); + }); +}); diff --git a/themes/squares/server/src/lib/routes/password-reset/form/post.ts b/themes/squares/server/src/lib/routes/password-reset/form/post.ts new file mode 100644 index 000000000..fccd7471b --- /dev/null +++ b/themes/squares/server/src/lib/routes/password-reset/form/post.ts @@ -0,0 +1,50 @@ + +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); +import exceptions = require("../../../Exceptions"); +import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; +import ErrorReplies = require("../../../ErrorReplies"); +import UserMessages = require("../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../ServerVariables"; + +import Constants = require("./../constants"); + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const newPassword = objectPath.get(req, "body.password"); + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (!authSession.identity_check) { + reject(new Error("No identity check initiated")); + return; + } + + vars.logger.info(req, "User %s wants to reset his/her password.", + authSession.identity_check.userid); + vars.logger.debug(req, "Challenge %s", authSession.identity_check.challenge); + + if (authSession.identity_check.challenge != Constants.CHALLENGE) { + reject(new Error("Bad challenge.")); + return; + } + resolve(); + }) + .then(function () { + return vars.usersDatabase.updatePassword(authSession.identity_check.userid, newPassword); + }) + .then(function () { + vars.logger.info(req, "Password reset for user '%s'", + authSession.identity_check.userid); + AuthenticationSessionHandler.reset(req); + res.status(204); + res.send(); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.RESET_PASSWORD_FAILED)); + }; +} diff --git a/themes/squares/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts b/themes/squares/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts new file mode 100644 index 000000000..ac6a41754 --- /dev/null +++ b/themes/squares/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts @@ -0,0 +1,92 @@ + +import PasswordResetHandler + from "./PasswordResetHandler"; +import { UserDataStore } from "../../../storage/UserDataStore"; +import Sinon = require("sinon"); +import winston = require("winston"); +import assert = require("assert"); +import BluebirdPromise = require("bluebird"); +import ExpressMock = require("../../../stubs/express.spec"); +import { ServerVariablesMock, ServerVariablesMockBuilder } + from "../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../ServerVariables"; + +describe("routes/password-reset/identity/PasswordResetHandler", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + req = { + originalUrl: "/non-api/xxx", + query: { + userid: "user" + }, + session: { + auth: { + userid: "user", + email: "user@example.com", + first_factor: true, + second_factor: false + } + }, + headers: { + host: "localhost" + } + }; + + const options = { + inMemoryOnly: true + }; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + mocks.userDataStore.saveU2FRegistrationStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub + .returns(BluebirdPromise.resolve({})); + res = ExpressMock.ResponseMock(); + }); + + describe("test reset password identity pre check", () => { + it("should fail when no userid is provided", function () { + req.query.userid = undefined; + const handler = new PasswordResetHandler(vars.logger, + vars.usersDatabase); + return handler.preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject("It should fail"); + }) + .catch(function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if ldap fail", function () { + mocks.usersDatabase.getEmailsStub + .returns(BluebirdPromise.reject("Internal error")); + new PasswordResetHandler(vars.logger, vars.usersDatabase) + .preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here")); + }, + function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should returns identity when ldap replies", function () { + mocks.usersDatabase.getEmailsStub + .returns(BluebirdPromise.resolve(["test@example.com"])); + return new PasswordResetHandler(vars.logger, vars.usersDatabase) + .preValidationInit(req as any); + }); + }); +}); diff --git a/themes/squares/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts b/themes/squares/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts new file mode 100644 index 000000000..42ae92cda --- /dev/null +++ b/themes/squares/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts @@ -0,0 +1,69 @@ +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); + +import exceptions = require("../../../Exceptions"); +import { Identity } from "../../../../../types/Identity"; +import { IdentityValidable } from "../../../IdentityValidable"; +import { PRE_VALIDATION_TEMPLATE } from "../../../IdentityCheckPreValidationTemplate"; +import Constants = require("../constants"); +import { IRequestLogger } from "../../../logging/IRequestLogger"; +import { IUsersDatabase } from "../../../authentication/backends/IUsersDatabase"; + +export const TEMPLATE_NAME = "password-reset-form"; + +export default class PasswordResetHandler implements IdentityValidable { + private logger: IRequestLogger; + private usersDatabase: IUsersDatabase; + + constructor(logger: IRequestLogger, usersDatabase: IUsersDatabase) { + this.logger = logger; + this.usersDatabase = usersDatabase; + } + + challenge(): string { + return Constants.CHALLENGE; + } + + preValidationInit(req: express.Request): BluebirdPromise { + const that = this; + const userid: string = + objectPath.get(req, "query.userid"); + return BluebirdPromise.resolve() + .then(function () { + that.logger.debug(req, "User '%s' requested a password reset", userid); + if (!userid) + return BluebirdPromise.reject( + new exceptions.AccessDeniedError("No user id provided")); + + return that.usersDatabase.getEmails(userid); + }) + .then(function (emails: string[]) { + if (!emails && emails.length <= 0) throw new Error("No email found"); + const identity = { + email: emails[0], + userid: userid + }; + return BluebirdPromise.resolve(identity); + }) + .catch(function (err: Error) { + return BluebirdPromise.reject(new exceptions.IdentityError(err.message)); + }); + } + + preValidationResponse(req: express.Request, res: express.Response) { + res.render(PRE_VALIDATION_TEMPLATE); + } + + postValidationInit(req: express.Request) { + return BluebirdPromise.resolve(); + } + + postValidationResponse(req: express.Request, res: express.Response) { + res.render(TEMPLATE_NAME); + } + + mailSubject(): string { + return "Reset your password"; + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/password-reset/request/get.ts b/themes/squares/server/src/lib/routes/password-reset/request/get.ts new file mode 100644 index 000000000..8f3ae2b4b --- /dev/null +++ b/themes/squares/server/src/lib/routes/password-reset/request/get.ts @@ -0,0 +1,13 @@ + +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); +import exceptions = require("../../../Exceptions"); + +import Constants = require("./../constants"); + +const TEMPLATE_NAME = "password-reset-request"; + +export default function (req: express.Request, res: express.Response) { + res.render(TEMPLATE_NAME); +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/secondfactor/get.spec.ts b/themes/squares/server/src/lib/routes/secondfactor/get.spec.ts new file mode 100644 index 000000000..6c77e1f69 --- /dev/null +++ b/themes/squares/server/src/lib/routes/secondfactor/get.spec.ts @@ -0,0 +1,44 @@ +import SecondFactorGet from "./get"; +import { ServerVariablesMockBuilder, ServerVariablesMock } + from "../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../ServerVariables"; +import Sinon = require("sinon"); +import ExpressMock = require("../../stubs/express.spec"); +import Assert = require("assert"); +import Endpoints = require("../../../../../shared/api"); +import BluebirdPromise = require("bluebird"); + +describe("routes/secondfactor/get", function () { + let mocks: ServerVariablesMock; + let vars: ServerVariables; + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false + } + }; + }); + + describe("test rendering", function () { + it("should render second factor page", function () { + req.session.auth.second_factor = false; + return SecondFactorGet(vars)(req as any, res as any) + .then(function () { + Assert(res.render.calledWith("secondfactor")); + return BluebirdPromise.resolve(); + }); + }); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/secondfactor/get.ts b/themes/squares/server/src/lib/routes/secondfactor/get.ts new file mode 100644 index 000000000..9f6deb4c6 --- /dev/null +++ b/themes/squares/server/src/lib/routes/secondfactor/get.ts @@ -0,0 +1,28 @@ + +import Express = require("express"); +import Endpoints = require("../../../../../shared/api"); +import BluebirdPromise = require("bluebird"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { ServerVariables } from "../../ServerVariables"; + +const TEMPLATE_NAME = "secondfactor"; + +export default function (vars: ServerVariables) { + function handler(req: Express.Request, res: Express.Response) + : BluebirdPromise { + + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + + res.render(TEMPLATE_NAME, { + username: authSession.userid, + totp_identity_start_endpoint: + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, + u2f_identity_start_endpoint: + Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET + }); + resolve(); + }); + } + return handler; +} diff --git a/themes/squares/server/src/lib/routes/secondfactor/redirect.spec.ts b/themes/squares/server/src/lib/routes/secondfactor/redirect.spec.ts new file mode 100644 index 000000000..ea66e6dca --- /dev/null +++ b/themes/squares/server/src/lib/routes/secondfactor/redirect.spec.ts @@ -0,0 +1,41 @@ +import Redirect from "./redirect"; +import ExpressMock = require("../../stubs/express.spec"); +import { ServerVariablesMockBuilder, ServerVariablesMock } +from "../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../ServerVariables"; +import Assert = require("assert"); + +describe("routes/secondfactor/redirect", function() { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + }); + + it("should redirect to default_redirection_url", function() { + vars.config.default_redirection_url = "http://default_redirection_url"; + Redirect(vars)(req as any, res as any) + .then(function() { + Assert(res.json.calledWith({ + redirect: "http://default_redirection_url" + })); + }); + }); + + it("should redirect to /", function() { + Redirect(vars)(req as any, res as any) + .then(function() { + Assert(res.json.calledWith({ + redirect: "/" + })); + }); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/secondfactor/redirect.ts b/themes/squares/server/src/lib/routes/secondfactor/redirect.ts new file mode 100644 index 000000000..5d84d9ebf --- /dev/null +++ b/themes/squares/server/src/lib/routes/secondfactor/redirect.ts @@ -0,0 +1,30 @@ + +import express = require("express"); +import objectPath = require("object-path"); +import Endpoints = require("../../../../../shared/api"); +import { ServerVariables } from "../../ServerVariables"; +import BluebirdPromise = require("bluebird"); +import ErrorReplies = require("../../ErrorReplies"); +import UserMessages = require("../../../../../shared/UserMessages"); +import { RedirectionMessage } from "../../../../../shared/RedirectionMessage"; +import Constants = require("../../../../../shared/constants"); + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response) + : BluebirdPromise { + + return new BluebirdPromise(function (resolve, reject) { + let redirectUrl: string = "/"; + if (vars.config.default_redirection_url) { + redirectUrl = vars.config.default_redirection_url; + } + vars.logger.debug(req, "Request redirection to \"%s\".", redirectUrl); + res.json({ + redirect: redirectUrl + } as RedirectionMessage); + return resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + }; +} diff --git a/themes/squares/server/src/lib/routes/secondfactor/totp/constants.ts b/themes/squares/server/src/lib/routes/secondfactor/totp/constants.ts new file mode 100644 index 000000000..7b5a1dcfe --- /dev/null +++ b/themes/squares/server/src/lib/routes/secondfactor/totp/constants.ts @@ -0,0 +1,4 @@ + +export const CHALLENGE = "totp-register"; +export const TEMPLATE_NAME = "totp-register"; + diff --git a/themes/squares/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts b/themes/squares/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts new file mode 100644 index 000000000..78b8ea3ea --- /dev/null +++ b/themes/squares/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts @@ -0,0 +1,116 @@ +import Sinon = require("sinon"); +import RegistrationHandler from "./RegistrationHandler"; +import { Identity } from "../../../../../../types/Identity"; +import { UserDataStore } from "../../../../storage/UserDataStore"; +import BluebirdPromise = require("bluebird"); +import ExpressMock = require("../../../../stubs/express.spec"); +import { ServerVariablesMock, ServerVariablesMockBuilder } + from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; +import Assert = require("assert"); + +describe("routes/secondfactor/totp/identity/RegistrationHandler", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + req.session = { + auth: { + userid: "user", + email: "user@example.com", + first_factor: true, + second_factor: false, + identity_check: { + userid: "user", + challenge: "totp-register" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.saveTOTPSecretStub + .returns(BluebirdPromise.resolve({})); + + res = ExpressMock.ResponseMock(); + }); + + describe("test totp registration pre validation", function () { + it("should fail if first_factor has not been passed", function () { + req.session.auth.first_factor = false; + return new RegistrationHandler(vars.logger, vars.userDataStore, + vars.totpHandler, vars.config.totp) + .preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("It should fail")); + }) + .catch(function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if userid is missing", function (done) { + req.session.auth.first_factor = false; + req.session.auth.userid = undefined; + + new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler, + vars.config.totp) + .preValidationInit(req as any) + .catch(function (err: Error) { + done(); + }); + }); + + it("should fail if email is missing", function (done) { + req.session.auth.first_factor = false; + req.session.auth.email = undefined; + + new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler, + vars.config.totp) + .preValidationInit(req as any) + .catch(function (err: Error) { + done(); + }); + }); + + it("should succeed if first factor passed, userid and email are provided", + function () { + return new RegistrationHandler(vars.logger, vars.userDataStore, + vars.totpHandler, vars.config.totp) + .preValidationInit(req as any); + }); + }); + + describe("test totp registration post validation", function () { + it("should generate a secret using userId as label and issuer defined in config", function () { + vars.config.totp = { + issuer: "issuer" + }; + return new RegistrationHandler(vars.logger, vars.userDataStore, + vars.totpHandler, vars.config.totp) + .postValidationResponse(req as any, res as any) + .then(function() { + Assert(mocks.totpHandler.generateStub.calledWithExactly("user", "issuer")); + }); + }); + }); +}); diff --git a/themes/squares/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts b/themes/squares/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts new file mode 100644 index 000000000..b39b6d045 --- /dev/null +++ b/themes/squares/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts @@ -0,0 +1,112 @@ + +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); + +import { Identity } from "../../../../../../types/Identity"; +import { IdentityValidable } from "../../../../IdentityValidable"; +import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; +import Constants = require("../constants"); +import Endpoints = require("../../../../../../../shared/api"); +import ErrorReplies = require("../../../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import FirstFactorValidator = require("../../../../FirstFactorValidator"); +import { IRequestLogger } from "../../../../logging/IRequestLogger"; +import { IUserDataStore } from "../../../../storage/IUserDataStore"; +import { ITotpHandler } from "../../../../authentication/totp/ITotpHandler"; +import { TOTPSecret } from "../../../../../../types/TOTPSecret"; +import { TotpConfiguration } from "../../../../configuration/schema/TotpConfiguration"; + + +export default class RegistrationHandler implements IdentityValidable { + private logger: IRequestLogger; + private userDataStore: IUserDataStore; + private totp: ITotpHandler; + private configuration: TotpConfiguration; + + constructor(logger: IRequestLogger, + userDataStore: IUserDataStore, + totp: ITotpHandler, configuration: TotpConfiguration) { + this.logger = logger; + this.userDataStore = userDataStore; + this.totp = totp; + this.configuration = configuration; + } + + challenge(): string { + return Constants.CHALLENGE; + } + + private retrieveIdentity(req: express.Request): BluebirdPromise { + const that = this; + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, that.logger); + const userid = authSession.userid; + const email = authSession.email; + + if (!(userid && email)) { + return reject(new Error("User ID or email is missing")); + } + + const identity = { + email: email, + userid: userid + }; + return resolve(identity); + }); + } + + preValidationInit(req: express.Request): BluebirdPromise { + const that = this; + return FirstFactorValidator.validate(req, this.logger) + .then(function () { + return that.retrieveIdentity(req); + }); + } + + preValidationResponse(req: express.Request, res: express.Response) { + res.render(PRE_VALIDATION_TEMPLATE); + } + + postValidationInit(req: express.Request) { + return FirstFactorValidator.validate(req, this.logger); + } + + postValidationResponse(req: express.Request, res: express.Response) + : BluebirdPromise { + const that = this; + let secret: TOTPSecret; + let userId: string; + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, that.logger); + userId = authSession.userid; + + if (authSession.identity_check.challenge != Constants.CHALLENGE + || !userId) + return reject(new Error("Bad challenge.")); + + resolve(); + }) + .then(function () { + secret = that.totp.generate(userId, + that.configuration.issuer); + that.logger.debug(req, "Save the TOTP secret in DB"); + return that.userDataStore.saveTOTPSecret(userId, secret); + }) + .then(function () { + AuthenticationSessionHandler.reset(req); + + res.render(Constants.TEMPLATE_NAME, { + base32_secret: secret.base32, + otpauth_url: secret.otpauth_url, + login_endpoint: Endpoints.FIRST_FACTOR_GET + }); + }) + .catch(ErrorReplies.replyWithError200(req, res, that.logger, UserMessages.OPERATION_FAILED)); + } + + mailSubject(): string { + return "Set up Authelia's one-time password"; + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts b/themes/squares/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts new file mode 100644 index 000000000..70a20d39d --- /dev/null +++ b/themes/squares/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts @@ -0,0 +1,76 @@ + +import BluebirdPromise = require("bluebird"); +import Sinon = require("sinon"); +import Assert = require("assert"); +import Exceptions = require("../../../../Exceptions"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import SignPost = require("./post"); +import { ServerVariables } from "../../../../ServerVariables"; + +import ExpressMock = require("../../../../stubs/express.spec"); +import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; +import { Level } from "../../../../authentication/Level"; + +describe("routes/secondfactor/totp/sign/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let authSession: AuthenticationSession; + let vars: ServerVariables; + let mocks: ServerVariablesMock; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + vars = s.variables; + mocks = s.mocks; + const app_get = Sinon.stub(); + req = { + originalUrl: "/api/totp-register", + app: {}, + body: { + token: "abc" + }, + session: {}, + query: { + redirect: "http://redirect" + } + }; + res = ExpressMock.ResponseMock(); + + const doc = { + userid: "user", + secret: { + base32: "ABCDEF" + } + }; + mocks.userDataStore.retrieveTOTPSecretStub.returns(BluebirdPromise.resolve(doc)); + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + authSession.userid = "user"; + authSession.authentication_level = Level.ONE_FACTOR; + }); + + + it("should send status code 200 when totp is valid", function () { + mocks.totpHandler.validateStub.returns(true); + return SignPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(authSession.authentication_level, Level.TWO_FACTOR); + return BluebirdPromise.resolve(); + }); + }); + + it("should send error message when totp is not valid", function () { + mocks.totpHandler.validateStub.returns(false); + return SignPost.default(vars)(req as any, res as any) + .then(function () { + Assert.notEqual(authSession.authentication_level, Level.TWO_FACTOR); + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); +}); + diff --git a/themes/squares/server/src/lib/routes/secondfactor/totp/sign/post.ts b/themes/squares/server/src/lib/routes/secondfactor/totp/sign/post.ts new file mode 100644 index 000000000..34a276d12 --- /dev/null +++ b/themes/squares/server/src/lib/routes/secondfactor/totp/sign/post.ts @@ -0,0 +1,42 @@ +import Bluebird = require("bluebird"); +import Express = require("express"); + +import { TOTPSecretDocument } from "../../../../storage/TOTPSecretDocument"; +import Endpoints = require("../../../../../../../shared/api"); +import Redirect from "../../redirect"; +import ErrorReplies = require("../../../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../../ServerVariables"; +import { Level } from "../../../../authentication/Level"; + +const UNAUTHORIZED_MESSAGE = "Unauthorized access"; + +export default function (vars: ServerVariables) { + function handler(req: Express.Request, res: Express.Response): Bluebird { + let authSession: AuthenticationSession; + const token = req.body.token; + + return new Bluebird(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + vars.logger.info(req, "Initiate TOTP validation for user \"%s\".", authSession.userid); + resolve(); + }) + .then(function () { + return vars.userDataStore.retrieveTOTPSecret(authSession.userid); + }) + .then(function (doc: TOTPSecretDocument) { + if (!vars.totpHandler.validate(token, doc.secret.base32)) + return Bluebird.reject(new Error("Invalid TOTP token.")); + + vars.logger.debug(req, "TOTP validation succeeded."); + authSession.authentication_level = Level.TWO_FACTOR; + Redirect(vars)(req, res); + return Bluebird.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + return handler; +} diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts new file mode 100644 index 000000000..7f16c0ee8 --- /dev/null +++ b/themes/squares/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts @@ -0,0 +1,11 @@ + +import util = require("util"); +import express = require("express"); + +function extract_app_id(req: express.Request): string { + return util.format("https://%s", req.headers.host); +} + +export = { + extract_app_id: extract_app_id +}; \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts new file mode 100644 index 000000000..a54bfbfe6 --- /dev/null +++ b/themes/squares/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts @@ -0,0 +1,96 @@ +import Sinon = require("sinon"); +import Assert = require("assert"); +import BluebirdPromise = require("bluebird"); + +import { Identity } from "../../../../../../types/Identity"; +import RegistrationHandler from "./RegistrationHandler"; +import ExpressMock = require("../../../../stubs/express.spec"); +import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; + +describe("routes/secondfactor/u2f/identity/RegistrationHandler", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + req.app = {}; + req.session = { + auth: { + userid: "user", + email: "user@example.com", + first_factor: true, + second_factor: false + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + + res = ExpressMock.ResponseMock(); + res.send = Sinon.spy(); + res.json = Sinon.spy(); + res.status = Sinon.spy(); + }); + + describe("test u2f registration check", test_registration_check); + + function test_registration_check() { + it("should fail if first_factor has not been passed", function () { + req.session.auth.first_factor = false; + return new RegistrationHandler(vars.logger).preValidationInit(req as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if userid is missing", function () { + req.session.auth.first_factor = false; + req.session.auth.userid = undefined; + + return new RegistrationHandler(vars.logger).preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here")); + }, + function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if email is missing", function () { + req.session.auth.first_factor = false; + req.session.auth.email = undefined; + + return new RegistrationHandler(vars.logger).preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here")); + }, + function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should succeed if first factor passed, userid and email are provided", function () { + req.session.auth.first_factor = true; + req.session.auth.email = "admin@example.com"; + req.session.auth.userid = "user"; + return new RegistrationHandler(vars.logger).preValidationInit(req as any); + }); + } +}); diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts new file mode 100644 index 000000000..bc4713c77 --- /dev/null +++ b/themes/squares/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts @@ -0,0 +1,73 @@ + +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import objectPath = require("object-path"); + +import { IdentityValidable } from "../../../../IdentityValidable"; +import { Identity } from "../../../../../../types/Identity"; +import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; +import FirstFactorValidator = require("../../../../FirstFactorValidator"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { IRequestLogger } from "../../../../logging/IRequestLogger"; + +const CHALLENGE = "u2f-register"; +const MAIL_SUBJECT = "Register your security key with Authelia"; + +const POST_VALIDATION_TEMPLATE_NAME = "u2f-register"; + + +export default class RegistrationHandler implements IdentityValidable { + private logger: IRequestLogger; + + constructor(logger: IRequestLogger) { + this.logger = logger; + } + + challenge(): string { + return CHALLENGE; + } + + private retrieveIdentity(req: express.Request): BluebirdPromise { + const that = this; + return new BluebirdPromise(function(resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, that.logger); + const userid = authSession.userid; + const email = authSession.email; + + if (!(userid && email)) { + return reject(new Error("User ID or email is missing")); + } + + const identity = { + email: email, + userid: userid + }; + return resolve(identity); + }); + } + + preValidationInit(req: express.Request): BluebirdPromise { + const that = this; + return FirstFactorValidator.validate(req, this.logger) + .then(function () { + return that.retrieveIdentity(req); + }); + } + + preValidationResponse(req: express.Request, res: express.Response) { + res.render(PRE_VALIDATION_TEMPLATE); + } + + postValidationInit(req: express.Request) { + return FirstFactorValidator.validate(req, this.logger); + } + + postValidationResponse(req: express.Request, res: express.Response) { + res.render(POST_VALIDATION_TEMPLATE_NAME); + } + + mailSubject(): string { + return MAIL_SUBJECT; + } +} + diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts new file mode 100644 index 000000000..de3347a21 --- /dev/null +++ b/themes/squares/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts @@ -0,0 +1,146 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import assert = require("assert"); +import U2FRegisterPost = require("./post"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import ExpressMock = require("../../../../stubs/express.spec"); +import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; + + +describe("routes/secondfactor/u2f/register/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + let authSession: AuthenticationSession; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.originalUrl = "/api/xxxx"; + req.app = {}; + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false, + identity_check: { + challenge: "u2f-register", + userid: "user" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + }); + + describe("test registration", test_registration); + + + function test_registration() { + it("should save u2f meta and return status code 200", function () { + const expectedStatus = { + keyHandle: "keyHandle", + publicKey: "pbk", + certificate: "cert" + }; + mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve(expectedStatus)); + + authSession.register_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { + assert.equal("user", mocks.userDataStore.saveU2FRegistrationStub.getCall(0).args[0]); + assert.equal(authSession.identity_check, undefined); + }); + }); + + it("should return error message on finishRegistration error", function () { + mocks.u2f.checkRegistrationStub.returns({ errorCode: 500 }); + + authSession.register_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function () { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); + + it("should return error message when register_request is not provided", function () { + mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); + authSession.register_request = undefined; + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function () { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); + + it("should return error message when no auth request has been initiated", function () { + mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); + authSession.register_request = undefined; + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function () { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); + + it("should return error message when identity has not been verified", function () { + authSession.identity_check = undefined; + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function () { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); + } +}); + diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/register/post.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/register/post.ts new file mode 100644 index 000000000..7296ccbe5 --- /dev/null +++ b/themes/squares/server/src/lib/routes/secondfactor/u2f/register/post.ts @@ -0,0 +1,64 @@ + +import { UserDataStore } from "../../../../storage/UserDataStore"; +import objectPath = require("object-path"); +import u2f_common = require("../U2FCommon"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import U2f = require("u2f"); +import { U2FRegistration } from "../../../../../../types/U2FRegistration"; +import redirect from "../../redirect"; +import ErrorReplies = require("../../../../ErrorReplies"); +import { ServerVariables } from "../../../../ServerVariables"; +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; + + +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const appid = u2f_common.extract_app_id(req); + const registrationResponse: U2f.RegistrationData = req.body; + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + const registrationRequest = authSession.register_request; + + if (!registrationRequest) { + return reject(new Error("No registration request")); + } + + if (!authSession.identity_check + || authSession.identity_check.challenge != "u2f-register") { + return reject(new Error("Bad challenge for registration request")); + } + + vars.logger.info(req, "Finishing registration"); + vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); + vars.logger.debug(req, "RegistrationResponse = %s", JSON.stringify(registrationResponse)); + + return resolve(vars.u2f.checkRegistration(registrationRequest, registrationResponse)); + }) + .then(function (u2fResult: U2f.RegistrationResult | U2f.Error): BluebirdPromise { + if (objectPath.has(u2fResult, "errorCode")) + return BluebirdPromise.reject(new Error("Error while registering.")); + + const registrationResult: U2f.RegistrationResult = u2fResult as U2f.RegistrationResult; + vars.logger.info(req, "Store registration and reply"); + vars.logger.debug(req, "RegistrationResult = %s", JSON.stringify(registrationResult)); + const registration: U2FRegistration = { + keyHandle: registrationResult.keyHandle, + publicKey: registrationResult.publicKey + }; + return vars.userDataStore.saveU2FRegistration(authSession.userid, appid, registration); + }) + .then(function () { + authSession.identity_check = undefined; + redirect(vars)(req, res); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + return handler; +} diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts new file mode 100644 index 000000000..a207c9109 --- /dev/null +++ b/themes/squares/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts @@ -0,0 +1,86 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import Assert = require("assert"); +import U2FRegisterRequestGet = require("./get"); +import ExpressMock = require("../../../../stubs/express.spec"); +import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; + +describe("routes/secondfactor/u2f/register_request/get", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.originalUrl = "/api/xxxx"; + req.app = {}; + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false, + identity_check: { + challenge: "u2f-register", + userid: "user" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + }); + + describe("test registration request", () => { + it("should send back the registration request and save it in the session", function () { + const expectedRequest = { + test: "abc" + }; + mocks.u2f.requestStub.returns(BluebirdPromise.resolve(expectedRequest)); + return U2FRegisterRequestGet.default(vars)(req as any, res as any) + .then(function () { + Assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); + }); + }); + + it("should return internal error on registration request", function () { + res.send = sinon.spy(); + const user_key_container = {}; + mocks.u2f.requestStub.returns(BluebirdPromise.reject("Internal error")); + return U2FRegisterRequestGet.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + }); + }); + + it("should return forbidden if identity has not been verified", function () { + req.session.auth.identity_check = undefined; + return U2FRegisterRequestGet.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(403, res.status.getCall(0).args[0]); + }); + }); + }); +}); + diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/register_request/get.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/register_request/get.ts new file mode 100644 index 000000000..f611af933 --- /dev/null +++ b/themes/squares/server/src/lib/routes/secondfactor/u2f/register_request/get.ts @@ -0,0 +1,43 @@ + +import { UserDataStore } from "../../../../storage/UserDataStore"; + +import objectPath = require("object-path"); +import u2f_common = require("../U2FCommon"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import U2f = require("u2f"); +import ErrorReplies = require("../../../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../../ServerVariables"; + +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const appid: string = u2f_common.extract_app_id(req); + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (!authSession.identity_check + || authSession.identity_check.challenge != "u2f-register") { + res.status(403); + res.send(); + return reject(new Error("Bad challenge.")); + } + + vars.logger.info(req, "Starting registration for appId '%s'", appid); + return resolve(vars.u2f.request(appid)); + }) + .then(function (registrationRequest: U2f.Request) { + vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); + authSession.register_request = registrationRequest; + res.json(registrationRequest); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + + return handler; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts new file mode 100644 index 000000000..9b137e66d --- /dev/null +++ b/themes/squares/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts @@ -0,0 +1,101 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import Assert = require("assert"); +import U2FSignPost = require("./post"); +import { ServerVariables } from "../../../../ServerVariables"; +import winston = require("winston"); + +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; +import ExpressMock = require("../../../../stubs/express.spec"); +import U2FMock = require("../../../../stubs/u2f.spec"); +import U2f = require("u2f"); +import { Level } from "../../../../authentication/Level"; + +describe("routes/secondfactor/u2f/sign/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.app = {}; + req.originalUrl = "/api/xxxx"; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req.session = { + auth: { + userid: "user", + authentication_level: Level.ONE_FACTOR, + identity_check: { + challenge: "u2f-register", + userid: "user" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const options = { + inMemoryOnly: true + }; + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + }); + + it("should return status code 204", function () { + const expectedStatus = { + keyHandle: "keyHandle", + publicKey: "pbk", + certificate: "cert" + }; + mocks.u2f.checkSignatureStub.returns(expectedStatus); + + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ + registration: { + publicKey: "PUBKEY" + } + })); + + req.session.auth.sign_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + return U2FSignPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(req.session.auth.authentication_level, Level.TWO_FACTOR); + }); + }); + + it("should return unauthorized error on registration request internal error", function () { + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ + registration: { + publicKey: "PUBKEY" + } + })); + mocks.u2f.checkSignatureStub.returns({ errorCode: 500 }); + + req.session.auth.sign_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + return U2FSignPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], + { error: "Operation failed." }); + }); + }); +}); + diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/sign/post.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/sign/post.ts new file mode 100644 index 000000000..7ee711c2c --- /dev/null +++ b/themes/squares/server/src/lib/routes/secondfactor/u2f/sign/post.ts @@ -0,0 +1,57 @@ + +import objectPath = require("object-path"); +import u2f_common = require("../U2FCommon"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import { UserDataStore } from "../../../../storage/UserDataStore"; +import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; +import { Winston } from "../../../../../../types/Dependencies"; +import U2f = require("u2f"); +import exceptions = require("../../../../Exceptions"); +import redirect from "../../redirect"; +import ErrorReplies = require("../../../../ErrorReplies"); +import { ServerVariables } from "../../../../ServerVariables"; +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import { Level } from "../../../../authentication/Level"; + +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const appId = u2f_common.extract_app_id(req); + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (!authSession.sign_request) { + const err = new Error("No sign request"); + ErrorReplies.replyWithError401(req, res, vars.logger)(err); + return reject(err); + } + resolve(); + }) + .then(function () { + const userid = authSession.userid; + return vars.userDataStore.retrieveU2FRegistration(userid, appId); + }) + .then(function (doc: U2FRegistrationDocument): BluebirdPromise { + const signRequest = authSession.sign_request; + const signData: U2f.SignatureData = req.body; + vars.logger.info(req, "Finish authentication"); + return BluebirdPromise.resolve(vars.u2f.checkSignature(signRequest, signData, doc.registration.publicKey)); + }) + .then(function (result: U2f.SignatureResult | U2f.Error): BluebirdPromise { + if (objectPath.has(result, "errorCode")) + return BluebirdPromise.reject(new Error("Error while signing")); + vars.logger.info(req, "Successful authentication"); + authSession.authentication_level = Level.TWO_FACTOR; + redirect(vars)(req, res); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + + return handler; +} + diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts new file mode 100644 index 000000000..dd52b27e0 --- /dev/null +++ b/themes/squares/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts @@ -0,0 +1,68 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import assert = require("assert"); +import U2FSignRequestGet = require("./get"); +import ExpressMock = require("../../../../stubs/express.spec"); +import { Request } from "u2f"; +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; + +import { SignMessage } from "../../../../../../../shared/SignMessage"; + +describe("routes/secondfactor/u2f/sign_request/get", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.originalUrl = "/api/xxxx"; + req.app = {}; + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false, + identity_check: { + challenge: "u2f-register", + userid: "user" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + }); + + it("should send back the sign request and save it in the session", function () { + const expectedRequest: Request = { + version: "U2F_V2", + appId: 'app', + challenge: 'challenge!' + }; + mocks.u2f.requestStub.returns(expectedRequest); + mocks.userDataStore.retrieveU2FRegistrationStub + .returns(BluebirdPromise.resolve({ + registration: { + keyHandle: "KeyHandle" + } + })); + + return U2FSignRequestGet.default(vars)(req as any, res as any) + .then(() => { + assert.deepEqual(expectedRequest, req.session.auth.sign_request); + assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); + }); + }); +}); + diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts new file mode 100644 index 000000000..9e93dde06 --- /dev/null +++ b/themes/squares/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts @@ -0,0 +1,42 @@ + +import u2f_common = require("../../../secondfactor/u2f/U2FCommon"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; +import exceptions = require("../../../../Exceptions"); +import ErrorReplies = require("../../../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../../ServerVariables"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; + +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const appId = u2f_common.extract_app_id(req); + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + resolve(); + }) + .then(function () { + return vars.userDataStore.retrieveU2FRegistration(authSession.userid, appId); + }) + .then(function (doc: U2FRegistrationDocument): BluebirdPromise { + if (!doc) + return BluebirdPromise.reject(new exceptions.AccessDeniedError("No U2F registration document found.")); + + const appId: string = u2f_common.extract_app_id(req); + vars.logger.info(req, "Start authentication of app '%s'", appId); + vars.logger.debug(req, "AppId = %s, keyHandle = %s", appId, JSON.stringify(doc.registration.keyHandle)); + + const request = vars.u2f.request(appId, doc.registration.keyHandle); + authSession.sign_request = request; + res.json(request); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + return handler; +} diff --git a/themes/squares/server/src/lib/routes/verify/access_control.ts b/themes/squares/server/src/lib/routes/verify/access_control.ts new file mode 100644 index 000000000..136239aeb --- /dev/null +++ b/themes/squares/server/src/lib/routes/verify/access_control.ts @@ -0,0 +1,51 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import Util = require("util"); + +import Exceptions = require("../../Exceptions"); + +import { Level as AuthorizationLevel } from "../../authorization/Level"; +import { Level as AuthenticationLevel } from "../../authentication/Level"; +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { ServerVariables } from "../../ServerVariables"; + +function isAuthorized( + authorization: AuthorizationLevel, + authentication: AuthenticationLevel): boolean { + + if (authorization == AuthorizationLevel.BYPASS) { + return true; + } else if (authorization == AuthorizationLevel.ONE_FACTOR && + authentication >= AuthenticationLevel.ONE_FACTOR) { + return true; + } else if (authorization == AuthorizationLevel.TWO_FACTOR && + authentication >= AuthenticationLevel.TWO_FACTOR) { + return true; + } + return false; +} + +export default function ( + req: Express.Request, + vars: ServerVariables, + domain: string, resource: string, + user: string, groups: string[], + authenticationLevel: AuthenticationLevel) { + + return new BluebirdPromise(function (resolve, reject) { + const authorizationLevel = vars.authorizer + .authorization({domain, resource}, {user, groups}); + + if (!isAuthorized(authorizationLevel, authenticationLevel)) { + if (authorizationLevel == AuthorizationLevel.DENY) { + reject(new Exceptions.NotAuthorizedError( + Util.format("User %s is not authorized to access %s%s", user, domain, resource))); + return; + } + reject(new Exceptions.NotAuthenticatedError(Util.format( + "User '%s' is not sufficiently authorized to access %s%s.", user, domain, resource))); + return; + } + resolve(); + }); +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/verify/get.spec.ts b/themes/squares/server/src/lib/routes/verify/get.spec.ts new file mode 100644 index 000000000..67cf19fb6 --- /dev/null +++ b/themes/squares/server/src/lib/routes/verify/get.spec.ts @@ -0,0 +1,320 @@ + +import Assert = require("assert"); +import BluebirdPromise = require("bluebird"); +import Express = require("express"); +import Sinon = require("sinon"); +import winston = require("winston"); + +import VerifyGet = require("./get"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../types/AuthenticationSession"; +import ExpressMock = require("../../stubs/express.spec"); +import { ServerVariables } from "../../ServerVariables"; +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../ServerVariablesMockBuilder.spec"; +import { Level } from "../../authentication/Level"; +import { Level as AuthorizationLevel } from "../../authorization/Level"; + +describe("routes/verify/get", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + let authSession: AuthenticationSession; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + req.originalUrl = "/api/xxxx"; + req.query = { + redirect: "undefined" + }; + AuthenticationSessionHandler.reset(req as any); + req.headers["x-original-url"] = "https://secret.example.com/"; + const s = ServerVariablesMockBuilder.build(false); + mocks = s.mocks; + vars = s.variables; + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + }); + + describe("with session cookie", function () { + it("should be already authenticated", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + authSession.authentication_level = Level.TWO_FACTOR; + authSession.userid = "myuser"; + authSession.groups = ["mygroup", "othergroup"]; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser"); + Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); + Assert.equal(204, res.status.getCall(0).args[0]); + }); + }); + + function test_session(_authSession: AuthenticationSession, status_code: number) { + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert.equal(status_code, res.status.getCall(0).args[0]); + }); + } + + function test_non_authenticated_401(authSession: AuthenticationSession) { + return test_session(authSession, 401); + } + + function test_unauthorized_403(authSession: AuthenticationSession) { + return test_session(authSession, 403); + } + + function test_authorized(authSession: AuthenticationSession) { + return test_session(authSession, 204); + } + + describe("given user tries to access a 2-factor endpoint", function () { + before(function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + }); + + describe("given different cases of session", function () { + it("should not be authenticated when second factor is missing", function () { + return test_non_authenticated_401({ + keep_me_logged_in: false, + userid: "user", + authentication_level: Level.ONE_FACTOR, + email: undefined, + groups: [], + last_activity_datetime: new Date().getTime() + }); + }); + + it("should not be authenticated when userid is missing", function () { + return test_non_authenticated_401({ + keep_me_logged_in: false, + userid: undefined, + authentication_level: Level.TWO_FACTOR, + email: undefined, + groups: [], + last_activity_datetime: new Date().getTime() + }); + }); + + it("should not be authenticated when level is insufficient", function () { + return test_non_authenticated_401({ + keep_me_logged_in: false, + userid: "user", + authentication_level: Level.NOT_AUTHENTICATED, + email: undefined, + groups: [], + last_activity_datetime: new Date().getTime() + }); + }); + + it("should not be authenticated when session has not be initiated", function () { + return test_non_authenticated_401(undefined); + }); + + it("should not be authenticated when domain is not allowed for user", function () { + authSession.authentication_level = Level.TWO_FACTOR; + authSession.userid = "myuser"; + req.headers["x-original-url"] = "https://test.example.com/"; + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.DENY); + + return test_unauthorized_403({ + keep_me_logged_in: false, + authentication_level: Level.TWO_FACTOR, + userid: "user", + groups: ["group1", "group2"], + email: undefined, + last_activity_datetime: new Date().getTime() + }); + }); + }); + }); + + describe("given user tries to access a single factor endpoint", function () { + beforeEach(function () { + req.headers["x-original-url"] = "https://redirect.url/"; + }); + + it("should be authenticated when first factor is validated", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); + authSession.authentication_level = Level.ONE_FACTOR; + authSession.userid = "user1"; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWith(204)); + Assert(res.send.calledOnce); + }); + }); + + it("should be rejected with 401 when not authenticated", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); + authSession.authentication_level = Level.NOT_AUTHENTICATED; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWith(401)); + }); + }); + }); + + describe("inactivity period", function () { + it("should update last inactivity period on requests on /api/verify", function () { + mocks.config.session.inactivity = 200000; + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + const currentTime = new Date().getTime() - 1000; + AuthenticationSessionHandler.reset(req as any); + authSession.authentication_level = Level.TWO_FACTOR; + authSession.userid = "myuser"; + authSession.groups = ["mygroup", "othergroup"]; + authSession.last_activity_datetime = currentTime; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + return AuthenticationSessionHandler.get(req as any, vars.logger); + }) + .then(function (authSession) { + Assert(authSession.last_activity_datetime > currentTime); + }); + }); + + it("should reset session when max inactivity period has been reached", function () { + mocks.config.session.inactivity = 1; + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + const currentTime = new Date().getTime() - 1000; + AuthenticationSessionHandler.reset(req as any); + authSession.authentication_level = Level.TWO_FACTOR; + authSession.userid = "myuser"; + authSession.groups = ["mygroup", "othergroup"]; + authSession.last_activity_datetime = currentTime; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + return AuthenticationSessionHandler.get(req as any, vars.logger); + }) + .then(function (authSession) { + Assert.equal(authSession.authentication_level, Level.NOT_AUTHENTICATED); + Assert.equal(authSession.userid, undefined); + }); + }); + }); + }); + + describe("response type 401 | 302", function() { + it("should return error code 401", function() { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( + "Invalid credentials")); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should redirect to provided redirection url", function() { + const REDIRECT_URL = "http://redirection_url.com"; + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( + "Invalid credentials")); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + req.query["rd"] = REDIRECT_URL; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.redirect.calledWithExactly(REDIRECT_URL)); + }); + }); + }); + + describe("with basic auth", function () { + it("should authenticate correctly", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.returns({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "john"); + Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); + Assert.equal(204, res.status.getCall(0).args[0]); + }); + }); + + it("should fail when endpoint is protected by two factors", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.config.access_control.rules = [{ + domain: "secret.example.com", + policy: "two_factor" + }]; + mocks.usersDatabase.checkUserPasswordStub.resolves({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should fail when base64 token is not valid", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.resolves({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic i_m*not_a_base64*token"; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should fail when base64 token has not format user:psswd", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.resolves({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzOmJhZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should fail when bad user password is provided", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( + "Invalid credentials")); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should fail when resource is restricted", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.resolves({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + }); +}); + diff --git a/themes/squares/server/src/lib/routes/verify/get.ts b/themes/squares/server/src/lib/routes/verify/get.ts new file mode 100644 index 000000000..f73861696 --- /dev/null +++ b/themes/squares/server/src/lib/routes/verify/get.ts @@ -0,0 +1,91 @@ +import BluebirdPromise = require("bluebird"); +import Express = require("express"); +import Exceptions = require("../../Exceptions"); +import ErrorReplies = require("../../ErrorReplies"); +import { ServerVariables } from "../../ServerVariables"; +import GetWithSessionCookieMethod from "./get_session_cookie"; +import GetWithBasicAuthMethod from "./get_basic_auth"; +import Constants = require("../../../../../shared/constants"); +import ObjectPath = require("object-path"); + +import { AuthenticationSessionHandler } + from "../../AuthenticationSessionHandler"; +import { AuthenticationSession } + from "../../../../types/AuthenticationSession"; + +const REMOTE_USER = "Remote-User"; +const REMOTE_GROUPS = "Remote-Groups"; + + +function verifyWithSelectedMethod(req: Express.Request, res: Express.Response, + vars: ServerVariables, authSession: AuthenticationSession) + : () => BluebirdPromise<{ username: string, groups: string[] }> { + return function () { + const authorization: string = "" + req.headers["proxy-authorization"]; + if (authorization && authorization.startsWith("Basic ")) + return GetWithBasicAuthMethod(req, res, vars, authorization); + + return GetWithSessionCookieMethod(req, res, vars, authSession); + }; +} + +function setRedirectHeader(req: Express.Request, res: Express.Response) { + return function () { + const originalUrl = ObjectPath.get( + req, "headers.x-original-url"); + res.set("Redirect", originalUrl); + return BluebirdPromise.resolve(); + }; +} + +function setUserAndGroupsHeaders(res: Express.Response) { + return function (u: { username: string, groups: string[] }) { + res.setHeader(REMOTE_USER, u.username); + res.setHeader(REMOTE_GROUPS, u.groups.join(",")); + return BluebirdPromise.resolve(); + }; +} + +function replyWith200(res: Express.Response) { + return function () { + res.status(204); + res.send(); + }; +} + +function getRedirectParam(req: Express.Request) { + return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; +} + +export default function (vars: ServerVariables) { + return function (req: Express.Request, res: Express.Response) + : BluebirdPromise { + let authSession: AuthenticationSession; + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + resolve(); + }) + .then(setRedirectHeader(req, res)) + .then(verifyWithSelectedMethod(req, res, vars, authSession)) + .then(setUserAndGroupsHeaders(res)) + .then(replyWith200(res)) + // The user is authenticated but has restricted access -> 403 + .catch(Exceptions.NotAuthorizedError, + ErrorReplies.replyWithError403(req, res, vars.logger)) + .catch(Exceptions.NotAuthenticatedError, + ErrorReplies.replyWithError401(req, res, vars.logger)) + // The user is not yet authenticated -> 401 + .catch((err) => { + const redirectUrl = getRedirectParam(req); + if (redirectUrl) { + ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err); + } + else { + ErrorReplies.replyWithError401(req, res, vars.logger)(err); + } + }); + }; +} + diff --git a/themes/squares/server/src/lib/routes/verify/get_basic_auth.ts b/themes/squares/server/src/lib/routes/verify/get_basic_auth.ts new file mode 100644 index 000000000..af23c76c9 --- /dev/null +++ b/themes/squares/server/src/lib/routes/verify/get_basic_auth.ts @@ -0,0 +1,55 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import ObjectPath = require("object-path"); +import { ServerVariables } from "../../ServerVariables"; +import { AuthenticationSession } + from "../../../../types/AuthenticationSession"; +import AccessControl from "./access_control"; +import { URLDecomposer } from "../../utils/URLDecomposer"; +import { Level } from "../../authentication/Level"; + +export default function (req: Express.Request, res: Express.Response, + vars: ServerVariables, authorizationHeader: string) + : BluebirdPromise<{ username: string, groups: string[] }> { + let username: string; + const uri = ObjectPath.get(req, "headers.x-original-url"); + const urlDecomposition = URLDecomposer.fromUrl(uri); + + return BluebirdPromise.resolve() + .then(() => { + const base64Re = new RegExp("^Basic ((?:[A-Za-z0-9+/]{4})*" + + "(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)$"); + const isTokenValidBase64 = base64Re.test(authorizationHeader); + + if (!isTokenValidBase64) { + return BluebirdPromise.reject(new Error("No valid base64 token found in the header")); + } + + const tokenMatches = authorizationHeader.match(base64Re); + const base64Token = tokenMatches[1]; + const decodedToken = Buffer.from(base64Token, "base64").toString(); + const splittedToken = decodedToken.split(":"); + + if (splittedToken.length != 2) { + return BluebirdPromise.reject(new Error( + "The authorization token is invalid. Expecting 'userid:password'")); + } + + username = splittedToken[0]; + const password = splittedToken[1]; + return vars.usersDatabase.checkUserPassword(username, password); + }) + .then(function (groupsAndEmails) { + return AccessControl(req, vars, urlDecomposition.domain, urlDecomposition.path, + username, groupsAndEmails.groups, Level.ONE_FACTOR) + .then(() => BluebirdPromise.resolve({ + username: username, + groups: groupsAndEmails.groups + })); + }) + .catch(function (err: Error) { + return BluebirdPromise.reject( + new Error("Unable to authenticate the user with basic auth. Cause: " + + err.message)); + }); +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/verify/get_session_cookie.ts b/themes/squares/server/src/lib/routes/verify/get_session_cookie.ts new file mode 100644 index 000000000..070344812 --- /dev/null +++ b/themes/squares/server/src/lib/routes/verify/get_session_cookie.ts @@ -0,0 +1,78 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import Util = require("util"); +import ObjectPath = require("object-path"); + +import Exceptions = require("../../Exceptions"); +import { Configuration } from "../../configuration/schema/Configuration"; +import { ServerVariables } from "../../ServerVariables"; +import { IRequestLogger } from "../../logging/IRequestLogger"; +import { AuthenticationSession } + from "../../../../types/AuthenticationSession"; +import { AuthenticationSessionHandler } + from "../../AuthenticationSessionHandler"; +import AccessControl from "./access_control"; +import { URLDecomposer } from "../../utils/URLDecomposer"; + +function verify_inactivity(req: Express.Request, + authSession: AuthenticationSession, + configuration: Configuration, logger: IRequestLogger) + : BluebirdPromise { + + // If inactivity is not specified, then inactivity timeout does not apply + if (!configuration.session.inactivity || authSession.keep_me_logged_in) { + return BluebirdPromise.resolve(); + } + + const lastActivityTime = authSession.last_activity_datetime; + const currentTime = new Date().getTime(); + authSession.last_activity_datetime = currentTime; + + const inactivityPeriodMs = currentTime - lastActivityTime; + logger.debug(req, "Inactivity period was %s s and max period was %s.", + inactivityPeriodMs / 1000, configuration.session.inactivity / 1000); + if (inactivityPeriodMs < configuration.session.inactivity) { + return BluebirdPromise.resolve(); + } + + logger.debug(req, "Session has been reset after too long inactivity period."); + AuthenticationSessionHandler.reset(req); + return BluebirdPromise.reject(new Error("Inactivity period exceeded.")); +} + +export default function (req: Express.Request, res: Express.Response, + vars: ServerVariables, authSession: AuthenticationSession) + : BluebirdPromise<{ username: string, groups: string[] }> { + + return BluebirdPromise.resolve() + .then(() => { + const username = authSession.userid; + const groups = authSession.groups; + + if (!authSession.userid) { + return BluebirdPromise.reject(new Exceptions.AccessDeniedError( + "userid is missing")); + } + + const originalUrl = ObjectPath.get( + req, "headers.x-original-url"); + const originalUri = + ObjectPath.get(req, "headers.x-original-uri"); + + const d = URLDecomposer.fromUrl(originalUrl); + vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", d.domain, + d.path, username, groups.join(",")); + return AccessControl(req, vars, d.domain, d.path, username, groups, + authSession.authentication_level); + }) + .then(() => { + return verify_inactivity(req, authSession, + vars.config, vars.logger); + }) + .then(() => { + return BluebirdPromise.resolve({ + username: authSession.userid, + groups: authSession.groups + }); + }); +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/AuthenticationTraceDocument.d.ts b/themes/squares/server/src/lib/storage/AuthenticationTraceDocument.d.ts new file mode 100644 index 000000000..69818c055 --- /dev/null +++ b/themes/squares/server/src/lib/storage/AuthenticationTraceDocument.d.ts @@ -0,0 +1,6 @@ + +export interface AuthenticationTraceDocument { + userId: string; + date: Date; + isAuthenticationSuccessful: boolean; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/CollectionFactoryFactory.ts b/themes/squares/server/src/lib/storage/CollectionFactoryFactory.ts new file mode 100644 index 000000000..92b29abfa --- /dev/null +++ b/themes/squares/server/src/lib/storage/CollectionFactoryFactory.ts @@ -0,0 +1,15 @@ +import { ICollectionFactory } from "./ICollectionFactory"; +import { NedbCollectionFactory } from "./nedb/NedbCollectionFactory"; +import { MongoCollectionFactory } from "./mongo/MongoCollectionFactory"; +import { IMongoClient } from "../connectors/mongo/IMongoClient"; + + +export class CollectionFactoryFactory { + static createNedb(options: Nedb.DataStoreOptions): ICollectionFactory { + return new NedbCollectionFactory(options); + } + + static createMongo(client: IMongoClient): ICollectionFactory { + return new MongoCollectionFactory(client); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/CollectionFactoryStub.spec.ts b/themes/squares/server/src/lib/storage/CollectionFactoryStub.spec.ts new file mode 100644 index 000000000..17f8bb021 --- /dev/null +++ b/themes/squares/server/src/lib/storage/CollectionFactoryStub.spec.ts @@ -0,0 +1,16 @@ +import BluebirdPromise = require("bluebird"); +import Sinon = require("sinon"); +import { ICollection } from "./ICollection"; +import { ICollectionFactory } from "./ICollectionFactory"; + +export class CollectionFactoryStub implements ICollectionFactory { + buildStub: Sinon.SinonStub; + + constructor() { + this.buildStub = Sinon.stub(); + } + + build(collectionName: string): ICollection { + return this.buildStub(collectionName); + } +} diff --git a/themes/squares/server/src/lib/storage/CollectionStub.spec.ts b/themes/squares/server/src/lib/storage/CollectionStub.spec.ts new file mode 100644 index 000000000..42895d672 --- /dev/null +++ b/themes/squares/server/src/lib/storage/CollectionStub.spec.ts @@ -0,0 +1,39 @@ +import BluebirdPromise = require("bluebird"); +import Sinon = require("sinon"); +import { ICollection } from "./ICollection"; + +export class CollectionStub implements ICollection { + findStub: Sinon.SinonStub; + findOneStub: Sinon.SinonStub; + updateStub: Sinon.SinonStub; + removeStub: Sinon.SinonStub; + insertStub: Sinon.SinonStub; + + constructor() { + this.findStub = Sinon.stub(); + this.findOneStub = Sinon.stub(); + this.updateStub = Sinon.stub(); + this.removeStub = Sinon.stub(); + this.insertStub = Sinon.stub(); + } + + find(filter: any, sortKeys: any, count: number): BluebirdPromise { + return this.findStub(filter, sortKeys, count); + } + + findOne(filter: any): BluebirdPromise { + return this.findOneStub(filter); + } + + update(filter: any, document: any, options: any): BluebirdPromise { + return this.updateStub(filter, document, options); + } + + remove(filter: any): BluebirdPromise { + return this.removeStub(filter); + } + + insert(document: any): BluebirdPromise { + return this.insertStub(document); + } +} diff --git a/themes/squares/server/src/lib/storage/ICollection.d.ts b/themes/squares/server/src/lib/storage/ICollection.d.ts new file mode 100644 index 000000000..caa6c2a87 --- /dev/null +++ b/themes/squares/server/src/lib/storage/ICollection.d.ts @@ -0,0 +1,11 @@ +/* istanbul ignore next */ +import BluebirdPromise = require("bluebird"); + +/* istanbul ignore next */ +export interface ICollection { + find(query: any, sortKeys: any, count: number): BluebirdPromise; + findOne(query: any): BluebirdPromise; + update(query: any, updateQuery: any, options?: any): BluebirdPromise; + remove(query: any): BluebirdPromise; + insert(document: any): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/ICollectionFactory.d.ts b/themes/squares/server/src/lib/storage/ICollectionFactory.d.ts new file mode 100644 index 000000000..39eb42c77 --- /dev/null +++ b/themes/squares/server/src/lib/storage/ICollectionFactory.d.ts @@ -0,0 +1,6 @@ + +import { ICollection } from "./ICollection"; + +export interface ICollectionFactory { + build(collectionName: string): ICollection; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/IUserDataStore.d.ts b/themes/squares/server/src/lib/storage/IUserDataStore.d.ts new file mode 100644 index 000000000..81df482aa --- /dev/null +++ b/themes/squares/server/src/lib/storage/IUserDataStore.d.ts @@ -0,0 +1,21 @@ +import BluebirdPromise = require("bluebird"); +import { TOTPSecretDocument } from "./TOTPSecretDocument"; +import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; +import { U2FRegistration } from "../../../types/U2FRegistration"; +import { TOTPSecret } from "../../../types/TOTPSecret"; +import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; +import { IdentityValidationDocument } from "./IdentityValidationDocument"; + +export interface IUserDataStore { + saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise; + retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise; + + saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; + retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise; + + produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise; + consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise; + + saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise; + retrieveTOTPSecret(userId: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/IdentityValidationDocument.d.ts b/themes/squares/server/src/lib/storage/IdentityValidationDocument.d.ts new file mode 100644 index 000000000..e7fd7b3f4 --- /dev/null +++ b/themes/squares/server/src/lib/storage/IdentityValidationDocument.d.ts @@ -0,0 +1,7 @@ + +export interface IdentityValidationDocument { + userId: string; + token: string; + challenge: string; + maxDate: Date; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/TOTPSecretDocument.d.ts b/themes/squares/server/src/lib/storage/TOTPSecretDocument.d.ts new file mode 100644 index 000000000..a6c0bf9e6 --- /dev/null +++ b/themes/squares/server/src/lib/storage/TOTPSecretDocument.d.ts @@ -0,0 +1,6 @@ +import { TOTPSecret } from "../../../types/TOTPSecret"; + +export interface TOTPSecretDocument { + userid: string; + secret: TOTPSecret; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/U2FRegistrationDocument.d.ts b/themes/squares/server/src/lib/storage/U2FRegistrationDocument.d.ts new file mode 100644 index 000000000..efec6cb1d --- /dev/null +++ b/themes/squares/server/src/lib/storage/U2FRegistrationDocument.d.ts @@ -0,0 +1,8 @@ + +import { U2FRegistration } from "../../../types/U2FRegistration"; + +export interface U2FRegistrationDocument { + userId: string; + appId: string; + registration: U2FRegistration; +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/UserDataStore.spec.ts b/themes/squares/server/src/lib/storage/UserDataStore.spec.ts new file mode 100644 index 000000000..66fb85461 --- /dev/null +++ b/themes/squares/server/src/lib/storage/UserDataStore.spec.ts @@ -0,0 +1,264 @@ + +import * as Assert from "assert"; +import * as Sinon from "sinon"; +import * as MockDate from "mockdate"; +import BluebirdPromise = require("bluebird"); + +import { UserDataStore } from "./UserDataStore"; +import { TOTPSecret } from "../../../types/TOTPSecret"; +import { U2FRegistration } from "../../../types/U2FRegistration"; +import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; +import { CollectionStub } from "./CollectionStub.spec"; +import { CollectionFactoryStub } from "./CollectionFactoryStub.spec"; + +describe("storage/UserDataStore", function () { + let factory: CollectionFactoryStub; + let collection: CollectionStub; + let userId: string; + let appId: string; + let totpSecret: TOTPSecret; + let u2fRegistration: U2FRegistration; + + beforeEach(function () { + factory = new CollectionFactoryStub(); + collection = new CollectionStub(); + + userId = "user"; + appId = "https://myappId"; + + totpSecret = { + ascii: "abc", + base32: "ABCDKZLEFZGREJK", + otpauth_url: "totp://test", + google_auth_qr: "dummy", + hex: "dummy", + qr_code_ascii: "dummy", + qr_code_base32: "dummy", + qr_code_hex: "dummy" + }; + + u2fRegistration = { + keyHandle: "KEY_HANDLE", + publicKey: "publickey" + }; + }); + + it("should correctly creates collections", function () { + new UserDataStore(factory); + + Assert.equal(4, factory.buildStub.callCount); + Assert(factory.buildStub.calledWith("authentication_traces")); + Assert(factory.buildStub.calledWith("identity_validation_tokens")); + Assert(factory.buildStub.calledWith("u2f_registrations")); + Assert(factory.buildStub.calledWith("totp_secrets")); + }); + + describe("TOTP secrets collection", function () { + it("should save a totp secret", function () { + factory.buildStub.returns(collection); + collection.updateStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.saveTOTPSecret(userId, totpSecret) + .then(function (doc) { + Assert(collection.updateStub.calledOnce); + Assert(collection.updateStub.calledWith({ userId: userId }, { + userId: userId, + secret: totpSecret + }, { upsert: true })); + return BluebirdPromise.resolve(); + }); + }); + + it("should retrieve a totp secret", function () { + factory.buildStub.returns(collection); + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.retrieveTOTPSecret(userId) + .then(function (doc) { + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ userId: userId })); + return BluebirdPromise.resolve(); + }); + }); + }); + + describe("U2F secrets collection", function () { + it("should save a U2F secret", function () { + factory.buildStub.returns(collection); + collection.updateStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.saveU2FRegistration(userId, appId, u2fRegistration) + .then(function (doc) { + Assert(collection.updateStub.calledOnce); + Assert(collection.updateStub.calledWith({ + userId: userId, + appId: appId + }, { + userId: userId, + appId: appId, + registration: u2fRegistration + }, { upsert: true })); + return BluebirdPromise.resolve(); + }); + }); + + it("should retrieve a U2F secret", function () { + factory.buildStub.returns(collection); + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.retrieveU2FRegistration(userId, appId) + .then(function (doc) { + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ + userId: userId, + appId: appId + })); + return BluebirdPromise.resolve(); + }); + }); + }); + + + describe("Regulator traces collection", function () { + it("should save a trace", function () { + factory.buildStub.returns(collection); + collection.insertStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.saveAuthenticationTrace(userId, true) + .then(function (doc) { + Assert(collection.insertStub.calledOnce); + Assert(collection.insertStub.calledWith({ + userId: userId, + date: Sinon.match.date, + isAuthenticationSuccessful: true + })); + return BluebirdPromise.resolve(); + }); + }); + + function should_retrieve_latest_authentication_traces(count: number) { + factory.buildStub.returns(collection); + collection.findStub.withArgs().returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.retrieveLatestAuthenticationTraces(userId, count) + .then(function (doc: AuthenticationTraceDocument[]) { + Assert(collection.findStub.calledOnce); + Assert(collection.findStub.calledWith({ + userId: userId, + }, { date: -1 }, count)); + return BluebirdPromise.resolve(); + }); + } + + it("should retrieve 3 latest failed authentication traces", function () { + should_retrieve_latest_authentication_traces(3); + }); + }); + + + describe("Identity validation collection", function () { + it("should save a identity validation token", function () { + factory.buildStub.returns(collection); + collection.insertStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + const maxAge = 400; + const token = "TOKEN"; + const challenge = "CHALLENGE"; + + return dataStore.produceIdentityValidationToken(userId, token, challenge, maxAge) + .then(function (doc) { + Assert(collection.insertStub.calledOnce); + Assert(collection.insertStub.calledWith({ + userId: userId, + token: token, + challenge: challenge, + maxDate: Sinon.match.date + })); + return BluebirdPromise.resolve(); + }); + }); + + it("should consume an identity token successfully", function () { + factory.buildStub.returns(collection); + + MockDate.set(100); + + const token = "TOKEN"; + const challenge = "CHALLENGE"; + + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ + userId: "USER", + token: token, + challenge: challenge, + maxDate: new Date() + })); + collection.removeStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + MockDate.set(80); + + return dataStore.consumeIdentityValidationToken(token, challenge) + .then(function (doc) { + MockDate.reset(); + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ + token: token, + challenge: challenge + })); + + Assert(collection.removeStub.calledOnce); + Assert(collection.removeStub.calledWith({ + token: token, + challenge: challenge + })); + return BluebirdPromise.resolve(); + }); + }); + + it("should consume an expired identity token", function () { + factory.buildStub.returns(collection); + + MockDate.set(0); + + const token = "TOKEN"; + const challenge = "CHALLENGE"; + + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ + userId: "USER", + token: token, + challenge: challenge, + maxDate: new Date() + })); + + const dataStore = new UserDataStore(factory); + + MockDate.set(80000); + + return dataStore.consumeIdentityValidationToken(token, challenge) + .then(function () { return BluebirdPromise.reject(new Error("should not be here")); }) + .catch(function () { + MockDate.reset(); + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ + token: token, + challenge: challenge + })); + return BluebirdPromise.resolve(); + }); + }); + }); +}); diff --git a/themes/squares/server/src/lib/storage/UserDataStore.ts b/themes/squares/server/src/lib/storage/UserDataStore.ts new file mode 100644 index 000000000..27b0cddbd --- /dev/null +++ b/themes/squares/server/src/lib/storage/UserDataStore.ts @@ -0,0 +1,143 @@ +import * as BluebirdPromise from "bluebird"; +import * as path from "path"; +import { IUserDataStore } from "./IUserDataStore"; +import { ICollection } from "./ICollection"; +import { ICollectionFactory } from "./ICollectionFactory"; +import { TOTPSecretDocument } from "./TOTPSecretDocument"; +import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; +import { U2FRegistration } from "../../../types/U2FRegistration"; +import { TOTPSecret } from "../../../types/TOTPSecret"; +import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; +import { IdentityValidationDocument } from "./IdentityValidationDocument"; + +// Constants + +const IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME = "identity_validation_tokens"; +const AUTHENTICATION_TRACES_COLLECTION_NAME = "authentication_traces"; + +const U2F_REGISTRATIONS_COLLECTION_NAME = "u2f_registrations"; +const TOTP_SECRETS_COLLECTION_NAME = "totp_secrets"; + + +export interface U2FRegistrationKey { + userId: string; + appId: string; +} + +// Source + +export class UserDataStore implements IUserDataStore { + private u2fSecretCollection: ICollection; + private identityCheckTokensCollection: ICollection; + private authenticationTracesCollection: ICollection; + private totpSecretCollection: ICollection; + + private collectionFactory: ICollectionFactory; + + constructor(collectionFactory: ICollectionFactory) { + this.collectionFactory = collectionFactory; + + this.u2fSecretCollection = this.collectionFactory.build(U2F_REGISTRATIONS_COLLECTION_NAME); + this.identityCheckTokensCollection = this.collectionFactory.build(IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME); + this.authenticationTracesCollection = this.collectionFactory.build(AUTHENTICATION_TRACES_COLLECTION_NAME); + this.totpSecretCollection = this.collectionFactory.build(TOTP_SECRETS_COLLECTION_NAME); + } + + saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { + const newDocument: U2FRegistrationDocument = { + userId: userId, + appId: appId, + registration: registration + }; + + const filter: U2FRegistrationKey = { + userId: userId, + appId: appId + }; + + return this.u2fSecretCollection.update(filter, newDocument, { upsert: true }); + } + + retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { + const filter: U2FRegistrationKey = { + userId: userId, + appId: appId + }; + return this.u2fSecretCollection.findOne(filter); + } + + saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { + const newDocument: AuthenticationTraceDocument = { + userId: userId, + date: new Date(), + isAuthenticationSuccessful: isAuthenticationSuccessful, + }; + + return this.authenticationTracesCollection.insert(newDocument); + } + + retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { + const q = { + userId: userId + }; + + return this.authenticationTracesCollection.find(q, { date: -1 }, count); + } + + produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { + const newDocument: IdentityValidationDocument = { + userId: userId, + token: token, + challenge: challenge, + maxDate: new Date(new Date().getTime() + maxAge) + }; + + return this.identityCheckTokensCollection.insert(newDocument); + } + + consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { + const that = this; + const filter = { + token: token, + challenge: challenge + }; + + let identityValidationDocument: IdentityValidationDocument; + + return this.identityCheckTokensCollection.findOne(filter) + .then(function (doc: IdentityValidationDocument) { + if (!doc) { + return BluebirdPromise.reject(new Error("Registration token does not exist")); + } + + identityValidationDocument = doc; + const current_date = new Date(); + if (current_date > doc.maxDate) + return BluebirdPromise.reject(new Error("Registration token is not valid anymore")); + + return that.identityCheckTokensCollection.remove(filter); + }) + .then(() => { + return BluebirdPromise.resolve(identityValidationDocument); + }); + } + + saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { + const doc = { + userId: userId, + secret: secret + }; + + const filter = { + userId: userId + }; + return this.totpSecretCollection.update(filter, doc, { upsert: true }); + } + + retrieveTOTPSecret(userId: string): BluebirdPromise { + const filter = { + userId: userId + }; + return this.totpSecretCollection.findOne(filter); + } +} diff --git a/themes/squares/server/src/lib/storage/UserDataStoreStub.spec.ts b/themes/squares/server/src/lib/storage/UserDataStoreStub.spec.ts new file mode 100644 index 000000000..5ea27a2de --- /dev/null +++ b/themes/squares/server/src/lib/storage/UserDataStoreStub.spec.ts @@ -0,0 +1,64 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); + +import { TOTPSecretDocument } from "./TOTPSecretDocument"; +import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; +import { U2FRegistration } from "../../../types/U2FRegistration"; +import { TOTPSecret } from "../../../types/TOTPSecret"; +import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; +import { IdentityValidationDocument } from "./IdentityValidationDocument"; +import { IUserDataStore } from "./IUserDataStore"; + +export class UserDataStoreStub implements IUserDataStore { + saveU2FRegistrationStub: Sinon.SinonStub; + retrieveU2FRegistrationStub: Sinon.SinonStub; + saveAuthenticationTraceStub: Sinon.SinonStub; + retrieveLatestAuthenticationTracesStub: Sinon.SinonStub; + produceIdentityValidationTokenStub: Sinon.SinonStub; + consumeIdentityValidationTokenStub: Sinon.SinonStub; + saveTOTPSecretStub: Sinon.SinonStub; + retrieveTOTPSecretStub: Sinon.SinonStub; + + constructor() { + this.saveU2FRegistrationStub = Sinon.stub(); + this.retrieveU2FRegistrationStub = Sinon.stub(); + this.saveAuthenticationTraceStub = Sinon.stub(); + this.retrieveLatestAuthenticationTracesStub = Sinon.stub(); + this.produceIdentityValidationTokenStub = Sinon.stub(); + this.consumeIdentityValidationTokenStub = Sinon.stub(); + this.saveTOTPSecretStub = Sinon.stub(); + this.retrieveTOTPSecretStub = Sinon.stub(); + } + + saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { + return this.saveU2FRegistrationStub(userId, appId, registration); + } + + retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { + return this.retrieveU2FRegistrationStub(userId, appId); + } + + saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { + return this.saveAuthenticationTraceStub(userId, isAuthenticationSuccessful); + } + + retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { + return this.retrieveLatestAuthenticationTracesStub(userId, count); + } + + produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { + return this.produceIdentityValidationTokenStub(userId, token, challenge, maxAge); + } + + consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { + return this.consumeIdentityValidationTokenStub(token, challenge); + } + + saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { + return this.saveTOTPSecretStub(userId, secret); + } + + retrieveTOTPSecret(userId: string): BluebirdPromise { + return this.retrieveTOTPSecretStub(userId); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/mongo/MongoCollection.spec.ts b/themes/squares/server/src/lib/storage/mongo/MongoCollection.spec.ts new file mode 100644 index 000000000..74a773a1e --- /dev/null +++ b/themes/squares/server/src/lib/storage/mongo/MongoCollection.spec.ts @@ -0,0 +1,110 @@ +import Assert = require("assert"); +import Sinon = require("sinon"); +import MongoDB = require("mongodb"); +import BluebirdPromise = require("bluebird"); +import { MongoClientStub } from "../../connectors/mongo/MongoClientStub.spec"; +import { MongoCollection } from "./MongoCollection"; + +describe("storage/mongo/MongoCollection", function () { + let mongoCollectionStub: any; + let mongoClientStub: MongoClientStub; + let findStub: Sinon.SinonStub; + let findOneStub: Sinon.SinonStub; + let insertOneStub: Sinon.SinonStub; + let updateStub: Sinon.SinonStub; + let removeStub: Sinon.SinonStub; + let countStub: Sinon.SinonStub; + const COLLECTION_NAME = "collection"; + + before(function () { + mongoClientStub = new MongoClientStub(); + mongoCollectionStub = Sinon.createStubInstance(require("mongodb").Collection as any); + findStub = mongoCollectionStub.find as Sinon.SinonStub; + findOneStub = mongoCollectionStub.findOne as Sinon.SinonStub; + insertOneStub = mongoCollectionStub.insertOne as Sinon.SinonStub; + updateStub = mongoCollectionStub.update as Sinon.SinonStub; + removeStub = mongoCollectionStub.remove as Sinon.SinonStub; + countStub = mongoCollectionStub.count as Sinon.SinonStub; + mongoClientStub.collectionStub.returns( + BluebirdPromise.resolve(mongoCollectionStub) + ); + }); + + describe("find", function () { + it("should find a document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + findStub.returns({ + sort: Sinon.stub().returns({ + limit: Sinon.stub().returns({ + toArray: Sinon.stub().returns(BluebirdPromise.resolve([])) + }) + }) + }); + + return collection.find({ key: "KEY" }) + .then(function () { + Assert(findStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("findOne", function () { + it("should find one document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + findOneStub.returns(BluebirdPromise.resolve({})); + + return collection.findOne({ key: "KEY" }) + .then(function () { + Assert(findOneStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("insert", function () { + it("should insert a document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + insertOneStub.returns(BluebirdPromise.resolve({})); + + return collection.insert({ key: "KEY" }) + .then(function () { + Assert(insertOneStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("update", function () { + it("should update a document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + updateStub.returns(BluebirdPromise.resolve({})); + + return collection.update({ key: "KEY" }, { key: "KEY", value: 1 }) + .then(function () { + Assert(updateStub.calledWith({ key: "KEY" }, { key: "KEY", value: 1 })); + }); + }); + }); + + describe("remove", function () { + it("should remove a document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + removeStub.returns(BluebirdPromise.resolve({})); + + return collection.remove({ key: "KEY" }) + .then(function () { + Assert(removeStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("count", function () { + it("should count documents in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + countStub.returns(BluebirdPromise.resolve({})); + + return collection.count({ key: "KEY" }) + .then(function () { + Assert(countStub.calledWith({ key: "KEY" })); + }); + }); + }); +}); diff --git a/themes/squares/server/src/lib/storage/mongo/MongoCollection.ts b/themes/squares/server/src/lib/storage/mongo/MongoCollection.ts new file mode 100644 index 000000000..9771389f1 --- /dev/null +++ b/themes/squares/server/src/lib/storage/mongo/MongoCollection.ts @@ -0,0 +1,50 @@ +import Bluebird = require("bluebird"); +import { ICollection } from "../ICollection"; +import MongoDB = require("mongodb"); +import { IMongoClient } from "../../connectors/mongo/IMongoClient"; + + +export class MongoCollection implements ICollection { + private mongoClient: IMongoClient; + private collectionName: string; + + constructor(collectionName: string, mongoClient: IMongoClient) { + this.collectionName = collectionName; + this.mongoClient = mongoClient; + } + + private collection(): Bluebird { + return this.mongoClient.collection(this.collectionName); + } + + find(query: any, sortKeys?: any, count?: number): Bluebird { + return this.collection() + .then((collection) => collection.find(query).sort(sortKeys).limit(count)) + .then((query) => query.toArray()); + } + + findOne(query: any): Bluebird { + return this.collection() + .then((collection) => collection.findOne(query)); + } + + update(query: any, updateQuery: any, options?: any): Bluebird { + return this.collection() + .then((collection) => collection.update(query, updateQuery, options)); + } + + remove(query: any): Bluebird { + return this.collection() + .then((collection) => collection.remove(query)); + } + + insert(document: any): Bluebird { + return this.collection() + .then((collection) => collection.insertOne(document)); + } + + count(query: any): Bluebird { + return this.collection() + .then((collection) => collection.count(query)); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts b/themes/squares/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts new file mode 100644 index 000000000..bd959cacb --- /dev/null +++ b/themes/squares/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts @@ -0,0 +1,21 @@ +import Assert = require("assert"); +import Sinon = require("sinon"); +import { MongoClientStub } from "../../connectors/mongo/MongoClientStub.spec"; +import { MongoCollectionFactory } from "./MongoCollectionFactory"; + +describe("storage/mongo/MongoCollectionFactory", function () { + let mongoClient: MongoClientStub; + + before(function() { + mongoClient = new MongoClientStub(); + }); + + describe("create", function () { + it("should create a collection", function () { + const COLLECTION_NAME = "COLLECTION_NAME"; + + const factory = new MongoCollectionFactory(mongoClient); + Assert(factory.build(COLLECTION_NAME)); + }); + }); +}); diff --git a/themes/squares/server/src/lib/storage/mongo/MongoCollectionFactory.ts b/themes/squares/server/src/lib/storage/mongo/MongoCollectionFactory.ts new file mode 100644 index 000000000..14a8262c9 --- /dev/null +++ b/themes/squares/server/src/lib/storage/mongo/MongoCollectionFactory.ts @@ -0,0 +1,19 @@ +import BluebirdPromise = require("bluebird"); +import { ICollection } from "../ICollection"; +import { ICollectionFactory } from "../ICollectionFactory"; +import { MongoCollection } from "./MongoCollection"; +import path = require("path"); +import MongoDB = require("mongodb"); +import { IMongoClient } from "../../connectors/mongo/IMongoClient"; + +export class MongoCollectionFactory implements ICollectionFactory { + private mongoClient: IMongoClient; + + constructor(mongoClient: IMongoClient) { + this.mongoClient = mongoClient; + } + + build(collectionName: string): ICollection { + return new MongoCollection(collectionName, this.mongoClient); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/nedb/NedbCollection.spec.ts b/themes/squares/server/src/lib/storage/nedb/NedbCollection.spec.ts new file mode 100644 index 000000000..a69962b67 --- /dev/null +++ b/themes/squares/server/src/lib/storage/nedb/NedbCollection.spec.ts @@ -0,0 +1,136 @@ +import Sinon = require("sinon"); +import Assert = require("assert"); + +import { NedbCollection } from "./NedbCollection"; + +describe("storage/nedb/NedbCollection", function () { + describe("insert", function () { + it("should insert one entry", function () { + const nedbOptions = { + inMemoryOnly: true + }; + const collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou" }); + + return collection.count({}).then(function (count: number) { + Assert.equal(1, count); + }); + }); + + it("should insert three entries", function () { + const nedbOptions = { + inMemoryOnly: true + }; + const collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou" }); + collection.insert({ key: "hello" }); + collection.insert({ key: "hey" }); + + return collection.count({}).then(function (count: number) { + Assert.equal(3, count); + }); + }); + }); + + describe("find", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "hello" }); + collection.insert({ key: "hey" }); + collection.insert({ key: "coucou", value: 2 }); + }); + + it("should find one hello", function () { + return collection.find({ key: "hello" }, { key: 1 }) + .then(function (docs: { key: string }[]) { + Assert.equal(1, docs.length); + Assert(docs[0].key == "hello"); + }); + }); + + it("should find two coucou", function () { + return collection.find({ key: "coucou" }, { value: 1 }) + .then(function (docs: { value: number }[]) { + Assert.equal(2, docs.length); + }); + }); + }); + + describe("findOne", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "coucou", value: 1 }); + }); + + it("should find two coucou", function () { + const doc = { key: "coucou", value: 1 }; + return collection.count(doc) + .then(function (count: number) { + Assert.equal(4, count); + return collection.findOne(doc); + }); + }); + }); + + describe("update", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou", value: 1 }); + }); + + it("should update the value", function () { + return collection.update({ key: "coucou" }, { key: "coucou", value: 2 }, { multi: true }) + .then(function () { + return collection.find({ key: "coucou" }); + }) + .then(function (docs: { key: string, value: number }[]) { + Assert.equal(1, docs.length); + Assert.equal(2, docs[0].value); + }); + }); + }); + + describe("update", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou" }); + collection.insert({ key: "hello" }); + }); + + it("should update the value", function () { + return collection.remove({ key: "coucou" }) + .then(function () { + return collection.count({}); + }) + .then(function (count: number) { + Assert.equal(1, count); + }); + }); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/nedb/NedbCollection.ts b/themes/squares/server/src/lib/storage/nedb/NedbCollection.ts new file mode 100644 index 000000000..88a93ad05 --- /dev/null +++ b/themes/squares/server/src/lib/storage/nedb/NedbCollection.ts @@ -0,0 +1,47 @@ +import BluebirdPromise = require("bluebird"); +import { ICollection } from "../ICollection"; +import Nedb = require("nedb"); + +declare module "nedb" { + export class NedbAsync extends Nedb { + constructor(pathOrOptions?: string | Nedb.DataStoreOptions); + updateAsync(query: any, updateQuery: any, options?: Nedb.UpdateOptions): BluebirdPromise; + findOneAsync(query: any): BluebirdPromise; + insertAsync(newDoc: T): BluebirdPromise; + removeAsync(query: any): BluebirdPromise; + countAsync(query: any): BluebirdPromise; + } +} + +export class NedbCollection implements ICollection { + private collection: Nedb.NedbAsync; + + constructor(options: Nedb.DataStoreOptions) { + this.collection = BluebirdPromise.promisifyAll(new Nedb(options)) as Nedb.NedbAsync; + } + + find(query: any, sortKeys?: any, count?: number): BluebirdPromise { + const q = this.collection.find(query).sort(sortKeys).limit(count); + return BluebirdPromise.promisify(q.exec, { context: q })(); + } + + findOne(query: any): BluebirdPromise { + return this.collection.findOneAsync(query); + } + + update(query: any, updateQuery: any, options?: any): BluebirdPromise { + return this.collection.updateAsync(query, updateQuery, options); + } + + remove(query: any): BluebirdPromise { + return this.collection.removeAsync(query); + } + + insert(document: any): BluebirdPromise { + return this.collection.insertAsync(document); + } + + count(query: any): BluebirdPromise { + return this.collection.countAsync(query); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts b/themes/squares/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts new file mode 100644 index 000000000..da90c661f --- /dev/null +++ b/themes/squares/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts @@ -0,0 +1,16 @@ +import Sinon = require("sinon"); +import Assert = require("assert"); + +import { NedbCollectionFactory } from "./NedbCollectionFactory"; + +describe("storage/nedb/NedbCollectionFactory", function() { + it("should create a nedb collection", function() { + const nedbOptions = { + inMemoryOnly: true + }; + const factory = new NedbCollectionFactory(nedbOptions); + + const collection = factory.build("mycollection"); + Assert(collection); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/nedb/NedbCollectionFactory.ts b/themes/squares/server/src/lib/storage/nedb/NedbCollectionFactory.ts new file mode 100644 index 000000000..49c4dc853 --- /dev/null +++ b/themes/squares/server/src/lib/storage/nedb/NedbCollectionFactory.ts @@ -0,0 +1,28 @@ +import { ICollection } from "../ICollection"; +import { ICollectionFactory } from "../ICollectionFactory"; +import { NedbCollection } from "./NedbCollection"; +import path = require("path"); +import Nedb = require("nedb"); + +export interface NedbOptions { + inMemoryOnly?: boolean; + directory?: string; +} + +export class NedbCollectionFactory implements ICollectionFactory { + private options: Nedb.DataStoreOptions; + + constructor(options: Nedb.DataStoreOptions) { + this.options = options; + } + + build(collectionName: string): ICollection { + const datastoreOptions: Nedb.DataStoreOptions = { + inMemoryOnly: this.options.inMemoryOnly || false, + autoload: true, + filename: (this.options.filename) ? path.resolve(this.options.filename, collectionName) : undefined + }; + + return new NedbCollection(datastoreOptions); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/stubs/express.spec.ts b/themes/squares/server/src/lib/stubs/express.spec.ts new file mode 100644 index 000000000..48f15d7e1 --- /dev/null +++ b/themes/squares/server/src/lib/stubs/express.spec.ts @@ -0,0 +1,103 @@ + +import sinon = require("sinon"); +import express = require("express"); + +export interface RequestMock { + app?: any; + body?: any; + session?: any; + headers?: any; + get?: any; + query?: any; + originalUrl: string; +} + +export interface ResponseMock { + send: sinon.SinonStub | sinon.SinonSpy; + sendStatus: sinon.SinonStub; + sendFile: sinon.SinonStub; + sendfile: sinon.SinonStub; + status: sinon.SinonStub | sinon.SinonSpy; + json: sinon.SinonStub | sinon.SinonSpy; + links: sinon.SinonStub; + jsonp: sinon.SinonStub; + download: sinon.SinonStub; + contentType: sinon.SinonStub; + type: sinon.SinonStub; + format: sinon.SinonStub; + attachment: sinon.SinonStub; + set: sinon.SinonStub; + header: sinon.SinonStub; + headersSent: boolean; + get: sinon.SinonStub; + clearCookie: sinon.SinonStub; + cookie: sinon.SinonStub; + location: sinon.SinonStub; + redirect: sinon.SinonStub | sinon.SinonSpy; + render: sinon.SinonStub | sinon.SinonSpy; + locals: sinon.SinonStub; + charset: string; + vary: sinon.SinonStub; + app: any; + write: sinon.SinonStub; + writeContinue: sinon.SinonStub; + writeHead: sinon.SinonStub; + statusCode: number; + statusMessage: string; + setHeader: sinon.SinonStub; + setTimeout: sinon.SinonStub; + sendDate: boolean; + getHeader: sinon.SinonStub; +} + +export function RequestMock(): RequestMock { + return { + originalUrl: "/non-api/xxx", + app: { + get: sinon.stub() + }, + headers: { + "x-forwarded-for": "127.0.0.1" + }, + session: {} + }; +} +export function ResponseMock(): ResponseMock { + return { + send: sinon.stub(), + status: sinon.stub(), + json: sinon.stub(), + sendStatus: sinon.stub(), + links: sinon.stub(), + jsonp: sinon.stub(), + sendFile: sinon.stub(), + sendfile: sinon.stub(), + download: sinon.stub(), + contentType: sinon.stub(), + type: sinon.stub(), + format: sinon.stub(), + attachment: sinon.stub(), + set: sinon.stub(), + header: sinon.stub(), + headersSent: true, + get: sinon.stub(), + clearCookie: sinon.stub(), + cookie: sinon.stub(), + location: sinon.stub(), + redirect: sinon.stub(), + render: sinon.stub(), + locals: sinon.stub(), + charset: "utf-8", + vary: sinon.stub(), + app: sinon.stub(), + write: sinon.stub(), + writeContinue: sinon.stub(), + writeHead: sinon.stub(), + statusCode: 200, + statusMessage: "message", + setHeader: sinon.stub(), + setTimeout: sinon.stub(), + sendDate: true, + getHeader: sinon.stub() + }; +} diff --git a/themes/squares/server/src/lib/stubs/ldapjs.spec.ts b/themes/squares/server/src/lib/stubs/ldapjs.spec.ts new file mode 100644 index 000000000..045c0e11b --- /dev/null +++ b/themes/squares/server/src/lib/stubs/ldapjs.spec.ts @@ -0,0 +1,50 @@ + +import Sinon = require("sinon"); + +export class LdapjsMock { + createClientStub: sinon.SinonStub; + + constructor() { + this.createClientStub = Sinon.stub(); + } + + createClient(params: any) { + return this.createClientStub(params); + } +} + +export class LdapjsClientMock { + bindStub: sinon.SinonStub; + unbindStub: sinon.SinonStub; + searchStub: sinon.SinonStub; + modifyStub: sinon.SinonStub; + onStub: sinon.SinonStub; + + constructor() { + this.bindStub = Sinon.stub(); + this.unbindStub = Sinon.stub(); + this.searchStub = Sinon.stub(); + this.modifyStub = Sinon.stub(); + this.onStub = Sinon.stub(); + } + + bind() { + return this.bindStub(); + } + + unbind() { + return this.unbindStub(); + } + + search() { + return this.searchStub(); + } + + modify() { + return this.modifyStub(); + } + + on() { + return this.onStub(); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/stubs/speakeasy.spec.ts b/themes/squares/server/src/lib/stubs/speakeasy.spec.ts new file mode 100644 index 000000000..023614dcd --- /dev/null +++ b/themes/squares/server/src/lib/stubs/speakeasy.spec.ts @@ -0,0 +1,7 @@ + +import sinon = require("sinon"); + +export = { + totp: sinon.stub(), + generateSecret: sinon.stub() +}; diff --git a/themes/squares/server/src/lib/stubs/u2f.spec.ts b/themes/squares/server/src/lib/stubs/u2f.spec.ts new file mode 100644 index 000000000..234b28c11 --- /dev/null +++ b/themes/squares/server/src/lib/stubs/u2f.spec.ts @@ -0,0 +1,16 @@ + +import sinon = require("sinon"); + +export interface U2FMock { + request: sinon.SinonStub; + checkSignature: sinon.SinonStub; + checkRegistration: sinon.SinonStub; +} + +export function U2FMock(): U2FMock { + return { + request: sinon.stub(), + checkSignature: sinon.stub(), + checkRegistration: sinon.stub() + }; +} diff --git a/themes/squares/server/src/lib/utils/HashGenerator.spec.ts b/themes/squares/server/src/lib/utils/HashGenerator.spec.ts new file mode 100644 index 000000000..f19619a65 --- /dev/null +++ b/themes/squares/server/src/lib/utils/HashGenerator.spec.ts @@ -0,0 +1,18 @@ +import Assert = require("assert"); +import { HashGenerator } from "./HashGenerator"; + +describe("utils/HashGenerator", function () { + it("should compute correct ssha512 (password)", function () { + return HashGenerator.ssha512("password", 500000, "jgiCMRyGXzoqpxS3") + .then(function (hash: string) { + Assert.equal(hash, "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"); + }); + }); + + it("should compute correct ssha512 (test)", function () { + return HashGenerator.ssha512("test", 500000, "abcdefghijklmnop") + .then(function (hash: string) { + Assert.equal(hash, "{CRYPT}$6$rounds=500000$abcdefghijklmnop$sTlNGf0VO/HTQIOXemmaBbV28HUch/qhWOA1/4dsDj6CDQYhUgXbYSPL6gccAsWMr2zD5fFWwhKmPdG.yxphs."); + }); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/utils/HashGenerator.ts b/themes/squares/server/src/lib/utils/HashGenerator.ts new file mode 100644 index 000000000..e67de32b7 --- /dev/null +++ b/themes/squares/server/src/lib/utils/HashGenerator.ts @@ -0,0 +1,23 @@ +import BluebirdPromise = require("bluebird"); +import RandomString = require("randomstring"); +import Util = require("util"); +const crypt = require("crypt3"); + +export class HashGenerator { + static ssha512( + password: string, + rounds: number = 500000, + salt?: string): BluebirdPromise { + const saltSize = 16; + // $6 means SHA512 + const _salt = Util.format("$6$rounds=%d$%s", rounds, + (salt) ? salt : RandomString.generate(16)); + + const cryptAsync = BluebirdPromise.promisify(crypt); + + return cryptAsync(password, _salt) + .then(function (hash: string) { + return BluebirdPromise.resolve(Util.format("{CRYPT}%s", hash)); + }); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/utils/ObjectCloner.ts b/themes/squares/server/src/lib/utils/ObjectCloner.ts new file mode 100644 index 000000000..3e125d749 --- /dev/null +++ b/themes/squares/server/src/lib/utils/ObjectCloner.ts @@ -0,0 +1,6 @@ + +export class ObjectCloner { + static clone(obj: any): any { + return JSON.parse(JSON.stringify(obj)); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/utils/SafeRedirection.spec.ts b/themes/squares/server/src/lib/utils/SafeRedirection.spec.ts new file mode 100644 index 000000000..4126949fd --- /dev/null +++ b/themes/squares/server/src/lib/utils/SafeRedirection.spec.ts @@ -0,0 +1,33 @@ +import Assert = require("assert"); +import Sinon = require("sinon"); +import { SafeRedirector } from "./SafeRedirection"; + +describe("web_server/middlewares/SafeRedirection", () => { + describe("Url is in protected domain", () => { + before(() => { + this.redirector = new SafeRedirector("example.com"); + this.res = {redirect: Sinon.stub()}; + }); + + it("should redirect to provided url", () => { + this.redirector.redirectOrElse(this.res, + "https://mysubdomain.example.com:8080/abc", + "https://authelia.example.com"); + Assert(this.res.redirect.calledWith("https://mysubdomain.example.com:8080/abc")); + }); + + it("should redirect to default url when wrong domain", () => { + this.redirector.redirectOrElse(this.res, + "https://mysubdomain.domain.rtf:8080/abc", + "https://authelia.example.com"); + Assert(this.res.redirect.calledWith("https://authelia.example.com")); + }); + + it("should redirect to default url when not terminating by domain", () => { + this.redirector.redirectOrElse(this.res, + "https://mysubdomain.example.com.rtf:8080/abc", + "https://authelia.example.com"); + Assert(this.res.redirect.calledWith("https://authelia.example.com")); + }); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/utils/SafeRedirection.ts b/themes/squares/server/src/lib/utils/SafeRedirection.ts new file mode 100644 index 000000000..9e6a32e0c --- /dev/null +++ b/themes/squares/server/src/lib/utils/SafeRedirection.ts @@ -0,0 +1,22 @@ +import Express = require("express"); +import { DomainExtractor } from "../../../../shared/DomainExtractor"; +import { BelongToDomain } from "../../../../shared/BelongToDomain"; + + +export class SafeRedirector { + private domain: string; + + constructor(domain: string) { + this.domain = domain; + } + + redirectOrElse( + res: Express.Response, + url: string, + defaultUrl: string): void { + if (BelongToDomain(url, this.domain)) { + res.redirect(url); + } + res.redirect(defaultUrl); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/utils/URLDecomposer.spec.ts b/themes/squares/server/src/lib/utils/URLDecomposer.spec.ts new file mode 100644 index 000000000..cbb038738 --- /dev/null +++ b/themes/squares/server/src/lib/utils/URLDecomposer.spec.ts @@ -0,0 +1,46 @@ +import { URLDecomposer } from "./URLDecomposer"; +import Assert = require("assert"); + +describe("utils/URLDecomposer", function () { + describe("test fromUrl", function () { + it("should return domain from https url", function () { + const d = URLDecomposer.fromUrl("https://www.example.com/test/abc"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/test/abc"); + }); + + it("should return domain from http url", function () { + const d = URLDecomposer.fromUrl("http://www.example.com/test/abc"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/test/abc"); + }); + + it("should return domain when url contains port", function () { + const d = URLDecomposer.fromUrl("https://www.example.com:8080/test/abc"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/test/abc"); + }); + + it("should return default path when no path provided", function () { + const d = URLDecomposer.fromUrl("https://www.example.com:8080"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/"); + }); + + it("should return default path when provided", function () { + const d = URLDecomposer.fromUrl("https://www.example.com:8080/"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/"); + }); + + it("should return undefined when does not match", function () { + const d = URLDecomposer.fromUrl("https:///abc/test"); + Assert.equal(d, undefined); + }); + + it("should return undefined when does not match", function () { + const d = URLDecomposer.fromUrl("https:///abc/test"); + Assert.equal(d, undefined); + }); + }); +}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/utils/URLDecomposer.ts b/themes/squares/server/src/lib/utils/URLDecomposer.ts new file mode 100644 index 000000000..9bdf2e9d2 --- /dev/null +++ b/themes/squares/server/src/lib/utils/URLDecomposer.ts @@ -0,0 +1,15 @@ +export class URLDecomposer { + static fromUrl(url: string): {domain: string, path: string} { + if (!url) return; + const match = url.match(/https?:\/\/([a-z0-9_.-]+)(:[0-9]+)?(.*)/); + + if (!match) return; + + if (match[1] && !match[3]) { + return {domain: match[1], path: "/"}; + } else if (match[1] && match[3]) { + return {domain: match[1], path: match[3]}; + } + return; + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/web_server/Configurator.ts b/themes/squares/server/src/lib/web_server/Configurator.ts new file mode 100644 index 000000000..6e404874a --- /dev/null +++ b/themes/squares/server/src/lib/web_server/Configurator.ts @@ -0,0 +1,47 @@ +import { Configuration } from "../configuration/schema/Configuration"; +import { GlobalDependencies } from "../../../types/Dependencies"; +import { SessionConfigurationBuilder } from + "../configuration/SessionConfigurationBuilder"; +import Path = require("path"); +import Express = require("express"); +import * as BodyParser from "body-parser"; +import { RestApi } from "./RestApi"; +import { WithHeadersLogged } from "./middlewares/WithHeadersLogged"; +import { ServerVariables } from "../ServerVariables"; +import Helmet = require("helmet"); + +const addRequestId = require("express-request-id")(); + +// Constants +const TRUST_PROXY = "trust proxy"; +const X_POWERED_BY = "x-powered-by"; +const VIEWS = "views"; +const VIEW_ENGINE = "view engine"; +const PUG = "pug"; + +export class Configurator { + static configure(config: Configuration, + app: Express.Application, + vars: ServerVariables, + deps: GlobalDependencies): void { + const viewsDirectory = Path.resolve(__dirname, "../../views"); + const publicHtmlDirectory = Path.resolve(__dirname, "../../public_html"); + + const expressSessionOptions = SessionConfigurationBuilder.build(config, deps); + + app.use(Express.static(publicHtmlDirectory)); + app.use(BodyParser.urlencoded({ extended: false })); + app.use(BodyParser.json()); + app.use(deps.session(expressSessionOptions)); + app.use(addRequestId); + app.use(WithHeadersLogged.middleware(vars.logger)); + app.disable(X_POWERED_BY); + app.enable(TRUST_PROXY); + app.use(Helmet()); + + app.set(VIEWS, viewsDirectory); + app.set(VIEW_ENGINE, PUG); + + RestApi.setup(app, vars); + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/web_server/RestApi.ts b/themes/squares/server/src/lib/web_server/RestApi.ts new file mode 100644 index 000000000..9144a15b9 --- /dev/null +++ b/themes/squares/server/src/lib/web_server/RestApi.ts @@ -0,0 +1,125 @@ +import Express = require("express"); + +import FirstFactorGet = require("../routes/firstfactor/get"); +import SecondFactorGet = require("../routes/secondfactor/get"); + +import FirstFactorPost = require("../routes/firstfactor/post"); +import LogoutGet = require("../routes/logout/get"); +import VerifyGet = require("../routes/verify/get"); +import TOTPSignGet = require("../routes/secondfactor/totp/sign/post"); + +import IdentityCheckMiddleware = require("../IdentityCheckMiddleware"); + +import TOTPRegistrationIdentityHandler from "../routes/secondfactor/totp/identity/RegistrationHandler"; +import U2FRegistrationIdentityHandler from "../routes/secondfactor/u2f/identity/RegistrationHandler"; +import ResetPasswordIdentityHandler from "../routes/password-reset/identity/PasswordResetHandler"; + +import U2FSignPost = require("../routes/secondfactor/u2f/sign/post"); +import U2FSignRequestGet = require("../routes/secondfactor/u2f/sign_request/get"); + +import U2FRegisterPost = require("../routes/secondfactor/u2f/register/post"); +import U2FRegisterRequestGet = require("../routes/secondfactor/u2f/register_request/get"); + +import ResetPasswordFormPost = require("../routes/password-reset/form/post"); +import ResetPasswordRequestPost = require("../routes/password-reset/request/get"); + +import Error401Get = require("../routes/error/401/get"); +import Error403Get = require("../routes/error/403/get"); +import Error404Get = require("../routes/error/404/get"); + +import LoggedIn = require("../routes/loggedin/get"); + +import { ServerVariables } from "../ServerVariables"; +import Endpoints = require("../../../../shared/api"); +import { RequireValidatedFirstFactor } from "./middlewares/RequireValidatedFirstFactor"; + +function setupTotp(app: Express.Application, vars: ServerVariables) { + app.post(Endpoints.SECOND_FACTOR_TOTP_POST, + RequireValidatedFirstFactor.middleware(vars.logger), + TOTPSignGet.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, + RequireValidatedFirstFactor.middleware(vars.logger)); + + app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, + RequireValidatedFirstFactor.middleware(vars.logger)); + + IdentityCheckMiddleware.register(app, + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, + new TOTPRegistrationIdentityHandler(vars.logger, + vars.userDataStore, vars.totpHandler, vars.config.totp), + vars); +} + +function setupU2f(app: Express.Application, vars: ServerVariables) { + app.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, + RequireValidatedFirstFactor.middleware(vars.logger), + U2FSignRequestGet.default(vars)); + + app.post(Endpoints.SECOND_FACTOR_U2F_SIGN_POST, + RequireValidatedFirstFactor.middleware(vars.logger), + U2FSignPost.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, + RequireValidatedFirstFactor.middleware(vars.logger), + U2FRegisterRequestGet.default(vars)); + + app.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, + RequireValidatedFirstFactor.middleware(vars.logger), + U2FRegisterPost.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, + RequireValidatedFirstFactor.middleware(vars.logger)); + + app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, + RequireValidatedFirstFactor.middleware(vars.logger)); + + IdentityCheckMiddleware.register(app, + Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, + Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, + new U2FRegistrationIdentityHandler(vars.logger), vars); +} + +function setupResetPassword(app: Express.Application, vars: ServerVariables) { + IdentityCheckMiddleware.register(app, + Endpoints.RESET_PASSWORD_IDENTITY_START_GET, + Endpoints.RESET_PASSWORD_IDENTITY_FINISH_GET, + new ResetPasswordIdentityHandler(vars.logger, vars.usersDatabase), + vars); + + app.get(Endpoints.RESET_PASSWORD_REQUEST_GET, + ResetPasswordRequestPost.default); + app.post(Endpoints.RESET_PASSWORD_FORM_POST, + ResetPasswordFormPost.default(vars)); +} + +function setupErrors(app: Express.Application, vars: ServerVariables) { + app.get(Endpoints.ERROR_401_GET, Error401Get.default(vars)); + app.get(Endpoints.ERROR_403_GET, Error403Get.default(vars)); + app.get(Endpoints.ERROR_404_GET, Error404Get.default); +} + +export class RestApi { + static setup(app: Express.Application, vars: ServerVariables): void { + app.get(Endpoints.FIRST_FACTOR_GET, FirstFactorGet.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_GET, + RequireValidatedFirstFactor.middleware(vars.logger), + SecondFactorGet.default(vars)); + + app.get(Endpoints.LOGOUT_GET, LogoutGet.default(vars)); + + app.get(Endpoints.VERIFY_GET, VerifyGet.default(vars)); + app.post(Endpoints.FIRST_FACTOR_POST, FirstFactorPost.default(vars)); + + setupTotp(app, vars); + setupU2f(app, vars); + setupResetPassword(app, vars); + setupErrors(app, vars); + + app.get(Endpoints.LOGGED_IN, + RequireValidatedFirstFactor.middleware(vars.logger), + LoggedIn.default(vars)); + } +} diff --git a/themes/squares/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts b/themes/squares/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts new file mode 100644 index 000000000..ecfd75765 --- /dev/null +++ b/themes/squares/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts @@ -0,0 +1,27 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import ErrorReplies = require("../../ErrorReplies"); +import { IRequestLogger } from "../../logging/IRequestLogger"; +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Exceptions = require("../../Exceptions"); +import { Level } from "../../authentication/Level"; + +export class RequireValidatedFirstFactor { + static middleware(logger: IRequestLogger) { + return function (req: Express.Request, res: Express.Response, + next: Express.NextFunction): BluebirdPromise { + + 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.")); + + next(); + resolve(); + }) + .catch(ErrorReplies.replyWithError401(req, res, logger)); + }; + } +} \ No newline at end of file diff --git a/themes/squares/server/src/lib/web_server/middlewares/WithHeadersLogged.ts b/themes/squares/server/src/lib/web_server/middlewares/WithHeadersLogged.ts new file mode 100644 index 000000000..139db1141 --- /dev/null +++ b/themes/squares/server/src/lib/web_server/middlewares/WithHeadersLogged.ts @@ -0,0 +1,12 @@ +import Express = require("express"); +import { IRequestLogger } from "../../logging/IRequestLogger"; + +export class WithHeadersLogged { + static middleware(logger: IRequestLogger) { + return function (req: Express.Request, res: Express.Response, + next: Express.NextFunction): void { + logger.debug(req, "Headers = %s", JSON.stringify(req.headers)); + next(); + }; + } +} \ No newline at end of file diff --git a/themes/squares/server/src/resources/email-template.ejs b/themes/squares/server/src/resources/email-template.ejs new file mode 100644 index 000000000..f59c2f944 --- /dev/null +++ b/themes/squares/server/src/resources/email-template.ejs @@ -0,0 +1,254 @@ + + + + + + Simples-Minimalistic Responsive Template + + + + + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + +
 
+ + + + + + + +
+

<%= title %>

+
+ +
 
+
+
+ + + + + + + + +
+ + + + + + +
 
+
+ + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + +
 
+ + + + + + + + + + + + + + + + + + +
+ This email has been sent to you in order to validate your identity. Please ignore it if you do not know why you received it. +
 
+ <%= button_title %> +
+
 
+
+
+ + + + + + + + +
+ + + + + + + + + + + + +
 
 
 
+
+ + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + +
+ Please ignore this email if you did not initiate the process. +
+
+
+ + + + + diff --git a/themes/squares/server/src/views/already-logged-in.pug b/themes/squares/server/src/views/already-logged-in.pug new file mode 100644 index 000000000..137bbea30 --- /dev/null +++ b/themes/squares/server/src/views/already-logged-in.pug @@ -0,0 +1,14 @@ +extends layout/layout.pug + +block form-header + h1 Sign in + +block content + img(class="header-img" src="/img/success.png" alt="success") + if redirection_url + p You are already logged in as #{ username }.

+ | If you are not redirected in few seconds, click here.

+ | Otherwise, click here to log off. + else + p You are already logged in as #{ username }.

+ | Click here to log off. diff --git a/themes/squares/server/src/views/errors/.directory b/themes/squares/server/src/views/errors/.directory new file mode 100644 index 000000000..33f71bea4 --- /dev/null +++ b/themes/squares/server/src/views/errors/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,59,57 +Version=3 +ViewMode=1 diff --git a/themes/squares/server/src/views/errors/401.pug b/themes/squares/server/src/views/errors/401.pug new file mode 100644 index 000000000..b7a222ad0 --- /dev/null +++ b/themes/squares/server/src/views/errors/401.pug @@ -0,0 +1,16 @@ +extends ../layout/layout.pug + +block variables + - page_classname = "error-401"; + +block form-header + h1 Error 401 + +block content + img(class="header-img" src="/img/warning.png" alt="warning") + if redirection_url + p You are not authorized to access this resource.

+ | Please click here if you are not + | redirected in few seconds. + else + p You are not authorized to access this resource. \ No newline at end of file diff --git a/themes/squares/server/src/views/errors/403.pug b/themes/squares/server/src/views/errors/403.pug new file mode 100644 index 000000000..f4b5ca8a2 --- /dev/null +++ b/themes/squares/server/src/views/errors/403.pug @@ -0,0 +1,16 @@ +extends ../layout/layout.pug + +block variables + - page_classname = "error-403"; + +block form-header + h1 Error 403 + +block content + img(class="header-img" src="/img/warning.png" alt="warning") + if redirection_url + p You don't have enough privileges to access this resource.

+ | Please click here if you are not + | redirected in few seconds. + else + p You don't have enough privileges to access this resource. diff --git a/themes/squares/server/src/views/errors/404.pug b/themes/squares/server/src/views/errors/404.pug new file mode 100644 index 000000000..06d6375fc --- /dev/null +++ b/themes/squares/server/src/views/errors/404.pug @@ -0,0 +1,11 @@ +extends ../layout/layout.pug + +block variables + - page_classname = "error-404"; + +block form-header +

Error 404

+ +block content + img(class="header-img" src="/img/warning.png" alt="warning") + p Page not found. diff --git a/themes/squares/server/src/views/firstfactor.pug b/themes/squares/server/src/views/firstfactor.pug new file mode 100644 index 000000000..574470710 --- /dev/null +++ b/themes/squares/server/src/views/firstfactor.pug @@ -0,0 +1,23 @@ +extends layout/layout.pug + +block variables + - page_classname = "firstfactor"; + +block form-header + h1 Sign in + +block content + div(class="notification") + img(class="header-img" src="/img/sharingan.png" alt="user profile") + p Enter your credentials to sign in + form(class="form-signin") + div(class="form-inputs") + input(type="text" class="form-control" id="username" placeholder="Username" required autofocus) + input(type="password" class="form-control" id="password" placeholder="Password" required) + button(id="signin" class="btn btn-lg btn-primary btn-block" type="submit") Sign in + div(class="keep-me-logged-in pull-left") + input(type="checkbox" id="keep_me_logged_in" name="keep_me_logged_in" value="true") + label(for="keep_me_logged_in") Keep me logged in + div(class="bottom-right-links pull-right") + a(href=reset_password_request_endpoint, class="link forgot-password") Forgot password? + span(class="clearfix") diff --git a/themes/squares/server/src/views/layout/layout.pug b/themes/squares/server/src/views/layout/layout.pug new file mode 100644 index 000000000..43247436d --- /dev/null +++ b/themes/squares/server/src/views/layout/layout.pug @@ -0,0 +1,28 @@ +block variables + +doctype html +html + head + title Authelia - 2FA + meta(name="viewport", content="width=device-width, initial-scale=1.0") + meta(name="robots", content="noindex, nofollow, nosnippet, noarchive") + meta(http-equiv="Content-Security-Policy", content="default-src 'self'; img-src 'self' data:;") + link(rel="icon", href="/img/icon.png" type="image/png" sizes="32x32") + link(rel="stylesheet", type="text/css", href="/css/authelia.css") + if redirection_url + meta(http-equiv="refresh" content="4;url=" + redirection_url) + body + div(class="container") + div(class="row") + div(class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-6 col-lg-offset-3") + div(class="account-wall " + page_classname) + div(class="row header") + block form-header + div(class="row body") + div(class="form col-xs-10 col-xs-offset-1 col-sm-8 col-sm-offset-2 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2") + block content + div(class="row footer poweredby-block") + div(class="poweredby col-xs-6 col-xs-offset-4 col-sm-6 col-sm-offset-4 col-md-6 col-md-offset-4") + | Powered by Authelia + block entrypoint + script(src="/js/authelia.js", type="text/javascript") diff --git a/themes/squares/server/src/views/need-identity-validation.pug b/themes/squares/server/src/views/need-identity-validation.pug new file mode 100644 index 000000000..4cfd62717 --- /dev/null +++ b/themes/squares/server/src/views/need-identity-validation.pug @@ -0,0 +1,12 @@ +extends layout/layout.pug + +block variables + - page_classname = "identity-validation"; + +block form-header + h1 Registration + +block content + img(class="header-img" src="/img/mail.png" alt="mail") + p A confirmation email has been sent to your mailbox. + | Please open it and click on the link within 15 minutes to confirm the registration. diff --git a/themes/squares/server/src/views/password-reset-form.pug b/themes/squares/server/src/views/password-reset-form.pug new file mode 100644 index 000000000..fd9311892 --- /dev/null +++ b/themes/squares/server/src/views/password-reset-form.pug @@ -0,0 +1,18 @@ +extends layout/layout.pug + +block variables + - page_classname = "password-reset-form"; + +block form-header + h1 Reset password + +block content + div(class="notification") + img(class="header-img" src="/img/password_white.png" alt="password") + p Set your new password and confirm it. + form(class="form-signin") + div(class="form-inputs") + input(class="form-control" type="password" name="password1" id="password1" placeholder="New password" required="required") + input(class="form-control" type="password" name="password2" id="password2" placeholder="Password confirmation" required="required") + button(id="reset-password-button" class="btn btn-lg btn-primary btn-block" type="submit") Reset Password + span(class="clearfix") diff --git a/themes/squares/server/src/views/password-reset-request.pug b/themes/squares/server/src/views/password-reset-request.pug new file mode 100644 index 000000000..855b5998b --- /dev/null +++ b/themes/squares/server/src/views/password-reset-request.pug @@ -0,0 +1,18 @@ +extends layout/layout.pug + +block variables + - page_classname = "password-reset-request"; + +block form-header + h1 Reset password + +block content + div(class="notification") + div + img(class="header-img" src="/img/password_white.png" alt="password") + p After giving your username, you will receive an email to change your password. + form(class="form-signin") + div(class="form-inputs") + input(type="text" class="form-control" name="username" id="username" placeholder="Your username" required="required") + button(id="reset-password-button" class="btn btn-lg btn-primary btn-block" type="submit") Reset Password + span(class="clearfix") diff --git a/themes/squares/server/src/views/secondfactor.pug b/themes/squares/server/src/views/secondfactor.pug new file mode 100644 index 000000000..87b578185 --- /dev/null +++ b/themes/squares/server/src/views/secondfactor.pug @@ -0,0 +1,31 @@ +extends layout/layout.pug + +block variables + - page_classname = "secondfactor"; + +block form-header + h1 Sign in + +block content + div + div(class="notification notification-totp") + h3 Hi #{username} + div(class="row") + div(class="u2f-token") + img(src="/img/pendrive.png", alt="security key") + p + | Please, touch your security key
+ b Or
+ | Get a one-time password + form(class="form-signin totp") + div(class="form-inputs") + input(type="text" autocomplete="off" class="form-control" id="token" placeholder="Token" required autofocus) + button(class="btn btn-lg btn-primary btn-block totp-button" type="submit") Sign in + div(class="pull-right bottom-right-links") + div Need to register? + div + a(href=u2f_identity_start_endpoint, class="link register-u2f", data-toggle="tooltip", title="A security key is required to register.") Security key + | | + a(href=totp_identity_start_endpoint, class="link register-totp") Google Authenticator + span(class="clearfix") + script(src="/js/u2f-api.js", type="text/javascript") diff --git a/themes/squares/server/src/views/totp-register.pug b/themes/squares/server/src/views/totp-register.pug new file mode 100644 index 000000000..1b4d98354 --- /dev/null +++ b/themes/squares/server/src/views/totp-register.pug @@ -0,0 +1,25 @@ +extends layout/layout.pug + +block variables + - page_classname = "totp-register"; + +block form-header + h1 One-time passwords + +block content + p Open Google Authenticator and add this entry + p(id="secret") #{ base32_secret } + p or scan this barcode + div(id="qrcode") #{ otpauth_url } + p + a(href=login_endpoint, id="login-button") Login + div(class="need-google-authenticator") + | Need Google Authenticator? + div(class="store-badges") + a(href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1', target="_blank") + img(alt='Get it on Google Play', src='/img/stores/googleplay-badge.svg', class="store-badge") + a(href='https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8', target="_blank") + img(alt='Get it on Apple Store' src='/img/stores/applestore-badge.svg' class="store-badge") + +block entrypoint + script(src="/js/qrcode.min.js", type="text/javascript" ) diff --git a/themes/squares/server/src/views/u2f-register.pug b/themes/squares/server/src/views/u2f-register.pug new file mode 100644 index 000000000..d52eba6ca --- /dev/null +++ b/themes/squares/server/src/views/u2f-register.pug @@ -0,0 +1,12 @@ +extends layout/layout.pug + +block variables + - page_classname = "u2f-register"; + +block form-header + h1 Register your security key + +block content + p Touch the token to register your security key. + img(src="/img/pendrive.png" alt="pendrive") + script(src="/js/u2f-api.js", type="text/javascript") \ No newline at end of file diff --git a/themes/squares/server/test/requests.ts b/themes/squares/server/test/requests.ts new file mode 100644 index 000000000..93fa0de47 --- /dev/null +++ b/themes/squares/server/test/requests.ts @@ -0,0 +1,94 @@ + +import BluebirdPromise = require("bluebird"); +import request = require("request"); +import assert = require("assert"); +import express = require("express"); +import nodemailer = require("nodemailer"); +import Endpoints = require("../../shared/api"); + +declare module "request" { + export interface RequestAPI { + getAsync(uri: string, options?: RequiredUriUrl): BluebirdPromise; + getAsync(uri: string): BluebirdPromise; + getAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; + + postAsync(uri: string, options?: CoreOptions): BluebirdPromise; + postAsync(uri: string): BluebirdPromise; + postAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; + } +} + +const requestAsync: typeof request = BluebirdPromise.promisifyAll(request) as typeof request; + +export = function (port: number) { + const PORT = port; + const BASE_URL = "http://localhost:" + PORT; + + function execute_totp(jar: request.CookieJar, token: string) { + return requestAsync.postAsync({ + url: BASE_URL + Endpoints.SECOND_FACTOR_TOTP_POST, + jar: jar, + form: { + token: token + } + }); + } + + function execute_u2f_authentication(jar: request.CookieJar) { + return requestAsync.getAsync({ + url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, + jar: jar + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200); + return requestAsync.postAsync({ + url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_POST, + jar: jar, + form: { + } + }); + }); + } + + function execute_verification(jar: request.CookieJar) { + return requestAsync.getAsync({ url: BASE_URL + Endpoints.VERIFY_GET, jar: jar }); + } + + function execute_login(jar: request.CookieJar) { + return requestAsync.getAsync({ url: BASE_URL + Endpoints.FIRST_FACTOR_GET, jar: jar }); + } + + function execute_first_factor(jar: request.CookieJar) { + return requestAsync.postAsync({ + url: BASE_URL + Endpoints.FIRST_FACTOR_POST, + jar: jar, + form: { + username: "test_ok", + password: "password" + } + }); + } + + function execute_failing_first_factor(jar: request.CookieJar) { + return requestAsync.postAsync({ + url: BASE_URL + Endpoints.FIRST_FACTOR_POST, + jar: jar, + form: { + username: "test_nok", + password: "password" + } + }); + } + + return { + login: execute_login, + verify: execute_verification, + u2f_authentication: execute_u2f_authentication, + first_factor: execute_first_factor, + failing_first_factor: execute_failing_first_factor, + totp: execute_totp, + }; +}; + diff --git a/themes/squares/server/tsconfig.json b/themes/squares/server/tsconfig.json new file mode 100644 index 000000000..ebe98c5ed --- /dev/null +++ b/themes/squares/server/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "moduleResolution": "node", + "noImplicitAny": true, + "sourceMap": true, + "removeComments": true, + "outDir": "../dist", + "baseUrl": ".", + "paths": { + "*": [ + "./types/*", + "../shared/types/*" + ] + } + }, + "exclude": [ + "src/**/*.spec.ts" + ] +} diff --git a/themes/squares/server/tslint.json b/themes/squares/server/tslint.json new file mode 100644 index 000000000..c2c1b7501 --- /dev/null +++ b/themes/squares/server/tslint.json @@ -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 + } +} diff --git a/themes/squares/server/types/.directory b/themes/squares/server/types/.directory new file mode 100644 index 000000000..1e65000ea --- /dev/null +++ b/themes/squares/server/types/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,58,27 +Version=3 +ViewMode=1 diff --git a/themes/squares/server/types/AuthenticationSession.ts b/themes/squares/server/types/AuthenticationSession.ts new file mode 100644 index 000000000..bbed0e715 --- /dev/null +++ b/themes/squares/server/types/AuthenticationSession.ts @@ -0,0 +1,18 @@ +import U2f = require("u2f"); +import { Level } from "../src/lib/authentication/Level"; + +export interface AuthenticationSession { + userid: string; + authentication_level: Level; + keep_me_logged_in: boolean; + last_activity_datetime: number; + identity_check?: { + challenge: string; + userid: string; + }; + register_request?: U2f.Request; + sign_request?: U2f.Request; + email: string; + groups: string[]; + redirect?: string; +} \ No newline at end of file diff --git a/themes/squares/server/types/Dependencies.ts b/themes/squares/server/types/Dependencies.ts new file mode 100644 index 000000000..f20404dbc --- /dev/null +++ b/themes/squares/server/types/Dependencies.ts @@ -0,0 +1,29 @@ +import winston = require("winston"); +import speakeasy = require("speakeasy"); +import nodemailer = require("nodemailer"); +import session = require("express-session"); +import nedb = require("nedb"); +import ldapjs = require("ldapjs"); +import u2f = require("u2f"); +import RedisSession = require("connect-redis"); +import Redis = require("redis"); + +export type Speakeasy = typeof speakeasy; +export type Winston = typeof winston; +export type Session = typeof session; +export type Nedb = typeof nedb; +export type Ldapjs = typeof ldapjs; +export type U2f = typeof u2f; +export type ConnectRedis = typeof RedisSession; +export type Redis = typeof Redis; + +export interface GlobalDependencies { + u2f: U2f; + ldapjs: Ldapjs; + session: Session; + Redis: Redis; + ConnectRedis: ConnectRedis; + winston: Winston; + speakeasy: Speakeasy; + nedb: Nedb; +} \ No newline at end of file diff --git a/themes/squares/server/types/Identity.ts b/themes/squares/server/types/Identity.ts new file mode 100644 index 000000000..e985984e2 --- /dev/null +++ b/themes/squares/server/types/Identity.ts @@ -0,0 +1,6 @@ + + +export interface Identity { + userid: string; + email: string; +} \ No newline at end of file diff --git a/themes/squares/server/types/TOTPSecret.ts b/themes/squares/server/types/TOTPSecret.ts new file mode 100644 index 000000000..d6775f2f0 --- /dev/null +++ b/themes/squares/server/types/TOTPSecret.ts @@ -0,0 +1,11 @@ + +export interface TOTPSecret { + ascii: string; + hex: string; + base32: string; + qr_code_ascii: string; + qr_code_hex: string; + qr_code_base32: string; + google_auth_qr: string; + otpauth_url: string; + } \ No newline at end of file diff --git a/themes/squares/server/types/U2FRegistration.ts b/themes/squares/server/types/U2FRegistration.ts new file mode 100644 index 000000000..b6080af07 --- /dev/null +++ b/themes/squares/server/types/U2FRegistration.ts @@ -0,0 +1,5 @@ + +export interface U2FRegistration { + keyHandle: string; + publicKey: string; +} \ No newline at end of file diff --git a/themes/squares/server/types/dovehash.d.ts b/themes/squares/server/types/dovehash.d.ts new file mode 100644 index 000000000..c354609c0 --- /dev/null +++ b/themes/squares/server/types/dovehash.d.ts @@ -0,0 +1,4 @@ + +declare module "dovehash" { + function encode(algo: string, text: string): string; +} \ No newline at end of file diff --git a/themes/squares/server/types/speakeasy.d.ts b/themes/squares/server/types/speakeasy.d.ts new file mode 100644 index 000000000..6ea06948b --- /dev/null +++ b/themes/squares/server/types/speakeasy.d.ts @@ -0,0 +1,96 @@ +declare module "speakeasy" { + export = speakeasy + + interface SharedOptions { + encoding?: string + algorithm?: string + } + + interface DigestOptions extends SharedOptions { + secret: string + counter: number + } + + interface HOTPOptions extends SharedOptions { + secret: string + counter: number + digest?: Buffer + digits?: number + } + + interface HOTPVerifyOptions extends SharedOptions { + secret: string + token: string + counter: number + digits?: number + window?: number + } + + interface TOTPOptions extends SharedOptions { + secret: string + time?: number + step?: number + epoch?: number + counter?: number + digits?: number + } + + interface TOTPVerifyOptions extends SharedOptions { + secret: string + token: string + time?: number + step?: number + epoch?: number + counter?: number + digits?: number + window?: number + } + + interface GenerateSecretOptions { + length?: number + symbols?: boolean + otpauth_url?: boolean + name?: string + issuer?: string + } + + interface GeneratedSecret { + ascii: string + hex: string + base32: string + qr_code_ascii: string + qr_code_hex: string + qr_code_base32: string + google_auth_qr: string + otpauth_url: string + } + + interface OTPAuthURLOptions extends SharedOptions { + secret: string + label: string + type?: string + counter?: number + issuer?: string + digits?: number + period?: number + } + + interface Speakeasy { + digest: (options: DigestOptions) => Buffer + hotp: { + (options: HOTPOptions): string, + verifyDelta: (options: HOTPVerifyOptions) => boolean, + verify: (options: HOTPVerifyOptions) => boolean, + } + totp: { + (options: TOTPOptions): string + verifyDelta: (options: TOTPVerifyOptions) => boolean, + verify: (options: TOTPVerifyOptions) => boolean, + } + generateSecret: (options?: GenerateSecretOptions) => GeneratedSecret + generateSecretASCII: (length?: number, symbols?: boolean) => string + otpauthURL: (options: OTPAuthURLOptions) => string + } + + const speakeasy: Speakeasy +} \ No newline at end of file diff --git a/themes/triangles/client/src/.directory b/themes/triangles/client/src/.directory new file mode 100644 index 000000000..72a940d6a --- /dev/null +++ b/themes/triangles/client/src/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,18,8,25,40 +Version=3 +ViewMode=1 diff --git a/themes/triangles/client/src/css/.directory b/themes/triangles/client/src/css/.directory new file mode 100644 index 000000000..6e4b3f639 --- /dev/null +++ b/themes/triangles/client/src/css/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,56,41 +Version=3 +ViewMode=1 diff --git a/themes/triangles/client/src/css/00-bootstrap.min.css b/themes/triangles/client/src/css/00-bootstrap.min.css new file mode 100644 index 000000000..dfeacbb88 --- /dev/null +++ b/themes/triangles/client/src/css/00-bootstrap.min.css @@ -0,0 +1,5768 @@ +/*! * Bootstrap v3.3.7 (http://getbootstrap.com) * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ +html{ + font-family:sans-serif; + -webkit-text-size-adjust:100%; + -ms-text-size-adjust:100% +} +body{ + margin:0 +} +article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{ + display:block +} +audio,canvas,progress,video{ + display:inline-block; + vertical-align:baseline +} +audio:not([controls]){ + display:none; + height:0 +} +[hidden],template{ + display:none +} +a{ + background-color:transparent +} +a:active,a:hover{ + outline:0 +} +abbr[title]{ + border-bottom:1px dotted +} +b,strong{ + font-weight:700 +} +dfn{ + font-style:italic +} +h1{ + margin:.67em 0; + font-size:2em +} +mark{ + color:#000; + background:#ff0 +} +small{ + font-size:80% +} +sub,sup{ + position:relative; + font-size:75%; + line-height:0; + vertical-align:baseline +} +sup{ + top:-.5em +} +sub{ + bottom:-.25em +} +img{ + border:0 +} +svg:not(:root){ + overflow:hidden +} +figure{ + margin:1em 40px +} +hr{ + height:0; + -webkit-box-sizing:content-box; + -moz-box-sizing:content-box; + box-sizing:content-box +} +pre{ + overflow:auto +} +code,kbd,pre,samp{ + font-family:monospace,monospace; + font-size:1em +} +button,input,optgroup,select,textarea{ + margin:0; + font:inherit; + color:inherit +} +button{ + overflow:visible +} +button,select{ + text-transform:none +} +button,html input[type=button],input[type=reset],input[type=submit]{ + -webkit-appearance:button; + cursor:pointer +} +button[disabled],html input[disabled]{ + cursor:default +} +button::-moz-focus-inner,input::-moz-focus-inner{ + padding:0; + border:0 +} +input{ + line-height:normal +} +input[type=checkbox],input[type=radio]{ + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + padding:0 +} +input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{ + height:auto +} +input[type=search]{ + -webkit-box-sizing:content-box; + -moz-box-sizing:content-box; + box-sizing:content-box; + -webkit-appearance:textfield +} +input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{ + -webkit-appearance:none +} +fieldset{ + padding:.35em .625em .75em; + margin:0 2px; + border:1px solid silver +} +legend{ + padding:0; + border:0 +} +textarea{ + overflow:auto +} +optgroup{ + font-weight:700 +} +table{ + border-spacing:0; + border-collapse:collapse +} +td,th{ + padding:0 +} +/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ +@media print{ + *,:after,:before{ + color:#000!important; + text-shadow:none!important; + background:0 0!important; + -webkit-box-shadow:none!important; + box-shadow:none!important + } + a,a:visited{ + text-decoration:underline + } + a[href]:after{ + content:" (" attr(href) ")" + } + abbr[title]:after{ + content:" (" attr(title) ")" + } + a[href^="javascript:"]:after,a[href^="#"]:after{ + content:"" + } + blockquote,pre{ + border:1px solid #999; + page-break-inside:avoid + } + thead{ + display:table-header-group + } + img,tr{ + page-break-inside:avoid + } + img{ + max-width:100%!important + } + h2,h3,p{ + orphans:3; + widows:3 + } + h2,h3{ + page-break-after:avoid + } + .navbar{ + display:none + } + .btn>.caret,.dropup>.btn>.caret{ + border-top-color:#000!important + } + .label{ + border:1px solid #000 + } + .table{ + border-collapse:collapse!important + } + .table td,.table th{ + background-color:#fff!important + } + .table-bordered td,.table-bordered th{ + border:1px solid #ddd!important + } +} +@font-face{ + font-family:'Glyphicons Halflings'; + src:url(../fonts/glyphicons-halflings-regular.eot); + src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg') +} +.glyphicon{ + position:relative; + top:1px; + display:inline-block; + font-family:'Glyphicons Halflings'; + font-style:normal; + font-weight:400; + line-height:1; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale +} +.glyphicon-asterisk:before{ + content:"\002a" +} +.glyphicon-plus:before{ + content:"\002b" +} +.glyphicon-eur:before,.glyphicon-euro:before{ + content:"\20ac" +} +.glyphicon-minus:before{ + content:"\2212" +} +.glyphicon-cloud:before{ + content:"\2601" +} +.glyphicon-envelope:before{ + content:"\2709" +} +.glyphicon-pencil:before{ + content:"\270f" +} +.glyphicon-glass:before{ + content:"\e001" +} +.glyphicon-music:before{ + content:"\e002" +} +.glyphicon-search:before{ + content:"\e003" +} +.glyphicon-heart:before{ + content:"\e005" +} +.glyphicon-star:before{ + content:"\e006" +} +.glyphicon-star-empty:before{ + content:"\e007" +} +.glyphicon-user:before{ + content:"\e008" +} +.glyphicon-film:before{ + content:"\e009" +} +.glyphicon-th-large:before{ + content:"\e010" +} +.glyphicon-th:before{ + content:"\e011" +} +.glyphicon-th-list:before{ + content:"\e012" +} +.glyphicon-ok:before{ + content:"\e013" +} +.glyphicon-remove:before{ + content:"\e014" +} +.glyphicon-zoom-in:before{ + content:"\e015" +} +.glyphicon-zoom-out:before{ + content:"\e016" +} +.glyphicon-off:before{ + content:"\e017" +} +.glyphicon-signal:before{ + content:"\e018" +} +.glyphicon-cog:before{ + content:"\e019" +} +.glyphicon-trash:before{ + content:"\e020" +} +.glyphicon-home:before{ + content:"\e021" +} +.glyphicon-file:before{ + content:"\e022" +} +.glyphicon-time:before{ + content:"\e023" +} +.glyphicon-road:before{ + content:"\e024" +} +.glyphicon-download-alt:before{ + content:"\e025" +} +.glyphicon-download:before{ + content:"\e026" +} +.glyphicon-upload:before{ + content:"\e027" +} +.glyphicon-inbox:before{ + content:"\e028" +} +.glyphicon-play-circle:before{ + content:"\e029" +} +.glyphicon-repeat:before{ + content:"\e030" +} +.glyphicon-refresh:before{ + content:"\e031" +} +.glyphicon-list-alt:before{ + content:"\e032" +} +.glyphicon-lock:before{ + content:"\e033" +} +.glyphicon-flag:before{ + content:"\e034" +} +.glyphicon-headphones:before{ + content:"\e035" +} +.glyphicon-volume-off:before{ + content:"\e036" +} +.glyphicon-volume-down:before{ + content:"\e037" +} +.glyphicon-volume-up:before{ + content:"\e038" +} +.glyphicon-qrcode:before{ + content:"\e039" +} +.glyphicon-barcode:before{ + content:"\e040" +} +.glyphicon-tag:before{ + content:"\e041" +} +.glyphicon-tags:before{ + content:"\e042" +} +.glyphicon-book:before{ + content:"\e043" +} +.glyphicon-bookmark:before{ + content:"\e044" +} +.glyphicon-print:before{ + content:"\e045" +} +.glyphicon-camera:before{ + content:"\e046" +} +.glyphicon-font:before{ + content:"\e047" +} +.glyphicon-bold:before{ + content:"\e048" +} +.glyphicon-italic:before{ + content:"\e049" +} +.glyphicon-text-height:before{ + content:"\e050" +} +.glyphicon-text-width:before{ + content:"\e051" +} +.glyphicon-align-left:before{ + content:"\e052" +} +.glyphicon-align-center:before{ + content:"\e053" +} +.glyphicon-align-right:before{ + content:"\e054" +} +.glyphicon-align-justify:before{ + content:"\e055" +} +.glyphicon-list:before{ + content:"\e056" +} +.glyphicon-indent-left:before{ + content:"\e057" +} +.glyphicon-indent-right:before{ + content:"\e058" +} +.glyphicon-facetime-video:before{ + content:"\e059" +} +.glyphicon-picture:before{ + content:"\e060" +} +.glyphicon-map-marker:before{ + content:"\e062" +} +.glyphicon-adjust:before{ + content:"\e063" +} +.glyphicon-tint:before{ + content:"\e064" +} +.glyphicon-edit:before{ + content:"\e065" +} +.glyphicon-share:before{ + content:"\e066" +} +.glyphicon-check:before{ + content:"\e067" +} +.glyphicon-move:before{ + content:"\e068" +} +.glyphicon-step-backward:before{ + content:"\e069" +} +.glyphicon-fast-backward:before{ + content:"\e070" +} +.glyphicon-backward:before{ + content:"\e071" +} +.glyphicon-play:before{ + content:"\e072" +} +.glyphicon-pause:before{ + content:"\e073" +} +.glyphicon-stop:before{ + content:"\e074" +} +.glyphicon-forward:before{ + content:"\e075" +} +.glyphicon-fast-forward:before{ + content:"\e076" +} +.glyphicon-step-forward:before{ + content:"\e077" +} +.glyphicon-eject:before{ + content:"\e078" +} +.glyphicon-chevron-left:before{ + content:"\e079" +} +.glyphicon-chevron-right:before{ + content:"\e080" +} +.glyphicon-plus-sign:before{ + content:"\e081" +} +.glyphicon-minus-sign:before{ + content:"\e082" +} +.glyphicon-remove-sign:before{ + content:"\e083" +} +.glyphicon-ok-sign:before{ + content:"\e084" +} +.glyphicon-question-sign:before{ + content:"\e085" +} +.glyphicon-info-sign:before{ + content:"\e086" +} +.glyphicon-screenshot:before{ + content:"\e087" +} +.glyphicon-remove-circle:before{ + content:"\e088" +} +.glyphicon-ok-circle:before{ + content:"\e089" +} +.glyphicon-ban-circle:before{ + content:"\e090" +} +.glyphicon-arrow-left:before{ + content:"\e091" +} +.glyphicon-arrow-right:before{ + content:"\e092" +} +.glyphicon-arrow-up:before{ + content:"\e093" +} +.glyphicon-arrow-down:before{ + content:"\e094" +} +.glyphicon-share-alt:before{ + content:"\e095" +} +.glyphicon-resize-full:before{ + content:"\e096" +} +.glyphicon-resize-small:before{ + content:"\e097" +} +.glyphicon-exclamation-sign:before{ + content:"\e101" +} +.glyphicon-gift:before{ + content:"\e102" +} +.glyphicon-leaf:before{ + content:"\e103" +} +.glyphicon-fire:before{ + content:"\e104" +} +.glyphicon-eye-open:before{ + content:"\e105" +} +.glyphicon-eye-close:before{ + content:"\e106" +} +.glyphicon-warning-sign:before{ + content:"\e107" +} +.glyphicon-plane:before{ + content:"\e108" +} +.glyphicon-calendar:before{ + content:"\e109" +} +.glyphicon-random:before{ + content:"\e110" +} +.glyphicon-comment:before{ + content:"\e111" +} +.glyphicon-magnet:before{ + content:"\e112" +} +.glyphicon-chevron-up:before{ + content:"\e113" +} +.glyphicon-chevron-down:before{ + content:"\e114" +} +.glyphicon-retweet:before{ + content:"\e115" +} +.glyphicon-shopping-cart:before{ + content:"\e116" +} +.glyphicon-folder-close:before{ + content:"\e117" +} +.glyphicon-folder-open:before{ + content:"\e118" +} +.glyphicon-resize-vertical:before{ + content:"\e119" +} +.glyphicon-resize-horizontal:before{ + content:"\e120" +} +.glyphicon-hdd:before{ + content:"\e121" +} +.glyphicon-bullhorn:before{ + content:"\e122" +} +.glyphicon-bell:before{ + content:"\e123" +} +.glyphicon-certificate:before{ + content:"\e124" +} +.glyphicon-thumbs-up:before{ + content:"\e125" +} +.glyphicon-thumbs-down:before{ + content:"\e126" +} +.glyphicon-hand-right:before{ + content:"\e127" +} +.glyphicon-hand-left:before{ + content:"\e128" +} +.glyphicon-hand-up:before{ + content:"\e129" +} +.glyphicon-hand-down:before{ + content:"\e130" +} +.glyphicon-circle-arrow-right:before{ + content:"\e131" +} +.glyphicon-circle-arrow-left:before{ + content:"\e132" +} +.glyphicon-circle-arrow-up:before{ + content:"\e133" +} +.glyphicon-circle-arrow-down:before{ + content:"\e134" +} +.glyphicon-globe:before{ + content:"\e135" +} +.glyphicon-wrench:before{ + content:"\e136" +} +.glyphicon-tasks:before{ + content:"\e137" +} +.glyphicon-filter:before{ + content:"\e138" +} +.glyphicon-briefcase:before{ + content:"\e139" +} +.glyphicon-fullscreen:before{ + content:"\e140" +} +.glyphicon-dashboard:before{ + content:"\e141" +} +.glyphicon-paperclip:before{ + content:"\e142" +} +.glyphicon-heart-empty:before{ + content:"\e143" +} +.glyphicon-link:before{ + content:"\e144" +} +.glyphicon-phone:before{ + content:"\e145" +} +.glyphicon-pushpin:before{ + content:"\e146" +} +.glyphicon-usd:before{ + content:"\e148" +} +.glyphicon-gbp:before{ + content:"\e149" +} +.glyphicon-sort:before{ + content:"\e150" +} +.glyphicon-sort-by-alphabet:before{ + content:"\e151" +} +.glyphicon-sort-by-alphabet-alt:before{ + content:"\e152" +} +.glyphicon-sort-by-order:before{ + content:"\e153" +} +.glyphicon-sort-by-order-alt:before{ + content:"\e154" +} +.glyphicon-sort-by-attributes:before{ + content:"\e155" +} +.glyphicon-sort-by-attributes-alt:before{ + content:"\e156" +} +.glyphicon-unchecked:before{ + content:"\e157" +} +.glyphicon-expand:before{ + content:"\e158" +} +.glyphicon-collapse-down:before{ + content:"\e159" +} +.glyphicon-collapse-up:before{ + content:"\e160" +} +.glyphicon-log-in:before{ + content:"\e161" +} +.glyphicon-flash:before{ + content:"\e162" +} +.glyphicon-log-out:before{ + content:"\e163" +} +.glyphicon-new-window:before{ + content:"\e164" +} +.glyphicon-record:before{ + content:"\e165" +} +.glyphicon-save:before{ + content:"\e166" +} +.glyphicon-open:before{ + content:"\e167" +} +.glyphicon-saved:before{ + content:"\e168" +} +.glyphicon-import:before{ + content:"\e169" +} +.glyphicon-export:before{ + content:"\e170" +} +.glyphicon-send:before{ + content:"\e171" +} +.glyphicon-floppy-disk:before{ + content:"\e172" +} +.glyphicon-floppy-saved:before{ + content:"\e173" +} +.glyphicon-floppy-remove:before{ + content:"\e174" +} +.glyphicon-floppy-save:before{ + content:"\e175" +} +.glyphicon-floppy-open:before{ + content:"\e176" +} +.glyphicon-credit-card:before{ + content:"\e177" +} +.glyphicon-transfer:before{ + content:"\e178" +} +.glyphicon-cutlery:before{ + content:"\e179" +} +.glyphicon-header:before{ + content:"\e180" +} +.glyphicon-compressed:before{ + content:"\e181" +} +.glyphicon-earphone:before{ + content:"\e182" +} +.glyphicon-phone-alt:before{ + content:"\e183" +} +.glyphicon-tower:before{ + content:"\e184" +} +.glyphicon-stats:before{ + content:"\e185" +} +.glyphicon-sd-video:before{ + content:"\e186" +} +.glyphicon-hd-video:before{ + content:"\e187" +} +.glyphicon-subtitles:before{ + content:"\e188" +} +.glyphicon-sound-stereo:before{ + content:"\e189" +} +.glyphicon-sound-dolby:before{ + content:"\e190" +} +.glyphicon-sound-5-1:before{ + content:"\e191" +} +.glyphicon-sound-6-1:before{ + content:"\e192" +} +.glyphicon-sound-7-1:before{ + content:"\e193" +} +.glyphicon-copyright-mark:before{ + content:"\e194" +} +.glyphicon-registration-mark:before{ + content:"\e195" +} +.glyphicon-cloud-download:before{ + content:"\e197" +} +.glyphicon-cloud-upload:before{ + content:"\e198" +} +.glyphicon-tree-conifer:before{ + content:"\e199" +} +.glyphicon-tree-deciduous:before{ + content:"\e200" +} +.glyphicon-cd:before{ + content:"\e201" +} +.glyphicon-save-file:before{ + content:"\e202" +} +.glyphicon-open-file:before{ + content:"\e203" +} +.glyphicon-level-up:before{ + content:"\e204" +} +.glyphicon-copy:before{ + content:"\e205" +} +.glyphicon-paste:before{ + content:"\e206" +} +.glyphicon-alert:before{ + content:"\e209" +} +.glyphicon-equalizer:before{ + content:"\e210" +} +.glyphicon-king:before{ + content:"\e211" +} +.glyphicon-queen:before{ + content:"\e212" +} +.glyphicon-pawn:before{ + content:"\e213" +} +.glyphicon-bishop:before{ + content:"\e214" +} +.glyphicon-knight:before{ + content:"\e215" +} +.glyphicon-baby-formula:before{ + content:"\e216" +} +.glyphicon-tent:before{ + content:"\26fa" +} +.glyphicon-blackboard:before{ + content:"\e218" +} +.glyphicon-bed:before{ + content:"\e219" +} +.glyphicon-apple:before{ + content:"\f8ff" +} +.glyphicon-erase:before{ + content:"\e221" +} +.glyphicon-hourglass:before{ + content:"\231b" +} +.glyphicon-lamp:before{ + content:"\e223" +} +.glyphicon-duplicate:before{ + content:"\e224" +} +.glyphicon-piggy-bank:before{ + content:"\e225" +} +.glyphicon-scissors:before{ + content:"\e226" +} +.glyphicon-bitcoin:before{ + content:"\e227" +} +.glyphicon-btc:before{ + content:"\e227" +} +.glyphicon-xbt:before{ + content:"\e227" +} +.glyphicon-yen:before{ + content:"\00a5" +} +.glyphicon-jpy:before{ + content:"\00a5" +} +.glyphicon-ruble:before{ + content:"\20bd" +} +.glyphicon-rub:before{ + content:"\20bd" +} +.glyphicon-scale:before{ + content:"\e230" +} +.glyphicon-ice-lolly:before{ + content:"\e231" +} +.glyphicon-ice-lolly-tasted:before{ + content:"\e232" +} +.glyphicon-education:before{ + content:"\e233" +} +.glyphicon-option-horizontal:before{ + content:"\e234" +} +.glyphicon-option-vertical:before{ + content:"\e235" +} +.glyphicon-menu-hamburger:before{ + content:"\e236" +} +.glyphicon-modal-window:before{ + content:"\e237" +} +.glyphicon-oil:before{ + content:"\e238" +} +.glyphicon-grain:before{ + content:"\e239" +} +.glyphicon-sunglasses:before{ + content:"\e240" +} +.glyphicon-text-size:before{ + content:"\e241" +} +.glyphicon-text-color:before{ + content:"\e242" +} +.glyphicon-text-background:before{ + content:"\e243" +} +.glyphicon-object-align-top:before{ + content:"\e244" +} +.glyphicon-object-align-bottom:before{ + content:"\e245" +} +.glyphicon-object-align-horizontal:before{ + content:"\e246" +} +.glyphicon-object-align-left:before{ + content:"\e247" +} +.glyphicon-object-align-vertical:before{ + content:"\e248" +} +.glyphicon-object-align-right:before{ + content:"\e249" +} +.glyphicon-triangle-right:before{ + content:"\e250" +} +.glyphicon-triangle-left:before{ + content:"\e251" +} +.glyphicon-triangle-bottom:before{ + content:"\e252" +} +.glyphicon-triangle-top:before{ + content:"\e253" +} +.glyphicon-console:before{ + content:"\e254" +} +.glyphicon-superscript:before{ + content:"\e255" +} +.glyphicon-subscript:before{ + content:"\e256" +} +.glyphicon-menu-left:before{ + content:"\e257" +} +.glyphicon-menu-right:before{ + content:"\e258" +} +.glyphicon-menu-down:before{ + content:"\e259" +} +.glyphicon-menu-up:before{ + content:"\e260" +} +*{ + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box +} +:after,:before{ + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box +} +html{ + font-size:10px; + -webkit-tap-highlight-color:rgba(0,0,0,0) +} +body{ + font-family:"Helvetica Neue",Helvetica,Arial,sans-serif; + font-size:14px; + line-height:1.42857143; + color:#333; + background-color:#fff +} +button,input,select,textarea{ + font-family:inherit; + font-size:inherit; + line-height:inherit +} +a{ + color:#337ab7; + text-decoration:none +} +a:focus,a:hover{ + color:#23527c; + text-decoration:underline +} +a:focus{ + outline:5px auto -webkit-focus-ring-color; + outline-offset:-2px +} +figure{ + margin:0 +} +img{ + vertical-align:middle +} +.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{ + display:block; + max-width:100%; + height:auto +} +.img-rounded{ + border-radius:6px +} +.img-thumbnail{ + display:inline-block; + max-width:100%; + height:auto; + padding:4px; + line-height:1.42857143; + background-color:#fff; + border:1px solid #ddd; + border-radius:4px; + -webkit-transition:all .2s ease-in-out; + -o-transition:all .2s ease-in-out; + transition:all .2s ease-in-out +} +.img-circle{ + border-radius:50% +} +hr{ + margin-top:20px; + margin-bottom:20px; + border:0; + border-top:1px solid #eee +} +.sr-only{ + position:absolute; + width:1px; + height:1px; + padding:0; + margin:-1px; + overflow:hidden; + clip:rect(0,0,0,0); + border:0 +} +.sr-only-focusable:active,.sr-only-focusable:focus{ + position:static; + width:auto; + height:auto; + margin:0; + overflow:visible; + clip:auto +} +[role=button]{ + cursor:pointer +} +.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{ + font-family:inherit; + font-weight:500; + line-height:1.1; + color:inherit +} +.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{ + font-weight:400; + line-height:1; + color:#777 +} +.h1,.h2,.h3,h1,h2,h3{ + margin-top:20px; + margin-bottom:10px +} +.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{ + font-size:65% +} +.h4,.h5,.h6,h4,h5,h6{ + margin-top:10px; + margin-bottom:10px +} +.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{ + font-size:75% +} +.h1,h1{ + font-size:36px +} +.h2,h2{ + font-size:30px +} +.h3,h3{ + font-size:24px +} +.h4,h4{ + font-size:18px +} +.h5,h5{ + font-size:14px +} +.h6,h6{ + font-size:12px +} +p{ + margin:0 0 10px +} +.lead{ + margin-bottom:20px; + font-size:16px; + font-weight:300; + line-height:1.4 +} +@media (min-width:768px){ + .lead{ + font-size:21px + } +} +.small,small{ + font-size:85% +} +.mark,mark{ + padding:.2em; + background-color:#fcf8e3 +} +.text-left{ + text-align:left +} +.text-right{ + text-align:right +} +.text-center{ + text-align:center +} +.text-justify{ + text-align:justify +} +.text-nowrap{ + white-space:nowrap +} +.text-lowercase{ + text-transform:lowercase +} +.text-uppercase{ + text-transform:uppercase +} +.text-capitalize{ + text-transform:capitalize +} +.text-muted{ + color:#777 +} +.text-primary{ + color:#337ab7 +} +a.text-primary:focus,a.text-primary:hover{ + color:#286090 +} +.text-success{ + color:#3c763d +} +a.text-success:focus,a.text-success:hover{ + color:#2b542c +} +.text-info{ + color:#31708f +} +a.text-info:focus,a.text-info:hover{ + color:#245269 +} +.text-warning{ + color:#8a6d3b +} +a.text-warning:focus,a.text-warning:hover{ + color:#66512c +} +.text-danger{ + color:#a94442 +} +a.text-danger:focus,a.text-danger:hover{ + color:#843534 +} +.bg-primary{ + color:#fff; + background-color:#337ab7 +} +a.bg-primary:focus,a.bg-primary:hover{ + background-color:#286090 +} +.bg-success{ + background-color:#dff0d8 +} +a.bg-success:focus,a.bg-success:hover{ + background-color:#c1e2b3 +} +.bg-info{ + background-color:#d9edf7 +} +a.bg-info:focus,a.bg-info:hover{ + background-color:#afd9ee +} +.bg-warning{ + background-color:#fcf8e3 +} +a.bg-warning:focus,a.bg-warning:hover{ + background-color:#f7ecb5 +} +.bg-danger{ + background-color:#f2dede +} +a.bg-danger:focus,a.bg-danger:hover{ + background-color:#e4b9b9 +} +.page-header{ + padding-bottom:9px; + margin:40px 0 20px; + border-bottom:1px solid #eee +} +ol,ul{ + margin-top:0; + margin-bottom:10px +} +ol ol,ol ul,ul ol,ul ul{ + margin-bottom:0 +} +.list-unstyled{ + padding-left:0; + list-style:none +} +.list-inline{ + padding-left:0; + margin-left:-5px; + list-style:none +} +.list-inline>li{ + display:inline-block; + padding-right:5px; + padding-left:5px +} +dl{ + margin-top:0; + margin-bottom:20px +} +dd,dt{ + line-height:1.42857143 +} +dt{ + font-weight:700 +} +dd{ + margin-left:0 +} +@media (min-width:768px){ + .dl-horizontal dt{ + float:left; + width:160px; + overflow:hidden; + clear:left; + text-align:right; + text-overflow:ellipsis; + white-space:nowrap + } + .dl-horizontal dd{ + margin-left:180px + } +} +abbr[data-original-title],abbr[title]{ + cursor:help; + border-bottom:1px dotted #777 +} +.initialism{ + font-size:90%; + text-transform:uppercase +} +blockquote{ + padding:10px 20px; + margin:0 0 20px; + font-size:17.5px; + border-left:5px solid #eee +} +blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{ + margin-bottom:0 +} +blockquote .small,blockquote footer,blockquote small{ + display:block; + font-size:80%; + line-height:1.42857143; + color:#777 +} +blockquote .small:before,blockquote footer:before,blockquote small:before{ + content:'\2014 \00A0' +} +.blockquote-reverse,blockquote.pull-right{ + padding-right:15px; + padding-left:0; + text-align:right; + border-right:5px solid #eee; + border-left:0 +} +.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{ + content:'' +} +.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{ + content:'\00A0 \2014' +} +address{ + margin-bottom:20px; + font-style:normal; + line-height:1.42857143 +} +code,kbd,pre,samp{ + font-family:Menlo,Monaco,Consolas,"Courier New",monospace +} +code{ + padding:2px 4px; + font-size:90%; + color:#c7254e; + background-color:#f9f2f4; + border-radius:4px +} +kbd{ + padding:2px 4px; + font-size:90%; + color:#fff; + background-color:#333; + border-radius:3px; + -webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25); + box-shadow:inset 0 -1px 0 rgba(0,0,0,.25) +} +kbd kbd{ + padding:0; + font-size:100%; + font-weight:700; + -webkit-box-shadow:none; + box-shadow:none +} +pre{ + display:block; + padding:9.5px; + margin:0 0 10px; + font-size:13px; + line-height:1.42857143; + color:#333; + word-break:break-all; + word-wrap:break-word; + background-color:#f5f5f5; + border:1px solid #ccc; + border-radius:4px +} +pre code{ + padding:0; + font-size:inherit; + color:inherit; + white-space:pre-wrap; + background-color:transparent; + border-radius:0 +} +.pre-scrollable{ + max-height:340px; + overflow-y:scroll +} +.container{ + padding-right:15px; + padding-left:15px; + margin-right:auto; + margin-left:auto +} +@media (min-width:768px){ + .container{ + width:750px + } +} +@media (min-width:992px){ + .container{ + width:970px + } +} +@media (min-width:1200px){ + .container{ + width:1170px + } +} +.container-fluid{ + padding-right:15px; + padding-left:15px; + margin-right:auto; + margin-left:auto +} +.row{ + margin-right:-15px; + margin-left:-15px +} +.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{ + position:relative; + min-height:1px; + padding-right:15px; + padding-left:15px +} +.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{ + float:left +} +.col-xs-12{ + width:100% +} +.col-xs-11{ + width:91.66666667% +} +.col-xs-10{ + width:83.33333333% +} +.col-xs-9{ + width:75% +} +.col-xs-8{ + width:66.66666667% +} +.col-xs-7{ + width:58.33333333% +} +.col-xs-6{ + width:50% +} +.col-xs-5{ + width:41.66666667% +} +.col-xs-4{ + width:33.33333333% +} +.col-xs-3{ + width:25% +} +.col-xs-2{ + width:16.66666667% +} +.col-xs-1{ + width:8.33333333% +} +.col-xs-pull-12{ + right:100% +} +.col-xs-pull-11{ + right:91.66666667% +} +.col-xs-pull-10{ + right:83.33333333% +} +.col-xs-pull-9{ + right:75% +} +.col-xs-pull-8{ + right:66.66666667% +} +.col-xs-pull-7{ + right:58.33333333% +} +.col-xs-pull-6{ + right:50% +} +.col-xs-pull-5{ + right:41.66666667% +} +.col-xs-pull-4{ + right:33.33333333% +} +.col-xs-pull-3{ + right:25% +} +.col-xs-pull-2{ + right:16.66666667% +} +.col-xs-pull-1{ + right:8.33333333% +} +.col-xs-pull-0{ + right:auto +} +.col-xs-push-12{ + left:100% +} +.col-xs-push-11{ + left:91.66666667% +} +.col-xs-push-10{ + left:83.33333333% +} +.col-xs-push-9{ + left:75% +} +.col-xs-push-8{ + left:66.66666667% +} +.col-xs-push-7{ + left:58.33333333% +} +.col-xs-push-6{ + left:50% +} +.col-xs-push-5{ + left:41.66666667% +} +.col-xs-push-4{ + left:33.33333333% +} +.col-xs-push-3{ + left:25% +} +.col-xs-push-2{ + left:16.66666667% +} +.col-xs-push-1{ + left:8.33333333% +} +.col-xs-push-0{ + left:auto +} +.col-xs-offset-12{ + margin-left:100% +} +.col-xs-offset-11{ + margin-left:91.66666667% +} +.col-xs-offset-10{ + margin-left:83.33333333% +} +.col-xs-offset-9{ + margin-left:75% +} +.col-xs-offset-8{ + margin-left:66.66666667% +} +.col-xs-offset-7{ + margin-left:58.33333333% +} +.col-xs-offset-6{ + margin-left:50% +} +.col-xs-offset-5{ + margin-left:41.66666667% +} +.col-xs-offset-4{ + margin-left:33.33333333% +} +.col-xs-offset-3{ + margin-left:25% +} +.col-xs-offset-2{ + margin-left:16.66666667% +} +.col-xs-offset-1{ + margin-left:8.33333333% +} +.col-xs-offset-0{ + margin-left:0 +} +@media (min-width:768px){ + .col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{ + float:left + } + .col-sm-12{ + width:100% + } + .col-sm-11{ + width:91.66666667% + } + .col-sm-10{ + width:83.33333333% + } + .col-sm-9{ + width:75% + } + .col-sm-8{ + width:66.66666667% + } + .col-sm-7{ + width:58.33333333% + } + .col-sm-6{ + width:50% + } + .col-sm-5{ + width:41.66666667% + } + .col-sm-4{ + width:33.33333333% + } + .col-sm-3{ + width:25% + } + .col-sm-2{ + width:16.66666667% + } + .col-sm-1{ + width:8.33333333% + } + .col-sm-pull-12{ + right:100% + } + .col-sm-pull-11{ + right:91.66666667% + } + .col-sm-pull-10{ + right:83.33333333% + } + .col-sm-pull-9{ + right:75% + } + .col-sm-pull-8{ + right:66.66666667% + } + .col-sm-pull-7{ + right:58.33333333% + } + .col-sm-pull-6{ + right:50% + } + .col-sm-pull-5{ + right:41.66666667% + } + .col-sm-pull-4{ + right:33.33333333% + } + .col-sm-pull-3{ + right:25% + } + .col-sm-pull-2{ + right:16.66666667% + } + .col-sm-pull-1{ + right:8.33333333% + } + .col-sm-pull-0{ + right:auto + } + .col-sm-push-12{ + left:100% + } + .col-sm-push-11{ + left:91.66666667% + } + .col-sm-push-10{ + left:83.33333333% + } + .col-sm-push-9{ + left:75% + } + .col-sm-push-8{ + left:66.66666667% + } + .col-sm-push-7{ + left:58.33333333% + } + .col-sm-push-6{ + left:50% + } + .col-sm-push-5{ + left:41.66666667% + } + .col-sm-push-4{ + left:33.33333333% + } + .col-sm-push-3{ + left:25% + } + .col-sm-push-2{ + left:16.66666667% + } + .col-sm-push-1{ + left:8.33333333% + } + .col-sm-push-0{ + left:auto + } + .col-sm-offset-12{ + margin-left:100% + } + .col-sm-offset-11{ + margin-left:91.66666667% + } + .col-sm-offset-10{ + margin-left:83.33333333% + } + .col-sm-offset-9{ + margin-left:75% + } + .col-sm-offset-8{ + margin-left:66.66666667% + } + .col-sm-offset-7{ + margin-left:58.33333333% + } + .col-sm-offset-6{ + margin-left:50% + } + .col-sm-offset-5{ + margin-left:41.66666667% + } + .col-sm-offset-4{ + margin-left:33.33333333% + } + .col-sm-offset-3{ + margin-left:25% + } + .col-sm-offset-2{ + margin-left:16.66666667% + } + .col-sm-offset-1{ + margin-left:8.33333333% + } + .col-sm-offset-0{ + margin-left:0 + } +} +@media (min-width:992px){ + .col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{ + float:left + } + .col-md-12{ + width:100% + } + .col-md-11{ + width:91.66666667% + } + .col-md-10{ + width:83.33333333% + } + .col-md-9{ + width:75% + } + .col-md-8{ + width:66.66666667% + } + .col-md-7{ + width:58.33333333% + } + .col-md-6{ + width:50% + } + .col-md-5{ + width:41.66666667% + } + .col-md-4{ + width:33.33333333% + } + .col-md-3{ + width:25% + } + .col-md-2{ + width:16.66666667% + } + .col-md-1{ + width:8.33333333% + } + .col-md-pull-12{ + right:100% + } + .col-md-pull-11{ + right:91.66666667% + } + .col-md-pull-10{ + right:83.33333333% + } + .col-md-pull-9{ + right:75% + } + .col-md-pull-8{ + right:66.66666667% + } + .col-md-pull-7{ + right:58.33333333% + } + .col-md-pull-6{ + right:50% + } + .col-md-pull-5{ + right:41.66666667% + } + .col-md-pull-4{ + right:33.33333333% + } + .col-md-pull-3{ + right:25% + } + .col-md-pull-2{ + right:16.66666667% + } + .col-md-pull-1{ + right:8.33333333% + } + .col-md-pull-0{ + right:auto + } + .col-md-push-12{ + left:100% + } + .col-md-push-11{ + left:91.66666667% + } + .col-md-push-10{ + left:83.33333333% + } + .col-md-push-9{ + left:75% + } + .col-md-push-8{ + left:66.66666667% + } + .col-md-push-7{ + left:58.33333333% + } + .col-md-push-6{ + left:50% + } + .col-md-push-5{ + left:41.66666667% + } + .col-md-push-4{ + left:33.33333333% + } + .col-md-push-3{ + left:25% + } + .col-md-push-2{ + left:16.66666667% + } + .col-md-push-1{ + left:8.33333333% + } + .col-md-push-0{ + left:auto + } + .col-md-offset-12{ + margin-left:100% + } + .col-md-offset-11{ + margin-left:91.66666667% + } + .col-md-offset-10{ + margin-left:83.33333333% + } + .col-md-offset-9{ + margin-left:75% + } + .col-md-offset-8{ + margin-left:66.66666667% + } + .col-md-offset-7{ + margin-left:58.33333333% + } + .col-md-offset-6{ + margin-left:50% + } + .col-md-offset-5{ + margin-left:41.66666667% + } + .col-md-offset-4{ + margin-left:33.33333333% + } + .col-md-offset-3{ + margin-left:25% + } + .col-md-offset-2{ + margin-left:16.66666667% + } + .col-md-offset-1{ + margin-left:8.33333333% + } + .col-md-offset-0{ + margin-left:0 + } +} +@media (min-width:1200px){ + .col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{ + float:left + } + .col-lg-12{ + width:100% + } + .col-lg-11{ + width:91.66666667% + } + .col-lg-10{ + width:83.33333333% + } + .col-lg-9{ + width:75% + } + .col-lg-8{ + width:66.66666667% + } + .col-lg-7{ + width:58.33333333% + } + .col-lg-6{ + width:50% + } + .col-lg-5{ + width:41.66666667% + } + .col-lg-4{ + width:33.33333333% + } + .col-lg-3{ + width:25% + } + .col-lg-2{ + width:16.66666667% + } + .col-lg-1{ + width:8.33333333% + } + .col-lg-pull-12{ + right:100% + } + .col-lg-pull-11{ + right:91.66666667% + } + .col-lg-pull-10{ + right:83.33333333% + } + .col-lg-pull-9{ + right:75% + } + .col-lg-pull-8{ + right:66.66666667% + } + .col-lg-pull-7{ + right:58.33333333% + } + .col-lg-pull-6{ + right:50% + } + .col-lg-pull-5{ + right:41.66666667% + } + .col-lg-pull-4{ + right:33.33333333% + } + .col-lg-pull-3{ + right:25% + } + .col-lg-pull-2{ + right:16.66666667% + } + .col-lg-pull-1{ + right:8.33333333% + } + .col-lg-pull-0{ + right:auto + } + .col-lg-push-12{ + left:100% + } + .col-lg-push-11{ + left:91.66666667% + } + .col-lg-push-10{ + left:83.33333333% + } + .col-lg-push-9{ + left:75% + } + .col-lg-push-8{ + left:66.66666667% + } + .col-lg-push-7{ + left:58.33333333% + } + .col-lg-push-6{ + left:50% + } + .col-lg-push-5{ + left:41.66666667% + } + .col-lg-push-4{ + left:33.33333333% + } + .col-lg-push-3{ + left:25% + } + .col-lg-push-2{ + left:16.66666667% + } + .col-lg-push-1{ + left:8.33333333% + } + .col-lg-push-0{ + left:auto + } + .col-lg-offset-12{ + margin-left:100% + } + .col-lg-offset-11{ + margin-left:91.66666667% + } + .col-lg-offset-10{ + margin-left:83.33333333% + } + .col-lg-offset-9{ + margin-left:75% + } + .col-lg-offset-8{ + margin-left:66.66666667% + } + .col-lg-offset-7{ + margin-left:58.33333333% + } + .col-lg-offset-6{ + margin-left:50% + } + .col-lg-offset-5{ + margin-left:41.66666667% + } + .col-lg-offset-4{ + margin-left:33.33333333% + } + .col-lg-offset-3{ + margin-left:25% + } + .col-lg-offset-2{ + margin-left:16.66666667% + } + .col-lg-offset-1{ + margin-left:8.33333333% + } + .col-lg-offset-0{ + margin-left:0 + } +} +table{ + background-color:transparent +} +caption{ + padding-top:8px; + padding-bottom:8px; + color:#777; + text-align:left +} +th{ + text-align:left +} +.table{ + width:100%; + max-width:100%; + margin-bottom:20px +} +.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{ + padding:8px; + line-height:1.42857143; + vertical-align:top; + border-top:1px solid #ddd +} +.table>thead>tr>th{ + vertical-align:bottom; + border-bottom:2px solid #ddd +} +.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{ + border-top:0 +} +.table>tbody+tbody{ + border-top:2px solid #ddd +} +.table .table{ + background-color:#fff +} +.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{ + padding:5px +} +.table-bordered{ + border:1px solid #ddd +} +.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{ + border:1px solid #ddd +} +.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{ + border-bottom-width:2px +} +.table-striped>tbody>tr:nth-of-type(odd){ + background-color:#f9f9f9 +} +.table-hover>tbody>tr:hover{ + background-color:#f5f5f5 +} +table col[class*=col-]{ + position:static; + display:table-column; + float:none +} +table td[class*=col-],table th[class*=col-]{ + position:static; + display:table-cell; + float:none +} +.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{ + background-color:#f5f5f5 +} +.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{ + background-color:#e8e8e8 +} +.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{ + background-color:#dff0d8 +} +.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{ + background-color:#d0e9c6 +} +.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{ + background-color:#d9edf7 +} +.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{ + background-color:#c4e3f3 +} +.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{ + background-color:#fcf8e3 +} +.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{ + background-color:#faf2cc +} +.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{ + background-color:#f2dede +} +.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{ + background-color:#ebcccc +} +.table-responsive{ + min-height:.01%; + overflow-x:auto +} +@media screen and (max-width:767px){ + .table-responsive{ + width:100%; + margin-bottom:15px; + overflow-y:hidden; + -ms-overflow-style:-ms-autohiding-scrollbar; + border:1px solid #ddd + } + .table-responsive>.table{ + margin-bottom:0 + } + .table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{ + white-space:nowrap + } + .table-responsive>.table-bordered{ + border:0 + } + .table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{ + border-left:0 + } + .table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{ + border-right:0 + } + .table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{ + border-bottom:0 + } +} +fieldset{ + min-width:0; + padding:0; + margin:0; + border:0 +} +legend{ + display:block; + width:100%; + padding:0; + margin-bottom:20px; + font-size:21px; + line-height:inherit; + color:#333; + border:0; + border-bottom:1px solid #e5e5e5 +} +label{ + display:inline-block; + max-width:100%; + margin-bottom:5px; + font-weight:700 +} +input[type=search]{ + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box +} +input[type=checkbox],input[type=radio]{ + margin:4px 0 0; + margin-top:1px\9; + line-height:normal +} +input[type=file]{ + display:block +} +input[type=range]{ + display:block; + width:100% +} +select[multiple],select[size]{ + height:auto +} +input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{ + outline:5px auto -webkit-focus-ring-color; + outline-offset:-2px +} +output{ + display:block; + padding-top:7px; + font-size:14px; + line-height:1.42857143; + color:#555 +} +.form-control{ + display:block; + width:100%; + height:34px; + padding:6px 12px; + font-size:14px; + line-height:1.42857143; + color:#555; + background-color:#fff; + background-image:none; + border:1px solid #ccc; + border-radius:4px; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075); + box-shadow:inset 0 1px 1px rgba(0,0,0,.075); + -webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s; + -o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s; + transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s +} +.form-control:focus{ + border-color:#66afe9; + outline:0; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6); + box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6) +} +.form-control::-moz-placeholder{ + color:#999; + opacity:1 +} +.form-control:-ms-input-placeholder{ + color:#999 +} +.form-control::-webkit-input-placeholder{ + color:#999 +} +.form-control::-ms-expand{ + background-color:transparent; + border:0 +} +.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{ + background-color:#eee; + opacity:1 +} +.form-control[disabled],fieldset[disabled] .form-control{ + cursor:not-allowed +} +textarea.form-control{ + height:auto +} +input[type=search]{ + -webkit-appearance:none +} +@media screen and (-webkit-min-device-pixel-ratio:0){ + input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{ + line-height:34px + } + .input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{ + line-height:30px + } + .input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{ + line-height:46px + } +} +.form-group{ + margin-bottom:15px +} +.checkbox,.radio{ + position:relative; + display:block; + margin-top:10px; + margin-bottom:10px +} +.checkbox label,.radio label{ + min-height:20px; + padding-left:20px; + margin-bottom:0; + font-weight:400; + cursor:pointer +} +.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{ + position:absolute; + margin-top:4px\9; + margin-left:-20px +} +.checkbox+.checkbox,.radio+.radio{ + margin-top:-5px +} +.checkbox-inline,.radio-inline{ + position:relative; + display:inline-block; + padding-left:20px; + margin-bottom:0; + font-weight:400; + vertical-align:middle; + cursor:pointer +} +.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{ + margin-top:0; + margin-left:10px +} +fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{ + cursor:not-allowed +} +.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{ + cursor:not-allowed +} +.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{ + cursor:not-allowed +} +.form-control-static{ + min-height:34px; + padding-top:7px; + padding-bottom:7px; + margin-bottom:0 +} +.form-control-static.input-lg,.form-control-static.input-sm{ + padding-right:0; + padding-left:0 +} +.input-sm{ + height:30px; + padding:5px 10px; + font-size:12px; + line-height:1.5; + border-radius:3px +} +select.input-sm{ + height:30px; + line-height:30px +} +select[multiple].input-sm,textarea.input-sm{ + height:auto +} +.form-group-sm .form-control{ + height:30px; + padding:5px 10px; + font-size:12px; + line-height:1.5; + border-radius:3px +} +.form-group-sm select.form-control{ + height:30px; + line-height:30px +} +.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{ + height:auto +} +.form-group-sm .form-control-static{ + height:30px; + min-height:32px; + padding:6px 10px; + font-size:12px; + line-height:1.5 +} +.input-lg{ + height:46px; + padding:10px 16px; + font-size:18px; + line-height:1.3333333; + border-radius:6px +} +select.input-lg{ + height:46px; + line-height:46px +} +select[multiple].input-lg,textarea.input-lg{ + height:auto +} +.form-group-lg .form-control{ + height:46px; + padding:10px 16px; + font-size:18px; + line-height:1.3333333; + border-radius:6px +} +.form-group-lg select.form-control{ + height:46px; + line-height:46px +} +.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{ + height:auto +} +.form-group-lg .form-control-static{ + height:46px; + min-height:38px; + padding:11px 16px; + font-size:18px; + line-height:1.3333333 +} +.has-feedback{ + position:relative +} +.has-feedback .form-control{ + padding-right:42.5px +} +.form-control-feedback{ + position:absolute; + top:0; + right:0; + z-index:2; + display:block; + width:34px; + height:34px; + line-height:34px; + text-align:center; + pointer-events:none +} +.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{ + width:46px; + height:46px; + line-height:46px +} +.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{ + width:30px; + height:30px; + line-height:30px +} +.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{ + color:#3c763d +} +.has-success .form-control{ + border-color:#3c763d; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075); + box-shadow:inset 0 1px 1px rgba(0,0,0,.075) +} +.has-success .form-control:focus{ + border-color:#2b542c; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168; + box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168 +} +.has-success .input-group-addon{ + color:#3c763d; + background-color:#dff0d8; + border-color:#3c763d +} +.has-success .form-control-feedback{ + color:#3c763d +} +.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{ + color:#8a6d3b +} +.has-warning .form-control{ + border-color:#8a6d3b; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075); + box-shadow:inset 0 1px 1px rgba(0,0,0,.075) +} +.has-warning .form-control:focus{ + border-color:#66512c; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b; + box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b +} +.has-warning .input-group-addon{ + color:#8a6d3b; + background-color:#fcf8e3; + border-color:#8a6d3b +} +.has-warning .form-control-feedback{ + color:#8a6d3b +} +.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{ + color:#a94442 +} +.has-error .form-control{ + border-color:#a94442; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075); + box-shadow:inset 0 1px 1px rgba(0,0,0,.075) +} +.has-error .form-control:focus{ + border-color:#843534; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483; + box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483 +} +.has-error .input-group-addon{ + color:#a94442; + background-color:#f2dede; + border-color:#a94442 +} +.has-error .form-control-feedback{ + color:#a94442 +} +.has-feedback label~.form-control-feedback{ + top:25px +} +.has-feedback label.sr-only~.form-control-feedback{ + top:0 +} +.help-block{ + display:block; + margin-top:5px; + margin-bottom:10px; + color:#737373 +} +@media (min-width:768px){ + .form-inline .form-group{ + display:inline-block; + margin-bottom:0; + vertical-align:middle + } + .form-inline .form-control{ + display:inline-block; + width:auto; + vertical-align:middle + } + .form-inline .form-control-static{ + display:inline-block + } + .form-inline .input-group{ + display:inline-table; + vertical-align:middle + } + .form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{ + width:auto + } + .form-inline .input-group>.form-control{ + width:100% + } + .form-inline .control-label{ + margin-bottom:0; + vertical-align:middle + } + .form-inline .checkbox,.form-inline .radio{ + display:inline-block; + margin-top:0; + margin-bottom:0; + vertical-align:middle + } + .form-inline .checkbox label,.form-inline .radio label{ + padding-left:0 + } + .form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{ + position:relative; + margin-left:0 + } + .form-inline .has-feedback .form-control-feedback{ + top:0 + } +} +.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{ + padding-top:7px; + margin-top:0; + margin-bottom:0 +} +.form-horizontal .checkbox,.form-horizontal .radio{ + min-height:27px +} +.form-horizontal .form-group{ + margin-right:-15px; + margin-left:-15px +} +@media (min-width:768px){ + .form-horizontal .control-label{ + padding-top:7px; + margin-bottom:0; + text-align:right + } +} +.form-horizontal .has-feedback .form-control-feedback{ + right:15px +} +@media (min-width:768px){ + .form-horizontal .form-group-lg .control-label{ + padding-top:11px; + font-size:18px + } +} +@media (min-width:768px){ + .form-horizontal .form-group-sm .control-label{ + padding-top:6px; + font-size:12px + } +} +.btn{ + display:inline-block; + padding:6px 12px; + margin-bottom:0; + font-size:14px; + font-weight:400; + line-height:1.42857143; + text-align:center; + white-space:nowrap; + vertical-align:middle; + -ms-touch-action:manipulation; + touch-action:manipulation; + cursor:pointer; + -webkit-user-select:none; + -moz-user-select:none; + -ms-user-select:none; + user-select:none; + background-image:none; + border:1px solid transparent; + border-radius:4px +} +.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{ + outline:5px auto -webkit-focus-ring-color; + outline-offset:-2px +} +.btn.focus,.btn:focus,.btn:hover{ + color:#333; + text-decoration:none +} +.btn.active,.btn:active{ + background-image:none; + outline:0; + -webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125); + box-shadow:inset 0 3px 5px rgba(0,0,0,.125) +} +.btn.disabled,.btn[disabled],fieldset[disabled] .btn{ + cursor:not-allowed; + filter:alpha(opacity=65); + -webkit-box-shadow:none; + box-shadow:none; + opacity:.65 +} +a.btn.disabled,fieldset[disabled] a.btn{ + pointer-events:none +} +.btn-default{ + color:#333; + background-color:#fff; + border-color:#ccc +} +.btn-default.focus,.btn-default:focus{ + color:#333; + background-color:#e6e6e6; + border-color:#8c8c8c +} +.btn-default:hover{ + color:#333; + background-color:#e6e6e6; + border-color:#adadad +} +.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{ + color:#333; + background-color:#e6e6e6; + border-color:#adadad +} +.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{ + color:#333; + background-color:#d4d4d4; + border-color:#8c8c8c +} +.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{ + background-image:none +} +.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{ + background-color:#fff; + border-color:#ccc +} +.btn-default .badge{ + color:#fff; + background-color:#333 +} +.btn-primary{ + color:#fff; + background-color:#d11010; + border-color:#c40f0f +} +.btn-primary.focus,.btn-primary:focus{ + color:#fff; + background-color:#b20c0c; + border-color:#c40f0f +} +.btn-primary:hover{ + color:#fff; + background-color:#b20c0c; + border-color:#c40f0f +} +.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{ + color:#fff; + background-color:#b20c0c; + border-color:#c40f0f +} +.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{ + color:#fff; + background-color:#b20c0c; + border-color:#c40f0f +} +.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{ + background-image:none +} +.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{ + background-color:#b20c0c; + border-color:#c40f0f +} +.btn-primary .badge{ + color:#337ab7; + background-color:#fff +} +.btn-success{ + color:#fff; + background-color:#5cb85c; + border-color:#4cae4c +} +.btn-success.focus,.btn-success:focus{ + color:#fff; + background-color:#449d44; + border-color:#255625 +} +.btn-success:hover{ + color:#fff; + background-color:#449d44; + border-color:#398439 +} +.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{ + color:#fff; + background-color:#449d44; + border-color:#398439 +} +.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{ + color:#fff; + background-color:#398439; + border-color:#255625 +} +.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{ + background-image:none +} +.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{ + background-color:#5cb85c; + border-color:#4cae4c +} +.btn-success .badge{ + color:#5cb85c; + background-color:#fff +} +.btn-info{ + color:#fff; + background-color:#5bc0de; + border-color:#46b8da +} +.btn-info.focus,.btn-info:focus{ + color:#fff; + background-color:#31b0d5; + border-color:#1b6d85 +} +.btn-info:hover{ + color:#fff; + background-color:#31b0d5; + border-color:#269abc +} +.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{ + color:#fff; + background-color:#31b0d5; + border-color:#269abc +} +.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{ + color:#fff; + background-color:#269abc; + border-color:#1b6d85 +} +.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{ + background-image:none +} +.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{ + background-color:#5bc0de; + border-color:#46b8da +} +.btn-info .badge{ + color:#5bc0de; + background-color:#fff +} +.btn-warning{ + color:#fff; + background-color:#f0ad4e; + border-color:#eea236 +} +.btn-warning.focus,.btn-warning:focus{ + color:#fff; + background-color:#ec971f; + border-color:#985f0d +} +.btn-warning:hover{ + color:#fff; + background-color:#ec971f; + border-color:#d58512 +} +.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{ + color:#fff; + background-color:#ec971f; + border-color:#d58512 +} +.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{ + color:#fff; + background-color:#d58512; + border-color:#985f0d +} +.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{ + background-image:none +} +.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{ + background-color:#f0ad4e; + border-color:#eea236 +} +.btn-warning .badge{ + color:#f0ad4e; + background-color:#fff +} +.btn-danger{ + color:#fff; + background-color:#d9534f; + border-color:#d43f3a +} +.btn-danger.focus,.btn-danger:focus{ + color:#fff; + background-color:#c9302c; + border-color:#761c19 +} +.btn-danger:hover{ + color:#fff; + background-color:#c9302c; + border-color:#ac2925 +} +.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{ + color:#fff; + background-color:#c9302c; + border-color:#ac2925 +} +.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{ + color:#fff; + background-color:#ac2925; + border-color:#761c19 +} +.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{ + background-image:none +} +.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{ + background-color:#d9534f; + border-color:#d43f3a +} +.btn-danger .badge{ + color:#d9534f; + background-color:#fff +} +.btn-link{ + font-weight:400; + color:#337ab7; + border-radius:0 +} +.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{ + background-color:transparent; + -webkit-box-shadow:none; + box-shadow:none +} +.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{ + border-color:transparent +} +.btn-link:focus,.btn-link:hover{ + color:#23527c; + text-decoration:underline; + background-color:transparent +} +.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{ + color:#777; + text-decoration:none +} +.btn-group-lg>.btn,.btn-lg{ + padding:10px 16px; + font-size:18px; + line-height:1.3333333; + border-radius:6px +} +.btn-group-sm>.btn,.btn-sm{ + padding:5px 10px; + font-size:12px; + line-height:1.5; + border-radius:3px +} +.btn-group-xs>.btn,.btn-xs{ + padding:1px 5px; + font-size:12px; + line-height:1.5; + border-radius:3px +} +.btn-block{ + display:block; + width:100% +} +.btn-block+.btn-block{ + margin-top:5px +} +input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{ + width:100% +} +.fade{ + opacity:0; + -webkit-transition:opacity .15s linear; + -o-transition:opacity .15s linear; + transition:opacity .15s linear +} +.fade.in{ + opacity:1 +} +.collapse{ + display:none +} +.collapse.in{ + display:block +} +tr.collapse.in{ + display:table-row +} +tbody.collapse.in{ + display:table-row-group +} +.collapsing{ + position:relative; + height:0; + overflow:hidden; + -webkit-transition-timing-function:ease; + -o-transition-timing-function:ease; + transition-timing-function:ease; + -webkit-transition-duration:.35s; + -o-transition-duration:.35s; + transition-duration:.35s; + -webkit-transition-property:height,visibility; + -o-transition-property:height,visibility; + transition-property:height,visibility +} +.caret{ + display:inline-block; + width:0; + height:0; + margin-left:2px; + vertical-align:middle; + border-top:4px dashed; + border-top:4px solid\9; + border-right:4px solid transparent; + border-left:4px solid transparent +} +.dropdown,.dropup{ + position:relative +} +.dropdown-toggle:focus{ + outline:0 +} +.dropdown-menu{ + position:absolute; + top:100%; + left:0; + z-index:1000; + display:none; + float:left; + min-width:160px; + padding:5px 0; + margin:2px 0 0; + font-size:14px; + text-align:left; + list-style:none; + background-color:#fff; + -webkit-background-clip:padding-box; + background-clip:padding-box; + border:1px solid #ccc; + border:1px solid rgba(0,0,0,.15); + border-radius:4px; + -webkit-box-shadow:0 6px 12px rgba(0,0,0,.175); + box-shadow:0 6px 12px rgba(0,0,0,.175) +} +.dropdown-menu.pull-right{ + right:0; + left:auto +} +.dropdown-menu .divider{ + height:1px; + margin:9px 0; + overflow:hidden; + background-color:#e5e5e5 +} +.dropdown-menu>li>a{ + display:block; + padding:3px 20px; + clear:both; + font-weight:400; + line-height:1.42857143; + color:#333; + white-space:nowrap +} +.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{ + color:#262626; + text-decoration:none; + background-color:#f5f5f5 +} +.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{ + color:#fff; + text-decoration:none; + background-color:#337ab7; + outline:0 +} +.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{ + color:#777 +} +.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{ + text-decoration:none; + cursor:not-allowed; + background-color:transparent; + background-image:none; + filter:progid:DXImageTransform.Microsoft.gradient(enabled=false) +} +.open>.dropdown-menu{ + display:block +} +.open>a{ + outline:0 +} +.dropdown-menu-right{ + right:0; + left:auto +} +.dropdown-menu-left{ + right:auto; + left:0 +} +.dropdown-header{ + display:block; + padding:3px 20px; + font-size:12px; + line-height:1.42857143; + color:#777; + white-space:nowrap +} +.dropdown-backdrop{ + position:fixed; + top:0; + right:0; + bottom:0; + left:0; + z-index:990 +} +.pull-right>.dropdown-menu{ + right:0; + left:auto +} +.dropup .caret,.navbar-fixed-bottom .dropdown .caret{ + content:""; + border-top:0; + border-bottom:4px dashed; + border-bottom:4px solid\9 +} +.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{ + top:auto; + bottom:100%; + margin-bottom:2px +} +@media (min-width:768px){ + .navbar-right .dropdown-menu{ + right:0; + left:auto + } + .navbar-right .dropdown-menu-left{ + right:auto; + left:0 + } +} +.btn-group,.btn-group-vertical{ + position:relative; + display:inline-block; + vertical-align:middle +} +.btn-group-vertical>.btn,.btn-group>.btn{ + position:relative; + float:left +} +.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{ + z-index:2 +} +.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{ + margin-left:-1px +} +.btn-toolbar{ + margin-left:-5px +} +.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{ + float:left +} +.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{ + margin-left:5px +} +.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){ + border-radius:0 +} +.btn-group>.btn:first-child{ + margin-left:0 +} +.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){ + border-top-right-radius:0; + border-bottom-right-radius:0 +} +.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){ + border-top-left-radius:0; + border-bottom-left-radius:0 +} +.btn-group>.btn-group{ + float:left +} +.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{ + border-radius:0 +} +.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{ + border-top-right-radius:0; + border-bottom-right-radius:0 +} +.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{ + border-top-left-radius:0; + border-bottom-left-radius:0 +} +.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{ + outline:0 +} +.btn-group>.btn+.dropdown-toggle{ + padding-right:8px; + padding-left:8px +} +.btn-group>.btn-lg+.dropdown-toggle{ + padding-right:12px; + padding-left:12px +} +.btn-group.open .dropdown-toggle{ + -webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125); + box-shadow:inset 0 3px 5px rgba(0,0,0,.125) +} +.btn-group.open .dropdown-toggle.btn-link{ + -webkit-box-shadow:none; + box-shadow:none +} +.btn .caret{ + margin-left:0 +} +.btn-lg .caret{ + border-width:5px 5px 0; + border-bottom-width:0 +} +.dropup .btn-lg .caret{ + border-width:0 5px 5px +} +.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{ + display:block; + float:none; + width:100%; + max-width:100% +} +.btn-group-vertical>.btn-group>.btn{ + float:none +} +.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{ + margin-top:-1px; + margin-left:0 +} +.btn-group-vertical>.btn:not(:first-child):not(:last-child){ + border-radius:0 +} +.btn-group-vertical>.btn:first-child:not(:last-child){ + border-top-left-radius:4px; + border-top-right-radius:4px; + border-bottom-right-radius:0; + border-bottom-left-radius:0 +} +.btn-group-vertical>.btn:last-child:not(:first-child){ + border-top-left-radius:0; + border-top-right-radius:0; + border-bottom-right-radius:4px; + border-bottom-left-radius:4px +} +.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{ + border-radius:0 +} +.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{ + border-bottom-right-radius:0; + border-bottom-left-radius:0 +} +.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{ + border-top-left-radius:0; + border-top-right-radius:0 +} +.btn-group-justified{ + display:table; + width:100%; + table-layout:fixed; + border-collapse:separate +} +.btn-group-justified>.btn,.btn-group-justified>.btn-group{ + display:table-cell; + float:none; + width:1% +} +.btn-group-justified>.btn-group .btn{ + width:100% +} +.btn-group-justified>.btn-group .dropdown-menu{ + left:auto +} +[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{ + position:absolute; + clip:rect(0,0,0,0); + pointer-events:none +} +.input-group{ + position:relative; + display:table; + border-collapse:separate +} +.input-group[class*=col-]{ + float:none; + padding-right:0; + padding-left:0 +} +.input-group .form-control{ + position:relative; + z-index:2; + float:left; + width:100%; + margin-bottom:0 +} +.input-group .form-control:focus{ + z-index:3 +} +.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{ + height:46px; + padding:10px 16px; + font-size:18px; + line-height:1.3333333; + border-radius:6px +} +select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{ + height:46px; + line-height:46px +} +select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{ + height:auto +} +.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{ + height:30px; + padding:5px 10px; + font-size:12px; + line-height:1.5; + border-radius:3px +} +select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{ + height:30px; + line-height:30px +} +select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{ + height:auto +} +.input-group .form-control,.input-group-addon,.input-group-btn{ + display:table-cell +} +.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){ + border-radius:0 +} +.input-group-addon,.input-group-btn{ + width:1%; + white-space:nowrap; + vertical-align:middle +} +.input-group-addon{ + padding:6px 12px; + font-size:14px; + font-weight:400; + line-height:1; + color:#555; + text-align:center; + background-color:#eee; + border:1px solid #ccc; + border-radius:4px +} +.input-group-addon.input-sm{ + padding:5px 10px; + font-size:12px; + border-radius:3px +} +.input-group-addon.input-lg{ + padding:10px 16px; + font-size:18px; + border-radius:6px +} +.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{ + margin-top:0 +} +.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){ + border-top-right-radius:0; + border-bottom-right-radius:0 +} +.input-group-addon:first-child{ + border-right:0 +} +.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{ + border-top-left-radius:0; + border-bottom-left-radius:0 +} +.input-group-addon:last-child{ + border-left:0 +} +.input-group-btn{ + position:relative; + font-size:0; + white-space:nowrap +} +.input-group-btn>.btn{ + position:relative +} +.input-group-btn>.btn+.btn{ + margin-left:-1px +} +.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{ + z-index:2 +} +.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{ + margin-right:-1px +} +.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{ + z-index:2; + margin-left:-1px +} +.nav{ + padding-left:0; + margin-bottom:0; + list-style:none +} +.nav>li{ + position:relative; + display:block +} +.nav>li>a{ + position:relative; + display:block; + padding:10px 15px +} +.nav>li>a:focus,.nav>li>a:hover{ + text-decoration:none; + background-color:#eee +} +.nav>li.disabled>a{ + color:#777 +} +.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{ + color:#777; + text-decoration:none; + cursor:not-allowed; + background-color:transparent +} +.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{ + background-color:#eee; + border-color:#337ab7 +} +.nav .nav-divider{ + height:1px; + margin:9px 0; + overflow:hidden; + background-color:#e5e5e5 +} +.nav>li>a>img{ + max-width:none +} +.nav-tabs{ + border-bottom:1px solid #ddd +} +.nav-tabs>li{ + float:left; + margin-bottom:-1px +} +.nav-tabs>li>a{ + margin-right:2px; + line-height:1.42857143; + border:1px solid transparent; + border-radius:4px 4px 0 0 +} +.nav-tabs>li>a:hover{ + border-color:#eee #eee #ddd +} +.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{ + color:#555; + cursor:default; + background-color:#fff; + border:1px solid #ddd; + border-bottom-color:transparent +} +.nav-tabs.nav-justified{ + width:100%; + border-bottom:0 +} +.nav-tabs.nav-justified>li{ + float:none +} +.nav-tabs.nav-justified>li>a{ + margin-bottom:5px; + text-align:center +} +.nav-tabs.nav-justified>.dropdown .dropdown-menu{ + top:auto; + left:auto +} +@media (min-width:768px){ + .nav-tabs.nav-justified>li{ + display:table-cell; + width:1% + } + .nav-tabs.nav-justified>li>a{ + margin-bottom:0 + } +} +.nav-tabs.nav-justified>li>a{ + margin-right:0; + border-radius:4px +} +.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{ + border:1px solid #ddd +} +@media (min-width:768px){ + .nav-tabs.nav-justified>li>a{ + border-bottom:1px solid #ddd; + border-radius:4px 4px 0 0 + } + .nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{ + border-bottom-color:#fff + } +} +.nav-pills>li{ + float:left +} +.nav-pills>li>a{ + border-radius:4px +} +.nav-pills>li+li{ + margin-left:2px +} +.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{ + color:#fff; + background-color:#337ab7 +} +.nav-stacked>li{ + float:none +} +.nav-stacked>li+li{ + margin-top:2px; + margin-left:0 +} +.nav-justified{ + width:100% +} +.nav-justified>li{ + float:none +} +.nav-justified>li>a{ + margin-bottom:5px; + text-align:center +} +.nav-justified>.dropdown .dropdown-menu{ + top:auto; + left:auto +} +@media (min-width:768px){ + .nav-justified>li{ + display:table-cell; + width:1% + } + .nav-justified>li>a{ + margin-bottom:0 + } +} +.nav-tabs-justified{ + border-bottom:0 +} +.nav-tabs-justified>li>a{ + margin-right:0; + border-radius:4px +} +.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{ + border:1px solid #ddd +} +@media (min-width:768px){ + .nav-tabs-justified>li>a{ + border-bottom:1px solid #ddd; + border-radius:4px 4px 0 0 + } + .nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{ + border-bottom-color:#fff + } +} +.tab-content>.tab-pane{ + display:none +} +.tab-content>.active{ + display:block +} +.nav-tabs .dropdown-menu{ + margin-top:-1px; + border-top-left-radius:0; + border-top-right-radius:0 +} +.navbar{ + position:relative; + min-height:50px; + margin-bottom:20px; + border:1px solid transparent +} +@media (min-width:768px){ + .navbar{ + border-radius:4px + } +} +@media (min-width:768px){ + .navbar-header{ + float:left + } +} +.navbar-collapse{ + padding-right:15px; + padding-left:15px; + overflow-x:visible; + -webkit-overflow-scrolling:touch; + border-top:1px solid transparent; + -webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1); + box-shadow:inset 0 1px 0 rgba(255,255,255,.1) +} +.navbar-collapse.in{ + overflow-y:auto +} +@media (min-width:768px){ + .navbar-collapse{ + width:auto; + border-top:0; + -webkit-box-shadow:none; + box-shadow:none + } + .navbar-collapse.collapse{ + display:block!important; + height:auto!important; + padding-bottom:0; + overflow:visible!important + } + .navbar-collapse.in{ + overflow-y:visible + } + .navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{ + padding-right:0; + padding-left:0 + } +} +.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{ + max-height:340px +} +@media (max-device-width:480px) and (orientation:landscape){ + .navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{ + max-height:200px + } +} +.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{ + margin-right:-15px; + margin-left:-15px +} +@media (min-width:768px){ + .container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{ + margin-right:0; + margin-left:0 + } +} +.navbar-static-top{ + z-index:1000; + border-width:0 0 1px +} +@media (min-width:768px){ + .navbar-static-top{ + border-radius:0 + } +} +.navbar-fixed-bottom,.navbar-fixed-top{ + position:fixed; + right:0; + left:0; + z-index:1030 +} +@media (min-width:768px){ + .navbar-fixed-bottom,.navbar-fixed-top{ + border-radius:0 + } +} +.navbar-fixed-top{ + top:0; + border-width:0 0 1px +} +.navbar-fixed-bottom{ + bottom:0; + margin-bottom:0; + border-width:1px 0 0 +} +.navbar-brand{ + float:left; + height:50px; + padding:15px 15px; + font-size:18px; + line-height:20px +} +.navbar-brand:focus,.navbar-brand:hover{ + text-decoration:none +} +.navbar-brand>img{ + display:block +} +@media (min-width:768px){ + .navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{ + margin-left:-15px + } +} +.navbar-toggle{ + position:relative; + float:right; + padding:9px 10px; + margin-top:8px; + margin-right:15px; + margin-bottom:8px; + background-color:transparent; + background-image:none; + border:1px solid transparent; + border-radius:4px +} +.navbar-toggle:focus{ + outline:0 +} +.navbar-toggle .icon-bar{ + display:block; + width:22px; + height:2px; + border-radius:1px +} +.navbar-toggle .icon-bar+.icon-bar{ + margin-top:4px +} +@media (min-width:768px){ + .navbar-toggle{ + display:none + } +} +.navbar-nav{ + margin:7.5px -15px +} +.navbar-nav>li>a{ + padding-top:10px; + padding-bottom:10px; + line-height:20px +} +@media (max-width:767px){ + .navbar-nav .open .dropdown-menu{ + position:static; + float:none; + width:auto; + margin-top:0; + background-color:transparent; + border:0; + -webkit-box-shadow:none; + box-shadow:none + } + .navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{ + padding:5px 15px 5px 25px + } + .navbar-nav .open .dropdown-menu>li>a{ + line-height:20px + } + .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{ + background-image:none + } +} +@media (min-width:768px){ + .navbar-nav{ + float:left; + margin:0 + } + .navbar-nav>li{ + float:left + } + .navbar-nav>li>a{ + padding-top:15px; + padding-bottom:15px + } +} +.navbar-form{ + padding:10px 15px; + margin-top:8px; + margin-right:-15px; + margin-bottom:8px; + margin-left:-15px; + border-top:1px solid transparent; + border-bottom:1px solid transparent; + -webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1); + box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1) +} +@media (min-width:768px){ + .navbar-form .form-group{ + display:inline-block; + margin-bottom:0; + vertical-align:middle + } + .navbar-form .form-control{ + display:inline-block; + width:auto; + vertical-align:middle + } + .navbar-form .form-control-static{ + display:inline-block + } + .navbar-form .input-group{ + display:inline-table; + vertical-align:middle + } + .navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{ + width:auto + } + .navbar-form .input-group>.form-control{ + width:100% + } + .navbar-form .control-label{ + margin-bottom:0; + vertical-align:middle + } + .navbar-form .checkbox,.navbar-form .radio{ + display:inline-block; + margin-top:0; + margin-bottom:0; + vertical-align:middle + } + .navbar-form .checkbox label,.navbar-form .radio label{ + padding-left:0 + } + .navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{ + position:relative; + margin-left:0 + } + .navbar-form .has-feedback .form-control-feedback{ + top:0 + } +} +@media (max-width:767px){ + .navbar-form .form-group{ + margin-bottom:5px + } + .navbar-form .form-group:last-child{ + margin-bottom:0 + } +} +@media (min-width:768px){ + .navbar-form{ + width:auto; + padding-top:0; + padding-bottom:0; + margin-right:0; + margin-left:0; + border:0; + -webkit-box-shadow:none; + box-shadow:none + } +} +.navbar-nav>li>.dropdown-menu{ + margin-top:0; + border-top-left-radius:0; + border-top-right-radius:0 +} +.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{ + margin-bottom:0; + border-top-left-radius:4px; + border-top-right-radius:4px; + border-bottom-right-radius:0; + border-bottom-left-radius:0 +} +.navbar-btn{ + margin-top:8px; + margin-bottom:8px +} +.navbar-btn.btn-sm{ + margin-top:10px; + margin-bottom:10px +} +.navbar-btn.btn-xs{ + margin-top:14px; + margin-bottom:14px +} +.navbar-text{ + margin-top:15px; + margin-bottom:15px +} +@media (min-width:768px){ + .navbar-text{ + float:left; + margin-right:15px; + margin-left:15px + } +} +@media (min-width:768px){ + .navbar-left{ + float:left!important + } + .navbar-right{ + float:right!important; + margin-right:-15px + } + .navbar-right~.navbar-right{ + margin-right:0 + } +} +.navbar-default{ + background-color:#f8f8f8; + border-color:#e7e7e7 +} +.navbar-default .navbar-brand{ + color:#777 +} +.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{ + color:#5e5e5e; + background-color:transparent +} +.navbar-default .navbar-text{ + color:#777 +} +.navbar-default .navbar-nav>li>a{ + color:#777 +} +.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{ + color:#333; + background-color:transparent +} +.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{ + color:#555; + background-color:#e7e7e7 +} +.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{ + color:#ccc; + background-color:transparent +} +.navbar-default .navbar-toggle{ + border-color:#ddd +} +.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{ + background-color:#ddd +} +.navbar-default .navbar-toggle .icon-bar{ + background-color:#888 +} +.navbar-default .navbar-collapse,.navbar-default .navbar-form{ + border-color:#e7e7e7 +} +.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{ + color:#555; + background-color:#e7e7e7 +} +@media (max-width:767px){ + .navbar-default .navbar-nav .open .dropdown-menu>li>a{ + color:#777 + } + .navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{ + color:#333; + background-color:transparent + } + .navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{ + color:#555; + background-color:#e7e7e7 + } + .navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{ + color:#ccc; + background-color:transparent + } +} +.navbar-default .navbar-link{ + color:#777 +} +.navbar-default .navbar-link:hover{ + color:#333 +} +.navbar-default .btn-link{ + color:#777 +} +.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{ + color:#333 +} +.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{ + color:#ccc +} +.navbar-inverse{ + background-color:#222; + border-color:#080808 +} +.navbar-inverse .navbar-brand{ + color:#9d9d9d +} +.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{ + color:#fff; + background-color:transparent +} +.navbar-inverse .navbar-text{ + color:#9d9d9d +} +.navbar-inverse .navbar-nav>li>a{ + color:#9d9d9d +} +.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{ + color:#fff; + background-color:transparent +} +.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{ + color:#fff; + background-color:#080808 +} +.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{ + color:#444; + background-color:transparent +} +.navbar-inverse .navbar-toggle{ + border-color:#333 +} +.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{ + background-color:#333 +} +.navbar-inverse .navbar-toggle .icon-bar{ + background-color:#fff +} +.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{ + border-color:#101010 +} +.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{ + color:#fff; + background-color:#080808 +} +@media (max-width:767px){ + .navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{ + border-color:#080808 + } + .navbar-inverse .navbar-nav .open .dropdown-menu .divider{ + background-color:#080808 + } + .navbar-inverse .navbar-nav .open .dropdown-menu>li>a{ + color:#9d9d9d + } + .navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{ + color:#fff; + background-color:transparent + } + .navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{ + color:#fff; + background-color:#080808 + } + .navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{ + color:#444; + background-color:transparent + } +} +.navbar-inverse .navbar-link{ + color:#9d9d9d +} +.navbar-inverse .navbar-link:hover{ + color:#fff +} +.navbar-inverse .btn-link{ + color:#9d9d9d +} +.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{ + color:#fff +} +.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{ + color:#444 +} +.breadcrumb{ + padding:8px 15px; + margin-bottom:20px; + list-style:none; + background-color:#f5f5f5; + border-radius:4px +} +.breadcrumb>li{ + display:inline-block +} +.breadcrumb>li+li:before{ + padding:0 5px; + color:#ccc; + content:"/\00a0" +} +.breadcrumb>.active{ + color:#777 +} +.pagination{ + display:inline-block; + padding-left:0; + margin:20px 0; + border-radius:4px +} +.pagination>li{ + display:inline +} +.pagination>li>a,.pagination>li>span{ + position:relative; + float:left; + padding:6px 12px; + margin-left:-1px; + line-height:1.42857143; + color:#337ab7; + text-decoration:none; + background-color:#fff; + border:1px solid #ddd +} +.pagination>li:first-child>a,.pagination>li:first-child>span{ + margin-left:0; + border-top-left-radius:4px; + border-bottom-left-radius:4px +} +.pagination>li:last-child>a,.pagination>li:last-child>span{ + border-top-right-radius:4px; + border-bottom-right-radius:4px +} +.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{ + z-index:2; + color:#23527c; + background-color:#eee; + border-color:#ddd +} +.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{ + z-index:3; + color:#fff; + cursor:default; + background-color:#337ab7; + border-color:#337ab7 +} +.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{ + color:#777; + cursor:not-allowed; + background-color:#fff; + border-color:#ddd +} +.pagination-lg>li>a,.pagination-lg>li>span{ + padding:10px 16px; + font-size:18px; + line-height:1.3333333 +} +.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{ + border-top-left-radius:6px; + border-bottom-left-radius:6px +} +.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{ + border-top-right-radius:6px; + border-bottom-right-radius:6px +} +.pagination-sm>li>a,.pagination-sm>li>span{ + padding:5px 10px; + font-size:12px; + line-height:1.5 +} +.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{ + border-top-left-radius:3px; + border-bottom-left-radius:3px +} +.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{ + border-top-right-radius:3px; + border-bottom-right-radius:3px +} +.pager{ + padding-left:0; + margin:20px 0; + text-align:center; + list-style:none +} +.pager li{ + display:inline +} +.pager li>a,.pager li>span{ + display:inline-block; + padding:5px 14px; + background-color:#fff; + border:1px solid #ddd; + border-radius:15px +} +.pager li>a:focus,.pager li>a:hover{ + text-decoration:none; + background-color:#eee +} +.pager .next>a,.pager .next>span{ + float:right +} +.pager .previous>a,.pager .previous>span{ + float:left +} +.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{ + color:#777; + cursor:not-allowed; + background-color:#fff +} +.label{ + display:inline; + padding:.2em .6em .3em; + font-size:75%; + font-weight:700; + line-height:1; + color:#fff; + text-align:center; + white-space:nowrap; + vertical-align:baseline; + border-radius:.25em +} +a.label:focus,a.label:hover{ + color:#fff; + text-decoration:none; + cursor:pointer +} +.label:empty{ + display:none +} +.btn .label{ + position:relative; + top:-1px +} +.label-default{ + background-color:#777 +} +.label-default[href]:focus,.label-default[href]:hover{ + background-color:#5e5e5e +} +.label-primary{ + background-color:#337ab7 +} +.label-primary[href]:focus,.label-primary[href]:hover{ + background-color:#286090 +} +.label-success{ + background-color:#5cb85c +} +.label-success[href]:focus,.label-success[href]:hover{ + background-color:#449d44 +} +.label-info{ + background-color:#5bc0de +} +.label-info[href]:focus,.label-info[href]:hover{ + background-color:#31b0d5 +} +.label-warning{ + background-color:#f0ad4e +} +.label-warning[href]:focus,.label-warning[href]:hover{ + background-color:#ec971f +} +.label-danger{ + background-color:#d9534f +} +.label-danger[href]:focus,.label-danger[href]:hover{ + background-color:#c9302c +} +.badge{ + display:inline-block; + min-width:10px; + padding:3px 7px; + font-size:12px; + font-weight:700; + line-height:1; + color:#fff; + text-align:center; + white-space:nowrap; + vertical-align:middle; + background-color:#777; + border-radius:10px +} +.badge:empty{ + display:none +} +.btn .badge{ + position:relative; + top:-1px +} +.btn-group-xs>.btn .badge,.btn-xs .badge{ + top:0; + padding:1px 5px +} +a.badge:focus,a.badge:hover{ + color:#fff; + text-decoration:none; + cursor:pointer +} +.list-group-item.active>.badge,.nav-pills>.active>a>.badge{ + color:#337ab7; + background-color:#fff +} +.list-group-item>.badge{ + float:right +} +.list-group-item>.badge+.badge{ + margin-right:5px +} +.nav-pills>li>a>.badge{ + margin-left:3px +} +.jumbotron{ + padding-top:30px; + padding-bottom:30px; + margin-bottom:30px; + color:inherit; + background-color:#eee +} +.jumbotron .h1,.jumbotron h1{ + color:inherit +} +.jumbotron p{ + margin-bottom:15px; + font-size:21px; + font-weight:200 +} +.jumbotron>hr{ + border-top-color:#d5d5d5 +} +.container .jumbotron,.container-fluid .jumbotron{ + padding-right:15px; + padding-left:15px; + border-radius:6px +} +.jumbotron .container{ + max-width:100% +} +@media screen and (min-width:768px){ + .jumbotron{ + padding-top:48px; + padding-bottom:48px + } + .container .jumbotron,.container-fluid .jumbotron{ + padding-right:60px; + padding-left:60px + } + .jumbotron .h1,.jumbotron h1{ + font-size:63px + } +} +.thumbnail{ + display:block; + padding:4px; + margin-bottom:20px; + line-height:1.42857143; + background-color:#fff; + border:1px solid #ddd; + border-radius:4px; + -webkit-transition:border .2s ease-in-out; + -o-transition:border .2s ease-in-out; + transition:border .2s ease-in-out +} +.thumbnail a>img,.thumbnail>img{ + margin-right:auto; + margin-left:auto +} +a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{ + border-color:#337ab7 +} +.thumbnail .caption{ + padding:9px; + color:#333 +} +.alert{ + padding:15px; + margin-bottom:20px; + border:1px solid transparent; + border-radius:4px +} +.alert h4{ + margin-top:0; + color:inherit +} +.alert .alert-link{ + font-weight:700 +} +.alert>p,.alert>ul{ + margin-bottom:0 +} +.alert>p+p{ + margin-top:5px +} +.alert-dismissable,.alert-dismissible{ + padding-right:35px +} +.alert-dismissable .close,.alert-dismissible .close{ + position:relative; + top:-2px; + right:-21px; + color:inherit +} +.alert-success{ + color:#3c763d; + background-color:#dff0d8; + border-color:#d6e9c6 +} +.alert-success hr{ + border-top-color:#c9e2b3 +} +.alert-success .alert-link{ + color:#2b542c +} +.alert-info{ + color:#31708f; + background-color:#d9edf7; + border-color:#bce8f1 +} +.alert-info hr{ + border-top-color:#a6e1ec +} +.alert-info .alert-link{ + color:#245269 +} +.alert-warning{ + color:#8a6d3b; + background-color:#fcf8e3; + border-color:#faebcc +} +.alert-warning hr{ + border-top-color:#f7e1b5 +} +.alert-warning .alert-link{ + color:#66512c +} +.alert-danger{ + color:#a94442; + background-color:#f2dede; + border-color:#ebccd1 +} +.alert-danger hr{ + border-top-color:#e4b9c0 +} +.alert-danger .alert-link{ + color:#843534 +} +@-webkit-keyframes progress-bar-stripes{ + from{ + background-position:40px 0 + } + to{ + background-position:0 0 + } +} +@-o-keyframes progress-bar-stripes{ + from{ + background-position:40px 0 + } + to{ + background-position:0 0 + } +} +@keyframes progress-bar-stripes{ + from{ + background-position:40px 0 + } + to{ + background-position:0 0 + } +} +.progress{ + height:20px; + margin-bottom:20px; + overflow:hidden; + background-color:#f5f5f5; + border-radius:4px; + -webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1); + box-shadow:inset 0 1px 2px rgba(0,0,0,.1) +} +.progress-bar{ + float:left; + width:0; + height:100%; + font-size:12px; + line-height:20px; + color:#fff; + text-align:center; + background-color:#337ab7; + -webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15); + box-shadow:inset 0 -1px 0 rgba(0,0,0,.15); + -webkit-transition:width .6s ease; + -o-transition:width .6s ease; + transition:width .6s ease +} +.progress-bar-striped,.progress-striped .progress-bar{ + background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + -webkit-background-size:40px 40px; + background-size:40px 40px +} +.progress-bar.active,.progress.active .progress-bar{ + -webkit-animation:progress-bar-stripes 2s linear infinite; + -o-animation:progress-bar-stripes 2s linear infinite; + animation:progress-bar-stripes 2s linear infinite +} +.progress-bar-success{ + background-color:#5cb85c +} +.progress-striped .progress-bar-success{ + background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent) +} +.progress-bar-info{ + background-color:#5bc0de +} +.progress-striped .progress-bar-info{ + background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent) +} +.progress-bar-warning{ + background-color:#f0ad4e +} +.progress-striped .progress-bar-warning{ + background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent) +} +.progress-bar-danger{ + background-color:#d9534f +} +.progress-striped .progress-bar-danger{ + background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); + background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent) +} +.media{ + margin-top:15px +} +.media:first-child{ + margin-top:0 +} +.media,.media-body{ + overflow:hidden; + zoom:1 +} +.media-body{ + width:10000px +} +.media-object{ + display:block +} +.media-object.img-thumbnail{ + max-width:none +} +.media-right,.media>.pull-right{ + padding-left:10px +} +.media-left,.media>.pull-left{ + padding-right:10px +} +.media-body,.media-left,.media-right{ + display:table-cell; + vertical-align:top +} +.media-middle{ + vertical-align:middle +} +.media-bottom{ + vertical-align:bottom +} +.media-heading{ + margin-top:0; + margin-bottom:5px +} +.media-list{ + padding-left:0; + list-style:none +} +.list-group{ + padding-left:0; + margin-bottom:20px +} +.list-group-item{ + position:relative; + display:block; + padding:10px 15px; + margin-bottom:-1px; + background-color:#fff; + border:1px solid #ddd +} +.list-group-item:first-child{ + border-top-left-radius:4px; + border-top-right-radius:4px +} +.list-group-item:last-child{ + margin-bottom:0; + border-bottom-right-radius:4px; + border-bottom-left-radius:4px +} +a.list-group-item,button.list-group-item{ + color:#555 +} +a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{ + color:#333 +} +a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{ + color:#555; + text-decoration:none; + background-color:#f5f5f5 +} +button.list-group-item{ + width:100%; + text-align:left +} +.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{ + color:#777; + cursor:not-allowed; + background-color:#eee +} +.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{ + color:inherit +} +.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{ + color:#777 +} +.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{ + z-index:2; + color:#fff; + background-color:#337ab7; + border-color:#337ab7 +} +.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{ + color:inherit +} +.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{ + color:#c7ddef +} +.list-group-item-success{ + color:#3c763d; + background-color:#dff0d8 +} +a.list-group-item-success,button.list-group-item-success{ + color:#3c763d +} +a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{ + color:inherit +} +a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{ + color:#3c763d; + background-color:#d0e9c6 +} +a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{ + color:#fff; + background-color:#3c763d; + border-color:#3c763d +} +.list-group-item-info{ + color:#31708f; + background-color:#d9edf7 +} +a.list-group-item-info,button.list-group-item-info{ + color:#31708f +} +a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{ + color:inherit +} +a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{ + color:#31708f; + background-color:#c4e3f3 +} +a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{ + color:#fff; + background-color:#31708f; + border-color:#31708f +} +.list-group-item-warning{ + color:#8a6d3b; + background-color:#fcf8e3 +} +a.list-group-item-warning,button.list-group-item-warning{ + color:#8a6d3b +} +a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{ + color:inherit +} +a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{ + color:#8a6d3b; + background-color:#faf2cc +} +a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{ + color:#fff; + background-color:#8a6d3b; + border-color:#8a6d3b +} +.list-group-item-danger{ + color:#a94442; + background-color:#f2dede +} +a.list-group-item-danger,button.list-group-item-danger{ + color:#a94442 +} +a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{ + color:inherit +} +a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{ + color:#a94442; + background-color:#ebcccc +} +a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{ + color:#fff; + background-color:#a94442; + border-color:#a94442 +} +.list-group-item-heading{ + margin-top:0; + margin-bottom:5px +} +.list-group-item-text{ + margin-bottom:0; + line-height:1.3 +} +.panel{ + margin-bottom:20px; + background-color:#fff; + border:1px solid transparent; + border-radius:4px; + -webkit-box-shadow:0 1px 1px rgba(0,0,0,.05); + box-shadow:0 1px 1px rgba(0,0,0,.05) +} +.panel-body{ + padding:15px +} +.panel-heading{ + padding:10px 15px; + border-bottom:1px solid transparent; + border-top-left-radius:3px; + border-top-right-radius:3px +} +.panel-heading>.dropdown .dropdown-toggle{ + color:inherit +} +.panel-title{ + margin-top:0; + margin-bottom:0; + font-size:16px; + color:inherit +} +.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{ + color:inherit +} +.panel-footer{ + padding:10px 15px; + background-color:#f5f5f5; + border-top:1px solid #ddd; + border-bottom-right-radius:3px; + border-bottom-left-radius:3px +} +.panel>.list-group,.panel>.panel-collapse>.list-group{ + margin-bottom:0 +} +.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{ + border-width:1px 0; + border-radius:0 +} +.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{ + border-top:0; + border-top-left-radius:3px; + border-top-right-radius:3px +} +.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{ + border-bottom:0; + border-bottom-right-radius:3px; + border-bottom-left-radius:3px +} +.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{ + border-top-left-radius:0; + border-top-right-radius:0 +} +.panel-heading+.list-group .list-group-item:first-child{ + border-top-width:0 +} +.list-group+.panel-footer{ + border-top-width:0 +} +.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{ + margin-bottom:0 +} +.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{ + padding-right:15px; + padding-left:15px +} +.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{ + border-top-left-radius:3px; + border-top-right-radius:3px +} +.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{ + border-top-left-radius:3px; + border-top-right-radius:3px +} +.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{ + border-top-left-radius:3px +} +.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{ + border-top-right-radius:3px +} +.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{ + border-bottom-right-radius:3px; + border-bottom-left-radius:3px +} +.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{ + border-bottom-right-radius:3px; + border-bottom-left-radius:3px +} +.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{ + border-bottom-left-radius:3px +} +.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{ + border-bottom-right-radius:3px +} +.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{ + border-top:1px solid #ddd +} +.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{ + border-top:0 +} +.panel>.table-bordered,.panel>.table-responsive>.table-bordered{ + border:0 +} +.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{ + border-left:0 +} +.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{ + border-right:0 +} +.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{ + border-bottom:0 +} +.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{ + border-bottom:0 +} +.panel>.table-responsive{ + margin-bottom:0; + border:0 +} +.panel-group{ + margin-bottom:20px +} +.panel-group .panel{ + margin-bottom:0; + border-radius:4px +} +.panel-group .panel+.panel{ + margin-top:5px +} +.panel-group .panel-heading{ + border-bottom:0 +} +.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{ + border-top:1px solid #ddd +} +.panel-group .panel-footer{ + border-top:0 +} +.panel-group .panel-footer+.panel-collapse .panel-body{ + border-bottom:1px solid #ddd +} +.panel-default{ + border-color:#ddd +} +.panel-default>.panel-heading{ + color:#333; + background-color:#f5f5f5; + border-color:#ddd +} +.panel-default>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#ddd +} +.panel-default>.panel-heading .badge{ + color:#f5f5f5; + background-color:#333 +} +.panel-default>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#ddd +} +.panel-primary{ + border-color:#337ab7 +} +.panel-primary>.panel-heading{ + color:#fff; + background-color:#337ab7; + border-color:#337ab7 +} +.panel-primary>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#337ab7 +} +.panel-primary>.panel-heading .badge{ + color:#337ab7; + background-color:#fff +} +.panel-primary>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#337ab7 +} +.panel-success{ + border-color:#d6e9c6 +} +.panel-success>.panel-heading{ + color:#3c763d; + background-color:#dff0d8; + border-color:#d6e9c6 +} +.panel-success>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#d6e9c6 +} +.panel-success>.panel-heading .badge{ + color:#dff0d8; + background-color:#3c763d +} +.panel-success>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#d6e9c6 +} +.panel-info{ + border-color:#bce8f1 +} +.panel-info>.panel-heading{ + color:#31708f; + background-color:#d9edf7; + border-color:#bce8f1 +} +.panel-info>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#bce8f1 +} +.panel-info>.panel-heading .badge{ + color:#d9edf7; + background-color:#31708f +} +.panel-info>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#bce8f1 +} +.panel-warning{ + border-color:#faebcc +} +.panel-warning>.panel-heading{ + color:#8a6d3b; + background-color:#fcf8e3; + border-color:#faebcc +} +.panel-warning>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#faebcc +} +.panel-warning>.panel-heading .badge{ + color:#fcf8e3; + background-color:#8a6d3b +} +.panel-warning>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#faebcc +} +.panel-danger{ + border-color:#ebccd1 +} +.panel-danger>.panel-heading{ + color:#a94442; + background-color:#f2dede; + border-color:#ebccd1 +} +.panel-danger>.panel-heading+.panel-collapse>.panel-body{ + border-top-color:#ebccd1 +} +.panel-danger>.panel-heading .badge{ + color:#f2dede; + background-color:#a94442 +} +.panel-danger>.panel-footer+.panel-collapse>.panel-body{ + border-bottom-color:#ebccd1 +} +.embed-responsive{ + position:relative; + display:block; + height:0; + padding:0; + overflow:hidden +} +.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{ + position:absolute; + top:0; + bottom:0; + left:0; + width:100%; + height:100%; + border:0 +} +.embed-responsive-16by9{ + padding-bottom:56.25% +} +.embed-responsive-4by3{ + padding-bottom:75% +} +.well{ + min-height:20px; + padding:19px; + margin-bottom:20px; + background-color:#f5f5f5; + border:1px solid #e3e3e3; + border-radius:4px; + -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05); + box-shadow:inset 0 1px 1px rgba(0,0,0,.05) +} +.well blockquote{ + border-color:#ddd; + border-color:rgba(0,0,0,.15) +} +.well-lg{ + padding:24px; + border-radius:6px +} +.well-sm{ + padding:9px; + border-radius:3px +} +.close{ + float:right; + font-size:21px; + font-weight:700; + line-height:1; + color:#000; + text-shadow:0 1px 0 #fff; + filter:alpha(opacity=20); + opacity:.2 +} +.close:focus,.close:hover{ + color:#000; + text-decoration:none; + cursor:pointer; + filter:alpha(opacity=50); + opacity:.5 +} +button.close{ + -webkit-appearance:none; + padding:0; + cursor:pointer; + background:0 0; + border:0 +} +.modal-open{ + overflow:hidden +} +.modal{ + position:fixed; + top:0; + right:0; + bottom:0; + left:0; + z-index:1050; + display:none; + overflow:hidden; + -webkit-overflow-scrolling:touch; + outline:0 +} +.modal.fade .modal-dialog{ + -webkit-transition:-webkit-transform .3s ease-out; + -o-transition:-o-transform .3s ease-out; + transition:transform .3s ease-out; + -webkit-transform:translate(0,-25%); + -ms-transform:translate(0,-25%); + -o-transform:translate(0,-25%); + transform:translate(0,-25%) +} +.modal.in .modal-dialog{ + -webkit-transform:translate(0,0); + -ms-transform:translate(0,0); + -o-transform:translate(0,0); + transform:translate(0,0) +} +.modal-open .modal{ + overflow-x:hidden; + overflow-y:auto +} +.modal-dialog{ + position:relative; + width:auto; + margin:10px +} +.modal-content{ + position:relative; + background-color:#fff; + -webkit-background-clip:padding-box; + background-clip:padding-box; + border:1px solid #999; + border:1px solid rgba(0,0,0,.2); + border-radius:6px; + outline:0; + -webkit-box-shadow:0 3px 9px rgba(0,0,0,.5); + box-shadow:0 3px 9px rgba(0,0,0,.5) +} +.modal-backdrop{ + position:fixed; + top:0; + right:0; + bottom:0; + left:0; + z-index:1040; + background-color:#000 +} +.modal-backdrop.fade{ + filter:alpha(opacity=0); + opacity:0 +} +.modal-backdrop.in{ + filter:alpha(opacity=50); + opacity:.5 +} +.modal-header{ + padding:15px; + border-bottom:1px solid #e5e5e5 +} +.modal-header .close{ + margin-top:-2px +} +.modal-title{ + margin:0; + line-height:1.42857143 +} +.modal-body{ + position:relative; + padding:15px +} +.modal-footer{ + padding:15px; + text-align:right; + border-top:1px solid #e5e5e5 +} +.modal-footer .btn+.btn{ + margin-bottom:0; + margin-left:5px +} +.modal-footer .btn-group .btn+.btn{ + margin-left:-1px +} +.modal-footer .btn-block+.btn-block{ + margin-left:0 +} +.modal-scrollbar-measure{ + position:absolute; + top:-9999px; + width:50px; + height:50px; + overflow:scroll +} +@media (min-width:768px){ + .modal-dialog{ + width:600px; + margin:30px auto + } + .modal-content{ + -webkit-box-shadow:0 5px 15px rgba(0,0,0,.5); + box-shadow:0 5px 15px rgba(0,0,0,.5) + } + .modal-sm{ + width:300px + } +} +@media (min-width:992px){ + .modal-lg{ + width:900px + } +} +.tooltip{ + position:absolute; + z-index:1070; + display:block; + font-family:"Helvetica Neue",Helvetica,Arial,sans-serif; + font-size:12px; + font-style:normal; + font-weight:400; + line-height:1.42857143; + text-align:left; + text-align:start; + text-decoration:none; + text-shadow:none; + text-transform:none; + letter-spacing:normal; + word-break:normal; + word-spacing:normal; + word-wrap:normal; + white-space:normal; + filter:alpha(opacity=0); + opacity:0; + line-break:auto +} +.tooltip.in{ + filter:alpha(opacity=90); + opacity:.9 +} +.tooltip.top{ + padding:5px 0; + margin-top:-3px +} +.tooltip.right{ + padding:0 5px; + margin-left:3px +} +.tooltip.bottom{ + padding:5px 0; + margin-top:3px +} +.tooltip.left{ + padding:0 5px; + margin-left:-3px +} +.tooltip-inner{ + max-width:200px; + padding:3px 8px; + color:#fff; + text-align:center; + background-color:#000; + border-radius:4px +} +.tooltip-arrow{ + position:absolute; + width:0; + height:0; + border-color:transparent; + border-style:solid +} +.tooltip.top .tooltip-arrow{ + bottom:0; + left:50%; + margin-left:-5px; + border-width:5px 5px 0; + border-top-color:#000 +} +.tooltip.top-left .tooltip-arrow{ + right:5px; + bottom:0; + margin-bottom:-5px; + border-width:5px 5px 0; + border-top-color:#000 +} +.tooltip.top-right .tooltip-arrow{ + bottom:0; + left:5px; + margin-bottom:-5px; + border-width:5px 5px 0; + border-top-color:#000 +} +.tooltip.right .tooltip-arrow{ + top:50%; + left:0; + margin-top:-5px; + border-width:5px 5px 5px 0; + border-right-color:#000 +} +.tooltip.left .tooltip-arrow{ + top:50%; + right:0; + margin-top:-5px; + border-width:5px 0 5px 5px; + border-left-color:#000 +} +.tooltip.bottom .tooltip-arrow{ + top:0; + left:50%; + margin-left:-5px; + border-width:0 5px 5px; + border-bottom-color:#000 +} +.tooltip.bottom-left .tooltip-arrow{ + top:0; + right:5px; + margin-top:-5px; + border-width:0 5px 5px; + border-bottom-color:#000 +} +.tooltip.bottom-right .tooltip-arrow{ + top:0; + left:5px; + margin-top:-5px; + border-width:0 5px 5px; + border-bottom-color:#000 +} +.popover{ + position:absolute; + top:0; + left:0; + z-index:1060; + display:none; + max-width:276px; + padding:1px; + font-family:"Helvetica Neue",Helvetica,Arial,sans-serif; + font-size:14px; + font-style:normal; + font-weight:400; + line-height:1.42857143; + text-align:left; + text-align:start; + text-decoration:none; + text-shadow:none; + text-transform:none; + letter-spacing:normal; + word-break:normal; + word-spacing:normal; + word-wrap:normal; + white-space:normal; + background-color:#fff; + -webkit-background-clip:padding-box; + background-clip:padding-box; + border:1px solid #ccc; + border:1px solid rgba(0,0,0,.2); + border-radius:6px; + -webkit-box-shadow:0 5px 10px rgba(0,0,0,.2); + box-shadow:0 5px 10px rgba(0,0,0,.2); + line-break:auto +} +.popover.top{ + margin-top:-10px +} +.popover.right{ + margin-left:10px +} +.popover.bottom{ + margin-top:10px +} +.popover.left{ + margin-left:-10px +} +.popover-title{ + padding:8px 14px; + margin:0; + font-size:14px; + background-color:#f7f7f7; + border-bottom:1px solid #ebebeb; + border-radius:5px 5px 0 0 +} +.popover-content{ + padding:9px 14px +} +.popover>.arrow,.popover>.arrow:after{ + position:absolute; + display:block; + width:0; + height:0; + border-color:transparent; + border-style:solid +} +.popover>.arrow{ + border-width:11px +} +.popover>.arrow:after{ + content:""; + border-width:10px +} +.popover.top>.arrow{ + bottom:-11px; + left:50%; + margin-left:-11px; + border-top-color:#999; + border-top-color:rgba(0,0,0,.25); + border-bottom-width:0 +} +.popover.top>.arrow:after{ + bottom:1px; + margin-left:-10px; + content:" "; + border-top-color:#fff; + border-bottom-width:0 +} +.popover.right>.arrow{ + top:50%; + left:-11px; + margin-top:-11px; + border-right-color:#999; + border-right-color:rgba(0,0,0,.25); + border-left-width:0 +} +.popover.right>.arrow:after{ + bottom:-10px; + left:1px; + content:" "; + border-right-color:#fff; + border-left-width:0 +} +.popover.bottom>.arrow{ + top:-11px; + left:50%; + margin-left:-11px; + border-top-width:0; + border-bottom-color:#999; + border-bottom-color:rgba(0,0,0,.25) +} +.popover.bottom>.arrow:after{ + top:1px; + margin-left:-10px; + content:" "; + border-top-width:0; + border-bottom-color:#fff +} +.popover.left>.arrow{ + top:50%; + right:-11px; + margin-top:-11px; + border-right-width:0; + border-left-color:#999; + border-left-color:rgba(0,0,0,.25) +} +.popover.left>.arrow:after{ + right:1px; + bottom:-10px; + content:" "; + border-right-width:0; + border-left-color:#fff +} +.carousel{ + position:relative +} +.carousel-inner{ + position:relative; + width:100%; + overflow:hidden +} +.carousel-inner>.item{ + position:relative; + display:none; + -webkit-transition:.6s ease-in-out left; + -o-transition:.6s ease-in-out left; + transition:.6s ease-in-out left +} +.carousel-inner>.item>a>img,.carousel-inner>.item>img{ + line-height:1 +} +@media all and (transform-3d),(-webkit-transform-3d){ + .carousel-inner>.item{ + -webkit-transition:-webkit-transform .6s ease-in-out; + -o-transition:-o-transform .6s ease-in-out; + transition:transform .6s ease-in-out; + -webkit-backface-visibility:hidden; + backface-visibility:hidden; + -webkit-perspective:1000px; + perspective:1000px + } + .carousel-inner>.item.active.right,.carousel-inner>.item.next{ + left:0; + -webkit-transform:translate3d(100%,0,0); + transform:translate3d(100%,0,0) + } + .carousel-inner>.item.active.left,.carousel-inner>.item.prev{ + left:0; + -webkit-transform:translate3d(-100%,0,0); + transform:translate3d(-100%,0,0) + } + .carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{ + left:0; + -webkit-transform:translate3d(0,0,0); + transform:translate3d(0,0,0) + } +} +.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{ + display:block +} +.carousel-inner>.active{ + left:0 +} +.carousel-inner>.next,.carousel-inner>.prev{ + position:absolute; + top:0; + width:100% +} +.carousel-inner>.next{ + left:100% +} +.carousel-inner>.prev{ + left:-100% +} +.carousel-inner>.next.left,.carousel-inner>.prev.right{ + left:0 +} +.carousel-inner>.active.left{ + left:-100% +} +.carousel-inner>.active.right{ + left:100% +} +.carousel-control{ + position:absolute; + top:0; + bottom:0; + left:0; + width:15%; + font-size:20px; + color:#fff; + text-align:center; + text-shadow:0 1px 2px rgba(0,0,0,.6); + background-color:rgba(0,0,0,0); + filter:alpha(opacity=50); + opacity:.5 +} +.carousel-control.left{ + background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%); + background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%); + background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001))); + background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); + background-repeat:repeat-x +} +.carousel-control.right{ + right:0; + left:auto; + background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%); + background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%); + background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5))); + background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); + background-repeat:repeat-x +} +.carousel-control:focus,.carousel-control:hover{ + color:#fff; + text-decoration:none; + filter:alpha(opacity=90); + outline:0; + opacity:.9 +} +.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{ + position:absolute; + top:50%; + z-index:5; + display:inline-block; + margin-top:-10px +} +.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{ + left:50%; + margin-left:-10px +} +.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{ + right:50%; + margin-right:-10px +} +.carousel-control .icon-next,.carousel-control .icon-prev{ + width:20px; + height:20px; + font-family:serif; + line-height:1 +} +.carousel-control .icon-prev:before{ + content:'\2039' +} +.carousel-control .icon-next:before{ + content:'\203a' +} +.carousel-indicators{ + position:absolute; + bottom:10px; + left:50%; + z-index:15; + width:60%; + padding-left:0; + margin-left:-30%; + text-align:center; + list-style:none +} +.carousel-indicators li{ + display:inline-block; + width:10px; + height:10px; + margin:1px; + text-indent:-999px; + cursor:pointer; + background-color:#000\9; + background-color:rgba(0,0,0,0); + border:1px solid #fff; + border-radius:10px +} +.carousel-indicators .active{ + width:12px; + height:12px; + margin:0; + background-color:#fff +} +.carousel-caption{ + position:absolute; + right:15%; + bottom:20px; + left:15%; + z-index:10; + padding-top:20px; + padding-bottom:20px; + color:#fff; + text-align:center; + text-shadow:0 1px 2px rgba(0,0,0,.6) +} +.carousel-caption .btn{ + text-shadow:none +} +@media screen and (min-width:768px){ + .carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{ + width:30px; + height:30px; + margin-top:-10px; + font-size:30px + } + .carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{ + margin-left:-10px + } + .carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{ + margin-right:-10px + } + .carousel-caption{ + right:20%; + left:20%; + padding-bottom:30px + } + .carousel-indicators{ + bottom:20px + } +} +.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{ + display:table; + content:" " +} +.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{ + clear:both +} +.center-block{ + display:block; + margin-right:auto; + margin-left:auto +} +.pull-right{ + float:right!important +} +.pull-left{ + float:left!important +} +.hide{ + display:none!important +} +.show{ + display:block!important +} +.invisible{ + visibility:hidden +} +.text-hide{ + font:0/0 a; + color:transparent; + text-shadow:none; + background-color:transparent; + border:0 +} +.hidden{ + display:none!important +} +.affix{ + position:fixed +} +@-ms-viewport{ + width:device-width +} +.visible-lg,.visible-md,.visible-sm,.visible-xs{ + display:none!important +} +.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{ + display:none!important +} +@media (max-width:767px){ + .visible-xs{ + display:block!important + } + table.visible-xs{ + display:table!important + } + tr.visible-xs{ + display:table-row!important + } + td.visible-xs,th.visible-xs{ + display:table-cell!important + } +} +@media (max-width:767px){ + .visible-xs-block{ + display:block!important + } +} +@media (max-width:767px){ + .visible-xs-inline{ + display:inline!important + } +} +@media (max-width:767px){ + .visible-xs-inline-block{ + display:inline-block!important + } +} +@media (min-width:768px) and (max-width:991px){ + .visible-sm{ + display:block!important + } + table.visible-sm{ + display:table!important + } + tr.visible-sm{ + display:table-row!important + } + td.visible-sm,th.visible-sm{ + display:table-cell!important + } +} +@media (min-width:768px) and (max-width:991px){ + .visible-sm-block{ + display:block!important + } +} +@media (min-width:768px) and (max-width:991px){ + .visible-sm-inline{ + display:inline!important + } +} +@media (min-width:768px) and (max-width:991px){ + .visible-sm-inline-block{ + display:inline-block!important + } +} +@media (min-width:992px) and (max-width:1199px){ + .visible-md{ + display:block!important + } + table.visible-md{ + display:table!important + } + tr.visible-md{ + display:table-row!important + } + td.visible-md,th.visible-md{ + display:table-cell!important + } +} +@media (min-width:992px) and (max-width:1199px){ + .visible-md-block{ + display:block!important + } +} +@media (min-width:992px) and (max-width:1199px){ + .visible-md-inline{ + display:inline!important + } +} +@media (min-width:992px) and (max-width:1199px){ + .visible-md-inline-block{ + display:inline-block!important + } +} +@media (min-width:1200px){ + .visible-lg{ + display:block!important + } + table.visible-lg{ + display:table!important + } + tr.visible-lg{ + display:table-row!important + } + td.visible-lg,th.visible-lg{ + display:table-cell!important + } +} +@media (min-width:1200px){ + .visible-lg-block{ + display:block!important + } +} +@media (min-width:1200px){ + .visible-lg-inline{ + display:inline!important + } +} +@media (min-width:1200px){ + .visible-lg-inline-block{ + display:inline-block!important + } +} +@media (max-width:767px){ + .hidden-xs{ + display:none!important + } +} +@media (min-width:768px) and (max-width:991px){ + .hidden-sm{ + display:none!important + } +} +@media (min-width:992px) and (max-width:1199px){ + .hidden-md{ + display:none!important + } +} +@media (min-width:1200px){ + .hidden-lg{ + display:none!important + } +} +.visible-print{ + display:none!important +} +@media print{ + .visible-print{ + display:block!important + } + table.visible-print{ + display:table!important + } + tr.visible-print{ + display:table-row!important + } + td.visible-print,th.visible-print{ + display:table-cell!important + } +} +.visible-print-block{ + display:none!important +} +@media print{ + .visible-print-block{ + display:block!important + } +} +.visible-print-inline{ + display:none!important +} +@media print{ + .visible-print-inline{ + display:inline!important + } +} +.visible-print-inline-block{ + display:none!important +} +@media print{ + .visible-print-inline-block{ + display:inline-block!important + } +} +@media print{ + .hidden-print{ + display:none!important + } +} +/*# sourceMappingURL=bootstrap.min.css.map */ + diff --git a/themes/triangles/client/src/css/01-main.css b/themes/triangles/client/src/css/01-main.css new file mode 100644 index 000000000..347c0b81c --- /dev/null +++ b/themes/triangles/client/src/css/01-main.css @@ -0,0 +1,77 @@ +body { + background-image: url("/img/LargeTriangles.svg"); + /*background-image: url("//*img//*RandomizedPattern.svg");*/ + /*background-image: url("//*img//*background.svg");*/ + /*background-color:#000000;*/ +} +canvas{ + position:absolute; + top:0; + left:0; +} +.authelia-brand { + font-weight: bold; + font-style: italic; + color: #ffffff +} +.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: white; +} +/* 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; + color: white; +} +.header { + background-color: #000000; + color: white; + margin: 0px; +} +.body { + padding: 10px; +} +h1 { + font-size: 25px; +} diff --git a/themes/triangles/client/src/css/02-login.css b/themes/triangles/client/src/css/02-login.css new file mode 100644 index 000000000..a6984267b --- /dev/null +++ b/themes/triangles/client/src/css/02-login.css @@ -0,0 +1,136 @@ +.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 #000; + margin-top: 20px; + padding-bottom: 20px; + background-color: #000000; + -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 1); + -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 1); + box-shadow: 0px 2px 2px rgba(0, 0, 0, 1); +} +.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; + color: white; +} +.account-wall .form-inputs +{ + margin-bottom: 10px; + border-color: #b20c0c; +} +.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; + color: white; +} + +.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; + color: white; +} + +.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 */ +} diff --git a/themes/triangles/client/src/css/03-errors.css b/themes/triangles/client/src/css/03-errors.css new file mode 100644 index 000000000..e9f97f33b --- /dev/null +++ b/themes/triangles/client/src/css/03-errors.css @@ -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%; +} \ No newline at end of file diff --git a/themes/triangles/client/src/css/03-password-reset-form.css b/themes/triangles/client/src/css/03-password-reset-form.css new file mode 100644 index 000000000..34066bc24 --- /dev/null +++ b/themes/triangles/client/src/css/03-password-reset-form.css @@ -0,0 +1,4 @@ + +.password-reset-form .header-img { + border-radius: 0%; +} diff --git a/themes/triangles/client/src/css/03-password-reset-request.css b/themes/triangles/client/src/css/03-password-reset-request.css new file mode 100644 index 000000000..1a2ad4df8 --- /dev/null +++ b/themes/triangles/client/src/css/03-password-reset-request.css @@ -0,0 +1,4 @@ + +.password-reset-request .header-img { + border-radius: 0%; +} diff --git a/themes/triangles/client/src/css/03-totp-register.css b/themes/triangles/client/src/css/03-totp-register.css new file mode 100644 index 000000000..cb76720ad --- /dev/null +++ b/themes/triangles/client/src/css/03-totp-register.css @@ -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; +} \ No newline at end of file diff --git a/themes/triangles/client/src/css/03-u2f-register.css b/themes/triangles/client/src/css/03-u2f-register.css new file mode 100644 index 000000000..e54cddf89 --- /dev/null +++ b/themes/triangles/client/src/css/03-u2f-register.css @@ -0,0 +1,5 @@ + +.u2f-register img { + display: block; + margin: 20px auto; +} \ No newline at end of file diff --git a/themes/triangles/client/src/img/LargeTriangles.svg b/themes/triangles/client/src/img/LargeTriangles.svg new file mode 100644 index 000000000..0988bcb39 --- /dev/null +++ b/themes/triangles/client/src/img/LargeTriangles.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/themes/triangles/client/src/img/background.jpg b/themes/triangles/client/src/img/background.jpg new file mode 100644 index 0000000000000000000000000000000000000000..974ea273fa87adccec4b4433fc4d097c04ae4c31 GIT binary patch literal 587 zcmb7za3BG;m3jxdn?P_E@h=1|8$YR;59+9 zW`I5}XRiI4&WC@yzle_&cQR_(0pU8Ji5j{Gs2||wia~Q)aTB2CVR=3aT5jO?)niY+ P^@QfW&?n$dCIR+GQO8ew literal 0 HcmV?d00001 diff --git a/themes/triangles/client/src/img/icon.png b/themes/triangles/client/src/img/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..040d10c1ab5feaf6720fa8e8c0a0aa336402894e GIT binary patch literal 1461 zcmV;m1xosfP)e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00kXML_t(o!_`+?Y*bYgUHhE- zn3=ZI7qzh{%0mzcVu-|44KcBlMg%4JNF)*ye@ry-m;Ooo@C(p{@FNm6K%#tzfV8Dp z(gHC?u?eAyEkY@6v9v&$K4#|5J!kuI=XL2!XD|}uPVUd$_w2p)Ui)z_@E;5PlS6Y? zzD$W&;aqBgCCWIgr3%#B17TU=LfaFniBfPfGHk@o-q0}>pSg3elo_&jXcox?pxuirC( z+64_w<1wcpt~Ct9s1v|4#r%X~rnEyMkPy>M!hxWQY!F}#Xodt_W_^yC&$ChylYpwS z&=(^YTQB4VP`#+(neoZ!X+^?|Fo&2@Mx>OVR#uekyFNbs3W#>4vjG6i{zq#-D?lmO zgwb%&er)to`<;|MBis1xiRpQoq=1lspman6NDy#26twqNR#fgA_~q+c*8$*>20$i( zX43)A%7y?m0U*TPM9rNfXHy+Yt03Y#2NpCw4d56E zr8CNk8ARxwwFZ)MCdHT&Pdu5eCDZpn@^ zg!ri-09%OTsSF5HzMiccym0J>)_N^~%UQkOi}{T!LDU98Rq07el%z1z(&37?5ok7xHr~3h{1!EAv9uU=n1bkaDm3plKtq}^CpAB|@>l6g=Th{?L zO><|;%(Uw^S3mw@#S9R`U6(-E0l-S{0N(|L(*w#PM1vt?cfmoFzLeC?=_s+x9)RJv zx_T-a+gbFrCOycqq7T~N44pf2vuFUnckNe}Et`c$GGYn$Em5~-bAIoXqv}29Cef4~ z2yM@YUF1_#SCxNnnX)rE+6@p6Qr(*6e&3X1gC-+a8c+&BL+%*qK6K@t0rdTNNQVOU z`=%5Qz|c$<11C_oX0Z=|Z|P*x4y9laU$!iJR{F$j%3FW5_5>uoM=4&3>Oj<9vSgh% zpy=+qV9L1*!8#Ez-`$zbhb**wobmo&McKSC1NF&>%U#7=0}m|ebs1q0HD!v9 zy$h(TT*3Ty{Ax#AA^WnJjZ3BTP6{EanRzux5Qt1y^Q$1#gAm97P#~o>v3^Gnb?z_5 zc2NL$a)2xAmbJ20*BIQu3@cgwp7UhUS%QW_rq~p}dg}0f`!}4kbp4`eG`2hHsz#kW zZxFdbQ|=801KV!&9J_Phe@-7=xryR#eBI=9yqTE?O9J-xk&7+8_&>_uokeL63{>@u P00000NkvXXu0mjfXm6c7 literal 0 HcmV?d00001 diff --git a/themes/triangles/client/src/img/mail.png b/themes/triangles/client/src/img/mail.png new file mode 100644 index 0000000000000000000000000000000000000000..834bfce9107a94be10da4c011fb78e37a192888f GIT binary patch literal 3545 zcmWkxc{CJ$5dZF4tXtNx*0s3`#S$s!5_0FZ2?r0sk5=BUk&_Rfm`^vTD z%#kBkY*-}m^PBf(=FQA|pU;~&f6Pq0iSbnyMie6efJIMN%k0Dx{#Q8NiEUML>OFC= zAWc0B_=((uyT+Zw3^#Rcf&ici|6d_HOJ!{*N#0;>T(G&ndvJ(Tpc@DY36b{n^A2)x zy6GnEAL#LPLj`rhV5O&}VG%m<>)Dz)r!}gh`sIE>yJEBH0oyW)_DS6=t)VW~UUAj$ zm+hiLcfH+H{fex~j_(uClb=^=A>;#1uWMUy{+4Tb4*!BdqZuD{5fW-KrVz0}Qw(L} za@q)nnRz60-@;pCi~Hhm>IUq&0(@ai{WjCs-tr zACjZUSIC+XRz?w>hDYM}b7}IWaF45pAB@ivor$T$1Y)i(IxTkrrMHkX&kWKPRp1hg zo=mAMrPeyF;iZ{Vt?q%>H0ZzDPZKJ|(U#Jl;f z06i6&8@wn?^&SkpMos(hkQeM4T*f2MgzTaNS1mA$!umiOb5LgSwFu%u(u zxl5l!lfKwD2^vQ@s^A+-wh}8pN2y1)F~TmnSLe`z?}x~jT2_L8%H|axd=D5X`;&{> zP>X5~|M;ND8k#(ZG=gC1d}?gqKl-%2@dGXVHzKB%M(%~U@vF1V8GNgTYJRv*V-%RB zE?7#4CY`tCW6Rh=X>>G{9qOo_vpc4hVxP@Q_CSH)2MD614Seip{M{ETb1p+mL1AN4 zP=*G!s#NrkF=IRj2zEi(Am6vfJ0yS9t@)iN$CIJClYjK3=j^;zF+d;xVL2xn_C8sB zbt&j|^EUgp$dr>75LtlGxGnY7c;~jzrPG&&5M|^-kJE=CYq(H>N7h^*^;cOA<@|L7 zo3*HDm~%9f-e&kdFH^h|Q4$a%B0dOms8)v!9&#R~<$lrR33!A8q6wc?Y!XMKRwfOH z4*XY5U)3mL|0YU{TbM`kq>j85d(Y6R1%<^YK(Rdy|IFlW%_GBRvvkXm9gFbA>2zT4%~wgv zAa{%nEl}FuVcZ8qkO%y;u}g;FXX8CA0(V zHo$2?uxjL&5mzp7SH^_L2C;OiCX>rmMyoy;vTG$k2nW{n0cDEV02>9Bh=VB)tdf$W zrorSuy@_@0rUOW3$ZL-KDckegvIY&h_WoG-|1ck^9o(Uuv9{`?2a#*EvstqrcU;OkT%&O z&?2q|bdqN?hn4Vf7m}CLABr98mRx8-;?q+v5XK08(*u@sx_(3j* zwBweN+hiAHpXTP~5Ms>Mi#JY+7E~4JK=DjQpvuacJBq6F3_{QaKJQs^b}qae=K8>g zmjUqkf+}w1a*BP5n{MVHzz2hy<-_{Dc)dbjoQebl&j$rIfuF2d!KJx{Xan~57;aKY z*cp0oD@2eKqn|i>$qf2D({4472CcKI7Y@N>p{=|McnE1GMUYE&cW91q>S4?h5CYdc zD>jt_+|+-^so@x@KH>st-l0mkN2n*Xf7vo~s4j$KQNxqukdo#tn*4rG<1cq8nW0=t~jx@i5$Yzh}-9R7YT}D7A6DYU5 zB}Sommbv#GGl+$48vKt(LSc7%-B7@|WNpltJ)j^na_{1~R9U)o$SYR#Q*m!7C{I!- zq6B&H1TBRWX!)D}Qy8DT#!Dy$H9;TT!7N8x&)vOj*_r9}hi)PIT%O8qz!uuIyOIMX zmf!B#>W3K9xc`td+{ymLd&R7@QovuOEPBz^2?*eD1G^Svw3mY z--Ts5TPh5EQ{L4ab8gTT(`bvI&U?)fx@UC?;hR>-?)nV@{H=LNX&YboU+{G`mW3mT4zkU=M{34>Ue(w*XT0L=|s)EEY=NJQB zyq{(qvcLA3&T++b|4(_N8|OlznVJ6QlY% zd}XHHJ#=tl^)=sY=cnyIL1U&?XP5cYh7^8gE_ewLFyi9T36^c`=bVb~+_)H4GN63; ze!01GwqHW2(hWpeKI4|we%qg&+E-E<7bRi~BbBbko-X)$BihDeI7>Ei*3(%H9CW6?+!kwP)K}$%-iX&Fx-dO8OayE0>WtE?}Ceu*lDoW3u)c+rZ+>D*wT{+UNT4v z7y!ad%qetWFY*OwQ>w&{#d9_*L_QC`=Oxtk0(TaG=JPJtgT10hh7L^Ni=L+Dk-}j_Zb{DeRbsUtukh*#)(O2maD^ebRpzH&k zc1-{#JRWW@uk?3xYia$^u=P^Uf$z}^$Eya-26HZIyOKl{fJMO_#eP&R3VmL0WNyWL z>|hHtJ009@t0p!BZ))12BVh4c+K$V^w=fsnlpnfpM6JX!51A~t*kfGwF1j9})Q*Uo=fMgwFn^O>KYtsCNIrx$_O6?4zks$w|$aiP)S7HbUnC?4#0( z?9RyvUybw@hfr{p?rAz#dHK3NU30$NVMv4d%yC=7jQgzx)^|a$^*x7mKbVyrWFU8GbZ8()Nlj2>E@cM*03ZNKL_t(|+U&e(l%>gC z=J$(;cfD)gn|oGfW$k-+S8vs=mb%qy)M!C1w9vw!BMAv%jIr5)Rn=8ldsSvuZg;=$ebfw`r{k`&r+VuEGcPz}!EZ+9YuRr_7;!<*7zA5(l_kuH1 zCc6?7)+@yA0ge3=^e%QWHHnB4%vDfGntIovK$RmRqMc*Z+^bxK%+uu-{bYDZW`Y`jJyV!Y6P0Hvqg~i~2u({5yZ_ z^0}q&U;5O!dh^({S8Qr&Hb=x4M#wuuCcm|Uks+PW53sUNaCe>hRGIYIgsszE%D0w@ zpB-Ww8nlPA4*9S^T1#+il`PLO*AsNBKv;vd5@$?N3@wf?F?Gk-R+8CFw3QN{(1ZLJ zjAii6_Mqf9^5(wp@=yQu=O;HGKXKygZ~a$0KDqzQ2S5LYu2T#D+2C3{Z8r$Kfs~a! z9`P{7`VQr4Nb3(eM5hMS54-qFCNT0bJ^V8v6sjtf7bmWK(+nm(U2y)?WxT{N zOnNN5d6C*=owcv7;0Q_cduPdBj1dikTlX1`QbtQ1)LBh>#o*Kga?)Yq)&+WMfLZDi z$TB%8gVR043KTaJY{_8S2H{(bV{vOCw#adt4q00hB}4qo!DI%l3^G+y*&uqo((Xrt zLzN$_yl(nR=114;MV$jUM|{kt{znG*o7O+={4$@v`;&a{`Tud@*kkv8uI|@w43pmP z$E{(Lfna!J#PIoR1d|hFrI@^z;dEWv!#17u9we4vM};g+8J!toB1wK_#Eu7d5X_b6 z42Cq{SjLnEg{K*D#LXov|MR1e}y5rhtAXi>&uj6iCMT5!NBl9d<=ONo%& z0Ky_e5A6!vp@G66JUe;YBX|9Q*(h!}bI#|OQ9FvW=|3{S|9QFZD`U>&0Gw=n{gyZV z;(LDS{PN4ck%;t=o4DbvyHD`^D^F3{QK9|#2Cgcz{*|kES0&kI0X0SQ;c1dVN_>6* zBT2p-W80e29W|U-(tfOkiUs*hhLw`q9TSjb?0#?`dfuhjikLh!$tcbdagNC>7DLbo zDK>LN4itjm^&WYY5*(|NzZRqW38iB-3Y`-?FhOx?fDI%Vh2ts;lQI|#Lpt6eRp!Uk zm~%js>}5N!YyZdqz^zZdiTu+Ml^>s}-1+z;FI7tZDK*=C|0s;r~=gaSyewH`YY5+5j9{m3w z0Cs)p@V3T(ajib~zwG$6{ksngK0N=CWzLHYkGtK4R z64^~bc*w)u?NNW*1Y0+HR0b7v+kh5K+%rqi2^c=zX6)_|bEHa9$_e(kXAzXAzW0gT@NqRjejWTj$ zkadZ41u_Uwp<(HK79a(3%uff{bA7%{j@Kf*P%mnS4)ZGs2zkC&?9kO1Gk%pqH5wT@(vWxB8 z?KIF;RNqx)^y+}HTqfJdKx>q8NiQcz$Hf&cS(GCT5DSdf6ulhf3vA2a%sbewgl>)r z1$i&R4L$Z8IrP5S#rb#lA5fpZ@>5U7{~!Ty`4i{4_r-UA&}l|b1{1;b>g5}_6II3* zny8v&>E>(PeEb4)-?5KyA;4`a#BzuYQ{=eI+>u4vPqY|2HckIj2fLL(P0@L-g7mbN&9(wmXxUhbjJklsf;WQ+9p|ODL2$Gc?Th39d4!yHOw9Jr>LoqT8 zpY0pEB<|pb4(>F$!x-b2M0l?HdW{8%Cl-?SC zEs6&}Y*S5S60)l?_nzj(PR%kRPDJC>=9^)@N=%k>3s!!TZIDhgC)uRofueZ^og8Y)^_Mf?j_F#kN zz0=etN{mVxc|?&u)kQ5j_=kP6QzLBKF#nF7v`@ECTYwahL+u@PVmYL|Yl7s32#LmN zy5xO>Yz`6Dr z6?=1P&Ai*7fV`QSltwKN{9ucD()wV>4BfAfY)w zO?k4yfw{YQ!amN8PoH7p-Mc6{8GhTtd0mB~zg?GKeR&lfS!lQnp6@a0k*^q zhEMj8(nAkZWKH2z73uX6V$z^j`dGkar|@EysynoLw&I zC_x53$}o;bL4DUG?bkLLT_2LK#5C^TLF`1> zvO&3k23dn_DMMB~%w&ql1V&488xzhnF-b;P^=H?vcRyTdH=h_??5qa&H7Rby-#kH} zzgUB$KzQ<9I*XsGH5)V9#Ec!EBMtgQGDUC3Shs{MD>hn}dEw8VX6$X_m=Z8$sNOP8 ze0qzU_1C!sGJk|c@$pV}yhxA$>HSYZi*8EY5NB?XGr{R%! zF=Ajb{hTx}5d9&#pR*+gI9bl{g^13VZZI)XqwkDJ<`QmPxI*WJ4U(5TwEnV->bQ)) z-lhG?Wv=|r8A^*4L|72DV!#qz9I^EuZelu;)jxifuq1Ijhy12x-=TvzmjxT2U4xz_ z?iZ*#TpABf5a$Ww@0!88+oOC}Nae&B^*ic_TV1RRfeILZU=n9n2uffih{7zvhIVl5Lf4+*`U8a7pOz-7_eAGvd2MnGcaQg#yv+ww=tR@%eeYK0z^l%p)yd5=6 zKOtMmQISGu@aF?`X0Ta-nhz-u=vI!%ECbOcz7nB(hG372URLCTl;}%+^w5%C&&bcj z6cGdqWmF`1`?r55XTJObQYs=bKn#J{ps!~b9D3Kc&|2Un5_Qa_y|RUFXJj40xBkF8 zId$qOkm+!9mltdBp3{A}r_%Xsh8`28khgYJ_*(MR@|X&&O}Ls7Zx)yiBQDqL2yY-66IPj<2x20mijB z`#hWm7|(KT=`tdh=*-{!-j@))DzHo8gFiz6x<3%N;E5Nn% z&-by*g7``YlUuTWf$YYRL#}i3HBD)Mlk!BF3m-d0Fj2 zUy4x+{x=>2q+f}t+*3c8RS2AEz!zMexNa0)?TQp$HvqNWu|He~YnB- z?ygd_GD?T4MCTGjtN@EswS;%ouqZ~)w5cpq$XyB906BCpQA+D(IvCrtG5u8_S zd}0;d6DS>^wIxps?N44u=Q%dF2)m6@J1o$i07u|WmdUe>;Yy5jEJybqhH((%4%8gP zP9HOfNiU_0o*3ewFfzd=nu%L?kgpEOmq)nMRZO*@JXeFsNs25c-%4;s29xG@(b@my z&)xTHR!+PBWdR_)Z30Q%{8^#p(c#zoxVb=R$m1L?Mx@o}74$cG~&9wn)m+O><} z<`#A!IQPm6q@7LbhZ@{`w#}R0`gUIbwugzfG}eMp3Tr%!jhTFCAC-63@%K0!{?I+N zFRtOJ0O3n?D`%`fK{2qAu*BR;>8QZt9=3|V6@fe&;I47 z(9a6;r$p?r4;MAoO19 z=`)18%S7ijFZ|+HsUI367YQPd z!N?h5uNwwu`;ZsNaY<=DK+XH8hDKEj{1cPt^#nDkPz@Jn%poDcse4rC#t8R^WUE8M zg$l!AjOTiYP-4>@r9!MPA8fpL$EOFMU1Q;scm4eWKzafJD98P{aqas>E|3+Akr_f8 zgcYDI!WwV{&Rhv+zD5}MbmA6g!xp@p7S9De8)rmMRU!bf{Kt6{$|~XFRebp=dzqh(eSUe&FGH5<4^$nWAW?SZnar_7N3P3fFRMOJh13YbC-! zX*>X5ZL{69gM+4ecpMHml<#REQh{EF@OT|H)Z`nMVznS&O)%DyuWMvikZoj`k%h?$ zZ~2)IGq}(t)DB6e2^tDpm7ohu=6He8c{Oj<~xG82!w(9(I)XI!<#N6V}UI>h&hS3!=3akVr5eGbEpuk!%2BaWQ3QVeTw1ZPv3RjX`>?5`yyD`8N1;TadKe35%!9k%G zEpD}p8g~$xAH??;30_r3qWoqmMxeD(YOegL3K&M^ePZ=^=1a=Qgu2`C&(k!XaYFxnu5!j=@yf={_# zCVygp*^E(@GB2f1vvq2N-ji*_sLbV!OPDJOYvS<`>4jjRb|BgfkpqVcd3E^{B)i5T-h3)DPVB~XrwMxawe=g&9cMN8|qSFwFf z@>&E}3r1@x`Lzh$EwHZ@6fYWzL?ELKVRA$w=zXSx-2^=-Kq!O(n`neJ=)jV{CU9ct zzS_cWXxy2Q@~w5$enDltj5`q!zCJ|B03lUQlfG zD9ujbh5^$Ql}c9TarqtzIHca3no_S=U)acuGTZ~6FtUI5(k7yrB2b%w8li?pBt zoC3Q!Bt1FAnGLZHh}42EkP|*C2pC@NP&+b#8D@-%J}1WBNZ!}@NUl6_j{I~Vr|wcd z60&@Kg<>NjiwlOfjrA1_G%5t$a4q)g@F`DKaK;^&aR{mkAq}GBf%Py_r+4Mc#87u#HWWeK~HV?9CR-MgqX#=#cl{&TIP_Ga;E#udc&@0I~Zzt|%D;pU_L zrNP@O^%D2}{F^xN`TLQF;Z48!?d-gFA9*Lmj@j)1$qLkji>QIq@ELybCgrIy>dkS2 za)qp+VM4%26P+0%rxo0=w4T3;TT?8ru2J7LM&u50s)o^plz7cBe7r^Hl{VonpZIJ< z@^}O@AyPwnIbm{l14;^G4Nl#`j%k!-$6_g~#fNDA@&qL^(@w=x9 zXIwahyfTYUdgo*OoET5IT}%t&b{TaJ`2v1TGFmO@dJD ztWOX&ONc8uGS%-9P4!_~8YbPpI{>&3RfPK%H9j@l{FJD`*pYEg{`!j?DjlNvzA08N zt#JGY?&O~T`5_kHuoqbeltmPzWwz&4k zix8HguW=59=!zmUIpqTZGyh-QH(NoYDW-<bgmQ7P z6vj~f;22`ofpG_G48~Y&A+er7;ZgA_EZ)7JpZwjQLzX0tP!xWS9SX9Ql;lcEygJ0V z8ao16R@lOz2Lk!44qPmdZjIj3!zcA`^*~(Ac|+=xUdh z$FE?OiwND1(QvD5=6`n*V6J4y>F{%&k^4I@^zj{+)|2ZL-hkb2y`A&F`z&&gz;D)> zx^t4vY!y$ASrBDvOK{@w#@y}V3(+EBf(MqW#Ba|h7_+emru z5B%4x_68(ZHpnjwh@+U`t~zC}0m|SVc1g|+Xg)fDR1Vq61TiCMylITl*$5pQym1#C zgBV(r28|@YIzkRS245T?>I$t3vUP)Ml*nrym9ijdr4+B`WWyX4Sac?lQebn96#^*~ zwqIa{+|CHB!AeP6Z8B>0`Pv_Sjck1cF4;YE73jo+wadl{AEH3l#CkP=u> zP*s$URWLU+%NJJ=yBvmJ+#<~iJf#q!C0|ZafKEY9JM4VNBK6}t=)AhY#$Q}RT8orI z+PruRiT#+UD9G&pHwF+@fnQc1w(G^Wjy*C*weB)H*~gADu6*Vk?K5lK_G7PStUAg0 zufE87Yl~f9eH_R}-9GPIS6QJ)ENd*aoVu z@TclL`R9MZmES+f))N~PQs9&nNn{zc`Xn1E#TR;%?wX+YmmTaVL^}rC)3knP3pxfR z1-4@lbKo5FaBnLycBGE38Kh^iK>;GiMuxPdv0E7|4T-xsp7ao&M0zeVx7flW3xgAS zh*pNw4#u@80gfk;fkIl%H9qPXW2FO5+dtaas<;tFbt&3Ysyl#f*=@^6{HA9yF@)E5ZEuwRMvda)Tzn@e>QIf9x{Z znnoCfzLBCgEjxBiVU8KZO;CGWgc8`IK(8hg7ZRK?pWs-T{MvRAM^qG*H`meY87lY4 z;so8wQ8k~+9Tnm@W&GhOvNR$+ouZ7zJ?h~!16g0tFGUz3P!&afHbVw3G8K%{1pQiy1%eZGvgHv3K7Lgo8j7Oq zk+%li`)zM#Qw+$?bt$g)Vb)>hj@={|2h2UZpYE%hJ%hGc0$ zwK|D(OQZvf@(d;wSgSCOVe@PoHOyh?(fi{K!VlCaPgIG57~?6_UdidOUKY}d*R$yd?M1rG8Hbz(r zdI;9B2uGunlp@a&TC=fqop7cBb`7fyQoC4b@EQ({{bO`GYXonoki0rT#3^RrApI1z z=(GFLL#!2RIKv5E`r1iG-8Q36jITh>PLMYxvSPXNYcCQ``V36MjyLRO>Dnne=i3y< z;249o0V-5r5MIsw;qKYq&;7rW0J+gj7t-Mf?T`JVp{Jscpk zWa>v|2oC$W8?gRME9^MBfO|-hy9zaCnW~i-#tEae!-4tZxT3`HN`h$_q>~bi1xVxJ z$T~$!lh;!E*H>}SsKpY_9)Twd`xr4o zs2sURPFX`?z(5-5Das9MrVW*nLezSs((1 zY`9nq;q7JQv?SPD#z=$pT-s+^=#>J2%Y)zcD6*Rzlyf^hZnBt&F^#PU>#}03ZNKL_t*5=g|HG|h^>U-^K0l< zLFtwzc3+9mc-(yQ0;NK1#}CI#*qVoTK%oMOyCcATNA+hXp*ity7yh41fbD9`jQ6q3 zq_-1GgmQ4^VKhp3?delQ=MvneKyN0bTbAfz2Pd(}eIe;+2t9%6S>nwR#ZpSN9@Cj! zW9`BQ_1QB1q)%GPC>^Qc&MNAACaLW3(0$4D;VCwsTSqorvNJJh3j2O&4_ZQDGiE+? z5K*(F%Og}npxYYLHpnrMuEcaTIs)qm%o)4w;olI%?LM}zQR5D(CrOKh@_`1?7rPj# zY1}u1E^CTGgxm5+ha>Dz;_epMfkGYHE(@D5<$HhSCwS$tryy~#0vuym-hGX=CsxQh z385d-m>Q>Zv5hz3l6NwsGK6(OHYg~cs1cuy@w7l!3`LStfBiT%^EmUXCu!a?Pm;wT zT)eP?Gwu;B_o$V}@#bCP^&aCVcJs<7zl^t7CpZz1UK`=~Aw`n1>*w|oP6eb#om;kk za`kuL^x}KC`tM)++X0|_e+6^U9FckWG0!hBg@Y+HI)sUXO$L{GXq{|3XO7niELh`W zBg5c@O?JFX)6ZZ4l}pzVX(eQDX22|T9@khK$% zt2vGPs$?n$G%f^&* zBkI+Z(!L5?f4Pq7YE&Q?Jl;VWLsI0-e#b2F&4|)HO`N)qzL62`F42E!NVb*Zjrq8H z9CX(qC@K6C0nToZJ&zuxwYiSdmH5ZYG|i$ZVXXIVO+sK z``Mr8(v1tW&ur2?yULZXT!a=B{ha(_3d&NN_Q?leJcHTFF@-|ZEUM}d>?`32kLa;Y z+zvNl9*`WTao zjn(D5I)AqDJAW$x*o!Ii_S;qWC(r(2{lo;h$*~FOE(qVS__lqNk2lzH`yPhZJ9uyN zDPGnDJ8GnZ2sx&3gk*FjK{dhKYHgb}y3BGH|Q;ky(h*u)mOrhoyzY>w1FA%kY=u{8g z(&T8IwkC@O=CVOtv%K``lMK%fx0_)VKpLKY<{6?J1BzaTjJMss7J>34GRrAkMR>T5 z6$NA8KFR3$5ezlXv?Mnf6K~y#TX7j)=n^)|)Nh@l{lX2*gimlpP`bZ_no`{H=DWxv zO;Gc>`K1-aO-tcfI40QnhJB>*kkUJvyyaWp!eAz1e&?=zSIoKJ{EI< zIX1l7L$4W>19oIU2wGoRr~S2c+Fw~CUX3{P_FGBkG{Q|V1BqErsP3JhJma(Y{{3uz zdIRbO?xLh}R~fUGAy6bYdhB@T0{O6D=R-3j{S;?D#4AY__a5ZpU%WzX=Oo!ypZNR; zD_q83UuFH{Ya~~Pq^&l?r+egQQ-tuy%Q<1)W$vMQHvVK4k%PY&;M594ASf@^DBKj$ zu+-l?x!rqDbE>yih=(acD_lRMNK?W?bsGDc^j3PvuE1IFD7JFETYU6TBOHqr4z`zJ z5^!riMq5NbC)YV5HsCmDt(khy6vNXa8Xw+)PIH{HB0igto*7|!8YplUtJsl7g^J2d zgTc!iXlu!1OL4tV^=&nrodGj)p54wKUi<86Mk{@$_AIcmx=y7WQa;gO@CPkAUs%VK zB-vl~dHD-3P;b;poCsF@?&!ttJlIQxBmmfm)0s1^u4z{5%iNV(AWfQ0b@s|=$;y) zFeK0P(91c=jXp??2*viSfCf7#kg39*uaG-2);Guj?0MrZrha@U&ABn;$pUvnasR&i zX&$dI``{vJ5z`p25xEJ)(*t(CZ!dPzq1en|(xoz9r;`m>Us)#Z4gbY|QUZF9w|LTg z<)Ne@zb`u-VYS89CB@C{AC|5pU{fVsb%?^o-lP{mV7ElNN~yl<)c-yGcguJ>1dr~YsmP~8goYu5_u6xtR*arZ@M{jY{`a@j zjXLOyny~3J@xV;0f1`KO_RZfc0do%@eC){KS%@9L7&011B3%b#eu}26OBLS(CW9T&o^+>0CPPd?PnB|5Ix;Tr3&4ONX{tw zFW-c#3F+m8WVOrLm#!c(i;WFJL+_b3`2|gUy$_o?`NbS3RzyjL>nn;P!BoIr2P<-l z$l^cLM1Q4=2^7tT#z=4EIC}(k#gb(yjuePSh~IQ^#sjp@sl8#0=?CZOU2mh4oW|Y; zX)D2hQ;A|D$7;)-2M*D@wuMOy!gmmbLwdD~8wQYDbPXB@#!1{h=0b{|5saQoP%9EG zQmz*xMy z1AJM+mSe*Gbxcq|swpZiZpUIO`F7f-1XgB{DI8-M{^14;Ei#d8{qY9Hnnk{rAw7W{ z8Jzt-!{>UWM&MQzHixa>US~L)AqQZ4F6|dv2rE(2MU+xJB$q$?8iE{9*fxTSdjdt4BPDS4DK z?6z2Yas?qIW9}sH-}her_47Z+mW?pSn>f!|@<|AGd&Jk|_MM0fr&=KD2IGUQ`pC@z zjx3*W>SdLlS=av;2Cx@1Cf`2)J<+K3uvk~vVGgcD%$1PRVp0vMMF3Vxqyg(296Wko zYoi)2qNT{XDQahkkr|E`;!gP_C;L=CI!9>`GQ1RHvu%ThYC2eBA=5b368TDkD!@cJ zp6ipf3qW9o26KJH=z4;_k(0MG^5+$DL4#{4?WmA#L=@Umu9V378D^B@&WHHpAR7Do&0B-;(9F@f$C80{mC z-L{7X8lgZoJ?LwU6x0t+ply!c(u{xCEOyL6g$^>b;8^0Vq)9S^U+11OR7TVPcE7FZ@{i^=Qh53Pu`)zzH18sDNPs z!i7R?+n~n++`5mBMp!8^dp*3=B7~yo#5m(s*osNkz&~0+UyD$u3gYe%>nNbWcoty< zA~jHVFp=K=i#5l=YS3E+c2FRMhc+N{1HMHrl*q3S(H#R@iflDPCjuvvz4l&feZ%v< z-)N3kz)T^LVe#)TQJpVvTMn&MeJ#cc63||={=#v3$GVKq4^U~${K}9JX;k5n+ZDyu zoZ@PRp9R!RD9aLsLiH@|!!4qA!upvFzHLuw)hcMi~2qoxj7)Ku`Oxz_wOPcA>m0^GhQ zUREoEFYY{~sO#zgsubL4hU5;6MTsL?)ToUJSC$laW~~40X_}{7{LO#*_h~-d#Xi@7 zRfAHF_~{;@U9#}QEvjwL{`MG@BJQN9NYDkytXB1i7?jeO08)X@z}ni;1Mg8rP~8}f z#c$3b1yg9GERngO)-2Lg=&r>&i}V7vV+k9A?OW8Lgy?umv(v!a2sO;`xvI$_t&u!H z7SJPM@@FGuow-nzr)8xz7oP8z37-*FU^ z43&c$l}x{v;V|d}!JJKDNi%wLOx%o+-9Yl52JL6MG@t4at#*i~u<*XM)PLA*bpPJk z|33|M*8GxG0SSNoR|QRt>Zj0&AQqH?XsLl1kIEFft8lGAc`N5z|I`1&{AN!1tvMz{ zG>^6sFQ_2QZ{}=#_I^|XddZ;s4kr}ID$vk*f^VLA%ki3nSs$O>_3S@_UVq>-bPsyl%(*s&#hXTs#% z1g#7@2^@d+UMMT#D1}{*Z5#A{MMzVMZ{DWbDeB*|eTtXoOtvztZE<~c3$vRs8$*93 zhMpnY%xNwgn*9_V8KjY*$3mn6*}zeKZ_F?K&Oar6sKMD^ew=iz!__~y$dN~mQ@l0h z-k&&27CC0GZQJ#eYtM{-WBcD<0jocEMCJ3@|0w%VyFi7%Q&C-*6SupR!x_5YAPh^~sKCt}ssh0iJVYH!H4T*8b7t3P zBvVT@5>N_CN96;}LoKr13DHUi=_}5D@+q!<^BmG@V07(n5{rgqO?6P91hloxFO3O> zFqq!rYIGBu3%B09MU+R#?SkS~ML2+bTGH8AW%~UgX7ihw2qMd5-PCA8~90yi7aaB@Djiolow=H)%1%~i1Vl_K+s58piM-z zH>SE=B5i|Nj!`CX;^&Vsxma-ILr3wCMufRXSw(szL9fQRafT{|*69_B;gsab2COPt zD_!)72)8?jT%pE+GIJ;s5D{?op{ZA6XhcX<5=Mc|?_P(4fbs#!>w=OOU1w9HfOtdu z=_UGSdMrG#$aFEMnCA>`jJfBBPLN+5Gk#Z)Q;zQA3oM-JvcGpgW8LC>gk6Wm^F2(a zX`E^>`qlwjE3#X2C_T!?l+!sl&*d(a-IJ@@^>gB+aOgCb$Jb-=%B5 z9O0&hxNj)6BWbq?mB740v|t1zD3sLe$6Z8Fd;Jv&a@_#@$ba>jU%2w!w?D2kLx?;& zO`)tvj<@hfG-fa0wmoLWVttBgQ3o_8VBgmvDk6+in2nfnn$e`g))%i6W*%cI(#Kb5 z{hdB?afGYth&*h2jMD^D#{m)>BxqFSP_adMC`NO%70&*>53u!>OW-vj3Q*|Uztb3W z91*++rBMb*1vNEPp+E-%K~PnwE#Sr#fk5-YHlb~aPb6fwGW@8-?-lsmqjZXzIF!;* z22{5}d43F8OuQCRjY@PBkt{dyrK`=Sj0RV;5mIWx&YWPuHdCxsNY5fJ(0jZ`W248$ zW5*d>+r#CK$A0BGWD=OZazHgN;7E&GU%G%DdX|3SG%vjHL3B6a!G*KDbL$P%*fP76 z(R$d{=-phQl&8E|A%({sIQ*zWQO`I)q*hpqs$agSI=)l#ZiM=p0hr0;f1ci&9l=Sx zXsrmTN}_hcdq43kJJ)X`-a%#YILf^Y<%H%B^>G)bxRIxt7r07LLq{bLs4z`~Sxs5| zxkVNimQV`~vKvFxtr#a2#%qET@Bw2iDg~_)q!9=+k4`N#gtbSGvi;gkboAGdW`IZ` zRjAV_ALP{j~z)<5~p_ZZHJR~)`?z4b26f!E&aZIr}Chc_@-rOa> zHb-eqIhiwf?IzNV5IoJpO^RD%64m6yr|x6Kh}+{UeE-V#nZGlm*-x45%@CzfeS=(4 z=(Z+|LR~R+p}wX{!5&KR&ei|h-BU{k0W>-=azTDi0~~yB`+d65Xs^Hs(UM?{!3WPn z&pb}FTHldM2fQoLu_Z)`;>{@`vxxWTm4v9JsIrRAeJxJ@)B`;J4?f7kXI5|rIjywM z?cY65dv%G*Ofe0O51|%PL>d|vKd%VdqZ#H6No5og zDH`|oYn*FVfsM(pjL?y-?b(!|79-qBgij$}Rul-A{`edRU*DtjQ_@FTq>F7_q6jvi zGNIfr@gtA&fKpTwhiHXLg>*T^SV3XID3qy(uPCFS#tDi0y53OU(mdWmKeEK&%`GOE zN9b0QkU+LMqx2fxPEkuS5C6zhU}C_NsDwzRG&_BUZ*S3futno=o6gCYPzmkj7OLwA zKA_tgY#=Cu7>5^7UTD2<5#jDNsTCouTi0IevvnG{*gYg(VVp-Tg4jTqJ5)C!X&HX= zSN{iI__3?RC6F6;!Vea1%%GiFMi! zHAveLu~h`CeqZ9X~+#>2yB&< zqUf*oIQPoyq(?faB~9bO7U53a6EF8?xBwEMv93tOKrWaLRGo_3Z(L>e%`u{cs7h!r zrbM(^dSMkm@<>?`sscTSa8OWA0xALbcFExKfJg;&1kz5hiG_}$cc{U_`;Xx!Bfuic zVPi|2rUX~eerN?z9u_SmCDq;Hl5W>kO-U;BEFhzTrRCGK?p-8`VC}t2v>xc7k2g5= zUz{PizYo5^dySbub#q84U9AQR0j0n+BBXM4Ft`}kSCCVL%Axy~;6N`X5E+ht@+?W) zf`;k&Au_8d_XEKTeqR1Sf#fp>g`qf9vT^t%&E+=lzJ8HN41O}F+6+vu8>Q4yU0ldk8%G=H(=mD@s&|zr z{F0&jaEsy1DZ8)SMh!fE&y!!uAf~S8XPS7$LJIqTw$Cifc=~gnLiUBxAMI0oZ;siE zn0{@K>E<3As}JJxg7Jktl0&Wfm@*MWLfkSn^*ahorU$qz$F-WEzz@q>l@nm;p<}Gw zw_e8%p`vqk5$CHKAO6P0} z=RHwsIrGyG@$lh?spc745=2+%G^QFASXZlpRHcz3FdI$Lag6R+qE55 z^(N)5DTKiFKX`+D+aYP78kQ8-rbyorwPQ?T@U9{xn$U*O4?O-$&vB~tFrjd4UA%?2 zAkhLV&n)2jLi?UJo#z&?u1PhkMJTK&q^~jB60BfXEM5zG*&;1npN>xM9uJ_De!pp* zY=2~UW%M_G=2w1^{he**2Qy3uw71N!jZn9s|2J1qi<5&#J0b|4A zPo8A<&XE1Dy-T@MkQ|M0bs2^3&#yAdcA0JN;xo_w*AIv*O>8asG{>Ydz9m?rF>4W4 z#kkEJyaOYcIL7u3_TfIpIaJp$erp%;274$%6XW-DRG|n(029^yg)-DNgE&DQ2^{*! zDM$*=e&k`6m+$4F{`;9Wci6qShq8fu=qaZK4?p|_Z-3?c_`D=imU2{~ts*_$rP`Y# z-lGyr+-cTMBq8ATbL?td`y^`UKE6tMYXWgpr(oTbu%Dshl#pknN80#p@Og>y1`{c~ zcEk&o?gL#qk1w*5-)1x$v$(Xx&X=yke#Q949@VDc_Y2HUK|U?e!-{GYV5~4!QD(I{ zmx(o!g6Sws71e@~QX?v$6=;!LI@>Hx#Vy5`e*X{IzqW%4wb`TJ$x$fL=uN_LRrmlrN(v)(MC$L-ePtwgWOVG@tBq(dR(*moRAN4P#^x;JJ2>X=|P_=@EC5>ixk+GZ7s zI+YX`P)-ji;;(*{%#7KN_UV6oh0<62)!Sc& z69H8S<;LZT?MjOM)^+&CxcHu0MiP@pT% z;=^nF?ce%kLa#s{X=2wEBEssE>u8;F^gpD>dV2)VwI}_gfL+>X_>NF^Lp_-R4 zGqfJ>Gu)oC^k|Q0rN#cWAqx+7NKdwj8Zj=_0Bb;$zo^8JRG^#n(e;mh^;c+Ybn61k zfe-|6F{a(2n&(W;?V;uY(Fvn3T%#P+4@U?{5JZFk(A1D>k|$#9=|$qDn58p^snRK< zDyQ1B_{EZNMv5 zd-D#5KD|LU_au!r%biuKc}}#_frW^zuU+EG`HP5#q+=L=@qpy19;PiQ+khCNBMlUR zPzLlu3)*1sZxD>2B8^y&#?)7U$h|tu8XQlN9oa5_V6XEc`k^5+#Zqt2knP@S>bQRKo>1@dE z>zmAHQxHd#q*U1$N=uw32zL!IhJos!MCJigc~tIcKe&L640ff(^x7Vwj8J*_!ONE- z&|B!U(LGYz*z^))Ac)Ud{pll=%K;hYxIxBfzDv}O+5gHdc-Qgz|NaWa%X8$CWAfS* zDj{xD!zgKiREDrsAVy=ALb;4ERrs$~-~~PNXf#3q@ra_SRZo_66XN9vy=1AH3fs`w zZUm)5d_wlk0e)DaOJH8Raw{K*ri$*FKHVo1^l-+kERlxblqFei)E&fq&G6+RJF?HM z-@Z)IE9gAi#`i7pN(U)CDsq^a;BL>6{fZZV{R`xq87c+S5HOnH0w_bW*g!P`&4*WN zV+KGMbr)0WwEzSk7c@WFM;})lymXUnA!95DjLJE?=XTh=be%8$>winnwHduTq_{H1 z-*R*x=rf;`_@u;b7f7jSRhm#i$c3XndLMLEebN*jae^{JsRAN3dmwX7ertl82UJrb z4Goc?JlH5^`@7fq+<)>fF|&xuJCapRe5T34%li~>jmTX|Sp-xnjH`@hyNP5TnN&2| zDe8e9z4vyA+D-C{V^mYH%P}f-*o6cY>Qh*m7#z5UCyF#KR_IQxp3-&8H`bDvg|MVC z{RlM&i2@RVk3ft==^USG(r!wyfzXELJzboI{!bmEY-qxnh(TFklR*0MK6VXUsOW!a zjoF{gP@9O1P=SD;dh3NtkAH*SP27 z4>N7&*xVzt29vjjR9giLM;6$-IK;-#`pIRcdpV|K5bxM}^%go)=!PX~7^novEtEnK zMQeSDs>n!>_s~7qc;*CRG%D!&5vl?`i7*2}jKM4#&V2H{$gG69(7bPf?qZiHHW(VT zR7~L8UW+;;|)Z;YtM0k!JzH*1T!a^GQE3FT%E$`Ewjd=l@G0aPWZZs5QF^1r6_ zKp$l_C?fsP-+i*Bvj}$(@R6c_F@W^8yVD##M(|`9dS`AQ6;jdSivZi=dE>w3W zxP}lv)La{^dNRP?P)BhZ2jSF#;@;E?h9ROP=_pj-ys7JUc=oc zn4`XWv=4}_E#P)uyo50comK-$4XqQ)#7aRD&|)x&AWe@$h>ttkpI+i)KmOws7OI;< zd25J75!D^f{`fZCrA2}`oUMBflVQN`I(n!2C={iyPz^)xM^@?}@UA_t4vOkdQLkfJ zLR`~JfBEKLKu-uZfME57-+DXEua0rs8BB%AOJmCKSD+!gSwJ_?e$NU^KX#bopL!TM zGTG5r?f?;db#=WSL$a*~}luQJ-3 zk(@}WbPhdBG%8Wjtc@TtPjzmBU|AN~yTqGc{w9)XWlw}_e%6017svkKF;*mjIm0#V?D@B0YxdWw_+BHHwx?EN#P zvO<6XrEqY$t-Z>y(hWxrOQ@p~0|Q>a3P8&s$`!Za-AxJC_u6`^x-5qEP)QFt0B8u+4p>si z00@e&e)%uR&rj;g>C7vWKHvXuC)WomX{7idsD_ZN#}rpG7%0qxhP`V8#+UB!)xUU= z&Ps&_pgX6tJgsbWKPGD5CZh^zSo@pXCc*mxIak)Q@ebs-D*8`!YHF#B zY5BmysTCHEZm@G<3%o{KOSn^^Lfv*1AJCD;J7J#980_yceDql~x_;pc(k zN`~?ZX(>!*QQqU`iqvSDOI`AAKqqz3Rl9Q~{Q1B59d5pL4fCKPd8$crs)crrKpZUuNd?qNDQ%yaIekqcRO%2Bcuf?QrJRcHf!0o zp9xW>kORm@j>geG_<(Z-y-zP;VuMK(_VH$I16u}UDA+h9K4u9fV6A15Wr}N_>8PZ* zSrT%Mi4EoL5+jo=H%Z!}QX6a%ij=zq-r*cWz=A z70hb{y#G)W>kVqnGJk1Gc78^DvVj2EEl;%XK?PJC5!xl)hZi9hcK>RVpgqbsR9CV3 zfel!!YY|dcy3tLA_8zxi5w-)WTZ?#-l}4Q%tS3n9(1U>Y1+Mfge)KT*kP1d^sQ;X@ zYD%QBu_Ek)UNxu;)OtjEq`^HOe1IEYe1~dRc+Y2_W}|tOH^1--D)SI((nNMDyejcy zjovNEHgn<=4OAjTm7%xNXLEQ5Rlv^IZ;-!TU~G(b0k`95o@%3$g!WIY;q(lD(IXOQ zoaqvB4-OEAG&Mf+NFh|tp%7F7I$9$iHgTQlSUn0vf;RF^Z7^%U7qoO<#!c`@bi zbL%WFta57UKCbUwt@Q?FMY`6+xq^752|dB08EqEqjyAdQ?gfgi5gqfwY%M0KY`FBn+y*k(d{y2aAz4YYE&dCvYf zN4RN)$_253>8$~s^)8)9`+WExe2RCz_6FgO!#py#p=$?q#)hT2`d@3R2&{}M<^5iiNc6WI2zQ-8n6Gkr`kXTDGuRuJjA3et8 z`~dfzUHq=XWe!~`%B_-kIYGw)Ct~bS(7lLgBO;0#l+zq-HO2}=5fTSlrx&O;Gn6&N zM-!@nudQ9hviJHe^1XujBTolU z8+x?U1SN6Wn}+ohq^mV}gU$@eZv=mne3|%(pUz!###y-z8mZ(R_A^XaNW@ zWn93Xruj@8uRY5@y2SK*6SUE=Y>;_i?Z?&_&WE{Hnc8*Z3|UMl-Uu9j_8xQuAu{M$ z#e5VHt5GeD?#9SGV7)=Ssz+w12FlfV>G<-1D2I5O;-`)zYT`F@s$R+7=55}0`Z-qZ z8YhmQAbvE(cLaOVpbsZ({n0h5y_^z*Pc*jD1Qk#*_(_hNdDzSGGef-4;HhVSg!y<( z+>UCsZLf*WYZ)0yhH1ypsy+PDqX|CIsQV0Nqs{Vr4zqlJpM0(%RD=L34m7$c(ftlP ziO8?zbRKJ=8U_;!&EGe`rCQK=aWy)ig)40LIp3bOoXV(p7|9 zQS8q#iAGm}y>IOyg`=1W>CuFEU6U7}OGtddVr2 z+ER@&x-MnS9w8Jt6oOebOuscib_2un87ft18=;IxbwRdc+N6c{#@#WorE_mgbPWF1 zKmX6r5@RZbIw3SaxWqj_af)gfP}>DKp^-LFML-vTuW$;iS4{tE$gMxVj&uv6Q*93q14OB7lZQ5qrE(SQC3 zVWUMHm2^7|jy!sVq}ia%9Z9=^pXYdSNLi7+rIAU2pE%rRNqId--xQ*OqS%{~yAje& z=q@Po%CY|ZGW{bRnkOyyJhRT>A8Qi#W6GdmIda6N*-!IWK!B1-% z*_1-|${6JgO4rn~%RHe{sH~ojW_*C-5m-g=710W)Zi+EN<7kJF2;C<-G?J94rI~(v z7qJmiIvOVz8P8|Tw?>@#htJS^yiNQ-O6W)UQgi1E*Ex3dB>HedbSz@E(ZJ_|M^U+g z$bng_Xgt+NAG37MG@13Q+$5)yXWXWHx@=SLo_i{8e)BTQTijlSDgu5Evzr4Djj~Ww zj@hLl-div=+sA@JrIz*w7ntXB8Xbeoz;q1Vw1e4*2~)V?-sOA4mrzs1?DCYP)uftc zs8S(TBZG?Sb_PQTE)WV&C^T+6Lua1S!}P|CSHJNxEALyPGzDS{3t!{loe7iejApM%F)9f@ zP~CPl6||aC9Tat1r)#HgtU@Y5Dk1%37d_IrS%pq4#%Kr*iJ`I9qB;<4q6qsQrG@TC z4%69alkH@LBXHvV58{oZv$RTHRBZhpm+Ae`3fblqH!cXJ$Hp3I343qvGkt50>}5z* zuhXTM&e2$HQQgj%ECgE8KrMKhPqoP|IrO8JfA;JD3*SEXRetGX|AfDK<@2u_yg7~Y zA4`*jd@rMQWQpBx+(tza-jp;?cjzDQbMW>6JqYw3U1IzDS72&Up@Qh{b}`!$_&a@cqA~3zlidM+IOX(1k8|mriL5Tc_)(5AG3A?c%DG236lT@p#}0G6MNwuP ze(ogGOZznMZ7{w#X1Fs)#RW^N3EzL^Jflg@bT>m8MPnnz#t{eK8*=!g8x(0pd9gr? z!ZxGYEj=|btGUBkxH3nyg-+c73=<87)FRldo+}_jjWh&FDi%(zGIeA6PcP8vE_3gp zGpt32*)iK3{F~e4TRFxF%4$^0;8f0ge)3tusN&$2+u#%?hma_u4NG*QNpW?GDI(l7 z$K@b<6)vwCfKe}J_u3Y(UH&HXtM2nU9QIdCqNr93q{7|V%!d1n&|K>H4tA_EnGu*aferLq^;x>D)?ef$8zr}M8 ze3(2HOt9qhvNlZiRULaAIOdn#Ys?>7p>+e!-jtDKyeUCf@>x$kJJ@lEF z+_-v|s~pp5;l@Jt+602aJ1A!b2-sjy5>QzDUO+c36doo4<^fbdTdD8;j-ma-i)iK1 zr6O7~Y+l)B>6t^|ET`A+XEfbss0L)aW7ItfA?d2?~lI1wcmdW zF9z)c-Y9Uw$zOVm`S-{5)bqZhv5=75ACbJ!!YoBph3C05&yfVh>`F%U((Jp&%%h$x z>0i`AkS&e01LM~=@eY~~^eAtS@Yf1hiec`+YSelIQ+dKX)cC6eXj(`#N<54MdI6$# zM0IV9-BXBIHh%OT7CMXE&TlhF_Hb7Um@B+dBuzy%1KY7A%L&y^j+BBib#X-%9yS&U z2N_%gnbkk-t)N?)l}Fc@+!_+*f#`G_+00NqL;t6n=%W#lGq^#43xXRLm`YLZmPn;3 zH%tEHbDw8+yQ-bLZ&z?5pc)aOGK8HeoJt9!5?2LmLu0xLVrsExm{$KLa=l_z~yJK84M^%Cp z73srmSX3nMiDEt8lQ%jqKS%?!64n7uKs5iU`r%JRsuCn2_oD)9}AHU`l` zK*+ZXLg^s2sMu3dp=Wja?n3Q%+{hZ}C5s;f-0l3TcC*Rt#Sd>#-70B)bO|d8J@QCy zkwL-mrF~Rlp%2DtO7GF5oKW6XoP@f@?57nn4z;h4Q7F;i6(I;A7h0q|@z@VjjU3%$ zU7}Qz9Io$sv)W?xwIOQg37eXOZ;q(4n#b1o;sc){8njt@W(mD)p&4PYXc1H+t$Pq@?f*~H)b1(DphVD0{U9AA(ln1Y zP;o$@chI7^jGK|>(e;Vc=reL@bc2^H9soJ zU!9SipHdbTHaOz5DP|)^Eem?b5C#=;b;k5hZ{fdxa94Iz`}e2{n3VWD)Kf!|T9BX= zk{wB1E7@4nKCFJa%FjpPwNk zK@8*v73DZ1Ozs*&Xpgd?1*Un1HO1~~`2d%XP0tN3{> z?og>lx8k~0E((Ie4J(?X29p;jl+zqPDj0rsk1AxCTbkeeO?diYmi^^sGW+9kDn&jC0c7_>hU)wFcA=iwjjD@;#f~mixwT# zVo#-N{=CGqeBTP&^8=c_1hd+pe|!NoDey&!T8nWz0e#7#qln#a-N26=D2;wd)3~<@ zN+S!3#``*$F0i0s!4OG3ph|{yDb~+C>RN&Jiut7(zxI#+8GrH5e~;}yyp6e;plreN z2bO63aGT_)CWs(jqbo2A8g(jxrz6tGVpgA7A`gy6tk~V%p&<#AEU($R+`(Ltk4hv_ zL=yArZ+(-&&Y%WVMe0#+#uByT+G7ZfUDUKb)Ihp1$$NYBeqx>B+uKyRV&}pRq7B2r zEvDZY;cib+Dn`6O8bCw z2G#8eey%a|=+z&50K5O{^8alHp3#@~h#E0bGiAOLkiiUD2&88gAqfO^H|PsB#VSgn z6e#K$u}bNBm~x#J{d+62$gumZP2RuqAvQj7n)^TY5ZC|o9A>3M@A-9-PK22Td=khm z&j%2JHOvK=Wg%bdpFQ%fB*=9 zAVE^3NRgDdXrbg-nNhPs@R^f9oHxx zYp1nQD%LL2IeQ@R=?_5zyUUW%~Fcoqf|hKKm|2Glo>qdc2943s}7OAREst z^Wy*hJVFIHVQ|(!VR41$OFA6xtmVuve~a+dF}1k{>VRNtWki^l*f8hN8}A{SlW05q z@@@c_nQ&7y)V7*WV8am12ZS9-G?X+RTjH*}?}UK{r02uqCw0O&ObN~!#0c#546`=I z>58|Fz)G-A;)KTPl18&eH`_+EVv3VJHa@XJo~M)zL3A{saesr(u^J|lIN_LkcmcJf zKuXH4p*UAokZ-U(utqc9DiFgQ)(pkfA?fEv=)7R-pKT+q3;JL0p;rvfN+#}Hpd4q6 zwV`p}B%&!$3lX(DLd0}H{Vi=sE$w?}s4XT$_chSBY8L*(Bk0DME5C9EyPV_34pQ$( zMyNvjKxrvnln@JoM!;yjAUY8f>}wIvCItIqW***;YBd;aZ(v2i z3zwck)Cy#*$S!3NhFtveIf67mWCEFaT>_!Piu@w|ejpIeQ}?r#lw&`04<@wOWry37 z;Ifk8<|---a4YUhy8;ol#SZy+_`cTNjkyPY@ojX|4YD(1%3(qMt~wWf?-b=m;WYv) zW{E&nDw!4II0U(;h7uB+X)wa)2SOksftphM$oqeaOX?iAPrRD57oH~%Q#ywyDVB3s z2YQ11+Q^G13=9}YJ~9-`IrYUl$?*i41n9JYLSQ-}p|ym!Odw)|2`tS+4bqJ5iRLYnt?&oB|+U`Mh+PQ&M^7-UY7pe5>Xkl|IWiqEY6Vi z3-(MevT=04QDPjyZv#6PX(i%h#W>tIWyAxhb1y|AEy~(!p5^e}lQd^i{ zd|?Z7mt)WCm*_vafzAy-`=9+I`s2YjuYT^_Z|?>`c_TyJ9v=uAlMfG8HyHQExErZA zA~;s3|6I?jX%u*9k*n^U07R&~cb)>~B2T6q`SGm)obnXvs8i#g{M?86=*K_Gwc;Wh zCzqK%x=3DblAkxQtYJbTXJSlfa08!11kC`O8f*#a^8@UP$H2M3A=46*I@rvR^ARp} z6w>k^|Mo9)@%(xECpR&Pz>N(^ftrdvNHSK;ylpS?a1A#yn3hD$Ko|v-y&PLO%!)x; zfgL&e+qq}_v|@Bt5>MAKRwI)DGxCH{TNvVYg0b57M-<432!x~a)=5myQ0k1{nUvvL zm+oet8|yC-H=A^(W*CSqx?j6Nc`+yZRv(dq5sq*m(W@G_TH;FloIxsIfrLS2Xz24X zAw8{6*qCq+^v?A$R|<50j9K;)wojdWlB?^>ziiH?&wYnT)Wl-@BgN)`|KRWZM@;US zA#5uM001BWNkl2GyYA7pZfvmI1!hp9gd)F|G5GX0 zwyQC#W9pq2!hlnWNIJ~pcM7R266)Q^^#&MMLI=sV~k5fRPG6? zl};L_@SFo_LDr-5GXZoZa@wMnVv<7*Y;K8{nmFq# zg!=x7pe@mq1d(L0UQj&Wr@U5x!)yAO9YF_CIf7N%ik12Y(jfxhdk{(>gg`n;|LILg z1?7bi!98{KW`RQCS}Ln8HUEpj)7y6U1lazgb$<%nI{(pZl%cPWsU2yeT|v-@k@E`K zl+}H25!l_bLr8~>Aee6wOe8qrvwnigSV?;DgtQW&ONvs`|H>vWe)Ai=boQG#G&B=L zC@`TUn30_L*+&Q$Bq)J8&_V@@Y-NCO5~F?G$x|cL(ChP%VhUHM^-jNkW%EPrNB59^ql;cGF;fy@zzz+r^wEbCH)~G;0)Z_{ zpZpbmNVL;rkq(>-5k})`5qJI4UDRINpf)!};S6rek$q!?xmG}FX&$X3nu^|2ee%tm z`iYn_v?PgFD4`Q#HwxrTjB5u-1L3T~&1qa1_=p~ssEHVdKnDgghZ`8=Kr-4GczSl3 z(Nv1@#WBV@%;g-03cHnI3Qtn?11akPpMmpZ4=0e$VZ06w!UhmqTxn4!TwZ2!ejjR2#KOW}F1>gey_%6eGs30@cAm2n z2%-F|>MEQhqDltwnt_EwlvTG8gDe89@^Zk*+dBlWZzCxvmn|kMA0tQqYkmRdi6K{h z|J=7JQ>9@}91^P3cxMmRFmk;J2F8zJen5!z(P$dF&Rs7*)q(um@s!(K^IN1NC=hblFq zVTq5``Nd!RZjbJS`9ML(fm_{CV*BP9`ee~m@aN0Lw|{u@#q6^FNqc&Tbs!p%@Ae5< zeF$;WRa6LE;*cW1N$WSgdQk2aA72Fu6FOWe*^X9O)5~m}-$G<1``>(&;f-zdnK5GM z;DH9Nfa2v5dQi}Md>*&xa91oQ&k(f+c6EqLz5cDuysF02AA5%JdLKJ1D0>Cvl?>6< zjIZScxp(afdxrgQKEc+7>jY5^OW;Kzw@XCiTdE@Pf=j|0#A1xSUQ&Ns3v*iIf(nx_ zoTsZBOId<&7CW*CXMJ*5+Ul7&n#XE%M`Id$W3tthxwkHH;=%j5k=!7Ziefoqv^Au@ zRHJ`pM0s|IwXSNU$=!|KIcX&hnh_$Fh|*uNs7m$2Qpvyop`eU3ulujx!lhFeu*)Uo zhW=m4-B(R&|ILme6CYY&`*&79oj$caz$#x@Ho!F%0*7>!KAyOF3Q& zfJ9&kwWL{V@Yo&il43lNd- ztyobF-a-Yjm(;a^Z^ODoAV!X-fA>lB$(-_BiTg+9fBv^RhGgFw0vH*ohpgk6G!{`; zPzu~Y`yK#3(-hz<0%yTUMBGpKAh2DHla6w0M~TxCHWJEh zgQ`nfx3?jXURt(hdH1h>kjq!k<0cFo35j(~v7w13K(-W(x6e>KKZH&|bf|_(1pnRd z|0ZAjm!HD)G$Im2vrTkf5Hv#cpdem|y)$VEQ9H(z1&u=;#w!^cm#>qEI!Pm8<-%2Z z;gB@%QaUK}0$E-6+aJG%y{x@P5Wckb@0Fku^ualwOxwaDM%6cJuTVl+J$EBP`2HX+ zB<&5c-4eSkKkPnfp7^euLuDv_(kG#nV|Gz#ioW7sdzl$iwIY2#8dEiYUO5O5UZv7m zfzX2Ti#eyi_zYh?^Ji$4a^_p##FajIv|WR}TrzunFR~qzryg?l?*IG)*tWse161Ar zTp3mpp;CB-4(l+vA?ORz%@jL=a;v2OjV{H7A!g(V3OM)arx~9clMHHF-P-L?w|Xs|2M|rmTc1RpOHxhebF+=lCSIzWF%T3SNKzJ2^i%&%~WA`d{m!da!w6 zmBBOXPzpruIQb`ELl%-ygPm4{6ES9>Q5|mrF>zvo(Z-l~I>B@-^0qo6_m1DK{cV^I zd`GhGxb=;9a{bgY&DSK1pBwv8Yw3FxqAh4>ML+Y`fT~Gk!;xQ3E54-9{=>-tGd3g> zq5u7w5;Hagdm`*qglj134>k#dI=!?@xUWT?4!HL0RYXuyY>x?ACAGyCTNgH&J2KDO z$8O-p!n1>%AFO@ndnEo1UyJH3_Hz@d6hb&$Ln7+FLc%)otLaxpAK&_4zB}K*%=*!8 zDJ}@d-$xpRR@lq~G@P$C5zgKGfX0H79t&^mcLac}`o_-rAW%4hBN4UxCg{Gj#WSCM zg8b?Tx2I0=l`*RA7))ePkuXd>Kyg_XRPxT;<30jiS2=tai zL=Mr2a6^Mq0XB13rHPv{c`qlnA>rW`#ibEZ0WK7bH^zv*qbv;dy)~F}l-&WU(V;Wl zfk@Gun`U$^WAK@^s?>8h{ffK4x<-W%yBq(?U|_d0bOLGHDno&NsYIj->oj7RB8?;nL(FuEOB`lh5Ff9j;sDhSsqe3O@Mshv zoWK+c7ge924Cp?;jq6&{4bACqJkR(-$@uGQ3|9Kc=>$niv?rl7n!#r`J-#8;IHSlj z14<$a8CmSdtavz zf}(F2e&G_!U%$jwcMGEhHXXvOq$k%{UEN^v@HAx(wOWKrb8h_FC4`6&LL)-upPj9g zy`86*{eRzjs2E9j**78avb)Y#3H8VJA?8Blwb3I&ih)@+-&+7wrAJC4Zxd47=}PDQ ze5`Nr?4C~6B86{GNC(#XKZWqv0VxF1R0oZaUJE!5=sY+@bht%&ZbD(QIG5 zGm3pbb_?yj(@de@Xn?#eqBu3AT-Mk!Ln%w+?s???1QmiCSg0vvO;WqRLF4Wzk4Kzx zC=q+ru(9(v!cur+i^4#nAP6XRPHnb9ZN7%+q%>w5OgE=dO+|e+L?jB41kf-r8K82Y zgt%9rMiOH+@qq-{7RaTDyf;SOR!1I&JAU;MIuB0+hW7Cq&6#QL|2vO@5o|VljGi0v z;+b#KYE1H~gAa1~<@27@Asq(e&vIAcE0iBFIpJyGPTJi>+leX<6UqZ_l=CNtlvP#a zaH~1)*y8_Gd-v?Wb$`{rD*$K<)O0u`6mJ#I4_K%YJfuS=s+zJ%-_n{< zU;~`hsHKQ|KJ)-*K6RRf)*cS-zngD;?ek16PLN*BNgkRaYFLUbP<74hY`s7 zLuL@t%gmciO}U+d_9Iz8i}3yc&Qwq}=WtRWML@Kmn0{c1?wJiXpI*Vqgx=?uIroKg zFtiMx?c;Lc`F|I4%qF;QjsO7(0gywlf? z(UjQ{BCf=g1gKErWC%ftBtQg?&G`{~jx93E`z+sh5i{9fwmFNPgV59{Z{*~w8TbCe z>oJWX^Y`y%`?(b;6nbn>MMUtLgm|Gzxjkm;wR5DS(s#i!>4VT#$Q6ZG&x`|fKf`R5 zh|FLMgBm+r=v&;igLR7SF|PDFK}5dOhN-GvtXKT15I8|oqI(9bEFw3^c8qLF%C!QK zNC+f_7^7wd^YpU(}EdS~$vgcF(%&MQE@Nj#q^X_e8r@2=32ed!> zcO6R{Ua8E0XjLabVP0^l`-Nc2+X4RN0ss)RO1QrMxOCzdusc(TYSM={e2}3}2GN%4 z^Hp6#AQlK~QG}p0)D_bQTSQ67%fEM;SAX!0+%fSgp8TCp!i44EUAL3J)MxNgmn_fN zI-$@bS4W7kKsaB{dgn8VlOPhw#ACB$ms6yZm^8;V1kFQJ%Jf3)&89ISFMaaZUgOZ$CCcLHvhZI0RY^(ab&ExB>WJ8sN6EW9IKy4D-3Se zp2XpHJ5xA^D?NwMSc}skq9fS%BgaS&NddOVj@UnNEsX8XI%-p>X zQF_HMGc1v#>N`T^r=J#ITMLIPVDQws7h<*oTRL19M$h+A)-w6VCFGRF2#~dqXf~#A z2K01B>p((27c=v~1Pe>Icp3aHFD^+n-k0`fnZ46(^+~kzL|Nl=EI|xx_rq21cukfHns;^b#?=~et1|@FY zu>R}~E`QpuACg?voY?nf z4wMI2{jJMh_Qr~xjtSOTtTnrO$NmuWva~yfPp+z_c0$E*vtc9b2W0*O%SW|iQo`?ms52yHEd}f?QGapsfAND%F*VYak)FSF2H=@X6>=F$i`u#{3EP&nr> zsvtSg#5Dz?86u|>%z8$i79^b*=`2xLGJW?PwqeO*i<*t;{OBzn*WVC`p(kNZ9Bg45 z7H0)vQ{Y+>Hx&_Qg5KF{-fC7G@?cE3zfOsubt0jC-y+xk>Rp52!B%j8K4+?G$ zWW-DWvA~WD!n&Ob);E+w;T^052<33o3UxH3`QRj@?ttcjNm@YzF&UC9b$I`t_i@c! zW&6T4Oy3e7O-NcXrZ5EaHO$zeS959)%wX43oB=CygqGN`mv)!h`bJ6BR+UD09pBty zhQ{j*3Q4*?_H?#PGQ2jzjRm5##7Rtgy^HM{Kity3spXubTrLr%Lo0!l7CknEnW1)g zmhP~JUCW3csna-ICtRvCciRE-e8luai(EMQ0{PQDY!c$OGI#~ZhKf<-ccy}Z30F-4 z#m!);UVW!`j|3b%M8UF6?f0~$lE1)m#(fjShYQnhGQL#uz+I2hX-{DDlBe?*h!04@y$MDOWG)%JFvb;<;(UoKEFy$* zIHq-C0z*l?(WDGC@t!)iX>m1g87D=6QxX??Ag8sCs2;+kK<cP4VPXxg&UUydm{oZF|`0Q%2~gBo{4>X z8C+UHc47M6iv-5(bkc9$`p)fIySd8gYi9=FUeOG4RjzgONxPpzyJv;`-nUczMFK!b zg|p^qiMq{UPdF=JYlx5vJ1B2r*{WYpnu;AHK{|`9!Tfs;aN+Z(nR&w^y{k8P_Jwb9 z;R|OdiV>}SQyfYUaARQ^V>F{@`$WwcV>P1g#hJyxBBFp|WsKZZK@ zN8ipJ?|e1qpMC*5kVKQ-kt+-&Qb=rU2o5ILbpy44Fj5#*xroRRF(-YkTv!hxU8-RR z8aroEYhWXR4lJ%H(JNI$NmtVjXAmj?tx<7^QVNZ!W*d%hI>Jl|Txw~)euCiE8r$Dk zqu9zw4krXbgSpq8?B%>2+E((9UdE+9WS@a3M6zL#IY>}60*U6muKhn@py zoCHh zl*I{wOal5(uOL)Rabdvt6C>)cN@(5RVP>ug6N>g!6I#? zp;iQLPNBOQloHc}pmdlii46@u{^1XD>WQam%-4C>ue^^lr=CS@8C($}Vz3j2sdpSe zUmMfBcghn^u8j~gY}D`Tu0lmFj6hRb8zw)Lam-LkowU|LW}|HcdFJqhg2RZtDqoOs$5 z_K{Jv{5>Uo-`@bx)kz@vEn)d7q?q)4KhdC?|ae$f!T^BT_yl-!|W~d!Y z(2I`YCkI5X&i1)&qQ#ilHF#|Q!_*Eoxp3|h@k~X78VJ;_5#@T}wS_XzNK(cpoSWA+ z7+vmSwdTt6SICx!NDa=&N>IU}&x|MrC9ZEUs~R`3n2|%&@%}nPi!j2gfm!ESIa46= z0EZ^%!~|_csRUx2A%XyI5lp{!f!6JFEZn`9?TrEE#WCc9^kN@-B|y(f8pqq@XNSAb zaQFD<6{xD69M4pUYf*Jv1NjV@c^#oJl3la{3+qwwEGS)1E|2e zaXKk*f#%ikeGE0Bn3$Po`tUs4XRnjLl#|?EBRmw~>K4_MWS0xBJ#&UD)7QBAwet+W zu}(H}M3Xh_4TBzxAqCWG6_qWH;=GEu z&xJujX8zJ!_k9TfA(TJH4!PfAD1H*lv_fH>H*)Y6GXg0UqA6L^tJt=rySmNxsTR zzbf5?rn!HD>_$o;Biwe0(H5D7h|*&65-}D8iveoep-YL&4QevLt{AMg$XF3{LR{To z3WwAoQftgWU`~%HyO!~aBMdY;&X{}f7H+(JmDz){)F#?Ia`i957rSDd`|kMZDiuu_oGMI zxNwc;eM>Apet`McFHtsg(v1#5)sT=G z7+0g!x`jew(*iffAN?aQQzrs}h%BNVA?hHOOI)IGdqQL`5eFo3QKECl!W#~?XA&cmF0`bAoeDK38Od7Kc0?K)yX;aU;;Vh=lHcX|p{o89?4-^1v{4lwxR`}W`W z$gsC!c9ZNMoB(*m3fG`?zbB+P2JX%sl8aNGXJDtjV!LGabrvPXa{?7 zo8n4Nc%se3(TLGkGIUL{dF3k2=^6BMeYT!jW&PvVu>w@7$hJ$I1ra#JwD4HM!duFU zvBfS38q;;kyzo=MMvUGY;u@aTEu|xPw2c^O)ItrjT@c+8V^=k*DRB|V`51Mufw?p! zd3A^4Qi>boOuT6p*@`GPHO(V!#^(m~&i5G)QlwPKp(IUHw!gTHGa*rK$z>> z+DSw~R1R5J)L+*mJQiZxl4I|^mtk{Av?s<*DQszJ>}wOhCS>%rQ7YOL?kr~86paZmm)@|`;;R~d3lID zkzlqAR>G@Z`w-_p`5eJijUpY>xMzy^aEt76A5qBN-FNI#fczL3fl%`2U1|T}cXZc& zpaY;PV?-cu+C3uz`E}Xk9l>ln$*=Tq4a?*2`!UXa<{OwziOovvTH*DAHxu+qpVAiS zt&-wghF$YI`D#8Qiekd05G7;APi-*!mSfBwoTRK-lA|#$cTf`qOMw@eib0NyH)xEU zXYk|})$pR0eqOF{u0+g+h|-WuG_cNiCUjwutpFD()D+Z@HOSU9(d{+LzG33_N$PV= z8V^n3#)9#)15{?wy2N!$7WXZ(b^01Ql=NP_?nlsLhwhfx6@UM`e23Xv;U{I)1VU^` z#5-MS{*W-JP`=;)CHX5_PCG;_ak>4p9-4*n`R$Wp7*m^{p)AMf`6jh{n#ghLn;3CI z?cR{&RnyFW4}FFvsE z@y`Bf*4#S6YSImj7(=`mVdH?}tJ{Rik-jjXxR@gYg;|5P39dbJp8PP>It{XJiqi@PC8{H^eL?5JX$CjC)Q+?{^qwR1 z&-IY|4TEp4p|cS30(l5r4+1GM9YZvec+0wmL_}WnDYijfI&m~!djdBj*^E@ZfFjvITvU)mcY`WCZlJ;u^s{nwq~xYO zwg7OOhM)Y@Kj8m*)Bo7}#`~Z9$mF%?e0*Z+-RZ?1bOrU}G4(ghlRh^fcvFX@6H=^< z5mKRMW8_?j>tokT5PS&72QaQnSV_VB z|1AIjPW|rJ0O;t*c7JE%+vQaH8(ekAoZvRoHI(O4%tlGl3CONxko$Yn2%oLHDFN3F z>9b?Rs$(Nv=HLfkL$*2M)}u!l6e-1GLD;BaohF=(P?Ew@<0n`$0YL|^m<0o*UZ290K=CyaebJ7&mEK( zHqluQx#h$=A7=I3CCsTY!Bm}cJE!axNFf`@;9)0 zAKj+?GYjN@Fx)6!NZ*yrgnMKuUY2(!)DP5o=$GDxnvmEu#iar_c0{ug;jsvLq(*o= zzzj6mw+-L?>HmkR+B{pjM|*Ax))jJ7;37@AVGzcU-xy)?lF7r<*meNQP(RwF{+bCy zOXFGr;X#3#50K3O>kRRub+3GqhsZk>?s`NrbbRPHK1}vv52qkV1nW;;#)ct*k_^{Z z2$})J4Qf%7Z~f6HkflXL5yizVR8b;Qfp*!M#lL^Yq7(LAWNZuN8P0zbOYt|f@~4LY z?Ei^d*!axa(Dco(H{ZAS$?RL(b1R>|c%;lMLF8S))MP->D^QuDyq4mYEv7K22}w8` zl1$cUE!0>)d!6z55u%o2ZwRaus4Y#_GdMK)R*vi|Y_1WpAb)9$%Rr1BW<^0uz)A^5 z5jI1Ls|6yG=u0U=S@NOb`q$30adw4(2)&gv{pdl)%WIfzs4X-oOHEWbibl!w8;?L~ zNiTE}GYyPTpHyS{^XaqQf3W?j<-sm)&+#|56o133_d@q3;_TV(MSZ#aJv}tPUx z=Vl5iLN%(`Mk9e3ILbjuSO9Hr%3voYdtZNqm;dkuns>Fa-H`U(b)tO<+1GlArXsnw zgWDSujx@pHklFX&fxWtkna8Vrj0@_Iv=9q5N>Ucza|@#vy3`+@Laz*|KeCsJ`4$^5 zu9CEBWW6C+u){IQv1#mJ%>GAC5KlE&d-fXb*#*{4USM)+ay&fOe?}~)Z_u0hM@(A0 z%n#6#KS&?Ou3O)?AD8F!wtYz5Q9tY^-|o5eFK`!eUPs=;sUa)L}=G;9Gsh{Q|B+5JouZs1IwTFK&`d%yQ$C zD-g$|&kPA#b%uX@jg>#W$kF?bdkMNiAVdgDErw^euw%>e#mfw?T*nGSy0JBoq5RqC z&WT%XuY6EDcRpwfe$bZuLHj^vaI@)QuB0pWh3q%Ym9jCukiEs7FaHIW%buenA#o&& z3FFhNT>Z=iTyAI{ZZq}J0isz+IPLJR6CurmO;kIlIp4ud32L*mG!~nP#6qYDq6nKr z$hib*GHfp+3Kavt#e0^}!va$(#O3YFM}G9-e_o%^KQuno zZQGOCZ!Q19sT;fSiN5=R3joCr@N!*?nvJ~HZ)#qaHTlO3#Xk)fbzy~jsvf|_0`Y)>gQi4oqUbmDBI2` z3rXu^7cjjealbTN8h5Jn@L5xSdKAPh^muZ}GsSWtvxMd$EbfAy=E zo^lQOm0-I5G`T&MetY}k?r}OFVONX4RnPNpT>yMH>xBD2eB&2C_PXsqNnc}E-Cd$A zZ?E0np3=jtX4h;|JKl)0&<33o^K$h0OBm6%c4MGzsf!?ynRZD^Hr}k=-Z^XL>))%BO3pDW&@U?SP)W=! zDc?Ui-aR+*QAzPrN%B%k(lRg6F)Yt8EY2<}@KQE7hu-4Yr*R{3Rwzk*2yw|}er{!*H?d|RE?(XmJ@9|Pe z@l#6iRZa2L)$&_X^xod}eRuVKcle{8_^zq=uB!R9vH9)o`n|dOzPkFrz5Mt0{o&yK z=jQ(M@&5Su|N8m=|NkefO3nZP04a1*PE!B}2pAb5A}ud5Nl;l_gqN7AtiQp*$ji&x z+SOM_51t${QmxXRol}500C!7L_t(I%XO1yLxV67h6AluQ2_@iwQ8v|?ok!@ z-YYn2>#nW&|35*)Th&V_rjo9T ze#S;A*_l5-mq!xibJ6!wKOoo|s*V1UP3zFvtN+N2dmiZIe60TG@1IwnMAb&U!p;(& z)}V2vi81~RiCPdeuW8l@`p5cH$Duxa;$!@DOgp9kjcwZ?dHnuDpI=KyB(DLJ)l%Oiwu0v z+v3DX>XcwFHCNP3#`xu`i0+p@$j}v zvupdO%QTv77cWeCo)rccc+eBSa%GrMK}}rR1TofuJ(!M(-Rha$>YCo`p5N@L;_j;B?zHCcxaje_>G8bj z^1kZw$L#db@AlE~_S5k8)$#Y&^7q~J_~G^W`|Nj5~ z|Ns9nkp&R|000qmQchC<2M8D$ElGrzn8C=}`_N%@rX)a0Z|ZgN7D!E%EzO4BF|T#Lrx_z7o7 zd?!cSUYQljo-O%8iN{bX7`_+_qS82+VrJ&JnEfw#z64H(U-54aTjihr0J@_p!#+G! zy3)t@`pEEzh@4#i^|Mv;7Key@{s%shl_<~B(S5fjCT5W<>1t;HU|V=x3WPOt0|0}0 z;R+};#==wo_zLLBoFPTVXzf+G2XtgWM5*d=5kONq(p*3b_?p!LkqWp59$A+NrCECi zE?74ypUGPQc2z*22i4BEz^)$kGw`7J#8S(ueFE+LvgEqg05q!2ySiGp5X?S+^Vhc> zR)P_dI{|=~6(ejrC)y2Qa66arabZmMn}^G$K_mOghco%~CLizL=l=r{;7UBdz#9|* O0000yM8sXh&8)@HnE5{yMH&b zi8!)}IJ|&4vWh&ugFU{3KE8xOzlA}+hC{%IMZkzm!;DhJkyFKyRK=22#*$UWlUBZ) zR>qT8#*|sVoM_0SZO)>6)2n{dtbo+6fz_^p&$W-)xRcttr{c%C=hM&c;Lz{k((vNc z@Z;k4?d0|EQchC<2M7rb5f~XBAS*37Npg6EmzbTd zzP`c1#K_v+#*M~}1NYv0PuyGb-`g~4NNM?g=#$?2rOjRL0O0aSsuqeyLz*gifKz@Yg191z zKL>l|7_uoQdqwWkkwdyK%Y$L$qHvJ!cjbnIeZ#AxJkseoWH&ZpK4mk%m=iCbf1Qqz zTu8Qxldn8tjL$}lm15K{;^3>#m}rMo%mX9`Uwbh*KI%1IB%rcG?JhFD&iHE|>={5R z(9G!4E&e^A{EhKx2w`_Z10fQfzcWIaePgxxPWjU$B|M90H0=dj*NGj+-Ny})4&FI4Kv;zI(5W!X~C zUYq=9P~`C<;j-pFNvaznhqborUbw;E+cw@2us2MiAKF!!+j5C68*eOdG_B3iQsUC~ zf}^?Ab#S<&p*YXzQAc}KVcy|29BtJBm-*)7Ur{cM|38x literal 0 HcmV?d00001 diff --git a/themes/triangles/client/src/img/notifications/warning.png b/themes/triangles/client/src/img/notifications/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..ab8b54ffc0ea43e214c5b0a9ca13a8b590ded293 GIT binary patch literal 580 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dy%*9TgAsieWw;%dH0CG7CJR*yM z^m`CyyfQK10+1nD;u=vBoS#-wo>-L1P+nfHmzkGcoSayYs+V7sKKq@G6j0H$0G|-o z|NsBro}_YrzQU{d+OKD7zL>B3e6H?`c{%Ew#`y9xfr~3@Z1+ro2RJG>m8qL$S znrCV?&(>?6YtTC1sCB+c>jDrpYh7&Cy4w(iq8y3c@UF36%l#cL8k7YddH`2{nu zb4W<)dbUiTaqz^g$Dclby}H6Mnt_3l+0(@_#Nu>o@X4}620T-m3x$N|_$<_Zxi6UO z#sB(s?ca`0x@1-??YV86;6(=US2wog3cr4k!4t14H$f}^j_b{hPT^vg?4v($bVq(W zKm8B)w9gM~zwM7*D_4BvynO4A|3VhRk^5e2Y)<88Xg}kvq@@4Kp5cJsmq(o|Cbcpi zSpGnCqPI~3>jRC7B9V~nvt(klsYhDhGT*w&)XQlP>IKiN(4Ay_`qsC<)~{1nmVR!PD}COZ z(3kxnC7)%nN;&_XJ{5J>)4TcQI?JNIU2Zq7t-5^t_xGRmjOKwIbEe*~^8khwgQu&X J%Q~loCIDF$0tNs8 literal 0 HcmV?d00001 diff --git a/themes/triangles/client/src/img/padlock.png b/themes/triangles/client/src/img/padlock.png new file mode 100644 index 0000000000000000000000000000000000000000..31abbaeefcbc507ac59450a76b222d0f4e2ef298 GIT binary patch literal 3265 zcmX9>2{aV`6aVgk-7GeD>sK}rA(Ry*>^gE3Epmj|b<4VQ&fYA4`JLL~x$GyxygiPF4+|0N z>#b^bO(w)_&M)^CaxMsCWK&r#aHm&lGhQsda2g)u)YZ+nA*$Y=^SD?-yyum$c%Fo+ zu$ZJykg9=Rm)m9Enp(-yxPTR<#xXC42HEHB_4DZg4O#W`_q$H2B2I8p9;H-yavPzH zq>PRmoiMumG_oM7?b?9OKx=_Dkk+mM`wBnshtOJ>eU=7mz?U3Q~$Mj}SCJX1OL zyqf1?ymH|9kdKx=DX?)8JOf!9^UJ%poV#?*v99*RQ2i$@z$~vES|EhDv3k?p4arGu`0~c! zB)j#@7}HKFW*v0jh^7s5seY+tT~QL61vGi3^n&;$V?sqb^m$mcJ2D+&AnT5%LkI5`dy*61((J>5;-?QSiAbncNza z=_&RvR%{Lm(ptkSN4tVOmF{n3?6j5{zR&sjwxo5xQ|}d}l%r)b#65Pmb-wHy%d{L5 zwPQ+6{)w~M9AWrc-NtIJ$ENRvF)A{Jcr?4>y9&B4!~eLW4p`>> zhOsvg0z;G;#Cd?1bqhHtaA;*Rw-MY>E-4btGR_itUOXXU;(CuwOo62{(s@)KAuvtf z94FvGsk{$ylrmS~owI-KK@53|%_`yM&Ga8O>wLP){j(dkJHc<&j@laS3U@w@+G6&w zEevGlg6D?SCiHJ!F%BgKr*L>vX>8T2Yz8_^rTT41_xEvRdH3@!{Y6nDd=hKj!2QFr~YXu#zGc(XeTQN|WFn zQL_-O7SLo_0!Z!Uw1xG0znZF6-!;c4eL1j}u0$=vD0_f6MZe}${`8%K>BVi{5{i64 z8|m|u%LIcFxg8~xj5`zYvL?g-%vn?irwONS$jMqAk3f>u{z3D7%BMV>yDEvMtTO}d zihwHDkS-^BS)X^O)qI+wO7Cc(2D*}y<0#8~eDLL6y<=>IF)Ap{9VFsh4qS?SFhufl z(BAyj_Xz^v|A{MSeU=T;7-%0JP+Fk$q2Jf_V0xC09?A!2-q?Aq)ofD99`8HUCp^82<@{syV+N=MB$R(;352@)bTYSwwhc~HL>YeOhXS2 z7`6_89MT2B;X|FdD&I|?onT7-6g&9z@)L`P1X{VjD@l`xFt`dSWnu-|L(T!BuDGCO zoNZT=%_xMbko*hlYL}Lr*taSNRW1F@1&kj9i@TIdC=fj&0N@eyB`7fWtiKMWbATr7 zg2A&7Cg4T5(+4hN01qd{VyFg4sxm;p3HcbRj)$mUv5*IoG^Zgs!BAG-W%*=6t-UX1 zU4QHH#qWz97EB%r^T_V%r{|qM{wSWG<4a_D^Ew7a$i-0Yk9WjL_=N`g?oYJ(W=ebM z|GFOF|0d|pVM$+nV+MucRs3I=LV87{<7gYRlcx$_ud;|+Mjjfc2%;Bv>51`;2WwAO z{c#Xh5c{q-09~!XFT$9OGxf4;JiF(&p>h>ky0Ibgx8JJ};Iy=w>Y;6~e>xfhJR=#) z?X7~{(N*kOUXV%aks!{hW#9|2?9tcRK#m>F`IY_s5%sSFibr9p&|^RqvfLL<+0hfZ zQ2#zFFl5GcWlkcj8mkvA&?_^zI}S)_xCX$VFb1XnngbM-h5}-I!jWl0to`{s>4a!>rl_SvhYH` z#z%W=pEP5bHRfR-e>gHKrz_RgoAR0_lc8 z1wgm>6)?zy0O_Y4nZOkhU=5=m57R>ePM|>OJ{JSjfjgDPqai|s(%GPC-5q!&@I~B> zSVkm*1!$!dLJMNhmHgZ1#hYDkem|p7SVlnEVB!7mzhr7(ZDwave%rx4k2Hgd2yY zu*oy(^w{+B6AuMNhcISd%EV%^s3qi>3E{XaHr<;vT zLLrQgWh590jnIhuyr>83`CRZp^K;+SWri)#8qXn&eeR2GDV6$hV3 zC;|yg5QWXs$In0}S(;{3+Q)f=6U2}}MMHFMPv@82v>(iUde7*h*H5ztB$Znr8fO>y z!y`qZhxbu(c4#2?(|Dsu>wHrr5L8Mm7v}nG-NOl%x7?KV{2zXuG5c zINn@IL5?joMBh>QMAAEjtiT~pmik?HjwA;M0d{)zA}fX%gcBdHORe)+lk^9Zr_Sa` zqG{W1mf=ek9)Nz+@^^gU;jecyJU>{AcJz_GD5nPjG8kaCTN}IbF=6ffiih+nz7Y=} zg(Wv$gUXyf=OEH{vqj2}e8wO2W(9OkmWPm8m*O-uDNz}#qhmDS>q!LhLM6GcT7m~%ocUNs)9JZz20hA~qKL^0J>S_J*M z)nRkU{h6XFfO=E#6g^4IIX9ymv=`Tp0?dRuu?g;HXq3gz7`?Bzmy1d)VRWn*uU)mN zYWD;+?tYeZ=I>*4nFQYCc=YDtY9sq84;X$x+%XA_Fj)!tqt%uuX0^Y0;;2xn*cJp` zv=$LqqjK7*r)2+UuQcF?iwJcHvD+aWYZ@o41Yr=8*nku=w5P9*7f0p$0HdYq1mjgn zqcU#ytbI(erQh)M!$DN=c zob)aNpOrt>_~Jc-Tol-c0s~2SUY7LD?svNDNk)x9cNst4vPu@PS+VaFOyBnbK1YM?0W`%*k-$2d-mZTsWZ}_GVrOLYb{)xfkykLOnSth0t7LBH#Q4w#lnEDYfbqtK!^A6}Be(TLV z()-%kH^MOnA$DAl#6;0q;(8P`@0<463$}+n65fZVc3~e`j;{ez1xa zR7M72GnlI3{adw^n53_0n$*%)F3AZFNit>LsO49xGM6sRglr-`t*zImB_($s9aRZ^ z&0Sep_H>jCoh9EvJK07LYM$|kb zpMKcaMs8yX1!vnOQ3S1L+uNGR(kMc|g8X>h+?Bjl?4OzwFoO6T89rbg>H_FjBFwpG6+EE zN%ri5OinH4dsL@S;9}~71zhj3Y|b1D%iMp!(}3tW*_M4A1FjC`Qt+|9(rrsBs=M=0 z=P+YB005w8wBBy`qm+sncXSHNI0pDs9p%c?uqAMYf5Av zN{`(i^Dk2lRfB2kc(bs=xZ_}>O$S@>*IfzJ9Y@A!D>DLplnl#flvd9#dW19lhV@6I zg7(;x6Y1R@esAAx9~EFH;#6!88D3Eo6OPeuc-V6VT`DY^)HSJcR#NXt4T&eU4}~d; z0Fn1_5&O5cWUe_WJTSb1w%n5aNVNNEyPOYMZwL;2F;NPSZX9Zi3L)HWibmQLj+Q(y z0|vJdKi|%eB4*b1?5dt^k4}gfY_i!2PY>}oezLHK-qSK`o|;jK9wVpcG5}UeqKKKV zwAkJnxh$?{^^vB_=9Cg$q-j=1o0^9CBc7Z^iN${{7~#b*!f)!zH+Xc-o)P}&iG3)h zkER+>l~ovZnn`*;i)yw=^L|J6U>XclESv1t-E7u9rt|iD(?qErP8e0iHuK3F21!Gg zhSkOKt|Ciw5UxfwXHkKcRXrWPvM8IVz|T`6$E^b`eY&tCdfJh>$AhoHLQwt+$EpO! z6qjCaD;a!aRH(InPN0EbSzqnEh(f9~=_5zh?iXvux`#GflkhLWzmsNU2WbVYg=#98 zR!{FzrukEN33nzgVQe%2RGq^S`$UjYG&sDS3At2f#P-eX#vVUSAf)_QU z3AI#$B!`)QqMm}ttH8SbL16sA2YJYMtCP)Da0RI2KJmKf zk?bV%RwA>;1o3;{QWk*Yswqj~UyA@R)(pI><)rc0%1zMg`1JYp+ADycv0%6eUnjOD zpjRCuzug>@^hTa0cD17lC_BcE5mulOf^dB%HKV3>ZL_UJ!y`6fZM~6IY6LTnDTSE6ZpA->X_J`@mnH*#(6w=9ulM zF&}ZjdH}YkBZz5=8aNVG!>SYZx{?&xY{Y5SBIzQ8Jn5Nse8K9>=9DBj9!N*te{{wzHdE1LHD0SFMO8uS$w15z|ff$5W~VZ?dA~#x&f7)3^VDFqjU?~x3iTCKPY)su;gfIrWzWt zmY;zU#mjxz_8jIeVK~BRY(i+UNsTK?pd8>KX~aDYM~A@lfk1sd8INvqz#p1*VP|sv z6{M(1n_N`2!2u@_8gOib+#6(VAGC)e3I=RW zy}poa@S*3i@zaZFH*z?V0 zZB}+k$~M2@GE?x}J0CZ#NKAbymHTw~w|u}b{vJJEAO7WH_=tFXT7iR0Tr8Szk{iak z$#SA6*7Sa6t;@q0y>b)E5d$d|Y@c|VrT;+LRFZB%&-YNz(ku(a|0Mrqk-fk>JIhVR zRW*>HMqwwFSuf@BH?oBqO0u%qq?+xL zi?KbyZN#e+D$Y>mt9S!g^7{m&_nicl1+?{!5skjq?T+645K*SH6W7{CS5^(RRnWnJ zu-*}(r+t`J;^h+-mC7F7R1k1?_bN&-EFkF>ubQO5tN%<1+ttb(nP5_1*QDPi2e`EL zEY70`;CH@E1Q`l?p#JQ!Dizgg)l~aAB63}`WX4QuUXBYWWK@=qOm)-gp!NSrUD>wb z`&5h{PU$g14}q&Z#goj779|-*)vx2-ytB*DAF56zyo3Lg!}3V-x3njX{{-`3ElULW z4KL!Oa3qmcYDl_gZ`0rUNg!6{^QlIS(&$J;khg@ueo|3ere~{xQ{{SF0z)o108z*m z{!O|{d1(bNr;2gj>s)J*%~)y|+NiC2HG*A#Q$rRplR)6YbyXt`LXG3a1fSHnhzB|oJlDT;8qDTuHM*ux? zEq^i5^@aY6=L$#|Y;4LWE}vd!^UJ;K3~TgGOWfauWzGz;@6&Wh^s$OJ^To6|yG`g% zHcEO>7uZsP*xQp7Bi&H%n@8`6cRABq`2ZhDBQbqWlr(m{j%lrB#Lk0=~ac5t)t|Z>Q4jH30;8kH$qkz)OmxP^z%n# zHVReRmb=7d0aq%{>^6wnX?C|2k+Z8VMX6f`ns9SSpP2GrWZN74=-djRJqLuJ zdd_1KD)m+WOq5k&&9B}mo1DH7NnghlGHbT~Hfc>?%q6t4LcQ&Hs zGz;n#p~(4*d4EDN0TT#v2`HsD;(#aLZ@Ao=;K*I~*WgWr2^$b{uxMu7VqfUn7muy= zlPs##Z`6iXy`8JsFe~?!K$A2LjiiX$X<`?&_7kCKLkUNvE4|y;1MwnUBn-lBClYYL`wT;ySc6LAr+HJ0qIS$BW zW+9q1)lG1I;~@K@BKI2vV_^^yK1#fVh>(pEETGjZ0h5 zhbF0{A1<-AOhG4(N%nV*a)MmydUw{{(@K}+fhrB^Tt2tp_UBh5gL}Gh$ zWNynbo30F7KSq=my*=9TWcV*rHl+TnD~T(}88f8Z5<}^xx^e(b=GagHMkWG-mf4F( z;=Zn(lqU&U4zZ#AMOyo~*yZ$30NTH8=#T?=0adgYV$f+GWnEMOcz;bfA=rVQD>qZg yfK{G;*sMIT^q}!}G=q*C=Lfs;R>;VR z$X=mHWZd7o&-e4k@4jC5^*qlx=Q+gJU`;0Wjzk14Kr2 zu=aZFj1G8jZ37D;`U@gDMxbk^E62`y1F$Uo{bPtXm}F7lP9NPfKIWI5ef;dboWRe| zPsZ(%ySIb=6(^a?UM^`1YJw^rtlsTWbmwuD!;DB%b?}U>BPr*-HV6K#5krRGRm-9 z$5_1ZA<|iRr?ly@qMTxS>fxa+^Is{+QLIMv;nu^Cs@f0Sh(FC4!V~br!kfBEZX+*{ zHOSF!r@C}H-*t$0jdvz?rRP)_dEd^zHNn17;yph+{<1yNp8ny&YUrP$!f&m$&faQ- zAc7`7BkRo?vc6c z0<-nnd#{u&*2GL*ml!r6UHJCpQ)_R~1i&ocb>33(%f-Sy%Dq-rJL0 z%@@6di!&emRP?wh&j9{X!HRK`*zEHZLr1WJJNeDrs=LCzEX`f>yHz_vejtT^zF*1RtrrIo!F_j4Ef`}>UWp5OND z%&DkR;*N>KSKkW|;8xV4j<=vLmqxSq{0(^y$l#;1`Hm7N;tw=?Z!xr_Q!{J!c!R9kXI2heg$7akcGqZIWikBl#xb5=fi-~bD7xU63(VmHW@61}0@z^5Y`d16 zBInALloRu$7ARjPII^BJ*y$eRb@E^U_|8YaN@Zm=eRtgK{lFYhgeJ;9oU)FHE!|r} zfYO>$5hL^4^$~qiS3OvjaPV20UZB!)$8~;xeF4w{l$#E|&pJv@J}-PZs(Gw)=s2sA zPR^6@i;?ms6N?dZ8$`)vh9g+G+ASBjX`4Z01G@W`zM9BtUsz|_0Ybmz_%Bg z=-&eGg^sBfIJf^!QW$D$Wdic^O&KzW)3u4U$=<+sR1wX=N`n_e<(GQW*S3YYy(%gE z_{8ElEVLe|+wsvM_3M#lp!_JanT4r&~g{&YS{q zoh-`(%lKCcZ#fcz#0ezBU1vxjW>WtG=haEQjXe%SF$6>DFzU1DzlGE~4( zhMMsykhrj~KbP;wKD5voof-m^e?VNDGSlX+hC}AIfF=sZZcvz0po6dwei8?USd?DF z8bcAea+)fbC7bFc3Mv1O3bfLKYzIy`R?J_naT0r;zKiwfp@Gay9Sf*+V7c* zFzh}-vJ3Hgj)g7lU{lAgk~ItYowK!JeRr&%r26<#O}>VFuri-fzQmQvE5wFxH574& z_*>d({ACwCO>Dlswg`ES&B{&75%ILC6eix!AV*^T+b)+ z+%)H219+3$`~Bphr&tR8ZE4>`@2kE$mP}xA zymwFSeM!4NGH)*S;Auj6N*b6Cac;GnYM3m?&DHM|+AIG8CJIB;#h`qTK@PBJ3#@mS zbLe-Z`A#?5`60`T(P%@U>z1*J(_S# zYgCtd=6Vgx`gd}#HC+PIUhMG?Rbm-7k=V4*%db{Np7C;ilZvDb= zM*NJzAU~sQx8`u2efkcd){^(Mz_C6`9Vol-Sd<|4=T8ob-h$it`P;=HRS`1A7^$Al z0q-azOA(NelC1-k7-k6buuY}g$@RIp zxu&Fo-lin?-o{@dm+s%7HOaQ&pq8ur*DhG$u*@mn#9qlAG0Zt0x;RA_TbFF(smm*N z?C2|+n#7<1LJ{?Ql+e69EO?VvKduclW$g2-nCaW2KbdH=jnDL==|%Z*2U!i}0z^$| zKV45=t@+`N-BW-XzK*!k#X#e0nBYLd9JCFLFP;mZpwEt8Rp@q#ZBoQ+93jL%Y_Ij; z414O{YoM$z0yIhNi%NK=?YWL4sXzLrxP3Qs^3b*YML+iscO1T6bK46bllZKYfyC4> z!GdJ-^v3Jo5f(ZadbUBg=QDO!lV(3nHB9JT+hI;vx=pz!11@IPwGd&rPx;LDuyA+0 zmctth9HnC3uVR*}g-=Yl4TvhfO#_2`$1K5LmUGIBJMkoymH62Lj7#esIe#hBYulnQ z!hy+K{>TRdIj@2+a{hx)C3}DT3J%)(&K50AkWspF(ljjlgQ+IaM6loaAl=Ne7Kp?5 z?3a2%TQaPpFYwI+DcYD}K}y;d#B(zhh_t?$H0jDc5!r8%-STGv(ZmlH zv&h{B-W1yotL@6l5B$Si)165L0hnVSWf)*{&qxjw3FM)oE)P%Ju6R#&N{K~Nb8QZN-qYz|vO-mLsyEF+fxTnYxL*hnRK;E<^ZiEGB zV%WRPATK42hRR7%&K!u3>BOROB7tpUhKm2d@daQS_aAtI8Kz7vQR-Zsk#sE03C-I8 z`?$)V!4*Ot+WAn9;}@i$JL?q+J{Cz~LkFPP;4fSu(zswT#`dVQOq8S>!|>hmoZ)-8 zvpX_dYFAjqMOR;aI$Qi22|9mX>H7gmEB5pNnJ=RU8owsWHtH{zCfF=8U?GIFJUjtO z2RZd==uh>J(~$_yvwV|tBEI8+?$f;^doMdS+(?yZY#^xd&ohAF$7c?=UpO4)WWyDC z1v32x{PQ*d!C05C-wKN*?%%=?hs~PRp?>ZqAbMl}#0PPwUH@xlx9aGVckHaLphgmp zU5tzo7!<&xa|1f9D^sM-66_tD_>qy*9k{+BKW#)5JZ@ zSk%{1(0J3zGu-RrzcVrMH_%!Oq#p7};J-07ff-2%&AsPoK6x+ha*gp3-&yU-Rbfj0 z#&aBSJNce?(_0DU56it^q|8TkW+;>s?WQP4s(Vsjg)TprT+giAdcZ2Ax*Lw&t5`RP ztb~=Lo(%-M?=P^OU$))WZSf;#of$7O0=MJWAH1RG08J~SIJuABO^XxCcuJktTMWVr zx!d3UKmc22YEz8y4^6kqfz2AL%Rt)ZLq4`oH2h{j=|ek`N7E@XzzPyb>F za`=~KcBnb=Ux_QOo9aVtbwmm$^WWN3o|=$yW`Gma&FqRXeX0JRM=VArth_PEv^m0Z zZ%YVE=@%N2Ieu{UeAZnaH5+VJU?%1at9ZRIpiZ;x!3no0%*PKZf#GLi(O?4WHFo5Q zO*X2JP}pEd@!}t#$<%Yt6N;3cB7JOvdW>vB`q($4MIs0*55jDIG8U7icszzp3lM|{ zIpG+0ffsJkjQ=f^o}-@QBVxhlP;0CGuZp_R=}yd0V&;e{a)U~*8{&n5Pior#)?a6q zr8qqtjq|^9Jo zkA?3=_dTW*qx%9Al;Y1BOjY1W9;caR==IuQSK?0pQ771S(E+ohhms)NWyqFe?>Fy3 zD7tW3ey6f~$pw^A{)a~@(kF~zptCz`uoOdS>WG|c{GHu9bcua z{wKB<>n3XrgTl%T;F5SQ)LNsT4J_ius^t=gme@3}KWB%dBV=Z}mvz}LJnVPzZ#sxIxN~XFm=e zHO34vL006>;`3t7bf2`v3-p)ICaDBuPX}KWv1A=`0O?Pm-&(upwy%79F}cH5N8>3a+)j#c~M@;4^; zel2p;mu926lT>dB?fg!N{onTUckx@K|Hm5j4}$C;8E|VoSA5#oXshz=Kh1<3DM+ znhU?B4KnR#LM(ToSY&Q`z=q%Yu|sIAo8ViMiwf&|;{7topfa z4aZU83gzvkeJ7Snoh6?$qiUR)^uve+l>t2a(-k^s8=vm}U1I=?I4iVsm^u^KGs*>_ zI^JfHU{Eu6$0g*Rm;a!VKPpXOCvQs><=8c@opO75p~y#M)0`9_*VIyk;5z=_xN# z0`rF>TNg^_vy9Qe@yV@_{%%meZk5Ymw=|#hb73}Zr`4j>J&nQ7TP3JRhS`vNI@`F@ zEA@`RpIDB&0n|c2vjHP%9}^y!9(0<{PCU)nmcsLBW-b>)Ym>NPWc$3!@-t`Z3(?Uc~|8Bkcn?o$f%jiPGZuY^-4TJSAHUDY;9WdQ0P%W;CJ^udUzzN36 z8*YrqSFs)O#c85>vKm#vy<%txeX3rG5(1yk>=xLtM>|D|AEc5+T0ICh@|QYd+H87 zjlmioUFGWVML!nJ_6-iTR_3ngm-6Eh|tbr$7LQ8W2Lkt z`-_$g-k>v$M-(nx#CvAze%M+ws&KJ=zsG8RTudEgJ;;^}c+R;O6PjnHY^f7%fuZFD zic8B6RuXc?g{APQ=AQ?R^CWG*8!&QHm9qzndFPepCoGd&r?ewaQ5%!xE&Ilh0%q5| z+Q%vw45D@BqOYLNmZZW!B<-)6sGImFezP><92Yn2CI-1)K;ZmjLG8=?Q(29>xdhRO zI89X`A@}bcu88ytKQY0|S0nNUm!;@J3!juEA>>jk+2Ec#qw512AjhgDBskIZoPdqU zP+hTpII$>hE}4s^5U{FLMGDv-Seu>X7ni{jkOeEWJjs6DjOc3*_7h9HbqPLknhlngQJ`( zU5QxCx4p}o3U%`ln(tLWj{kZ+xq^Z$wl>BUPap7J)kS@R>X(CT!*`VYVjv zWIH}tg9jL^D!TOP|+c&KR|ifYSVbz>1=5IeJrRsbi_F;8LNFXCfQvvi+lO~bKPXh?#Q(@ zk0x7rUqBv{e4MNJ_I*hnO5(#z_=ik>T2sxqc6)h>FTXtMRsUY}J9I((E-q>({gFu%7jo0_>I8H0YfRO8S#q;8YL$e+j(RPv7q;Gw3T&=i! zpzd(R`zonJfnR@gi~qS$dw6NQxMI&EmZT@2RGg>O=p0T9x)1t)$FZZ`3K-~~(0QS4 Hcm4kWRQYbi literal 0 HcmV?d00001 diff --git a/themes/triangles/client/src/img/sharingan.png b/themes/triangles/client/src/img/sharingan.png new file mode 100644 index 0000000000000000000000000000000000000000..526787d3be6a78d936b6a21941291994d0eca38e GIT binary patch literal 9213 zcmW++Wk3{98$U`)y1NcIN~F8<=n#-b8bl-|rMtUB8tLvhq`OlZq(MqL-u=HH_Gb6P z%|5d;^E|(pa5WV<3^Yyph0zz*L4P3+5axMWH};j z5QrKCm6rJ6o^_h-;SILPKJV80o_h#h6JSjI95ZHqMxv7Zh-;v>h(LgpB!jc46N7ae zQ3d(2%^Z+ljZ)XP9dX9;V;dBK@^%R3Fs8JpWSPd2Sf6-yo!xcxTrk6NSasB%=;tk# z`!ZHo)>hV*V=OZH_udezWBhl=xY)a2-W%eS zVv{j~=TYhNpxyHX;~Go1uiO1U_fnzT4&qDZmk~CGK*MN&_cf3NiT0 z`FZ&fDF~<+eYA$3LN!45#w}&ujU~{>}wAp%2}NZ$r$W5kCUg+XxFrb_JxiG2vMDtI{6$xsWgvd zMj=o8>A@OMIDRU3;$?zNiAt5pRH3#OTXiAA%BFzk+H+u4W$EjyCie7w>rNdm%r}&e zJUQV+m{g;7?VV-vbNtT+v9DJiTVdD z)}8qGp}!pSo_woHdJh`Dd-;FSX18vRB`fWHe_$yb2TO0qm=-N38 z(y#GyOr$=3+X*kfxykgu)hOQjVbFbo0IQB8NY!QK&1iMLCne0}ocE{O!k&zYy`1Ue z|NXqKVOUM@DSY`CeHFty2VS+J(Ss#sNn;2Ms+A_!ThITk+GdKng>aL4;vRC=gft1lPRU~ zkRl=?YJ3vLUH=&6Ka7$%EWN;3Cb3B|-)OMBI-n3)nIAAkg?e${DRiJi6hIdD%q*ih zvX&YDeI;|ojhiHDwcQB;m|#j0l1-0TFO~LNhQN6H@@xV^Aqq=$3{Se~H+03q+|ND3 z?+d0O?eKWlcZ^E}=S-eAh?`@iL^|w8KYFutP^8q!BZKa)&lVj9@x30Qn@S2b#Dpv$ zcn5Z;rG*M-Vww2cd7gS#(^JJ>o#Q?2W!YKVrFp|9%$eIMabV04c;REm ztCKrOXCAELnG|t-$9D55CFU@%>c33YVmWynlw^e*>aPizaZ@|de84})!Erazrh066B6H+R1#)yhWfjdSWIu54cmmj%^ zQ+VeWS%c$f<(+n&{?sU0YFgujv1DJ1^W+g#Kt3T^r3pmADB^tQ&U-b-i)ipVyv-Ml z^lJ@iW9VgDvmCbEz*PLno>#d+evTT*ci-mVjm6BzL0niV8*zGlquz@a89jbRQ*>xc zhb<8!JP|30xS2WxO?8&6?-^P%Ja%s`-%=)E+?*q8X{kburmmS4lTb=n+PkV@+1vrk zDeHy=2?oYna}Ixdm40{!=RxdFOgy@8aKAnt3>zO)Uwl=OV{@ziI7nNN&*#xYhXPM_ zGM}|ux;2l=-X)#&GW=dO8l!$iPbOP}hxy%CZQXMB^)2gERZ(~c0;d6${*9M%qQ3 zIqJCbb`Z`xtpxCDEre8J^CP&>oOuC60rO4lWlcOjk~N{45_C3;R#B}~Q8mucs=%+Z zw*8Zl5`$EEkQ@Mp3UhM8d?}BzRp{Gcx|56*xQYzn3eXri+Mq;Kpn}P};Ht7J zNG6~#;JsCr4FluJ<0>gYc?zbk>HXd4aF&mAJn!2*`Bm;;D&R1S+?_r_HUzZef3@OP#Do$`im7WoI8B$V_LCZ z`Ofsel8KB_%C|@@`ASj}qQG7Q_Irt-t%pvdv(ff#Ksdi@Z1iP|S6KE-ctjslN<7pA zb*j2S^K`h8ri5)W!oMy5XY%B`voZcVA_Td+p6NjPHH%sRTxA(*4(jr}n@{U?s%78Y z*@}nDAthso6REc{dLKcH^M~jn5=NmQG>5qPZ8rd8)*8wG)zFJ`Tvl_Sr`9~-^G(OZ zgm{RBt%?m#M(fZ#?0(`6>8Obv+=(I*NE z@Q$&+iE)BF?Umwq)RxCfZDEU~v*!mH$Z^m)$q!>UP~N%C1ntzbWxJ$RmJzufTpu9C zr!owncmAzQLc>jS>Qj$@8xNKwln4(varG081 z$=8cPO->Cu;4jY4YRxW*k zjd|FaUo2gGl)Do0xz{e~&}^ZXfgY6XQ6k1?+%13JvhJKca^?PI8o|TI%s$@l^aZu) zk-dP=v4cQ^vS@8++gxdbvB?0>P^qMa>>tt|a&L*m7UY{kfI zXhi3lYHDHVSy((jU1J==J7_<^0b60IF}O%NJODOMVMx63YhIL)_9w5~;WaDY?1m)X1?n-< z0d}{_?}@W@et(A+GUuZO!1TNb+5<@#Jkjg0g+-pTNrZ?B)K=Y%uyxWShfttLZTtR~ZZ%9b?y3mz1nnCEi7>@wpZK8&MUm08S&&a-UQ;v`S19mK@;5981)NJ^iH#gz;=A;t=(R9RQA?P{F-vu zh=e5LtF?AK#@2>q#cEoJaLfA|V?0(qB@$l?6xgjIj566Zsfqz1i~xS}pLxYSgktbHzi zmCCxzV4j+Y`tXttAGfu~LXW*84tY=U;8Ir*Ql9aa6b7^?~996AtKDL19Or!L-D+{ zh#aCFZ7G<149ug!LQBhm^PVIfiuccg$Pi8scukZ581fO3qjq$8#lCVL0b#)({vdk9GY7P+nr31yVI%oZHox22 zA;1l|`g|-<%e-Tigfu?aS-!L!Da#(_?R?1m&WUGYW&Kmr52hcJ9ATy*owCEOVluK= zo6S{w7#`vDA>I>(cI>TMYvBGkU%&?Y&# zJwt{zLaUd*`P20$R^aSR3a+2vOtBwnP!tEfF&Jv<&3xG?BY}n_0g$RwCt`PXogwqY z&WwJ72{10Azv58_nkUzM>B3P<$!7{iZHo_!>BR*giVCey9o5rha|<(>-)@R-tX6~7 z=?F!hq3}6Oa1KXFUO1naFRqGj{#fXq|BWJD%-Y3oAQ`tE(OL&k=u?ZA)C4x~XE=J7 zWKU2ab-t!bUAq0>x%Ic|T#h$gHN7al~ z_*uD+%(6i6#f9@TcYTFPa`DpPg%Ek;YQ3pQqsNi9tg;V#uo1Ry88t{xLXD+cY;FB# zCrMzZEVcsa;fuZK_+Qq~Uog)`ug2vE%jWl0>U%L9F^}IMUtBUcF>W zOomRC(0Bc69)!+FZ)222&085Glv24pvbWHY+$+y1P(ouZp#7LO0FWtwV74^W7D14B zHVD-+q4Gyx_@5CpCi&^~9XZokBSH9dvU(D`87V4Vjdnau!bgaAI~)Pd#G`yka$Osy z1>ukFe0u~+<+7Zya#n;qWePS131LiDY|1)aN~q$pn_du_5(rTsY(qpdUiw1xAD6Ac zllMP{w#u7|L1l(S@|=SFB+z5POU0vV^|#Ar8M%^4D05c&(R{S~!ThT*$3+HcYIX8TS_>|9~AX?TOXBcwm=1 z$oY@tkah_>;ZBrm89VSa+D5?7Mkja{7aYp<(KA((sYwRDr@;6SW2YNfy7kax;mtJ~ zcbD>H*(I`ww@AWW$#t{%xr_JOMVN{gXkdm zIK<(98lP<51W+sSeGr%}o2YQS80BHC8_qdA2T1P)pQ=%CZ3F`o^vU);-B^SxFEUn@ zsK2HH@!#2t!5xA61mF)Ar#7;4i)3k4-08R=EHqDX(UBNZCe;tE=(2poQBNlQf-kO) zj1Syg`>GFE9J+TiXC7BGeRmakvRr@s9=0M}Eq*@nB=&BZOrRbAt)4vJoTOEJR93uD zqJM9O%~sSVGxE`i`eT=%BolW4Y%o*L!(?7o&x+sAQwq+<=PAteNav@!8Aq3Mi(8wb zvVe1y&0)pRljvebuOXqn%9UHn%_z1XzZ2R8D`x7{_hx0DF%xdiQxaCbQC42j1#Y3H z&3ynE9lM7pXyQ(rCsb3da(&JlL^s5+t57?EgMA~A;p(@Dj&wdEW$vx+i=scaim;ar z4qZ3-anQ_U!LBNgE0RvYHY(fpO3eUWb#lO(jEYSuVg%%y|38#itd=Qvjz|cn@ABlf zfn3s9v(o7Vp$TBmWUmcS-30O&p~p-sx^}e-VdmH@!>mGa6icfm-7uK%_H>&l zP%bA$MClu$gg+b!NiyS*{6fB>Wwa_Of~>+*u>^b;CD$-}Y{d)pz9h zm_yRlMmWbKnJeuDdH;G$ew^kFHULmS{`dc)biSs}U3(P#{e3;aGthyyClEodrNfG> z`ln0(yo~3&tF6NeC4a@*72Ex+l~o!2M@cxItGkCSSo9QEyKcZY%`(;bm!$Yj|p$y zAP66m{6W2SQ0Xkvn3ju|maH9w)4lJ^`=V>)6HMt!ub~C%5XW)~j_am6y;VsKW=Y~{ zDm(1Q$Z^2Zkp@Xiz>2kqKyi&Zz&pO(F=EH}mKX?ky zFM5pcYw3SFA=&-pv(RdFNsC6;3}1y;d`qjyL84fk>;$Rby|U4mU~Ub-2Ss^ z^`}WILzcBuiQXUd45!0b*n#}CD+>VfzrZgdlcyiSW0Nua1``ZlxdVQr zk!qo4U<|zFbE!qdbF>$zVeB=%tsUt1{y|fqr>gQF5hyo!VRiUm6S~5F&l8siefoCi zuFhasu07gnvo~t|mb{2h&d!j=C1Y-MDJAinrcIw3@6}cj?u~191om3C!=qiez z#v7G9K+fJC-A=xeBv#_Q#ESMU_TPg2--`CLCtAio&o_)oVadRfGix3xXlnRLYRFks% z-9t}jeE<);3$MfDjFt?$P`8JRJlnNBygT)8H)q2OGKe|G+M;im1J1YS*|O{ifxm8) zXDrL&g*ZX^ohkR*GslW`0(Q3^tv}uNS!GTOqS?G#>&p)YZU@bt?8LS5d_W%BB??vL zUB9C81DMuPknas~zE*1Ew;JTB%@ro?7C(!euyQz+rj6(Y*R%!UU#E8L7&QA1Hq1f; zVai`di$|si#LC^ROLHauEOk>m-R346=%o1RtCjNI84qIZXk!U!|M9l!+!hv0QgT@9 zqBt4CK~IfVKo{q*aHfa;r^+u*T6?$4j=Hrk;PcF94vT}w*pczA+^E^Ste+fBBA{#2_^Hgq|434?xxWZ}dqcJT z8Tm#j)SaC}zr|O}Mj()SP|xSlp*mynE278U1XNU2QPN02xFGlbYAH;^@FA;XR(*7s zS-Z-FwkP`0?fm@wA|QCQV!EzaveQqqgnbB6m@?97=Y^7z-^ab}v7>K(=y{E7v<`c3 zZ<^*MrIlMLLdd^k;J|y{lHEz&pKHbq_~ahV<#|6Mv;(N8EE+E!<9iNG6S$zDNv}cT zXl1|xFjQOZ}!?EN`uOIN5y@GcawB!TvTru z>7MkBV^`n}MPT#dMjqdT$1;r&-XW4&+P|g8dn-qGSw@RQ{JzZ9(=@>|pC0yN;%7A= zcUZRs)PzgHZgqT*mbbGbE}| z1NAQY%7>h6_QL2N%SCr42IzM%umi}0WJY(xi;ldnc%HC=zPf4o{y5F^Ex8{brCGff zim&hl^Jjt(@~>*i+Jk^-9*JqCvjf2HvQ|*yFk&~U@WA-@_7crOG7Q;RkQ?v( zBP|o0Ukpx5u##eA>!w5!Ao&B5#N+6J)kpiLOCYC^h^T?%H>cy_?9YCW&+ZgR7l8qD zizV}Siz}2bn;!qSwrS+xxLfS?Pmyw;-Q*rLg~N9PE;qmCf-Nz^P{A*I6g#Uc4*|56 z#~8A>35BG-Vu=o%Jy+`-FT0eoj1dhPV-aSy8VPsx3f8V11{#apqddQXBNOJ8Jwx{~q1S*i3QY-XYx(d9IuG*OM&;NPgb} zJRz@kKKzCWHP-aqmCow=PI0L?06x;{ApCXYnX#`^LbeUTb$sXk@4l|`?qc>V8W51| z{>5CSdBst@gxmOrN#J|LmdD5PG}V+!=GmA{Lv&7k=zE>{gM&JzO14*Et&k$$2kdh( z$TcmQ9p>4QUSzFbde4-3(5P3_7E>KWrhRyfWd0qmmR9{Q)BSSfn;IDb-YRP8-CyI# zlJ&_|df7RV0KriE?!4~$8qq@)d({ersq7`a!`$3v7R zgMn2o7L3K$P&1;fRqImY;(bBPceXKHeiY$|()xQtd2$3-XFlKtyrvdKa;Kv(?7oyg~8>*Q?d>I8{rAN z2mH9(geFezLXF?c~&7U^Mu@4*bi28)O|^7P;Gt@nv)*2jNEu76PK$F~z9t6N=P(K4#rL40GLbwsWK<7X*+jL%jL;9FO zdrLj7fZrid#qiEP{kQI09#|o8Pb}v88_)6xFEi_=*CeLsA-JOt^)4jTfDKI3jxR3ys?^jbM?*lwmlxSEffKLU0l7P;71CGsstmPpx1pm48eoyM&OR8baDAYmwg6@}yq z)&jn`3<|Bp+W}lR{(49lFhpnDxGY#$+Cg;fT>hnCK=seo8SMakR>1=(I$P?5~Q3r4ReR0XxEDLNG?|m+i}W zfodpWkcBdOWI$=8wz3~m}iSk;j)t3|QV z&L5}Yo-X)VDtNC0(x$Nig4{q#1o$X1XmeCg3j1iJqaQ$$31Fbq-I9w8HDjKZu;ULk zdP-uq5VjBm^N=(57@&fN2+F7sD~4D;vKR?5Ku_K(ooD9xz=tD7MisxE%!i<4ebnlu z4GbshG+S<)32;v>NKxfZ?z$@kL&>9qTB={)HD57DP}uP*_fmu?u5-DA+h z+}sb4&#zo<@BM%Kx2M%63>t74!-Gta?LqeNmz>Q!WJ-YLIOs9qDQJ23+F487+*zBt z9}%XeaxuKeD>|ECOi+~$rVn5Lk;^z3;sMaBRBf^2@*#SCd-1Aie)h2vP~m|%<<&d- z?)R=9_n#Ak6Ulk9j85}MO?x2`PnM;*RFpy=9jN6qhsS)YMd8eH&elw5CvyB^_l517 zJO>3xQ3*IvphNLOeAHy)d&$AnAnJ|5Vc23EhCI?C1A}@mXJoRLwh(YrK!@P*8=V{F z$>EP6FN9T+28x>}ZCW;C zhNBCHur9`L!fCzaH+okmn%14t{8ednI54Ys+N zyt~LmBC=v!$!fv{h?_=+C1>H#@<{oS_=)%_K8~*nd!x1<{36_4K~+TvRhQ56t@!15HTxsKi=o^Lvr7`NT z^6ihlviy+Kp3qnWP6ynIhGZH4m8G;K>~lH&vR+L4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/themes/triangles/client/src/img/stores/googleplay-badge.svg b/themes/triangles/client/src/img/stores/googleplay-badge.svg new file mode 100644 index 000000000..9e33e3aa2 --- /dev/null +++ b/themes/triangles/client/src/img/stores/googleplay-badge.svg @@ -0,0 +1,429 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/themes/triangles/client/src/img/success.png b/themes/triangles/client/src/img/success.png new file mode 100644 index 0000000000000000000000000000000000000000..ee9d6841bbb7208220a831c1c42917eb8facffa5 GIT binary patch literal 3147 zcmV-R47Br!P)@~8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H13&=@CK~#9!?VWj0Ro5NIWpwOJQDRbwNDwr}T5D`fYExS^ZNS!! z({?)3WhP^$({{SdG*eL?n@DR!L!g~W+NQSuv}t7VDXU9R+=7CjxFCufZipa@@1Y=Q z0fk@B56^R+j|cDGckj99-22XPzT=Dw+=`0CLjfNNgnC0Sw=jZrT^)0zv?CqKirDB=}*$K3Qy8 z0m#XXB}uo)ojuDZ2JEA;mJ)zo>nkK+#i=mr-oOa>%SoIPs_I~$EjO(IrB0#^a(-M+lx<9lN`o{IqVIbS~NiSdXS z#6y4#+4;gG!%zX}@U9%;6Klk9xgi43i5)q%FXr4q1XBU%l&+lB6MGhMEK32>WhV=t zbS47Ol|1sgZ;tzc#kvYW*YL?@T;^I{Hcn3gY_d~L7n?2u(7S%QdRK>hyX-yq-N}z3 znAFu7~3~!grS-_H?$ML&Q-^1W1)o)xkf}aUEDS zXKy#WS(dKX@un&fK(6jp0srJ3cJN)TsPU9;tBFSyfR$Ka zd)I0$W;x!Q0MQcg=vhqQ*W9@QJld}-vWEn~JeRu~UMbZiL9{mkHcH5&!#{nO4}5H| z@5p!}0A^CoDwww?#ozPU=uLo133>$hpKXBJyN%r7clAAhXR=m5B>*NQI~L~d_Dh1w zrv#uS*ZU=G@!(^7r^-GIzAF>6VqwmwYy7El9_%i_G6~x|{4VmS0-$Z zh1t83)t=uncL6eabTBT7di&VFV8i+CU~jB~e_YuQf2hfVm_zS#hrdJs`1PG_Fk@S) zPXRE&n^(fD9Z71=G1FauGNU{At)rXZ(!Cb1Cyq;{{(89-7O-72H!A$IoZ(+0@TYHE zs|db(UMx;jdY)zO0<;*}!JEiWI!<)>YUjF|;d_VwY$$wOpI6t}#1;Obs;}|&gAf2c_BYu$vKA@y7)sad{=9{?}TNC zGnpRSw-7%0#NpX45cpABwZnHMY?}>U^|43<1y~_6d{?`1-vQrcBmu%d&l&#tyDhrH zcjfuf=MPq(0BNGbe#g z>Rjl$#TEX!JGYp^cjejK)$o>w3e-M>eC{6(T{j!bOv~xH8b;KQ(l`Gr0`~ zaOf?-%9?CLJN0sRtIw6Q`1keqTDihMBMAH_5|E$(eR>Pt7 zZATNlU{4kXzAH=wz#T1ICQ!)7J6_{G;p1gO!uLLn%;XaA!-^01&7!$UfH#P|;h%Ok zLHHK&;M2##0VX$rU)}ddXnt^q-pY4~L!-c(xknO=02F!J%GekAc5E`5&K^ zF<7?Vfqn_!M<(0jx|j_FVG5d#Lm;+O&PJHJB}r)bbfT!8=}_?353Gao+m`|g{z->9 z;A5ud*{(643_=Mi9DyOh3C9fxGYNdUXr#uF>0qpxbHq_E;R}|Cg7_VVEeKItHOoxx zxC!B7rWGXo#n2^T{#MTtxmDk8fGN3&rh`u}0Y5iMu|$jr`|&+n1%V%$6K@&#bO2~Oh*~5{gss_DhL8E;&g7qh z$YtVC0^VD+%iFG468@x|Rgb)ui37n3MZUptw{Cgs6_{*6_;d|G9hjw5ERT(whg&aO z7Ct6CFaGynree`X?%~6+eJjI%HfL2Yn6X?uO2B$`Z$r1!((vj0e*u{Fg%K#hEk-Mi zdmi>en29HLlnwq26_tDm%>2q2-1*b`zq`7^fAFXuUM@_tG<-Vue;%yx(x~Vm2|9Iw zfB#`$K)}b1ptE-+$ADE{8;3i7+B8al@V{KR-tzDt5B=Jjz-lj!T&h6=gg>X?-GG3P znO>CmHdyi1u}jM`pyffQgg<(Ls5@Zgmq%m4s2e>_Mev=yJ@BOff=_S$;=$^#kAHpt zI{4_~K^PeHz84YqDCS(PhDly(?pryb_PCp%<@NDYfC!Rq(IvoQh>Lb-K+^Fd*wuIz z>bu%ufAa-ccWM{RFMcnez|%#agENW~=V=yzN?f)qcl}Oe#7qFUrvj&xK)k#DsG@}VYI&*_fJ(63EcXL}B0+dw{Ca;_`XvCB zAj3?rfU~j$5e4zP{pF}dfFKgPlz{tCFIIUAGF$++rR4jh1l)=|-Ed#xZMXnbf(axi zqy&b~@jG5AT{}@LKg|MA2|`6%O>AWe$m3r3%ezyi>cLJI0jLC#QUdmQE_d~VIiIeX zr588d1aSK#ke35?+q&-0C{CQICo^3IaJwgvH-`Gx^=+S4kPxOfFWm)jdpeMhi26P1 zogvr!&#_>|L;$yOf($ugsBLEN#p1-RTAuT{3E(koEawi?*YbZjYiH6gSo2{nfZGI; zoJCY#!ljMe>CZHRjUfWKO(mzxMJ1!};33!hJ!+?PO(B5C$|AYEZ20qiPh?@jFOBA5 zxBzZz3~gpvZOl>v@@zjqz<;cC4WA_h@VGa~4@3#@4xc>NpPsiTWjap=Tm|shfm9v{ z65w?{JTJa!ZpoS-h|u>!2;i|HwbZH-gxcd;UfPlL6OsB{C;>d`6BLQ~#GqCJ{HUL@ z&327^+`lDaUkfRKN4c6*(PkNaNr10pCC~ErrWYkQ%Z2PeMs@COMQUL#i2&V5l z$fR#RVfp%-7WzFh>Gupad;ghUfDxn!p*NB%=z9@!=^Iij=)01y)3>Jg(KzUz)zW`c lLjO-1{TleSLaf+V{|9F)WMK{Ekgos$002ovPDHLkV1l3q_6h(1 literal 0 HcmV?d00001 diff --git a/themes/triangles/client/src/img/user.png b/themes/triangles/client/src/img/user.png new file mode 100644 index 0000000000000000000000000000000000000000..00941399d34f0f2d85d323f61daa67c2c00c494d GIT binary patch literal 2933 zcmV-*3ySoKP)>+Y_1buC zheF*1yf*GR)m>@otnGcw%sqGZe)vfU+q>t^|F^R<_ujcTYIJlo%4jV9WAaR39I1IcZ1yCpS zIY0`~LOAsCXaEv{=cNF_2S1lxgn0!F-rv{6d0@U2VDjK!2d)?(^zpb(O#~{z<1L%h zz`>Ihfp_q;fNbDV1ck2z&vq|HoB+`1Xdnp~jL_hfe8nynpNI$nprH$ZHW~ak_zd`^ zVFiH3O##fnhzxsNhFJ&KlZFxi8uS9|F_R$&2KI2yF@ylnkj21V8EVk<1HThm0M?5q z!wen#x-@Ay#-LU3Q-KpQK-IVm#0Vh(6gr)CbjaY;1?MwNCoKRJI2)*zVQI!~;5m{4 zKw;knT4hL@Q-{ZzlmKWYD~#$v`_Iw@bH~becUG-S%q-brIc!hQ%CTE>@`@1rH~t^~ z41WiI7ypKEd+>ucn}h)DJO4JN0|WtfTGFky((<~-_O71$J^cftp@4sjuYs?HuSraT zYBjD2!VN!$U7T<~AWPy*p3UYD9S&zzZK#1y2EI1F2fi1fQN^iT1YmRXE_461$D3Uj zt`?WMJ^jO41wR&iFMLmYZvqmW;3a^GcjNIpyUo>oPp{y|g71wr5Rf26R{^k#lJlU> z;E|J`7V-*yELaPHq3y-G2*4(4_H+EMA1JSBYNZms53C7m<0t@*x4fXK060v^Myhx< z^3f-GCAX-B?*nV&Ji7@;T&HL$KpM}}+_t|Sw^I$@2iC~>tH7*@061HPO<3U4u(#Ll zsCIk$ywnB28nIT+BZv!B6#(a}u_;U3%(j=$=Do z#TDQ{E@c6dl>%I)Smctph;09_P|%>BeJ>}(((xfktJ z7s0;&KLKzvB#H$t|I_wH(ZlzFZ6PYb%qX@a7P;cyPd?-@iU8Oaq5{l|V%wsTEAFGy z*NTiH0Jepw04t){-gxASJA0|%rcngIwottjh+=#5NC6^K0BaQ6B~uCznF16>v8__2 z0Ffy`B??e41&CMy8c~2ADL}*u(1`*(ASuA*0;d!}b9zyLhw=;%u>#yj0luVI+|iR) z3XLLwHTR!XJA#H#02jsLHvDC;VR{11e>-6#D!>C2powB}vGJxJqX>XY&=VEFjRI6q zowRWAT5*}^;a|S)yiN7^;*#@GRF-=mZ=;7B#TC_}hF@9V(it0Xb`w6nHnOvU6tjZv zQCu5%GVtsb8)v#heEXR*D8MP|`{X294%5Wjv`g+KzVLjis&La`#O6b=`CF3!uv2g`gE;3*M` z#;x4QFBQM@Sb;#{W9HL+)51GMDB4+3-$eJ~&*NPZXX+P7fL8JkhK%LUCb~0)Po2vz z6DWM-oDh$Ojp1QkbGw`1CJ`gcS6NyF6W{}-0KXTd@WgEg$(;rE9XVelSop{vlmbkn zIxoQA5N1tN8<~EArgql=;phJ*x&WLxc7cc+qBP*Wtyu+B1lX9gzgWOezti6eA)5ti zfVZT)u7UgBK&6$ng5CG~*y2xspNrIpH&+Myu$D9>mOOMdV9Tq;UhoxC%|Js zpGXZc?K;kFCGqsE9J`?5w@)Hw&vy)6A®tx`|Cc!K{R~B2jc1N|ng#YPY z01+^g*}>)+v;a(kg*ZQ7)0sYS5YJw62)S4!2N7^)@}f|(3`qbcL4r0*!k9bJT`jgp}PPI5fXrZ%isracGF5;IdBtzN$?_Y z46k-0FnAp9&IZ2EGXt&yC`6b8>;=AxfZ)FdvVb2@IQAq2P>3)S*uhSVMm*K;1fHjK z%t;BL5Mc(81b7S(JhovA@HE9^O;P}b2rkn{6#(u#g!>KLW&ICB?B8ANXZSm~9Q{dlI$z1o fINvtfu|oa}IMcn@FFyTa00000NkvXXu0mjfV5=n> literal 0 HcmV?d00001 diff --git a/themes/triangles/client/src/img/warning.png b/themes/triangles/client/src/img/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..c6acd953b142a8e43e293c353a60e6313a8d39eb GIT binary patch literal 4038 zcmZ8kc{CK>7k@Km%nZhmB)g(al4J@g%*Ybi@{R0UV<~HPGGh#)vXiopt-hj2MImcp zY$;2&EGf;GWSK0JWq#AYzjw}k@4R>Jx$oW2UEdvZQv)=Q7!LpdZFo`Fg0oWo5|W!U zw`;lda28mgj^Py~XG9{237kF3|DsJG09+w|2|^mC_HqtIgY>L}{_%4U3Uvu^1EHaz zO4oh80%A^>WZHKCL(h;uQN z$(4fz2^gkydwnSstYvq?z;jH?4dHpjOIsi?F~5mv-=X~B1+IfT=c@TV0XW+s38jp3 zIm`8ol6U1Y3*FZ4G|uCdEr5XCymsumB+=8f9+kg$8eP;^u9f!rnogW5jie4Qx&UZq%;ioEF43zF#7yy~zM-eI zO?nd_=$d}*1Zdj89sj6I?h<22KC>b!%CwF;CBL2iff7f&(SHL%Y3gOm9%rm>z`BQ< zags$B!oM7@imA=`0F#zKMyK{>K{K;0WBK$OE(4l07$MAiJTzu}?T}$Eu6~)JEgc2l zf=SEgU@ShC_l!E7E{fG`pTiY)xdsO#!EcwQcM9VpAIi>eEYTaUlkwl)(ssK%=%={( zyLCh_G^+Jtx;(pcl>ozzx!j?_lkK#SDH7&caoLxQxBIRw{7*%VTL*&Jr`VX~UcGAs z20}q%Os|>1VApPeKz|^ki=-gYS z9Y@17L~Ki*qd&(F9GL7rg&nPk`xaYE{WG~yqtOG|AGd;=yeICQ3?P8Wl(?P zFZ7V8e_`3g%Z~$IWK8`8B+2Uaw1D@FC+7 z-$J8b@&2r&R`X87_vAo=1n`SqVsgn1)n)~(GkKZ^+y9#v%9lDe!PdCl<}d?4l&Z)= zLn&Pk*;lOwuF)@Xq&>Lrv$yIxxEBk9P*f6q;C&Gxctr z>UH@f5#<~SKVIH7{8TvYp}h5)?t8@rj)0$ z=(|UP)`a4gb}Nfh$}-Aot~qRv-2NG&Pw?!YJ_L{-cI0;gx6C17f#<(JU-shN4s}Xm zsr`bk<+y&nAzmdTfd%Fm{E6d?o6#b_8AlL0OZR6X0~$FN0^=Va&iV>~OZ62FfY!fr z+G@7W*EE5ULNUF6+k=dc=OuP(y~w^QFmG}IU~z1DYm-#g5&k$r%25&3Dt&RF(P6Qy z!hED53NQMhpoEN40!T@U!U(PZ!f|Zebkh%KFy7&Ljg0S%@80Y9{`L*_p0Gq(-Xa_n zWwVhZa3tlb{&BvDJc*;YmC03wmleLmlfhCIJloH!f053eR9|}uD3}(DuE;rORKDF~ z8OF`gF;dSjm&vc;j;H-8dLiCxQey{Hz=`iQ)BzERRTZji7F7reYFTV&VRD5#mM;P( zOnzY7S=<_1pZRu#%rT>*GCwrZtj>RI@Q?16O+_|h&X4@_qT z{7RG|7N$Y~RTfv^0XLX@uwvGl4+Up|W7er#Ssc0MwmZo`pUyz>8<(%}N)*8iLT<*$ zO7nn-T=u0({;o{JGM*TzB-T(t)5aqsCE#OXne$tyGP|&oKl$KN3NP@5DkD^g0|1p@ z4(AS`US%VnTz+8Z^%`KGKzv~a`sOjR&m!IZiOqmEvGch4*@fGRIo{5gpM0we7|#}f z^CTPH)gw)SBkBRwS>h6OBTukHNYUs@{TU=wUvp<|w38svkaGfXF17 zWRj^r4BA@-?+x!da`Cm2p9d}nF!q(p6NOeC5{5={Kr7n#qM8v0M{@o_vGlW5W}(rbH_w)a22X{DsMW>q`VKlT zXk-Y086nM7%Po?soy))lmj}z#H&QEp9}4F_KI_@4j-*^)ygEts*RkiKn62eo$G03- zOi`)M-~=((Ir@vJgpNwW2eeFB3y_uX|F=QcfJs>&llMuD- z;!BoDIu2Ioq}!HTPb2l-x0v3yVVB%8h?y?A85*|%wy3iFW1CtV+P>P+YD!=Qp&6nh zpIe}j|SXo%pJ_yAdzPv0Hd-05QBz!d!eVX!e_ z-js2+CCJYh%8^@Wg9~fkF6Qu>bT&Ro^dIN&*5>tgan0OP%w$V%fA-%-^*%>}*H>=U z!jTaxwGN=$~V1I2jb9T`^h|OQK?o+N?D|4 z%z=m!488|@gJ9jym%RV<;yy5CvE=1Zp4VzOXMit87iM+GU@MG@F&tH|m(TSZJC+>> zVV7QsOjvSjJ`@G(vuKq+ovB&m7i#;On+EK^sDZq)O`%>Aot~S$T6tk$6miZ z37nON_ttN@aBChO!q9G=2`91_S^1OMbP-+Qzb~7M7k%ZmY*tzbI>2DcboU%unRGe_ z^g6>eZQIZhx<$yz=BE-TqN+ya*KDr>e5)V=r(T0o09y}kAUswax2W8N6l5)zy{FXT zBCBMbJKg8c8{RJV4?lJfI=NJj3;36BD84{BOqIIaih|ifAHPMW@E@HB`s-w(SAV(o z8BYmb{tLZdgx&WRQ_Flp++DL9J%{!bU5L_O@OV^m-WN_@GfqN+t#6a>ZJ)fIEc54H z_Ahf?jW>B!W)Hfw>LIikDG8h3(g;8IH78vtMT}9pZk9gqaafF_b|zx9h71z{H7U<{ z+UCstV>w|G^N5qtm{_m!4V!Cc_#$GTw0CkB!PSFBD(8Zm(v9+Jmfk6V{CR2+G|9x>_Jus|!Ap5u#`Lp(Fwz*3I*OOGUT(q-M!Afy{tE@U zJ2F=Cu$IgfDXw+ZF2+tPekHA`bg@1(%I2vkB|8wwf zRU;Ls;@*5M8xL*Febm=t`gFhF?0_;=b~>tWNj@wC1yr}RMl6=> zX6-I}2_osksR_lEmhYexY@erD5yZS_@mPt{mq!Rag+PIruP1DNV3J?1b5O|b#ua+1 zKK*Qlxf{Zjnf!_bXYF=)Kg>LK0X;cl;x&6Us;m43gbnAydy`^s!rJ|y34o}Mlo51w#*DkxKu*ZSO^rjzY4`S%iTMPk?%#D9Hq0u zXO7WS9c7da`CWXmSEK4H`Zo6u$TGFji_X0wF zYHis}o|pf`w?RCV<8AhzFDtAF=BVX(@&7)kpvZHP-ER?mQZc1eo%81ghI*#D&vcxr F{{xrIJYfI; literal 0 HcmV?d00001 diff --git a/themes/triangles/client/src/index.ts b/themes/triangles/client/src/index.ts new file mode 100644 index 000000000..802004a8f --- /dev/null +++ b/themes/triangles/client/src/index.ts @@ -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 () { + (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, (global as any).u2f); + 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, (global as any).u2f); + 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); +})(); diff --git a/themes/triangles/client/src/lib/GetPromised.ts b/themes/triangles/client/src/lib/GetPromised.ts new file mode 100644 index 000000000..779139654 --- /dev/null +++ b/themes/triangles/client/src/lib/GetPromised.ts @@ -0,0 +1,14 @@ +import BluebirdPromise = require("bluebird"); + +export default function ($: JQueryStatic, url: string, data: Object, fn: any, + dataType: string): BluebirdPromise { + return new BluebirdPromise((resolve, reject) => { + $.get(url, {}, undefined, dataType) + .done((data: any) => { + resolve(data); + }) + .fail((xhr: JQueryXHR, textStatus: string) => { + reject(textStatus); + }); + }); +} \ No newline at end of file diff --git a/themes/triangles/client/src/lib/INotifier.ts b/themes/triangles/client/src/lib/INotifier.ts new file mode 100644 index 000000000..df947538f --- /dev/null +++ b/themes/triangles/client/src/lib/INotifier.ts @@ -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; +} \ No newline at end of file diff --git a/themes/triangles/client/src/lib/Notifier.ts b/themes/triangles/client/src/lib/Notifier.ts new file mode 100644 index 000000000..c0252b9b9 --- /dev/null +++ b/themes/triangles/client/src/lib/Notifier.ts @@ -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('status %s\ + %s', 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); + } +} \ No newline at end of file diff --git a/themes/triangles/client/src/lib/QueryParametersRetriever.ts b/themes/triangles/client/src/lib/QueryParametersRetriever.ts new file mode 100644 index 000000000..a529adb6e --- /dev/null +++ b/themes/triangles/client/src/lib/QueryParametersRetriever.ts @@ -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, " ")); + } +} \ No newline at end of file diff --git a/themes/triangles/client/src/lib/SafeRedirect.ts b/themes/triangles/client/src/lib/SafeRedirect.ts new file mode 100644 index 000000000..7e7684b8a --- /dev/null +++ b/themes/triangles/client/src/lib/SafeRedirect.ts @@ -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(); +} \ No newline at end of file diff --git a/themes/triangles/client/src/lib/firstfactor/FirstFactorValidator.ts b/themes/triangles/client/src/lib/firstfactor/FirstFactorValidator.ts new file mode 100644 index 000000000..eaa496fdd --- /dev/null +++ b/themes/triangles/client/src/lib/firstfactor/FirstFactorValidator.ts @@ -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 { + return new BluebirdPromise(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)); + }); + }); +} diff --git a/themes/triangles/client/src/lib/firstfactor/UISelectors.ts b/themes/triangles/client/src/lib/firstfactor/UISelectors.ts new file mode 100644 index 000000000..0e971b3c3 --- /dev/null +++ b/themes/triangles/client/src/lib/firstfactor/UISelectors.ts @@ -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"; diff --git a/themes/triangles/client/src/lib/firstfactor/index.ts b/themes/triangles/client/src/lib/firstfactor/index.ts new file mode 100644 index 000000000..24affee2d --- /dev/null +++ b/themes/triangles/client/src/lib/firstfactor/index.ts @@ -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); + }); +} + diff --git a/themes/triangles/client/src/lib/reset-password/constants.ts b/themes/triangles/client/src/lib/reset-password/constants.ts new file mode 100644 index 000000000..d48d4e67d --- /dev/null +++ b/themes/triangles/client/src/lib/reset-password/constants.ts @@ -0,0 +1,2 @@ + +export const FORM_SELECTOR = ".form-signin"; \ No newline at end of file diff --git a/themes/triangles/client/src/lib/reset-password/reset-password-form.ts b/themes/triangles/client/src/lib/reset-password/reset-password-form.ts new file mode 100644 index 000000000..b94279cde --- /dev/null +++ b/themes/triangles/client/src/lib/reset-password/reset-password-form.ts @@ -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); + }); +} diff --git a/themes/triangles/client/src/lib/reset-password/reset-password-request.ts b/themes/triangles/client/src/lib/reset-password/reset-password-request.ts new file mode 100644 index 000000000..846226d75 --- /dev/null +++ b/themes/triangles/client/src/lib/reset-password/reset-password-request.ts @@ -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); + }); +} + diff --git a/themes/triangles/client/src/lib/secondfactor/TOTPValidator.ts b/themes/triangles/client/src/lib/secondfactor/TOTPValidator.ts new file mode 100644 index 000000000..5394139a6 --- /dev/null +++ b/themes/triangles/client/src/lib/secondfactor/TOTPValidator.ts @@ -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 { + return new BluebirdPromise(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)); + }); + }); +} \ No newline at end of file diff --git a/themes/triangles/client/src/lib/secondfactor/U2FValidator.ts b/themes/triangles/client/src/lib/secondfactor/U2FValidator.ts new file mode 100644 index 000000000..5812922f7 --- /dev/null +++ b/themes/triangles/client/src/lib/secondfactor/U2FValidator.ts @@ -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 { + return new BluebirdPromise(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 { + 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, $); + }); +} diff --git a/themes/triangles/client/src/lib/secondfactor/constants.ts b/themes/triangles/client/src/lib/secondfactor/constants.ts new file mode 100644 index 000000000..50bba7571 --- /dev/null +++ b/themes/triangles/client/src/lib/secondfactor/constants.ts @@ -0,0 +1,3 @@ + +export const TOTP_FORM_SELECTOR = ".form-signin.totp"; +export const TOTP_TOKEN_SELECTOR = ".form-signin #token"; diff --git a/themes/triangles/client/src/lib/secondfactor/index.ts b/themes/triangles/client/src/lib/secondfactor/index.ts new file mode 100644 index 000000000..279723dce --- /dev/null +++ b/themes/triangles/client/src/lib/secondfactor/index.ts @@ -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); + }); +} \ No newline at end of file diff --git a/themes/triangles/client/src/lib/totp-register/totp-register.ts b/themes/triangles/client/src/lib/totp-register/totp-register.ts new file mode 100644 index 000000000..6a9aa7ee0 --- /dev/null +++ b/themes/triangles/client/src/lib/totp-register/totp-register.ts @@ -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); +} diff --git a/themes/triangles/client/src/lib/totp-register/ui-selector.ts b/themes/triangles/client/src/lib/totp-register/ui-selector.ts new file mode 100644 index 000000000..9d43fabea --- /dev/null +++ b/themes/triangles/client/src/lib/totp-register/ui-selector.ts @@ -0,0 +1,2 @@ + +export const QRCODE_ID_SELECTOR = "#qrcode"; \ No newline at end of file diff --git a/themes/triangles/client/src/lib/u2f-register/u2f-register.ts b/themes/triangles/client/src/lib/u2f-register/u2f-register.ts new file mode 100644 index 000000000..abf40ee05 --- /dev/null +++ b/themes/triangles/client/src/lib/u2f-register/u2f-register.ts @@ -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 { + return new BluebirdPromise(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 { + 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); + }); + }); +} diff --git a/themes/triangles/client/src/thirdparties/qrcode.min.js b/themes/triangles/client/src/thirdparties/qrcode.min.js new file mode 100644 index 000000000..993e88f39 --- /dev/null +++ b/themes/triangles/client/src/thirdparties/qrcode.min.js @@ -0,0 +1 @@ +var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); \ No newline at end of file diff --git a/themes/triangles/client/src/thirdparties/u2f-api.js b/themes/triangles/client/src/thirdparties/u2f-api.js new file mode 100644 index 000000000..8c7801e3b --- /dev/null +++ b/themes/triangles/client/src/thirdparties/u2f-api.js @@ -0,0 +1,749 @@ +//Copyright 2014-2015 Google Inc. All rights reserved. + +//Use of this source code is governed by a BSD-style +//license that can be found in the LICENSE file or at +//https://developers.google.com/open-source/licenses/bsd + +/** + * @fileoverview The U2F api. + */ +'use strict'; + + +/** + * Namespace for the U2F api. + * @type {Object} + */ +var u2f = u2f || {}; + +/** + * FIDO U2F Javascript API Version + * @number + */ +var js_api_version; + +/** + * The U2F extension id + * @const {string} + */ +// The Chrome packaged app extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the package Chrome app and does not require installing the U2F Chrome extension. + u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; +// The U2F Chrome extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the U2F Chrome extension to authenticate. +// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; + + +/** + * Message types for messsages to/from the extension + * @const + * @enum {string} + */ +u2f.MessageTypes = { + 'U2F_REGISTER_REQUEST': 'u2f_register_request', + 'U2F_REGISTER_RESPONSE': 'u2f_register_response', + 'U2F_SIGN_REQUEST': 'u2f_sign_request', + 'U2F_SIGN_RESPONSE': 'u2f_sign_response', + 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', + 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' +}; + + +/** + * Response status codes + * @const + * @enum {number} + */ +u2f.ErrorCodes = { + 'OK': 0, + 'OTHER_ERROR': 1, + 'BAD_REQUEST': 2, + 'CONFIGURATION_UNSUPPORTED': 3, + 'DEVICE_INELIGIBLE': 4, + 'TIMEOUT': 5 +}; + + +/** + * A message for registration requests + * @typedef {{ + * type: u2f.MessageTypes, + * appId: ?string, + * timeoutSeconds: ?number, + * requestId: ?number + * }} + */ +u2f.U2fRequest; + + +/** + * A message for registration responses + * @typedef {{ + * type: u2f.MessageTypes, + * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), + * requestId: ?number + * }} + */ +u2f.U2fResponse; + + +/** + * An error object for responses + * @typedef {{ + * errorCode: u2f.ErrorCodes, + * errorMessage: ?string + * }} + */ +u2f.Error; + +/** + * Data object for a single sign request. + * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} + */ +u2f.Transport; + + +/** + * Data object for a single sign request. + * @typedef {Array} + */ +u2f.Transports; + +/** + * Data object for a single sign request. + * @typedef {{ + * version: string, + * challenge: string, + * keyHandle: string, + * appId: string + * }} + */ +u2f.SignRequest; + + +/** + * Data object for a sign response. + * @typedef {{ + * keyHandle: string, + * signatureData: string, + * clientData: string + * }} + */ +u2f.SignResponse; + + +/** + * Data object for a registration request. + * @typedef {{ + * version: string, + * challenge: string + * }} + */ +u2f.RegisterRequest; + + +/** + * Data object for a registration response. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: Transports, + * appId: string + * }} + */ +u2f.RegisterResponse; + + +/** + * Data object for a registered key. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: ?Transports, + * appId: ?string + * }} + */ +u2f.RegisteredKey; + + +/** + * Data object for a get API register response. + * @typedef {{ + * js_api_version: number + * }} + */ +u2f.GetJsApiVersionResponse; + + +//Low level MessagePort API support + +/** + * Sets up a MessagePort to the U2F extension using the + * available mechanisms. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + */ +u2f.getMessagePort = function(callback) { + if (typeof chrome != 'undefined' && chrome.runtime) { + // The actual message here does not matter, but we need to get a reply + // for the callback to run. Thus, send an empty signature request + // in order to get a failure response. + var msg = { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: [] + }; + chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { + if (!chrome.runtime.lastError) { + // We are on a whitelisted origin and can talk directly + // with the extension. + u2f.getChromeRuntimePort_(callback); + } else { + // chrome.runtime was available, but we couldn't message + // the extension directly, use iframe + u2f.getIframePort_(callback); + } + }); + } else if (u2f.isAndroidChrome_()) { + u2f.getAuthenticatorPort_(callback); + } else if (u2f.isIosChrome_()) { + u2f.getIosPort_(callback); + } else { + // chrome.runtime was not available at all, which is normal + // when this origin doesn't have access to any extensions. + u2f.getIframePort_(callback); + } +}; + +/** + * Detect chrome running on android based on the browser's useragent. + * @private + */ +u2f.isAndroidChrome_ = function() { + var userAgent = navigator.userAgent; + return userAgent.indexOf('Chrome') != -1 && + userAgent.indexOf('Android') != -1; +}; + +/** + * Detect chrome running on iOS based on the browser's platform. + * @private + */ +u2f.isIosChrome_ = function() { + return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; +}; + +/** + * Connects directly to the extension via chrome.runtime.connect. + * @param {function(u2f.WrappedChromeRuntimePort_)} callback + * @private + */ +u2f.getChromeRuntimePort_ = function(callback) { + var port = chrome.runtime.connect(u2f.EXTENSION_ID, + {'includeTlsChannelId': true}); + setTimeout(function() { + callback(new u2f.WrappedChromeRuntimePort_(port)); + }, 0); +}; + +/** + * Return a 'port' abstraction to the Authenticator app. + * @param {function(u2f.WrappedAuthenticatorPort_)} callback + * @private + */ +u2f.getAuthenticatorPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedAuthenticatorPort_()); + }, 0); +}; + +/** + * Return a 'port' abstraction to the iOS client app. + * @param {function(u2f.WrappedIosPort_)} callback + * @private + */ +u2f.getIosPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedIosPort_()); + }, 0); +}; + +/** + * A wrapper for chrome.runtime.Port that is compatible with MessagePort. + * @param {Port} port + * @constructor + * @private + */ +u2f.WrappedChromeRuntimePort_ = function(port) { + this.port_ = port; +}; + +/** + * Format and return a sign request compliant with the JS API version supported by the extension. + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatSignRequest_ = + function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: challenge, + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: signRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + appId: appId, + challenge: challenge, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; +}; + +/** + * Format and return a register request compliant with the JS API version supported by the extension.. + * @param {Array} signRequests + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatRegisterRequest_ = + function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + for (var i = 0; i < registerRequests.length; i++) { + registerRequests[i].appId = appId; + } + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: registerRequests[0], + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + signRequests: signRequests, + registerRequests: registerRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + appId: appId, + registerRequests: registerRequests, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; +}; + + +/** + * Posts a message on the underlying channel. + * @param {Object} message + */ +u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { + this.port_.postMessage(message); +}; + + +/** + * Emulates the HTML 5 addEventListener interface. Works only for the + * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedChromeRuntimePort_.prototype.addEventListener = + function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message' || name == 'onmessage') { + this.port_.onMessage.addListener(function(message) { + // Emulate a minimal MessageEvent object + handler({'data': message}); + }); + } else { + console.error('WrappedChromeRuntimePort only supports onMessage'); + } +}; + +/** + * Wrap the Authenticator app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedAuthenticatorPort_ = function() { + this.requestId_ = -1; + this.requestObject_ = null; +} + +/** + * Launch the Authenticator intent. + * @param {Object} message + */ +u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { + var intentUrl = + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + + ';S.request=' + encodeURIComponent(JSON.stringify(message)) + + ';end'; + document.location = intentUrl; +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { + return "WrappedAuthenticatorPort_"; +}; + + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message') { + var self = this; + /* Register a callback to that executes when + * chrome injects the response. */ + window.addEventListener( + 'message', self.onRequestUpdate_.bind(self, handler), false); + } else { + console.error('WrappedAuthenticatorPort only supports message'); + } +}; + +/** + * Callback invoked when a response is received from the Authenticator. + * @param function({data: Object}) callback + * @param {Object} message message Object + */ +u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = + function(callback, message) { + var messageObject = JSON.parse(message.data); + var intentUrl = messageObject['intentURL']; + + var errorCode = messageObject['errorCode']; + var responseObject = null; + if (messageObject.hasOwnProperty('data')) { + responseObject = /** @type {Object} */ ( + JSON.parse(messageObject['data'])); + } + + callback({'data': responseObject}); +}; + +/** + * Base URL for intents to Authenticator. + * @const + * @private + */ +u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = + 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; + +/** + * Wrap the iOS client app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedIosPort_ = function() {}; + +/** + * Launch the iOS client app request + * @param {Object} message + */ +u2f.WrappedIosPort_.prototype.postMessage = function(message) { + var str = JSON.stringify(message); + var url = "u2f://auth?" + encodeURI(str); + location.replace(url); +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedIosPort_.prototype.getPortType = function() { + return "WrappedIosPort_"; +}; + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name !== 'message') { + console.error('WrappedIosPort only supports message'); + } +}; + +/** + * Sets up an embedded trampoline iframe, sourced from the extension. + * @param {function(MessagePort)} callback + * @private + */ +u2f.getIframePort_ = function(callback) { + // Create the iframe + var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; + var iframe = document.createElement('iframe'); + iframe.src = iframeOrigin + '/u2f-comms.html'; + iframe.setAttribute('style', 'display:none'); + document.body.appendChild(iframe); + + var channel = new MessageChannel(); + var ready = function(message) { + if (message.data == 'ready') { + channel.port1.removeEventListener('message', ready); + callback(channel.port1); + } else { + console.error('First event on iframe port was not "ready"'); + } + }; + channel.port1.addEventListener('message', ready); + channel.port1.start(); + + iframe.addEventListener('load', function() { + // Deliver the port to the iframe and initialize + iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); + }); +}; + + +//High-level JS API + +/** + * Default extension response timeout in seconds. + * @const + */ +u2f.EXTENSION_TIMEOUT_SEC = 30; + +/** + * A singleton instance for a MessagePort to the extension. + * @type {MessagePort|u2f.WrappedChromeRuntimePort_} + * @private + */ +u2f.port_ = null; + +/** + * Callbacks waiting for a port + * @type {Array} + * @private + */ +u2f.waitingForPort_ = []; + +/** + * A counter for requestIds. + * @type {number} + * @private + */ +u2f.reqCounter_ = 0; + +/** + * A map from requestIds to client callbacks + * @type {Object.} + * @private + */ +u2f.callbackMap_ = {}; + +/** + * Creates or retrieves the MessagePort singleton to use. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + * @private + */ +u2f.getPortSingleton_ = function(callback) { + if (u2f.port_) { + callback(u2f.port_); + } else { + if (u2f.waitingForPort_.length == 0) { + u2f.getMessagePort(function(port) { + u2f.port_ = port; + u2f.port_.addEventListener('message', + /** @type {function(Event)} */ (u2f.responseHandler_)); + + // Careful, here be async callbacks. Maybe. + while (u2f.waitingForPort_.length) + u2f.waitingForPort_.shift()(u2f.port_); + }); + } + u2f.waitingForPort_.push(callback); + } +}; + +/** + * Handles response messages from the extension. + * @param {MessageEvent.} message + * @private + */ +u2f.responseHandler_ = function(message) { + var response = message.data; + var reqId = response['requestId']; + if (!reqId || !u2f.callbackMap_[reqId]) { + console.error('Unknown or missing requestId in response.'); + return; + } + var cb = u2f.callbackMap_[reqId]; + delete u2f.callbackMap_[reqId]; + cb(response['responseData']); +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the sign request. + * @param {string=} appId + * @param {string=} challenge + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual sign request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual sign request in the supported API version. + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * @param {string=} appId + * @param {string=} challenge + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the register request. + * @param {string=} appId + * @param {Array} registerRequests + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual register request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual register request in the supported API version. + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * @param {string=} appId + * @param {Array} registerRequests + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatRegisterRequest_( + appId, registeredKeys, registerRequests, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + + +/** + * Dispatches a message to the extension to find out the supported + * JS API version. + * If the user is on a mobile phone and is thus using Google Authenticator instead + * of the Chrome extension, don't send the request and simply return 0. + * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.getApiVersion = function(callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + // If we are using Android Google Authenticator or iOS client app, + // do not fire an intent to ask which JS API version to use. + if (port.getPortType) { + var apiVersion; + switch (port.getPortType()) { + case 'WrappedIosPort_': + case 'WrappedAuthenticatorPort_': + apiVersion = 1.1; + break; + + default: + apiVersion = 0; + break; + } + callback({ 'js_api_version': apiVersion }); + return; + } + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var req = { + type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, + timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), + requestId: reqId + }; + port.postMessage(req); + }); +}; + diff --git a/themes/triangles/client/test/Notifier.test.ts b/themes/triangles/client/test/Notifier.test.ts new file mode 100644 index 000000000..70bfea146 --- /dev/null +++ b/themes/triangles/client/test/Notifier.test.ts @@ -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"); + }); +}); \ No newline at end of file diff --git a/themes/triangles/client/test/firstfactor/FirstFactorValidator.test.ts b/themes/triangles/client/test/firstfactor/FirstFactorValidator.test.ts new file mode 100644 index 000000000..ac8353278 --- /dev/null +++ b/themes/triangles/client/test/firstfactor/FirstFactorValidator.test.ts @@ -0,0 +1,44 @@ + +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", "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", "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."); + }); + }); +}); \ No newline at end of file diff --git a/themes/triangles/client/test/mocks/NotifierStub.ts b/themes/triangles/client/test/mocks/NotifierStub.ts new file mode 100644 index 000000000..9c268d66d --- /dev/null +++ b/themes/triangles/client/test/mocks/NotifierStub.ts @@ -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(); + } +} \ No newline at end of file diff --git a/themes/triangles/client/test/mocks/jquery.ts b/themes/triangles/client/test/mocks/jquery.ts new file mode 100644 index 000000000..273f90861 --- /dev/null +++ b/themes/triangles/client/test/mocks/jquery.ts @@ -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() + }; +} diff --git a/themes/triangles/client/test/mocks/u2f-api.ts b/themes/triangles/client/test/mocks/u2f-api.ts new file mode 100644 index 000000000..d123f6a95 --- /dev/null +++ b/themes/triangles/client/test/mocks/u2f-api.ts @@ -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() + }; +} \ No newline at end of file diff --git a/themes/triangles/client/test/secondfactor/TOTPValidator.test.ts b/themes/triangles/client/test/secondfactor/TOTPValidator.test.ts new file mode 100644 index 000000000..5dd6f15c3 --- /dev/null +++ b/themes/triangles/client/test/secondfactor/TOTPValidator.test.ts @@ -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(); + }); + }); +}); \ No newline at end of file diff --git a/themes/triangles/client/test/totp-register/totp-register.test.ts b/themes/triangles/client/test/totp-register/totp-register.test.ts new file mode 100644 index 000000000..86fc455a1 --- /dev/null +++ b/themes/triangles/client/test/totp-register/totp-register.test.ts @@ -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); + }); +}); \ No newline at end of file diff --git a/themes/triangles/client/tsconfig.json b/themes/triangles/client/tsconfig.json new file mode 100644 index 000000000..0bb4d62ff --- /dev/null +++ b/themes/triangles/client/tsconfig.json @@ -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/**/*" + ] +} diff --git a/themes/triangles/client/tslint.json b/themes/triangles/client/tslint.json new file mode 100644 index 000000000..c2c1b7501 --- /dev/null +++ b/themes/triangles/client/tslint.json @@ -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 + } +} diff --git a/themes/triangles/server/.directory b/themes/triangles/server/.directory new file mode 100644 index 000000000..b7754766c --- /dev/null +++ b/themes/triangles/server/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,58,20 +Version=3 +ViewMode=1 diff --git a/themes/triangles/server/src/index.ts b/themes/triangles/server/src/index.ts new file mode 100755 index 000000000..fcbf4d022 --- /dev/null +++ b/themes/triangles/server/src/index.ts @@ -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 "); + 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); diff --git a/themes/triangles/server/src/lib/.directory b/themes/triangles/server/src/lib/.directory new file mode 100644 index 000000000..006b379ad --- /dev/null +++ b/themes/triangles/server/src/lib/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,59,13 +Version=3 +ViewMode=1 diff --git a/themes/triangles/server/src/lib/AuthenticationSessionHandler.ts b/themes/triangles/server/src/lib/AuthenticationSessionHandler.ts new file mode 100644 index 000000000..57361bf8a --- /dev/null +++ b/themes/triangles/server/src/lib/AuthenticationSessionHandler.ts @@ -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; + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/ErrorReplies.ts b/themes/triangles/server/src/lib/ErrorReplies.ts new file mode 100644 index 000000000..f1c5f4fd1 --- /dev/null +++ b/themes/triangles/server/src/lib/ErrorReplies.ts @@ -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 }); +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/Exceptions.ts b/themes/triangles/server/src/lib/Exceptions.ts new file mode 100644 index 000000000..83fa4eb6b --- /dev/null +++ b/themes/triangles/server/src/lib/Exceptions.ts @@ -0,0 +1,88 @@ + +export class LdapSearchError extends Error { + constructor(message?: string) { + super(message); + this.name = "LdapSearchError"; + (Object).setPrototypeOf(this, LdapSearchError.prototype); + } +} + +export class LdapBindError extends Error { + constructor(message?: string) { + super(message); + this.name = "LdapBindError"; + (Object).setPrototypeOf(this, LdapBindError.prototype); + } +} + +export class LdapError extends Error { + constructor(message?: string) { + super(message); + this.name = "LdapError"; + (Object).setPrototypeOf(this, LdapError.prototype); + } +} + +export class IdentityError extends Error { + constructor(message?: string) { + super(message); + this.name = "IdentityError"; + (Object).setPrototypeOf(this, IdentityError.prototype); + } +} + +export class AccessDeniedError extends Error { + constructor(message?: string) { + super(message); + this.name = "AccessDeniedError"; + (Object).setPrototypeOf(this, AccessDeniedError.prototype); + } +} + +export class AuthenticationRegulationError extends Error { + constructor(message?: string) { + super(message); + this.name = "AuthenticationRegulationError"; + (Object).setPrototypeOf(this, AuthenticationRegulationError.prototype); + } +} + +export class InvalidTOTPError extends Error { + constructor(message?: string) { + super(message); + this.name = "InvalidTOTPError"; + (Object).setPrototypeOf(this, InvalidTOTPError.prototype); + } +} + +export class NotAuthenticatedError extends Error { + constructor(message?: string) { + super(message); + this.name = "NotAuthenticatedError"; + (Object).setPrototypeOf(this, NotAuthenticatedError.prototype); + } +} + +export class NotAuthorizedError extends Error { + constructor(message?: string) { + super(message); + this.name = "NotAuthanticatedError"; + (Object).setPrototypeOf(this, NotAuthorizedError.prototype); + } +} + +export class FirstFactorValidationError extends Error { + constructor(message?: string) { + super(message); + this.name = "FirstFactorValidationError"; + (Object).setPrototypeOf(this, FirstFactorValidationError.prototype); + } +} + +export class SecondFactorValidationError extends Error { + constructor(message?: string) { + super(message); + this.name = "SecondFactorValidationError"; + (Object).setPrototypeOf(this, FirstFactorValidationError.prototype); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/FirstFactorValidator.ts b/themes/triangles/server/src/lib/FirstFactorValidator.ts new file mode 100644 index 000000000..231060002 --- /dev/null +++ b/themes/triangles/server/src/lib/FirstFactorValidator.ts @@ -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 { + 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(); + }); +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/IdentityCheckMiddleware.spec.ts b/themes/triangles/server/src/lib/IdentityCheckMiddleware.spec.ts new file mode 100644 index 000000000..842ed6bcb --- /dev/null +++ b/themes/triangles/server/src/lib/IdentityCheckMiddleware.spec.ts @@ -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")); + }); + }); + }); +}); diff --git a/themes/triangles/server/src/lib/IdentityCheckMiddleware.ts b/themes/triangles/server/src/lib/IdentityCheckMiddleware.ts new file mode 100644 index 000000000..e72ea4db1 --- /dev/null +++ b/themes/triangles/server/src/lib/IdentityCheckMiddleware.ts @@ -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 { + + 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 { + 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 { + 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 { + + let authSession: AuthenticationSession; + const identityToken = objectPath.get( + 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 { + 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)); + }; +} diff --git a/themes/triangles/server/src/lib/IdentityCheckPreValidationTemplate.ts b/themes/triangles/server/src/lib/IdentityCheckPreValidationTemplate.ts new file mode 100644 index 000000000..0161ce406 --- /dev/null +++ b/themes/triangles/server/src/lib/IdentityCheckPreValidationTemplate.ts @@ -0,0 +1,3 @@ + + +export const PRE_VALIDATION_TEMPLATE = "need-identity-validation"; \ No newline at end of file diff --git a/themes/triangles/server/src/lib/IdentityValidable.ts b/themes/triangles/server/src/lib/IdentityValidable.ts new file mode 100644 index 000000000..075580c9e --- /dev/null +++ b/themes/triangles/server/src/lib/IdentityValidable.ts @@ -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; + postValidationInit(req: Express.Request): Bluebird; + + // 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; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/IdentityValidableStub.spec.ts b/themes/triangles/server/src/lib/IdentityValidableStub.spec.ts new file mode 100644 index 000000000..20a977140 --- /dev/null +++ b/themes/triangles/server/src/lib/IdentityValidableStub.spec.ts @@ -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 { + return this.preValidationInitStub(req); + } + + postValidationInit(req: Express.Request): Bluebird { + 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(); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/Server.spec.ts b/themes/triangles/server/src/lib/Server.spec.ts new file mode 100644 index 000000000..365163254 --- /dev/null +++ b/themes/triangles/server/src/lib/Server.spec.ts @@ -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(); + }); + }); +}); diff --git a/themes/triangles/server/src/lib/Server.ts b/themes/triangles/server/src/lib/Server.ts new file mode 100644 index 000000000..4090f6294 --- /dev/null +++ b/themes/triangles/server/src/lib/Server.ts @@ -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 { + 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((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 { + 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(); + } +} + diff --git a/themes/triangles/server/src/lib/ServerVariables.ts b/themes/triangles/server/src/lib/ServerVariables.ts new file mode 100644 index 000000000..cd3dd6dc8 --- /dev/null +++ b/themes/triangles/server/src/lib/ServerVariables.ts @@ -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; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/ServerVariablesInitializer.ts b/themes/triangles/server/src/lib/ServerVariablesInitializer.ts new file mode 100644 index 000000000..df79238cc --- /dev/null +++ b/themes/triangles/server/src/lib/ServerVariablesInitializer.ts @@ -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 { + 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 { + + 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); + }); + } +} diff --git a/themes/triangles/server/src/lib/ServerVariablesMockBuilder.spec.ts b/themes/triangles/server/src/lib/ServerVariablesMockBuilder.spec.ts new file mode 100644 index 000000000..7874702a0 --- /dev/null +++ b/themes/triangles/server/src/lib/ServerVariablesMockBuilder.spec.ts @@ -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 + }; + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/Level.ts b/themes/triangles/server/src/lib/authentication/Level.ts new file mode 100644 index 000000000..57b6a2346 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/Level.ts @@ -0,0 +1,5 @@ +export enum Level { + NOT_AUTHENTICATED = 0, + ONE_FACTOR = 1, + TWO_FACTOR = 2 +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/GroupsAndEmails.ts b/themes/triangles/server/src/lib/authentication/backends/GroupsAndEmails.ts new file mode 100644 index 000000000..3434ba66f --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/GroupsAndEmails.ts @@ -0,0 +1,5 @@ + +export interface GroupsAndEmails { + groups: string[]; + emails: string[]; +} diff --git a/themes/triangles/server/src/lib/authentication/backends/IUsersDatabase.ts b/themes/triangles/server/src/lib/authentication/backends/IUsersDatabase.ts new file mode 100644 index 000000000..d7fa13b7a --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/IUsersDatabase.ts @@ -0,0 +1,10 @@ +import Bluebird = require("bluebird"); + +import { GroupsAndEmails } from "./GroupsAndEmails"; + +export interface IUsersDatabase { + checkUserPassword(username: string, password: string): Bluebird; + getEmails(username: string): Bluebird; + getGroups(username: string): Bluebird; + updatePassword(username: string, newPassword: string): Bluebird; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts b/themes/triangles/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts new file mode 100644 index 000000000..19341a5dd --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts @@ -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 { + return this.checkUserPasswordStub(username, password); + } + + getEmails(username: string): Bluebird { + return this.getEmailsStub(username); + } + + getGroups(username: string): Bluebird { + return this.getGroupsStub(username); + } + + updatePassword(username: string, newPassword: string): Bluebird { + return this.updatePasswordStub(username, newPassword); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts b/themes/triangles/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts new file mode 100644 index 000000000..a258a78f7 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts @@ -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()); + }); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/file/FileUsersDatabase.ts b/themes/triangles/server/src/lib/authentication/backends/file/FileUsersDatabase.ts new file mode 100644 index 000000000..d34dde21e --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/file/FileUsersDatabase.ts @@ -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 { + return new Bluebird((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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + return this.readDatabase() + .then((database) => { + return this.checkUserExists(database, username) + .then(() => this.retrieveEmails(database, username)); + }); + } + + getGroups(username: string): Bluebird { + return this.readDatabase() + .then((database) => { + return this.checkUserExists(database, username) + .then(() => this.retrieveGroups(database, username)); + }); + } + + updatePassword(username: string, newPassword: string): Bluebird { + return this.readDatabase() + .then((database) => { + return this.checkUserExists(database, username) + .then(() => this.replacePassword(database, username, newPassword)); + }); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/file/ReadWriteQueue.ts b/themes/triangles/server/src/lib/authentication/backends/file/ReadWriteQueue.ts new file mode 100644 index 000000000..957ddaec5 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/file/ReadWriteQueue.ts @@ -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(); + } + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/ISession.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/ISession.ts new file mode 100644 index 000000000..da2c74433 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/ldap/ISession.ts @@ -0,0 +1,12 @@ + +import BluebirdPromise = require("bluebird"); + +export interface ISession { + open(): BluebirdPromise; + close(): BluebirdPromise; + + searchUserDn(username: string): BluebirdPromise; + searchEmails(username: string): BluebirdPromise; + searchGroups(username: string): BluebirdPromise; + modifyPassword(username: string, newPassword: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/ISessionFactory.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/ISessionFactory.ts new file mode 100644 index 000000000..014d1eea5 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/ldap/ISessionFactory.ts @@ -0,0 +1,6 @@ + +import { ISession } from "./ISession"; + +export interface ISessionFactory { + create(userDN: string, password: string): ISession; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts new file mode 100644 index 000000000..f4a6e6306 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts @@ -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); + }) + }); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts new file mode 100644 index 000000000..edda62ec6 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts @@ -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 = (session: ISession) => Bluebird; + +export class LdapUsersDatabase implements IUsersDatabase { + private sessionFactory: ISessionFactory; + private configuration: LdapConfiguration; + + constructor( + sessionFactory: ISessionFactory, + configuration: LdapConfiguration) { + this.sessionFactory = sessionFactory; + this.configuration = configuration; + } + + private withSession( + username: string, + password: string, + cb: SessionCallback): Bluebird { + const session = this.sessionFactory.create(username, password); + return session.open() + .then(() => cb(session)) + .finally(() => session.close()); + } + + checkUserPassword(username: string, password: string): Bluebird { + const that = this; + function verifyUserPassword(userDN: string) { + return that.withSession( + 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 { + 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 { + 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 { + 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)); + }); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts new file mode 100644 index 000000000..9dedfcb7c --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts @@ -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"); + }); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/SafeSession.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/SafeSession.ts new file mode 100644 index 000000000..572209066 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/ldap/SafeSession.ts @@ -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 { + return this.sesion.open(); + } + + close(): BluebirdPromise { + return this.sesion.close(); + } + + searchGroups(username: string): BluebirdPromise { + 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 { + 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 { + 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 { + try { + const sanitizedUsername = Sanitizer.sanitize(username); + return this.sesion.modifyPassword(sanitizedUsername, newPassword); + } + catch (e) { + return BluebirdPromise.reject(new Error(SPECIAL_CHAR_USED_MESSAGE)); + } + } +} diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts new file mode 100644 index 000000000..9dd33fed4 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts @@ -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 { 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); + }); +}); diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/Sanitizer.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/Sanitizer.ts new file mode 100644 index 000000000..be74132ad --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/ldap/Sanitizer.ts @@ -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; + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/Session.spec.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/Session.spec.ts new file mode 100644 index 000000000..d55f6a805 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/ldap/Session.spec.ts @@ -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"]); + }); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/Session.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/Session.ts new file mode 100644 index 000000000..e0284b3c4 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/ldap/Session.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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(); + }); + } +} diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/SessionFactory.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/SessionFactory.ts new file mode 100644 index 000000000..0b6c4bff3 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/ldap/SessionFactory.ts @@ -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 + ) + ); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts new file mode 100644 index 000000000..face3930d --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts @@ -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); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts new file mode 100644 index 000000000..5faf2ba18 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts @@ -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 { + return this.openStub(); + } + + close(): Bluebird { + return this.closeStub(); + } + + searchUserDn(username: string): Bluebird { + return this.searchUserDnStub(username); + } + + searchEmails(username: string): Bluebird { + return this.searchEmailsStub(username); + } + + searchGroups(username: string): Bluebird { + return this.searchGroupsStub(username); + } + + modifyPassword(username: string, newPassword: string): Bluebird { + return this.modifyPasswordStub(username, newPassword); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/connector/Connector.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/connector/Connector.ts new file mode 100644 index 000000000..2542ea7f9 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/ldap/connector/Connector.ts @@ -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; + unbindAsync(): Bluebird; + searchAsync(base: string, query: LdapJs.SearchOptions): Bluebird; + modifyAsync(userdn: string, change: LdapJs.Change): Bluebird; +} + +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 { + return this.client.bindAsync(username, password); + } + + unbindAsync(): Bluebird { + return this.client.unbindAsync(); + } + + searchAsync(base: string, query: any): Bluebird { + const that = this; + return this.client.searchAsync(base, query) + .then(function (res: EventEmitter) { + const doc: SearchEntry[] = []; + return new Bluebird((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 { + return this.client.modifyAsync(dn, changeRequest); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts new file mode 100644 index 000000000..61fef07a4 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts @@ -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); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts new file mode 100644 index 000000000..d11fa6386 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts @@ -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(); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts new file mode 100644 index 000000000..0b78225bf --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts @@ -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 { + return this.bindAsyncStub(username, password); + } + + unbindAsync(): BluebirdPromise { + return this.unbindAsyncStub(); + } + + searchAsync(base: string, query: any): BluebirdPromise { + return this.searchAsyncStub(base, query); + } + + modifyAsync(dn: string, changeRequest: any): BluebirdPromise { + return this.modifyAsyncStub(dn, changeRequest); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/connector/IConnector.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/connector/IConnector.ts new file mode 100644 index 000000000..1e63ab193 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/ldap/connector/IConnector.ts @@ -0,0 +1,9 @@ +import Bluebird = require("bluebird"); +import EventEmitter = require("events"); + +export interface IConnector { + bindAsync(username: string, password: string): Bluebird; + unbindAsync(): Bluebird; + searchAsync(base: string, query: any): Bluebird; + modifyAsync(dn: string, changeRequest: any): Bluebird; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts new file mode 100644 index 000000000..f9ed65eff --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts @@ -0,0 +1,5 @@ +import { IConnector } from "./IConnector"; + +export interface IConnectorFactory { + create(): IConnector; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/totp/ITotpHandler.ts b/themes/triangles/server/src/lib/authentication/totp/ITotpHandler.ts new file mode 100644 index 000000000..d600d31e4 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/totp/ITotpHandler.ts @@ -0,0 +1,6 @@ +import { TOTPSecret } from "../../../../types/TOTPSecret"; + +export interface ITotpHandler { + generate(label: string, issuer: string): TOTPSecret; + validate(token: string, secret: string): boolean; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/totp/TotpHandler.spec.ts b/themes/triangles/server/src/lib/authentication/totp/TotpHandler.spec.ts new file mode 100644 index 000000000..67cffa638 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/totp/TotpHandler.spec.ts @@ -0,0 +1,39 @@ + +import { TotpHandler } from "./TotpHandler"; +import Sinon = require("sinon"); +import Speakeasy = require("speakeasy"); +import Assert = require("assert"); + +describe("authentication/totp/TotpHandler", function() { + let totpValidator: TotpHandler; + let validateStub: Sinon.SinonStub; + + beforeEach(() => { + validateStub = Sinon.stub(Speakeasy.totp, "verify"); + totpValidator = new TotpHandler(Speakeasy); + }); + + afterEach(function() { + validateStub.restore(); + }); + + it("should validate the TOTP token", function() { + const totp_secret = "NBD2ZV64R9UV1O7K"; + const token = "token"; + validateStub.withArgs({ + secret: totp_secret, + token: token, + encoding: "base32", + window: 1 + }).returns(true); + Assert(totpValidator.validate(token, totp_secret)); + }); + + it("should not validate a wrong TOTP token", function() { + const totp_secret = "NBD2ZV64R9UV1O7K"; + const token = "wrong token"; + validateStub.returns(false); + Assert(!totpValidator.validate(token, totp_secret)); + }); +}); + diff --git a/themes/triangles/server/src/lib/authentication/totp/TotpHandler.ts b/themes/triangles/server/src/lib/authentication/totp/TotpHandler.ts new file mode 100644 index 000000000..dfab502a7 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/totp/TotpHandler.ts @@ -0,0 +1,36 @@ +import { ITotpHandler } from "./ITotpHandler"; +import { TOTPSecret } from "../../../../types/TOTPSecret"; +import Speakeasy = require("speakeasy"); + +const TOTP_ENCODING = "base32"; +const WINDOW: number = 1; + +export class TotpHandler implements ITotpHandler { + private speakeasy: typeof Speakeasy; + + constructor(speakeasy: typeof Speakeasy) { + this.speakeasy = speakeasy; + } + + generate(label: string, issuer: string): TOTPSecret { + const secret = this.speakeasy.generateSecret({ + otpauth_url: false + }) as TOTPSecret; + + secret.otpauth_url = this.speakeasy.otpauthURL({ + secret: secret.ascii, + label: label, + issuer: issuer + }); + return secret; + } + + validate(token: string, secret: string): boolean { + return this.speakeasy.totp.verify({ + secret: secret, + encoding: TOTP_ENCODING, + token: token, + window: WINDOW + }); + } +} diff --git a/themes/triangles/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts b/themes/triangles/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts new file mode 100644 index 000000000..ea93330d9 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts @@ -0,0 +1,22 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import { ITotpHandler } from "./ITotpHandler"; +import { TOTPSecret } from "../../../../types/TOTPSecret"; + +export class TotpHandlerStub implements ITotpHandler { + generateStub: Sinon.SinonStub; + validateStub: Sinon.SinonStub; + + constructor() { + this.generateStub = Sinon.stub(); + this.validateStub = Sinon.stub(); + } + + generate(label: string, issuer: string): TOTPSecret { + return this.generateStub(label, issuer); + } + + validate(token: string, secret: string): boolean { + return this.validateStub(token, secret); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/u2f/IU2fHandler.ts b/themes/triangles/server/src/lib/authentication/u2f/IU2fHandler.ts new file mode 100644 index 000000000..b9b7d6f26 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/u2f/IU2fHandler.ts @@ -0,0 +1,9 @@ +import U2f = require("u2f"); + +export interface IU2fHandler { + request(appId: string, keyHandle?: string): U2f.Request; + checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) + : U2f.RegistrationResult | U2f.Error; + checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) + : U2f.SignatureResult | U2f.Error; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/u2f/U2fHandler.ts b/themes/triangles/server/src/lib/authentication/u2f/U2fHandler.ts new file mode 100644 index 000000000..bf3891e5b --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/u2f/U2fHandler.ts @@ -0,0 +1,24 @@ +import { IU2fHandler } from "./IU2fHandler"; +import U2f = require("u2f"); + +export class U2fHandler implements IU2fHandler { + private u2f: typeof U2f; + + constructor(u2f: typeof U2f) { + this.u2f = u2f; + } + + request(appId: string, keyHandle?: string): U2f.Request { + return this.u2f.request(appId, keyHandle); + } + + checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) + : U2f.RegistrationResult | U2f.Error { + return this.u2f.checkRegistration(registrationRequest, registrationResponse); + } + + checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) + : U2f.SignatureResult | U2f.Error { + return this.u2f.checkSignature(signatureRequest, signatureResponse, publicKey); + } +} diff --git a/themes/triangles/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts b/themes/triangles/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts new file mode 100644 index 000000000..135d7eb06 --- /dev/null +++ b/themes/triangles/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts @@ -0,0 +1,31 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import U2f = require("u2f"); +import { IU2fHandler } from "./IU2fHandler"; + + +export class U2fHandlerStub implements IU2fHandler { + requestStub: Sinon.SinonStub; + checkRegistrationStub: Sinon.SinonStub; + checkSignatureStub: Sinon.SinonStub; + + constructor() { + this.requestStub = Sinon.stub(); + this.checkRegistrationStub = Sinon.stub(); + this.checkSignatureStub = Sinon.stub(); + } + + request(appId: string, keyHandle?: string): U2f.Request { + return this.requestStub(appId, keyHandle); + } + + checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) + : U2f.RegistrationResult | U2f.Error { + return this.checkRegistrationStub(registrationRequest, registrationResponse); + } + + checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) + : U2f.SignatureResult | U2f.Error { + return this.checkSignatureStub(signatureRequest, signatureResponse, publicKey); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authorization/Authorizer.spec.ts b/themes/triangles/server/src/lib/authorization/Authorizer.spec.ts new file mode 100644 index 000000000..586814042 --- /dev/null +++ b/themes/triangles/server/src/lib/authorization/Authorizer.spec.ts @@ -0,0 +1,372 @@ + +import Assert = require("assert"); +import winston = require("winston"); +import { Authorizer } from "./Authorizer"; +import { ACLConfiguration, ACLRule } from "../configuration/schema/AclConfiguration"; +import { Level } from "./Level"; + +describe("authorization/Authorizer", function () { + let authorizer: Authorizer; + let configuration: ACLConfiguration; + + describe("configuration is null", function() { + it("should allow access to anything, anywhere for anybody", function() { + configuration = undefined; + authorizer = new Authorizer(configuration, winston); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1", "group2"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "admin.example.com", resource: "/"}, {user: "user3", groups: ["group3"]}), Level.BYPASS); + }); + }); + + describe("configuration is not null", function () { + beforeEach(function () { + configuration = { + default_policy: "deny", + rules: [] + }; + authorizer = new Authorizer(configuration, winston); + }); + + describe("check access control with default policy to deny", function () { + beforeEach(function () { + configuration.default_policy = "deny"; + }); + + it("should deny access when no rule is provided", function () { + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should control access when multiple domain matcher is provided", function () { + configuration.rules = [{ + domain: "*.mail.example.com", + policy: "two_factor", + subject: "user:user1", + resources: [".*"] + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should allow access to all resources when resources is not provided", function () { + configuration.rules = [{ + domain: "*.mail.example.com", + policy: "two_factor", + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + describe("check user rules", function () { + it("should allow access when user has a matching allowing rule", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: [".*"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should deny to other users", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: [".*"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user2", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); + }); + + it("should allow user access only to specific resources", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: ["/private/.*", "^/begin", "/end$"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/middle/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/begin"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/not/begin"}, {user: "user1", groups: ["group1"]}), Level.DENY); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end/x"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should allow access to multiple domains", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: [".*"], + subject: "user:user1" + }, { + domain: "home1.example.com", + policy: "one_factor", + resources: [".*"], + subject: "user:user1" + }, { + domain: "home2.example.com", + policy: "deny", + resources: [".*"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home1.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); + Assert.equal(authorizer.authorization({domain: "home2.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home3.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + }); + + it("should apply rules in order", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "one_factor", + resources: ["/my/private/resource"], + subject: "user:user1" + }, { + domain: "home.example.com", + policy: "deny", + resources: ["^/my/private/.*"], + subject: "user:user1" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/my/.*"], + subject: "user:user1" + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/poney"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/duck"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/resource"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); + }); + }); + + describe("check group rules", function () { + it("should allow access when user is in group having a matching allowing rule", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: ["^/$"], + subject: "group:group1" + }, { + domain: "home.example.com", + policy: "one_factor", + resources: ["^/test$"], + subject: "group:group2" + }, { + domain: "home.example.com", + policy: "deny", + resources: ["^/private$"], + subject: "group:group2" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.ONE_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); + }); + }); + }); + + describe("check any rules", function () { + it("should control access when any rules are defined", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "bypass", + resources: ["^/public$"] + }, { + domain: "home.example.com", + policy: "deny", + resources: ["^/private$"] + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, + {user: "user4", groups: ["group5"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, + {user: "user4", groups: ["group5"]}), Level.DENY); + }); + }); + + describe("check access control with default policy to allow", function () { + beforeEach(function () { + configuration.default_policy = "bypass"; + }); + + it("should allow access to anything when no rule is provided", function () { + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + }); + + it("should deny access to one resource when defined", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["/test"], + subject: "user:user1" + }]; + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + }); + }); + + describe("check access control with complete use case", function () { + beforeEach(function () { + configuration.default_policy = "deny"; + }); + + it("should control access of multiple user (real use case)", function () { + // Let say we have three users: admin, john, harry. + // admin is in groups ["admins"] + // john is in groups ["dev", "admin-private"] + // harry is in groups ["dev"] + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: ["^/public$", "^/$"] + }, { + domain: "home.example.com", + policy: "two_factor", + resources: [".*"], + subject: "group:admins" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/private/?.*"], + subject: "group:admin-private" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/private/john$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/private/harry"], + subject: "user:harry" + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); + }); + + it("should allow when allowed at group level and denied at user level", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"], + subject: "group:dev" + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); + }); + + it("should allow access when allowed at 'any' level and denied at user level", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"] + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); + }); + + it("should allow access when allowed at 'any' level and denied at group level", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "group:dev" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"] + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); + }); + + it("should respect rules precedence", function () { + // the priority from least to most is 'default_policy', 'all', 'group', 'user' + // and the first rules in each category as a lower priority than the latest. + // You can think of it that way: they override themselves inside each category. + configuration.rules = [{ + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "group:dev" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"] + }]; + + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + }); + }); + }); +}); diff --git a/themes/triangles/server/src/lib/authorization/Authorizer.ts b/themes/triangles/server/src/lib/authorization/Authorizer.ts new file mode 100644 index 000000000..889b7ec20 --- /dev/null +++ b/themes/triangles/server/src/lib/authorization/Authorizer.ts @@ -0,0 +1,85 @@ + +import { ACLConfiguration, ACLPolicy, ACLRule } from "../configuration/schema/AclConfiguration"; +import { IAuthorizer } from "./IAuthorizer"; +import { Winston } from "../../../types/Dependencies"; +import { MultipleDomainMatcher } from "./MultipleDomainMatcher"; +import { Level } from "./Level"; +import { Object } from "./Object"; +import { Subject } from "./Subject"; + +function MatchDomain(actualDomain: string) { + return function (rule: ACLRule): boolean { + return MultipleDomainMatcher.match(actualDomain, rule.domain); + }; +} + +function MatchResource(actualResource: string) { + return function (rule: ACLRule): boolean { + // If resources key is not provided, the rule applies to all resources. + if (!rule.resources) return true; + + for (let i = 0; i < rule.resources.length; ++i) { + const regexp = new RegExp(rule.resources[i]); + if (regexp.test(actualResource)) return true; + } + return false; + }; +} + +function MatchSubject(subject: Subject) { + return (rule: ACLRule) => { + // If no subject, matches anybody + if (!rule.subject) return true; + + if (rule.subject.startsWith("user:")) { + const ruleUser = rule.subject.split(":")[1]; + if (subject.user == ruleUser) return true; + } + + if (rule.subject.startsWith("group:")) { + const ruleGroup = rule.subject.split(":")[1]; + if (subject.groups.indexOf(ruleGroup) > -1) return true; + } + return false; + }; +} + +export class Authorizer implements IAuthorizer { + private logger: Winston; + private readonly configuration: ACLConfiguration; + + constructor(configuration: ACLConfiguration, logger_: Winston) { + this.logger = logger_; + this.configuration = configuration; + } + + private getMatchingRules(object: Object, subject: Subject): ACLRule[] { + const rules = this.configuration.rules; + if (!rules) return []; + return rules + .filter(MatchDomain(object.domain)) + .filter(MatchResource(object.resource)) + .filter(MatchSubject(subject)); + } + + private ruleToLevel(policy: string): Level { + if (policy == "bypass") { + return Level.BYPASS; + } else if (policy == "one_factor") { + return Level.ONE_FACTOR; + } else if (policy == "two_factor") { + return Level.TWO_FACTOR; + } + return Level.DENY; + } + + authorization(object: Object, subject: Subject): Level { + if (!this.configuration) return Level.BYPASS; + + const rules = this.getMatchingRules(object, subject); + + return (rules.length > 0) + ? this.ruleToLevel(rules[0].policy) // extract the policy of the first matching rule + : this.ruleToLevel(this.configuration.default_policy); // otherwise use the default policy + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authorization/AuthorizerStub.spec.ts b/themes/triangles/server/src/lib/authorization/AuthorizerStub.spec.ts new file mode 100644 index 000000000..9bd6f4a85 --- /dev/null +++ b/themes/triangles/server/src/lib/authorization/AuthorizerStub.spec.ts @@ -0,0 +1,17 @@ +import Sinon = require("sinon"); +import { IAuthorizer } from "./IAuthorizer"; +import { Level } from "./Level"; +import { Object } from "./Object"; +import { Subject } from "./Subject"; + +export class AuthorizerStub implements IAuthorizer { + authorizationMock: Sinon.SinonStub; + + constructor() { + this.authorizationMock = Sinon.stub(); + } + + authorization(object: Object, subject: Subject): Level { + return this.authorizationMock(object, subject); + } +} diff --git a/themes/triangles/server/src/lib/authorization/IAuthorizer.ts b/themes/triangles/server/src/lib/authorization/IAuthorizer.ts new file mode 100644 index 000000000..fe7ba367a --- /dev/null +++ b/themes/triangles/server/src/lib/authorization/IAuthorizer.ts @@ -0,0 +1,7 @@ +import { Level } from "./Level"; +import { Subject } from "./Subject"; +import { Object } from "./Object"; + +export interface IAuthorizer { + authorization(object: Object, subject: Subject): Level; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authorization/Level.ts b/themes/triangles/server/src/lib/authorization/Level.ts new file mode 100644 index 000000000..d12802610 --- /dev/null +++ b/themes/triangles/server/src/lib/authorization/Level.ts @@ -0,0 +1,6 @@ +export enum Level { + BYPASS = 0, + ONE_FACTOR = 1, + TWO_FACTOR = 2, + DENY = 3 +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authorization/MultipleDomainMatcher.ts b/themes/triangles/server/src/lib/authorization/MultipleDomainMatcher.ts new file mode 100644 index 000000000..64c647a4a --- /dev/null +++ b/themes/triangles/server/src/lib/authorization/MultipleDomainMatcher.ts @@ -0,0 +1,12 @@ + +export class MultipleDomainMatcher { + static match(domain: string, pattern: string): boolean { + if (pattern.startsWith("*") && + domain.endsWith(pattern.substr(1))) { + return true; + } + else if (domain == pattern) { + return true; + } + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authorization/Object.ts b/themes/triangles/server/src/lib/authorization/Object.ts new file mode 100644 index 000000000..5411b0d24 --- /dev/null +++ b/themes/triangles/server/src/lib/authorization/Object.ts @@ -0,0 +1,5 @@ + +export interface Object { + domain: string; + resource: string; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authorization/Subject.ts b/themes/triangles/server/src/lib/authorization/Subject.ts new file mode 100644 index 000000000..310d6b4c0 --- /dev/null +++ b/themes/triangles/server/src/lib/authorization/Subject.ts @@ -0,0 +1,5 @@ + +export interface Subject { + user: string; + groups: string[]; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/ConfigurationParser.spec.ts b/themes/triangles/server/src/lib/configuration/ConfigurationParser.spec.ts new file mode 100644 index 000000000..60c0f6182 --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/ConfigurationParser.spec.ts @@ -0,0 +1,171 @@ +import * as Assert from "assert"; +import { Configuration } from "./schema/Configuration"; +import { ACLConfiguration } from "./schema/AclConfiguration"; +import { ConfigurationParser } from "./ConfigurationParser"; + +describe("configuration/ConfigurationParser", function () { + function buildYamlConfig(): Configuration { + const yaml_config: Configuration = { + port: 8080, + authentication_backend: { + ldap: { + url: "http://ldap", + base_dn: "dc=example,dc=com", + additional_users_dn: "ou=users", + additional_groups_dn: "ou=groups", + user: "user", + password: "pass" + }, + }, + session: { + domain: "example.com", + secret: "secret", + expiration: 40000 + }, + storage: { + local: { + path: "/mydirectory" + } + }, + regulation: { + max_retries: 3, + find_time: 5 * 60, + ban_time: 5 * 60 + }, + logs_level: "debug", + notifier: { + email: { + username: "user", + password: "password", + sender: "admin@example.com", + service: "gmail" + } + } + }; + return yaml_config; + } + + describe("port", function () { + it("should read the port from the yaml file", function () { + const yaml_config = buildYamlConfig(); + yaml_config.port = 7070; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.port, 7070); + }); + + it("should default the port to 8080 if not provided", function () { + const yaml_config = buildYamlConfig(); + delete yaml_config.port; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.port, 8080); + }); + }); + + describe("test session configuration", function() { + it("should get the session attributes", function () { + const yaml_config = buildYamlConfig(); + yaml_config.session = { + domain: "example.com", + secret: "secret", + expiration: 3600, + inactivity: 4000 + }; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.session.domain, "example.com"); + Assert.equal(config.session.secret, "secret"); + Assert.equal(config.session.expiration, 3600); + Assert.equal(config.session.inactivity, 4000); + }); + + it("should be ok not specifying inactivity", function () { + const yaml_config = buildYamlConfig(); + yaml_config.session = { + domain: "example.com", + secret: "secret", + expiration: 3600 + }; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.session.domain, "example.com"); + Assert.equal(config.session.secret, "secret"); + Assert.equal(config.session.expiration, 3600); + Assert.equal(config.session.inactivity, undefined); + }); + }); + + it("should get the log level", function () { + const yaml_config = buildYamlConfig(); + yaml_config.logs_level = "debug"; + const config = ConfigurationParser.parse(yaml_config); + Assert.equal(config.logs_level, "debug"); + }); + + it("should get the notifier config", function () { + const userConfig = buildYamlConfig(); + userConfig.notifier = { + email: { + username: "user", + password: "pass", + sender: "admin@example.com", + service: "gmail" + } + }; + const config = ConfigurationParser.parse(userConfig); + Assert.deepEqual(config.notifier, { + email: { + username: "user", + password: "pass", + sender: "admin@example.com", + service: "gmail" + } + }); + }); + + describe("access_control", function() { + it("should adapt access_control when it is already ok", function () { + const userConfig = buildYamlConfig(); + userConfig.access_control = { + default_policy: "deny", + rules: [{ + domain: "www.example.com", + policy: "two_factor", + subject: "user:user" + }, { + domain: "public.example.com", + policy: "two_factor" + }] + }; + const config = ConfigurationParser.parse(userConfig); + Assert.deepEqual(config.access_control, { + default_policy: "deny", + rules: [{ + domain: "www.example.com", + policy: "two_factor", + subject: "user:user" + }, { + domain: "public.example.com", + policy: "two_factor" + }] + } as ACLConfiguration); + }); + + + it("should adapt access_control when it is empty", function () { + const userConfig = buildYamlConfig(); + userConfig.access_control = {} as any; + const config = ConfigurationParser.parse(userConfig); + Assert.deepEqual(config.access_control, { + default_policy: "bypass", + rules: [] + }); + }); + }); + + describe("default_redirection_url", function() { + it("should parse default_redirection_url", function() { + const userConfig = buildYamlConfig(); + userConfig.default_redirection_url = "dummy_url"; + const config = ConfigurationParser.parse(userConfig); + Assert.deepEqual(config.default_redirection_url, "dummy_url"); + }); + }); +}); diff --git a/themes/triangles/server/src/lib/configuration/ConfigurationParser.ts b/themes/triangles/server/src/lib/configuration/ConfigurationParser.ts new file mode 100644 index 000000000..d92d163c4 --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/ConfigurationParser.ts @@ -0,0 +1,39 @@ + +import * as ObjectPath from "object-path"; +import { Configuration, complete } from "./schema/Configuration"; +import Ajv = require("ajv"); +import Path = require("path"); +import Util = require("util"); + +export class ConfigurationParser { + private static parseTypes(configuration: Configuration): string[] { + const schema = require(Path.resolve(__dirname, "./Configuration.schema.json")); + const ajv = new Ajv({ + allErrors: true, + missingRefs: "fail" + }); + ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json")); + const valid = ajv.validate(schema, configuration); + if (!valid) + return ajv.errors.map( + (e: Ajv.ErrorObject) => { return ajv.errorsText([e]); }); + return []; + } + + static parse(configuration: Configuration): Configuration { + const validationErrors = this.parseTypes(configuration); + if (validationErrors.length > 0) { + validationErrors.forEach((e: string) => { console.log(e); }); + throw new Error("Malformed configuration (schema). Please double-check your configuration file."); + } + + const [newConfiguration, completionErrors] = complete(configuration); + + if (completionErrors.length > 0) { + completionErrors.forEach((e: string) => { console.log(e); }); + throw new Error("Malformed configuration (validator). Please double-check your configuration file."); + } + return newConfiguration; + } +} + diff --git a/themes/triangles/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts b/themes/triangles/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts new file mode 100644 index 000000000..d4a3093ee --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts @@ -0,0 +1,149 @@ +import { SessionConfigurationBuilder } from "./SessionConfigurationBuilder"; +import { Configuration } from "./schema/Configuration"; +import { GlobalDependencies } from "../../../types/Dependencies"; + +import ExpressSession = require("express-session"); +import ConnectRedis = require("connect-redis"); +import Sinon = require("sinon"); +import Assert = require("assert"); + +describe("configuration/SessionConfigurationBuilder", function () { + const configuration: Configuration = { + access_control: { + default_policy: "deny", + rules: [] + }, + totp: { + issuer: "authelia.com" + }, + authentication_backend: { + ldap: { + url: "ldap://ldap", + user: "user", + base_dn: "dc=example,dc=com", + password: "password", + additional_groups_dn: "ou=groups", + additional_users_dn: "ou=users", + group_name_attribute: "", + groups_filter: "", + mail_attribute: "", + users_filter: "" + }, + }, + logs_level: "debug", + notifier: { + filesystem: { + filename: "/test" + } + }, + port: 8080, + session: { + name: "authelia_session", + domain: "example.com", + expiration: 3600, + secret: "secret" + }, + regulation: { + max_retries: 3, + ban_time: 5 * 60, + find_time: 5 * 60 + }, + storage: { + local: { + in_memory: true + } + } + }; + + const deps: GlobalDependencies = { + ConnectRedis: Sinon.spy() as any, + ldapjs: Sinon.spy() as any, + nedb: Sinon.spy() as any, + session: Sinon.spy() as any, + speakeasy: Sinon.spy() as any, + u2f: Sinon.spy() as any, + winston: Sinon.spy() as any, + Redis: Sinon.spy() as any + }; + + it("should return session options without redis options", function () { + const options = SessionConfigurationBuilder.build(configuration, deps); + const expectedOptions = { + name: "authelia_session", + secret: "secret", + resave: false, + saveUninitialized: true, + cookie: { + secure: true, + httpOnly: true, + maxAge: 3600, + domain: "example.com" + } + }; + + Assert.deepEqual(expectedOptions, options); + }); + + it("should return session options with redis options", function () { + configuration.session["redis"] = { + host: "redis.example.com", + port: 6379 + }; + const RedisStoreMock = Sinon.spy(); + const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); + + deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; + deps.Redis = { + createClient: Sinon.mock().returns(redisClient) + } as any; + + const options = SessionConfigurationBuilder.build(configuration, deps); + + const expectedOptions: ExpressSession.SessionOptions = { + secret: "secret", + resave: false, + saveUninitialized: true, + name: "authelia_session", + cookie: { + secure: true, + httpOnly: true, + maxAge: 3600, + domain: "example.com" + }, + store: Sinon.match.object as any + }; + + Assert((deps.ConnectRedis as Sinon.SinonStub).calledWith(deps.session)); + Assert.equal(options.secret, expectedOptions.secret); + Assert.equal(options.resave, expectedOptions.resave); + Assert.equal(options.saveUninitialized, expectedOptions.saveUninitialized); + Assert.deepEqual(options.cookie, expectedOptions.cookie); + Assert(options.store != undefined); + }); + + it("should return session options with redis password", function () { + configuration.session["redis"] = { + host: "redis.example.com", + port: 6379, + password: "authelia_pass" + }; + const RedisStoreMock = Sinon.spy(); + const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); + const createClientStub = Sinon.stub(); + + deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; + deps.Redis = { + createClient: createClientStub + } as any; + + createClientStub.returns(redisClient); + + const options = SessionConfigurationBuilder.build(configuration, deps); + + Assert(createClientStub.calledWith({ + host: "redis.example.com", + port: 6379, + password: "authelia_pass" + })); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/SessionConfigurationBuilder.ts b/themes/triangles/server/src/lib/configuration/SessionConfigurationBuilder.ts new file mode 100644 index 000000000..6ce643d9d --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/SessionConfigurationBuilder.ts @@ -0,0 +1,52 @@ +import ExpressSession = require("express-session"); +import Redis = require("redis"); + +import { Configuration } from "./schema/Configuration"; +import { GlobalDependencies } from "../../../types/Dependencies"; +import { RedisStoreOptions } from "connect-redis"; + +export class SessionConfigurationBuilder { + + static build(configuration: Configuration, deps: GlobalDependencies): ExpressSession.SessionOptions { + const sessionOptions: ExpressSession.SessionOptions = { + name: configuration.session.name, + secret: configuration.session.secret, + resave: false, + saveUninitialized: true, + cookie: { + secure: true, + httpOnly: true, + maxAge: configuration.session.expiration, + domain: configuration.session.domain + }, + }; + + if (configuration.session.redis) { + let redisOptions; + const options: Redis.ClientOpts = { + host: configuration.session.redis.host, + port: configuration.session.redis.port + }; + + if (configuration.session.redis.password) { + options["password"] = configuration.session.redis.password; + } + const client = deps.Redis.createClient(options); + + client.on("error", function (err: Error) { + console.error("Redis error:", err); + }); + + redisOptions = { + client: client, + logErrors: true + }; + + if (redisOptions) { + const RedisStore = deps.ConnectRedis(deps.session); + sessionOptions.store = new RedisStore(redisOptions); + } + } + return sessionOptions; + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/AclConfiguration.spec.ts b/themes/triangles/server/src/lib/configuration/schema/AclConfiguration.spec.ts new file mode 100644 index 000000000..d1e2a03a2 --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/schema/AclConfiguration.spec.ts @@ -0,0 +1,34 @@ +import { ACLConfiguration, complete } from "./AclConfiguration"; +import Assert = require("assert"); + +describe("configuration/schema/AclConfiguration", function() { + it("should complete ACLConfiguration", function() { + const configuration: ACLConfiguration = {}; + const [newConfiguration, errors] = complete(configuration); + + Assert.deepEqual(newConfiguration.default_policy, "bypass"); + Assert.deepEqual(newConfiguration.rules, []); + }); + + it("should return errors when subject is not good", function() { + const configuration: ACLConfiguration = { + default_policy: "deny", + rules: [{ + domain: "dev.example.com", + subject: "user:abc", + policy: "bypass" + }, { + domain: "dev.example.com", + subject: "user:def", + policy: "bypass" + }, { + domain: "dev.example.com", + subject: "badkey:abc", + policy: "bypass" + }] + }; + const [newConfiguration, errors] = complete(configuration); + + Assert.deepEqual(errors, ["Rule 2 has wrong subject. It should be starting with user: or group:."]); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/AclConfiguration.ts b/themes/triangles/server/src/lib/configuration/schema/AclConfiguration.ts new file mode 100644 index 000000000..40401dd64 --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/schema/AclConfiguration.ts @@ -0,0 +1,41 @@ + +export type ACLPolicy = "deny" | "bypass" | "one_factor" | "two_factor"; + +export type ACLRule = { + domain: string; + resources?: string[]; + subject?: string; + policy: ACLPolicy; +}; + +export interface ACLConfiguration { + default_policy?: ACLPolicy; + rules?: ACLRule[]; +} + +export function complete(configuration: ACLConfiguration): [ACLConfiguration, string[]] { + const newConfiguration: ACLConfiguration = (configuration) + ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.default_policy) { + newConfiguration.default_policy = "bypass"; + } + + if (!newConfiguration.rules) { + newConfiguration.rules = []; + } + + if (newConfiguration.rules.length > 0) { + const errors: string[] = []; + newConfiguration.rules.forEach((r, idx) => { + if (r.subject && !r.subject.match(/^(user|group):[a-zA-Z0-9]+$/)) { + errors.push(`Rule ${idx} has wrong subject. It should be starting with user: or group:.`); + } + }); + if (errors.length > 0) { + return [newConfiguration, errors]; + } + } + + return [newConfiguration, []]; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts b/themes/triangles/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts new file mode 100644 index 000000000..3ca86381c --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts @@ -0,0 +1,11 @@ +import { AuthenticationBackendConfiguration, complete } from "./AuthenticationBackendConfiguration"; +import Assert = require("assert"); + +describe("configuration/schema/AuthenticationBackendConfiguration", function() { + it("should ensure there is at least one key", function() { + const configuration: AuthenticationBackendConfiguration = {} as any; + const [newConfiguration, error] = complete(configuration); + + Assert.equal(error, "Authentication backend must have one of the following keys:`ldap` or `file`"); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts b/themes/triangles/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts new file mode 100644 index 000000000..7f77f894f --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts @@ -0,0 +1,25 @@ +import { LdapConfiguration } from "./LdapConfiguration"; +import { FileUsersDatabaseConfiguration } from "./FileUsersDatabaseConfiguration"; + +export interface AuthenticationBackendConfiguration { + ldap?: LdapConfiguration; + file?: FileUsersDatabaseConfiguration; +} + +export function complete( + configuration: AuthenticationBackendConfiguration) + : [AuthenticationBackendConfiguration, string] { + + const newConfiguration: AuthenticationBackendConfiguration = (configuration) + ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (Object.keys(newConfiguration).length != 1) { + return [ + newConfiguration, + "Authentication backend must have one of the following keys:" + + "`ldap` or `file`" + ]; + } + + return [newConfiguration, undefined]; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/Configuration.ts b/themes/triangles/server/src/lib/configuration/schema/Configuration.ts new file mode 100644 index 000000000..8d16a5fb2 --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/schema/Configuration.ts @@ -0,0 +1,68 @@ +import { ACLConfiguration, complete as AclConfigurationComplete } from "./AclConfiguration"; +import { AuthenticationBackendConfiguration, complete as AuthenticationBackendComplete } from "./AuthenticationBackendConfiguration"; +import { NotifierConfiguration, complete as NotifierConfigurationComplete } from "./NotifierConfiguration"; +import { RegulationConfiguration, complete as RegulationConfigurationComplete } from "./RegulationConfiguration"; +import { SessionConfiguration, complete as SessionConfigurationComplete } from "./SessionConfiguration"; +import { StorageConfiguration, complete as StorageConfigurationComplete } from "./StorageConfiguration"; +import { TotpConfiguration, complete as TotpConfigurationComplete } from "./TotpConfiguration"; + +export interface Configuration { + access_control?: ACLConfiguration; + authentication_backend: AuthenticationBackendConfiguration; + default_redirection_url?: string; + logs_level?: string; + notifier?: NotifierConfiguration; + port?: number; + regulation?: RegulationConfiguration; + session?: SessionConfiguration; + storage?: StorageConfiguration; + totp?: TotpConfiguration; +} + +export function complete( + configuration: Configuration): + [Configuration, string[]] { + + const newConfiguration: Configuration = JSON.parse( + JSON.stringify(configuration)); + const errors: string[] = []; + + const [acls, aclsErrors] = AclConfigurationComplete( + newConfiguration.access_control); + + newConfiguration.access_control = acls; + if (aclsErrors.length > 0) { + errors.concat(aclsErrors); + } + + const [backend, error] = + AuthenticationBackendComplete( + newConfiguration.authentication_backend); + + if (error) errors.push(error); + newConfiguration.authentication_backend = backend; + + if (!newConfiguration.logs_level) { + newConfiguration.logs_level = "info"; + } + + const [notifier, notifierError] = NotifierConfigurationComplete( + newConfiguration.notifier); + newConfiguration.notifier = notifier; + if (notifierError) errors.push(notifierError); + + if (!newConfiguration.port) { + newConfiguration.port = 8080; + } + + newConfiguration.regulation = RegulationConfigurationComplete( + newConfiguration.regulation); + newConfiguration.session = SessionConfigurationComplete( + newConfiguration.session); + newConfiguration.storage = StorageConfigurationComplete( + newConfiguration.storage); + newConfiguration.totp = TotpConfigurationComplete( + newConfiguration.totp); + + return [newConfiguration, errors]; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts b/themes/triangles/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts new file mode 100644 index 000000000..d19002ba9 --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts @@ -0,0 +1,4 @@ + +export interface FileUsersDatabaseConfiguration { + path: string; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/LdapConfiguration.spec.ts b/themes/triangles/server/src/lib/configuration/schema/LdapConfiguration.spec.ts new file mode 100644 index 000000000..cc73d1085 --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/schema/LdapConfiguration.spec.ts @@ -0,0 +1,25 @@ +import Assert = require("assert"); +import { LdapConfiguration, complete } from "./LdapConfiguration"; + +describe("configuration/schema/AuthenticationMethodsConfiguration", function() { + it("should ensure at least one key is provided", function() { + const configuration: LdapConfiguration = { + url: "ldap.example.com", + base_dn: "dc=example,dc=com", + user: "admin", + password: "password" + }; + const newConfiguration = complete(configuration); + + Assert.deepEqual(newConfiguration, { + url: "ldap.example.com", + base_dn: "dc=example,dc=com", + user: "admin", + password: "password", + users_filter: "cn={0}", + group_name_attribute: "cn", + groups_filter: "member={dn}", + mail_attribute: "mail" + }); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/LdapConfiguration.ts b/themes/triangles/server/src/lib/configuration/schema/LdapConfiguration.ts new file mode 100644 index 000000000..5dacb9390 --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/schema/LdapConfiguration.ts @@ -0,0 +1,40 @@ +import Util = require("util"); + +export interface LdapConfiguration { + url: string; + base_dn: string; + + additional_users_dn?: string; + users_filter?: string; + + additional_groups_dn?: string; + groups_filter?: string; + + group_name_attribute?: string; + mail_attribute?: string; + + user: string; // admin username + password: string; // admin password +} + +export function complete(configuration: LdapConfiguration): LdapConfiguration { + const newConfiguration: LdapConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.users_filter) { + newConfiguration.users_filter = "cn={0}"; + } + + if (!newConfiguration.groups_filter) { + newConfiguration.groups_filter = "member={dn}"; + } + + if (!newConfiguration.group_name_attribute) { + newConfiguration.group_name_attribute = "cn"; + } + + if (!newConfiguration.mail_attribute) { + newConfiguration.mail_attribute = "mail"; + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts b/themes/triangles/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts new file mode 100644 index 000000000..6c576e8e0 --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts @@ -0,0 +1,40 @@ +import Assert = require("assert"); +import { NotifierConfiguration, complete } from "./NotifierConfiguration"; + +describe("configuration/schema/NotifierConfiguration", function() { + it("should use a default notifier when none is provided", function() { + const configuration: NotifierConfiguration = {}; + const [newConfiguration, error] = complete(configuration); + + Assert.deepEqual(newConfiguration.filesystem, {filename: "/tmp/authelia/notification.txt"}); + }); + + it("should ensure correct key is provided", function() { + const configuration = { + abc: "badvalue" + }; + const [newConfiguration, error] = complete(configuration as any); + + Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"); + }); + + it("should ensure there is no more than one key", function() { + const configuration: NotifierConfiguration = { + smtp: { + host: "smtp.example.com", + port: 25, + secure: false, + sender: "test@example.com" + }, + email: { + username: "test", + password: "test", + sender: "test@example.com", + service: "gmail" + } + }; + const [newConfiguration, error] = complete(configuration); + + Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/NotifierConfiguration.ts b/themes/triangles/server/src/lib/configuration/schema/NotifierConfiguration.ts new file mode 100644 index 000000000..7bcce15c2 --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/schema/NotifierConfiguration.ts @@ -0,0 +1,45 @@ + +export interface EmailNotifierConfiguration { + username: string; + password: string; + sender: string; + service: string; +} + +export interface SmtpNotifierConfiguration { + username?: string; + password?: string; + host: string; + port: number; + secure: boolean; + sender: string; +} + +export interface FileSystemNotifierConfiguration { + filename: string; +} + +export interface NotifierConfiguration { + email?: EmailNotifierConfiguration; + smtp?: SmtpNotifierConfiguration; + filesystem?: FileSystemNotifierConfiguration; +} + +export function complete(configuration: NotifierConfiguration): [NotifierConfiguration, string] { + const newConfiguration: NotifierConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (Object.keys(newConfiguration).length == 0) + newConfiguration.filesystem = { filename: "/tmp/authelia/notification.txt" }; + + const ERROR = "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"; + + if (Object.keys(newConfiguration).length != 1) + return [newConfiguration, ERROR]; + + const key = Object.keys(newConfiguration)[0]; + + if (key != "filesystem" && key != "smtp" && key != "email") + return [newConfiguration, ERROR]; + + return [newConfiguration, undefined]; +} diff --git a/themes/triangles/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts b/themes/triangles/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts new file mode 100644 index 000000000..dce2caf4e --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts @@ -0,0 +1,13 @@ +import Assert = require("assert"); +import { RegulationConfiguration, complete } from "./RegulationConfiguration"; + +describe("configuration/schema/RegulationConfiguration", function() { + it("should return default regulation configuration", function() { + const configuration: RegulationConfiguration = {}; + const newConfiguration = complete(configuration); + + Assert.equal(newConfiguration.ban_time, 300); + Assert.equal(newConfiguration.find_time, 120); + Assert.equal(newConfiguration.max_retries, 3); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/RegulationConfiguration.ts b/themes/triangles/server/src/lib/configuration/schema/RegulationConfiguration.ts new file mode 100644 index 000000000..117463f43 --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/schema/RegulationConfiguration.ts @@ -0,0 +1,23 @@ +export interface RegulationConfiguration { + max_retries?: number; + find_time?: number; + ban_time?: number; +} + +export function complete(configuration: RegulationConfiguration): RegulationConfiguration { + const newConfiguration: RegulationConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.max_retries) { + newConfiguration.max_retries = 3; + } + + if (!newConfiguration.find_time) { + newConfiguration.find_time = 120; // seconds + } + + if (!newConfiguration.ban_time) { + newConfiguration.ban_time = 300; // seconds + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/SessionConfiguration.spec.ts b/themes/triangles/server/src/lib/configuration/schema/SessionConfiguration.spec.ts new file mode 100644 index 000000000..e54010837 --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/schema/SessionConfiguration.spec.ts @@ -0,0 +1,16 @@ +import Assert = require("assert"); +import { SessionConfiguration, complete } from "./SessionConfiguration"; + +describe("configuration/schema/SessionConfiguration", function() { + it("should return default regulation configuration", function() { + const configuration: SessionConfiguration = { + domain: "example.com", + secret: "unsecure_secret" + }; + const newConfiguration = complete(configuration); + + Assert.equal(newConfiguration.name, 'authelia_session'); + Assert.equal(newConfiguration.expiration, 3600000); + Assert.equal(newConfiguration.inactivity, undefined); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/SessionConfiguration.ts b/themes/triangles/server/src/lib/configuration/schema/SessionConfiguration.ts new file mode 100644 index 000000000..2c88bb215 --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/schema/SessionConfiguration.ts @@ -0,0 +1,32 @@ +export interface SessionRedisOptions { + host: string; + port: number; + password?: string; +} + +export interface SessionConfiguration { + name?: string; + domain: string; + secret: string; + expiration?: number; + inactivity?: number; + redis?: SessionRedisOptions; +} + +export function complete(configuration: SessionConfiguration): SessionConfiguration { + const newConfiguration: SessionConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.name) { + newConfiguration.name = "authelia_session"; + } + + if (!newConfiguration.expiration) { + newConfiguration.expiration = 3600000; // 1 hour + } + + if (!newConfiguration.inactivity) { + newConfiguration.inactivity = undefined; // disabled + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/StorageConfiguration.spec.ts b/themes/triangles/server/src/lib/configuration/schema/StorageConfiguration.spec.ts new file mode 100644 index 000000000..9d02a11b6 --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/schema/StorageConfiguration.spec.ts @@ -0,0 +1,15 @@ +import Assert = require("assert"); +import { StorageConfiguration, complete } from "./StorageConfiguration"; + +describe("configuration/schema/StorageConfiguration", function() { + it("should return default regulation configuration", function() { + const configuration: StorageConfiguration = {}; + const newConfiguration = complete(configuration); + + Assert.deepEqual(newConfiguration, { + local: { + in_memory: true + } + }); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/StorageConfiguration.ts b/themes/triangles/server/src/lib/configuration/schema/StorageConfiguration.ts new file mode 100644 index 000000000..47e356ef4 --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/schema/StorageConfiguration.ts @@ -0,0 +1,30 @@ +export interface MongoStorageConfiguration { + url: string; + database: string; + auth?: { + username: string; + password: string; + }; +} + +export interface LocalStorageConfiguration { + path?: string; + in_memory?: boolean; +} + +export interface StorageConfiguration { + local?: LocalStorageConfiguration; + mongo?: MongoStorageConfiguration; +} + +export function complete(configuration: StorageConfiguration): StorageConfiguration { + const newConfiguration: StorageConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.local && !newConfiguration.mongo) { + newConfiguration.local = { + in_memory: true + }; + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/TotpConfiguration.ts b/themes/triangles/server/src/lib/configuration/schema/TotpConfiguration.ts new file mode 100644 index 000000000..683135639 --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/schema/TotpConfiguration.ts @@ -0,0 +1,13 @@ +export interface TotpConfiguration { + issuer: string; +} + +export function complete(configuration: TotpConfiguration): TotpConfiguration { + const newConfiguration: TotpConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (!newConfiguration.issuer) { + newConfiguration.issuer = "authelia.com"; + } + + return newConfiguration; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts b/themes/triangles/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts new file mode 100644 index 000000000..8008b4833 --- /dev/null +++ b/themes/triangles/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts @@ -0,0 +1,9 @@ + +export interface UserInfo { + username: string; + password_hash: string; + email: string; + groups?: string[]; +} + +export type UserDatabaseConfiguration = UserInfo[]; \ No newline at end of file diff --git a/themes/triangles/server/src/lib/connectors/mongo/IMongoClient.d.ts b/themes/triangles/server/src/lib/connectors/mongo/IMongoClient.d.ts new file mode 100644 index 000000000..36cb4b8bf --- /dev/null +++ b/themes/triangles/server/src/lib/connectors/mongo/IMongoClient.d.ts @@ -0,0 +1,6 @@ +import MongoDB = require("mongodb"); +import Bluebird = require("bluebird"); + +export interface IMongoClient { + collection(name: string): Bluebird +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/connectors/mongo/MongoClient.spec.ts b/themes/triangles/server/src/lib/connectors/mongo/MongoClient.spec.ts new file mode 100644 index 000000000..ca0c68593 --- /dev/null +++ b/themes/triangles/server/src/lib/connectors/mongo/MongoClient.spec.ts @@ -0,0 +1,119 @@ +import Assert = require("assert"); +import Bluebird = require("bluebird"); +import MongoDB = require("mongodb"); +import Sinon = require("sinon"); + +import { MongoClient } from "./MongoClient"; +import { GlobalLoggerStub } from "../../logging/GlobalLoggerStub.spec"; +import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration"; + +describe("connectors/mongo/MongoClient", function () { + let MongoClientStub: any; + let mongoClientStub: any; + let mongoDatabaseStub: any; + let logger: GlobalLoggerStub = new GlobalLoggerStub(); + + const configuration: MongoStorageConfiguration = { + url: "mongo://url", + database: "databasename" + }; + + describe("connection", () => { + before(() => { + mongoClientStub = { + db: Sinon.stub() + }; + mongoDatabaseStub = { + on: Sinon.stub(), + collection: Sinon.stub() + } + MongoClientStub = Sinon.stub( + MongoDB.MongoClient, "connect"); + MongoClientStub.yields( + undefined, mongoClientStub); + mongoClientStub.db.returns( + mongoDatabaseStub); + }); + + after(() => { + MongoClientStub.restore(); + }); + + it("should use credentials from configuration", () => { + configuration.auth = { + username: "authelia", + password: "authelia_pass" + }; + + const client = new MongoClient(configuration, logger); + return client.collection("test") + .then(() => { + Assert(MongoClientStub.calledWith("mongo://url", { + auth: { + user: "authelia", + password: "authelia_pass" + } + })) + }); + }); + }); + + describe("collection", () => { + before(function() { + mongoClientStub = { + db: Sinon.stub() + }; + mongoDatabaseStub = { + on: Sinon.stub(), + collection: Sinon.stub() + } + }); + + describe("Connection to mongo is ok", function() { + before(function () { + MongoClientStub = Sinon.stub( + MongoDB.MongoClient, "connect"); + MongoClientStub.yields( + undefined, mongoClientStub); + mongoClientStub.db.returns( + mongoDatabaseStub); + }); + + after(function () { + MongoClientStub.restore(); + }); + + it("should create a collection", function () { + const COLLECTION_NAME = "mycollection"; + const client = new MongoClient(configuration, logger); + + mongoDatabaseStub.collection.returns("COL"); + return client.collection(COLLECTION_NAME) + .then((collection) => mongoDatabaseStub.collection.calledWith(COLLECTION_NAME)); + }); + }); + + describe("Connection to mongo is broken", function() { + before(function () { + MongoClientStub = Sinon.stub( + MongoDB.MongoClient, "connect"); + MongoClientStub.yields( + new Error("Failed connection"), undefined); + }); + + after(function () { + MongoClientStub.restore(); + }); + + it("should fail creating the collection", function() { + const COLLECTION_NAME = "mycollection"; + const client = new MongoClient(configuration, logger); + + mongoDatabaseStub.collection.returns("COL"); + return client.collection(COLLECTION_NAME) + .then((collection) => Bluebird.reject(new Error("should not be here."))) + .catch((err) => Bluebird.resolve()); + }); + }) + }); +}); diff --git a/themes/triangles/server/src/lib/connectors/mongo/MongoClient.ts b/themes/triangles/server/src/lib/connectors/mongo/MongoClient.ts new file mode 100644 index 000000000..d15731e97 --- /dev/null +++ b/themes/triangles/server/src/lib/connectors/mongo/MongoClient.ts @@ -0,0 +1,76 @@ + +import MongoDB = require("mongodb"); +import { IMongoClient } from "./IMongoClient"; +import Bluebird = require("bluebird"); +import { AUTHENTICATION_FAILED } from "../../../../../shared/UserMessages"; +import { IGlobalLogger } from "../../logging/IGlobalLogger"; +import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration"; + +export class MongoClient implements IMongoClient { + private configuration: MongoStorageConfiguration; + + private database: MongoDB.Db; + private client: MongoDB.MongoClient; + private logger: IGlobalLogger; + + constructor( + configuration: MongoStorageConfiguration, + logger: IGlobalLogger) { + + this.configuration = configuration; + this.logger = logger; + } + + connect(): Bluebird { + const that = this; + const options: MongoDB.MongoClientOptions = {}; + if (that.configuration.auth) { + options["auth"] = { + user: that.configuration.auth.username, + password: that.configuration.auth.password + }; + } + + return new Bluebird((resolve, reject) => { + MongoDB.MongoClient.connect( + this.configuration.url, + options, + function(err, client) { + if (err) { + reject(err); + return; + } + resolve(client); + }); + }) + .then(function (client: MongoDB.MongoClient) { + that.database = client.db(that.configuration.database); + that.database.on("close", () => { + that.logger.info("[MongoClient] Lost connection."); + }); + that.database.on("reconnect", () => { + that.logger.info("[MongoClient] Reconnected."); + }); + that.client = client; + }); + } + + close(): Bluebird { + if (this.client) { + this.client.close(); + this.database = undefined; + this.client = undefined; + } + return Bluebird.resolve(); + } + + collection(name: string): Bluebird { + if (!this.client) { + const that = this; + return this.connect() + .then(() => Bluebird.resolve(that.database.collection(name))); + } + + return Bluebird.resolve(this.database.collection(name)); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/connectors/mongo/MongoClientStub.spec.ts b/themes/triangles/server/src/lib/connectors/mongo/MongoClientStub.spec.ts new file mode 100644 index 000000000..1cfd48e32 --- /dev/null +++ b/themes/triangles/server/src/lib/connectors/mongo/MongoClientStub.spec.ts @@ -0,0 +1,16 @@ +import Sinon = require("sinon"); +import MongoDB = require("mongodb"); +import Bluebird = require("bluebird"); +import { IMongoClient } from "../../../../src/lib/connectors/mongo/IMongoClient"; + +export class MongoClientStub implements IMongoClient { + public collectionStub: Sinon.SinonStub; + + constructor() { + this.collectionStub = Sinon.stub(); + } + + collection(name: string): Bluebird { + return this.collectionStub(name); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/logging/GlobalLogger.ts b/themes/triangles/server/src/lib/logging/GlobalLogger.ts new file mode 100644 index 000000000..4da7acf49 --- /dev/null +++ b/themes/triangles/server/src/lib/logging/GlobalLogger.ts @@ -0,0 +1,34 @@ +import { IGlobalLogger } from "./IGlobalLogger"; +import Util = require("util"); +import Express = require("express"); +import Winston = require("winston"); + +declare module "express" { + interface Request { + id: string; + } +} + +export class GlobalLogger implements IGlobalLogger { + private winston: typeof Winston; + constructor(winston: typeof Winston) { + this.winston = winston; + } + + private buildMessage(message: string, ...args: any[]): string { + return Util.format("date='%s' message='%s'", new Date(), + Util.format(message, ...args)); + } + + info(message: string, ...args: any[]): void { + this.winston.info(this.buildMessage(message, ...args)); + } + + debug(message: string, ...args: any[]): void { + this.winston.debug(this.buildMessage(message, ...args)); + } + + error(message: string, ...args: any[]): void { + this.winston.debug(this.buildMessage(message, ...args)); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/logging/GlobalLoggerStub.spec.ts b/themes/triangles/server/src/lib/logging/GlobalLoggerStub.spec.ts new file mode 100644 index 000000000..d4bb13710 --- /dev/null +++ b/themes/triangles/server/src/lib/logging/GlobalLoggerStub.spec.ts @@ -0,0 +1,38 @@ +import Sinon = require("sinon"); +import { GlobalLogger } from "./GlobalLogger"; +import Winston = require("winston"); +import Express = require("express"); +import { IGlobalLogger } from "./IGlobalLogger"; + +export class GlobalLoggerStub implements IGlobalLogger { + infoStub: Sinon.SinonStub; + debugStub: Sinon.SinonStub; + errorStub: Sinon.SinonStub; + private globalLogger: IGlobalLogger; + + constructor(enableLogging?: boolean) { + this.infoStub = Sinon.stub(); + this.debugStub = Sinon.stub(); + this.errorStub = Sinon.stub(); + if (enableLogging) + this.globalLogger = new GlobalLogger(Winston); + } + + info(message: string, ...args: any[]): void { + if (this.globalLogger) + this.globalLogger.info(message, ...args); + this.infoStub(message, ...args); + } + + debug(message: string, ...args: any[]): void { + if (this.globalLogger) + this.globalLogger.info(message, ...args); + this.debugStub(message, ...args); + } + + error(message: string, ...args: any[]): void { + if (this.globalLogger) + this.globalLogger.info(message, ...args); + this.errorStub(message, ...args); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/logging/IGlobalLogger.ts b/themes/triangles/server/src/lib/logging/IGlobalLogger.ts new file mode 100644 index 000000000..548515ec7 --- /dev/null +++ b/themes/triangles/server/src/lib/logging/IGlobalLogger.ts @@ -0,0 +1,5 @@ +export interface IGlobalLogger { + info(message: string, ...args: any[]): void; + debug(message: string, ...args: any[]): void; + error(message: string, ...args: any[]): void; +} diff --git a/themes/triangles/server/src/lib/logging/IRequestLogger.ts b/themes/triangles/server/src/lib/logging/IRequestLogger.ts new file mode 100644 index 000000000..126a601fe --- /dev/null +++ b/themes/triangles/server/src/lib/logging/IRequestLogger.ts @@ -0,0 +1,7 @@ +import Express = require("express"); + +export interface IRequestLogger { + info(req: Express.Request, message: string, ...args: any[]): void; + debug(req: Express.Request, message: string, ...args: any[]): void; + error(req: Express.Request, message: string, ...args: any[]): void; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/logging/RequestLogger.ts b/themes/triangles/server/src/lib/logging/RequestLogger.ts new file mode 100644 index 000000000..c45c66018 --- /dev/null +++ b/themes/triangles/server/src/lib/logging/RequestLogger.ts @@ -0,0 +1,45 @@ +import { IRequestLogger } from "./IRequestLogger"; +import Util = require("util"); +import Express = require("express"); +import Winston = require("winston"); + +declare module "express" { + interface Request { + id: string; + } +} + +export class RequestLogger implements IRequestLogger { + private winston: typeof Winston; + + constructor(winston: typeof Winston) { + this.winston = winston; + } + + private formatHeader(req: Express.Request) { + const clientIP = req.ip; // The IP of the original client going through the proxy chain. + return Util.format("date='%s' method='%s', path='%s' requestId='%s' sessionId='%s' ip='%s'", + new Date(), req.method, req.path, req.id, req.sessionID, clientIP); + } + + private formatBody(message: string) { + return Util.format("message='%s'", message); + } + + private formatMessage(req: Express.Request, message: string) { + return Util.format("%s %s", this.formatHeader(req), + this.formatBody(message)); + } + + info(req: Express.Request, message: string, ...args: any[]): void { + this.winston.info(this.formatMessage(req, message), ...args); + } + + debug(req: Express.Request, message: string, ...args: any[]): void { + this.winston.debug(this.formatMessage(req, message), ...args); + } + + error(req: Express.Request, message: string, ...args: any[]): void { + this.winston.error(this.formatMessage(req, message), ...args); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/logging/RequestLoggerStub.spec.ts b/themes/triangles/server/src/lib/logging/RequestLoggerStub.spec.ts new file mode 100644 index 000000000..b0e375210 --- /dev/null +++ b/themes/triangles/server/src/lib/logging/RequestLoggerStub.spec.ts @@ -0,0 +1,38 @@ +import { IRequestLogger } from "./IRequestLogger"; +import Sinon = require("sinon"); +import { RequestLogger } from "./RequestLogger"; +import Winston = require("winston"); +import Express = require("express"); + +export class RequestLoggerStub implements IRequestLogger { + infoStub: Sinon.SinonStub; + debugStub: Sinon.SinonStub; + errorStub: Sinon.SinonStub; + private requestLogger: RequestLogger; + + constructor(enableLogging?: boolean) { + this.infoStub = Sinon.stub(); + this.debugStub = Sinon.stub(); + this.errorStub = Sinon.stub(); + if (enableLogging) + this.requestLogger = new RequestLogger(Winston); + } + + info(req: Express.Request, message: string, ...args: any[]): void { + if (this.requestLogger) + this.requestLogger.info(req, message, ...args); + this.infoStub(req, message, ...args); + } + + debug(req: Express.Request, message: string, ...args: any[]): void { + if (this.requestLogger) + this.requestLogger.info(req, message, ...args); + this.debugStub(req, message, ...args); + } + + error(req: Express.Request, message: string, ...args: any[]): void { + if (this.requestLogger) + this.requestLogger.info(req, message, ...args); + this.errorStub(req, message, ...args); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/AbstractEmailNotifier.ts b/themes/triangles/server/src/lib/notifiers/AbstractEmailNotifier.ts new file mode 100644 index 000000000..198e4e5db --- /dev/null +++ b/themes/triangles/server/src/lib/notifiers/AbstractEmailNotifier.ts @@ -0,0 +1,23 @@ + +import { INotifier } from "../notifiers/INotifier"; +import { Identity } from "../../../types/Identity"; + +import Fs = require("fs"); +import Path = require("path"); +import Ejs = require("ejs"); +import BluebirdPromise = require("bluebird"); + +const email_template = Fs.readFileSync(Path.join(__dirname, "../../resources/email-template.ejs"), "UTF-8"); + +export abstract class AbstractEmailNotifier implements INotifier { + notify(to: string, subject: string, link: string): BluebirdPromise { + const d = { + url: link, + button_title: "Continue", + title: subject + }; + return this.sendEmail(to, subject, Ejs.render(email_template, d)); + } + + abstract sendEmail(to: string, subject: string, content: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/EmailNotifier.spec.ts b/themes/triangles/server/src/lib/notifiers/EmailNotifier.spec.ts new file mode 100644 index 000000000..8211bbc02 --- /dev/null +++ b/themes/triangles/server/src/lib/notifiers/EmailNotifier.spec.ts @@ -0,0 +1,54 @@ +import * as sinon from "sinon"; +import * as Assert from "assert"; +import BluebirdPromise = require("bluebird"); + +import { MailSenderStub } from "./MailSenderStub.spec"; +import EmailNotifier = require("./EmailNotifier"); + + +describe("notifiers/EmailNotifier", function () { + it("should send an email to given user", function () { + const mailSender = new MailSenderStub(); + const options = { + username: "user_gmail", + password: "pass_gmail", + sender: "admin@example.com", + service: "gmail" + }; + + mailSender.sendStub.returns(BluebirdPromise.resolve()); + const sender = new EmailNotifier.EmailNotifier(options, mailSender); + const subject = "subject"; + const url = "http://test.com"; + + return sender.notify("user@example.com", subject, url) + .then(function () { + Assert.equal(mailSender.sendStub.getCall(0).args[0].to, "user@example.com"); + Assert.equal(mailSender.sendStub.getCall(0).args[0].subject, "subject"); + return BluebirdPromise.resolve(); + }); + }); + + it("should fail while sending an email", function () { + const mailSender = new MailSenderStub(); + const options = { + username: "user_gmail", + password: "pass_gmail", + sender: "admin@example.com", + service: "gmail" + }; + + mailSender.sendStub.returns(BluebirdPromise.reject(new Error("Failed to send mail"))); + const sender = new EmailNotifier.EmailNotifier(options, mailSender); + const subject = "subject"; + const url = "http://test.com"; + + return sender.notify("user@example.com", subject, url) + .then(function () { + return BluebirdPromise.reject(new Error()); + }, function() { + Assert.equal(mailSender.sendStub.getCall(0).args[0].from, "admin@example.com"); + return BluebirdPromise.resolve(); + }); + }); +}); diff --git a/themes/triangles/server/src/lib/notifiers/EmailNotifier.ts b/themes/triangles/server/src/lib/notifiers/EmailNotifier.ts new file mode 100644 index 000000000..4df7c861e --- /dev/null +++ b/themes/triangles/server/src/lib/notifiers/EmailNotifier.ts @@ -0,0 +1,27 @@ + +import * as BluebirdPromise from "bluebird"; + +import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; +import { EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; +import { IMailSender } from "./IMailSender"; + +export class EmailNotifier extends AbstractEmailNotifier { + private mailSender: IMailSender; + private sender: string; + + constructor(options: EmailNotifierConfiguration, mailSender: IMailSender) { + super(); + this.mailSender = mailSender; + this.sender = options.sender; + } + + sendEmail(to: string, subject: string, content: string) { + const mailOptions = { + from: this.sender, + to: to, + subject: subject, + html: content + }; + return this.mailSender.send(mailOptions); + } +} diff --git a/themes/triangles/server/src/lib/notifiers/FileSystemNotifier.ts b/themes/triangles/server/src/lib/notifiers/FileSystemNotifier.ts new file mode 100644 index 000000000..23f6242c4 --- /dev/null +++ b/themes/triangles/server/src/lib/notifiers/FileSystemNotifier.ts @@ -0,0 +1,22 @@ +import * as BluebirdPromise from "bluebird"; +import * as util from "util"; +import * as Fs from "fs"; +import { INotifier } from "./INotifier"; +import { Identity } from "../../../types/Identity"; + +import { FileSystemNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; + +export class FileSystemNotifier implements INotifier { + private filename: string; + + constructor(options: FileSystemNotifierConfiguration) { + this.filename = options.filename; + } + + notify(to: string, subject: string, link: string): BluebirdPromise { + const content = util.format("Date: %s\nEmail: %s\nSubject: %s\nLink: %s", + new Date().toString(), to, subject, link); + const writeFilePromised: any = BluebirdPromise.promisify(Fs.writeFile); + return writeFilePromised(this.filename, content); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/IMailSender.ts b/themes/triangles/server/src/lib/notifiers/IMailSender.ts new file mode 100644 index 000000000..34ac464a8 --- /dev/null +++ b/themes/triangles/server/src/lib/notifiers/IMailSender.ts @@ -0,0 +1,6 @@ +import BluebirdPromise = require("bluebird"); +import Nodemailer = require("nodemailer"); + +export interface IMailSender { + send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/IMailSenderBuilder.ts b/themes/triangles/server/src/lib/notifiers/IMailSenderBuilder.ts new file mode 100644 index 000000000..36d4dcdf7 --- /dev/null +++ b/themes/triangles/server/src/lib/notifiers/IMailSenderBuilder.ts @@ -0,0 +1,7 @@ +import { IMailSender } from "./IMailSender"; +import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; + +export interface IMailSenderBuilder { + buildEmail(options: EmailNotifierConfiguration): IMailSender; + buildSmtp(options: SmtpNotifierConfiguration): IMailSender; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/INotifier.ts b/themes/triangles/server/src/lib/notifiers/INotifier.ts new file mode 100644 index 000000000..b9a6b138c --- /dev/null +++ b/themes/triangles/server/src/lib/notifiers/INotifier.ts @@ -0,0 +1,5 @@ +import * as BluebirdPromise from "bluebird"; + +export interface INotifier { + notify(to: string, subject: string, link: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/MailSender.ts b/themes/triangles/server/src/lib/notifiers/MailSender.ts new file mode 100644 index 000000000..536a88e63 --- /dev/null +++ b/themes/triangles/server/src/lib/notifiers/MailSender.ts @@ -0,0 +1,42 @@ +import { IMailSender } from "./IMailSender"; +import Nodemailer = require("nodemailer"); +import NodemailerDirectTransport = require("nodemailer-direct-transport"); +import NodemailerSmtpTransport = require("nodemailer-smtp-transport"); +import BluebirdPromise = require("bluebird"); + +export class MailSender implements IMailSender { + private transporter: Nodemailer.Transporter; + + constructor(options: NodemailerDirectTransport.DirectOptions | + NodemailerSmtpTransport.SmtpOptions, nodemailer: typeof Nodemailer) { + this.transporter = nodemailer.createTransport(options); + } + + verify(): BluebirdPromise { + const that = this; + return new BluebirdPromise(function (resolve, reject) { + that.transporter.verify(function (error: Error, success: any) { + if (error) { + reject(new Error("Unable to connect to SMTP server. \ + Please check the service is running and your credentials are correct.")); + return; + } + resolve(); + }); + }); + } + + send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise { + const that = this; + return new BluebirdPromise(function (resolve, reject) { + that.transporter.sendMail(mailOptions, (error: Error, + data: Nodemailer.SentMessageInfo) => { + if (error) { + reject(new Error("Error while sending email: " + error.message)); + return; + } + resolve(); + }); + }); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/MailSenderBuilder.spec.ts b/themes/triangles/server/src/lib/notifiers/MailSenderBuilder.spec.ts new file mode 100644 index 000000000..41e0db426 --- /dev/null +++ b/themes/triangles/server/src/lib/notifiers/MailSenderBuilder.spec.ts @@ -0,0 +1,67 @@ + +import { MailSenderBuilder } from ".//MailSenderBuilder"; +import Nodemailer = require("nodemailer"); +import Sinon = require("sinon"); +import Assert = require("assert"); + +describe("notifiers/MailSenderBuilder", function() { + let createTransportStub: Sinon.SinonStub; + beforeEach(function() { + createTransportStub = Sinon.stub(Nodemailer, "createTransport"); + }); + + afterEach(function() { + createTransportStub.restore(); + }); + + it("should create a email mail sender", function() { + const mailSenderBuilder = new MailSenderBuilder(Nodemailer); + mailSenderBuilder.buildEmail({ + username: "user_gmail", + password: "pass_gmail", + sender: "admin@example.com", + service: "gmail" + }); + Assert.equal(createTransportStub.getCall(0).args[0].auth.user, "user_gmail"); + Assert.equal(createTransportStub.getCall(0).args[0].auth.pass, "pass_gmail"); + Assert.equal(createTransportStub.getCall(0).args[0].service, "gmail"); + }); + + describe("build smtp mail sender", function() { + it("should create a smtp mail sender with authenticated user", function() { + const mailSenderBuilder = new MailSenderBuilder(Nodemailer); + mailSenderBuilder.buildSmtp({ + host: "mail.example.com", + password: "password", + port: 25, + secure: true, + username: "user", + sender: "admin@example.com" + }); + Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { + host: "mail.example.com", + auth: { + pass: "password", + user: "user" + }, + port: 25, + secure: true, + }); + }); + + it("should create a smtp mail sender with anonymous user", function() { + const mailSenderBuilder = new MailSenderBuilder(Nodemailer); + mailSenderBuilder.buildSmtp({ + host: "mail.example.com", + port: 25, + secure: true, + sender: "admin@example.com" + }); + Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { + host: "mail.example.com", + port: 25, + secure: true, + }); + }); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/MailSenderBuilder.ts b/themes/triangles/server/src/lib/notifiers/MailSenderBuilder.ts new file mode 100644 index 000000000..1d06be52f --- /dev/null +++ b/themes/triangles/server/src/lib/notifiers/MailSenderBuilder.ts @@ -0,0 +1,42 @@ +import { IMailSender } from "./IMailSender"; +import { IMailSenderBuilder } from "./IMailSenderBuilder"; +import { MailSender } from "./MailSender"; +import Nodemailer = require("nodemailer"); +import NodemailerSmtpTransport = require("nodemailer-smtp-transport"); +import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; + +export class MailSenderBuilder implements IMailSenderBuilder { + private nodemailer: typeof Nodemailer; + + constructor(nodemailer: typeof Nodemailer) { + this.nodemailer = nodemailer; + } + + buildEmail(options: EmailNotifierConfiguration): IMailSender { + const emailOptions = { + service: options.service, + auth: { + user: options.username, + pass: options.password + } + }; + return new MailSender(emailOptions, this.nodemailer); + } + + buildSmtp(options: SmtpNotifierConfiguration): IMailSender { + const smtpOptions: NodemailerSmtpTransport.SmtpOptions = { + host: options.host, + port: options.port, + secure: options.secure, // upgrade later with STARTTLS + }; + + if (options.username && options.password) { + smtpOptions.auth = { + user: options.username, + pass: options.password + }; + } + + return new MailSender(smtpOptions, this.nodemailer); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts b/themes/triangles/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts new file mode 100644 index 000000000..5b76f6e56 --- /dev/null +++ b/themes/triangles/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts @@ -0,0 +1,25 @@ +import { IMailSenderBuilder } from "../../../src/lib/notifiers/IMailSenderBuilder"; +import BluebirdPromise = require("bluebird"); +import Nodemailer = require("nodemailer"); +import Sinon = require("sinon"); +import { IMailSender } from "../../../src/lib/notifiers/IMailSender"; +import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../../../src/lib/configuration/schema/NotifierConfiguration"; + +export class MailSenderBuilderStub implements IMailSenderBuilder { + buildEmailStub: Sinon.SinonStub; + buildSmtpStub: Sinon.SinonStub; + + constructor() { + this.buildEmailStub = Sinon.stub(); + this.buildSmtpStub = Sinon.stub(); + } + + buildEmail(options: EmailNotifierConfiguration): IMailSender { + return this.buildEmailStub(options); + } + + buildSmtp(options: SmtpNotifierConfiguration): IMailSender { + return this.buildSmtpStub(options); + } + +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/MailSenderStub.spec.ts b/themes/triangles/server/src/lib/notifiers/MailSenderStub.spec.ts new file mode 100644 index 000000000..d57c458fc --- /dev/null +++ b/themes/triangles/server/src/lib/notifiers/MailSenderStub.spec.ts @@ -0,0 +1,16 @@ +import { IMailSender } from "../../../src/lib/notifiers/IMailSender"; +import BluebirdPromise = require("bluebird"); +import Nodemailer = require("nodemailer"); +import Sinon = require("sinon"); + +export class MailSenderStub implements IMailSender { + sendStub: Sinon.SinonStub; + + constructor() { + this.sendStub = Sinon.stub(); + } + + send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise { + return this.sendStub(mailOptions); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/NotifierFactory.spec.ts b/themes/triangles/server/src/lib/notifiers/NotifierFactory.spec.ts new file mode 100644 index 000000000..f15e7667f --- /dev/null +++ b/themes/triangles/server/src/lib/notifiers/NotifierFactory.spec.ts @@ -0,0 +1,42 @@ + +import * as sinon from "sinon"; +import * as BluebirdPromise from "bluebird"; +import * as assert from "assert"; + +import { NotifierFactory } from "./NotifierFactory"; +import { EmailNotifier } from "./EmailNotifier"; +import { SmtpNotifier } from "./SmtpNotifier"; +import { MailSenderBuilderStub } from "./MailSenderBuilderStub.spec"; + + +describe("notifiers/NotifierFactory", function () { + let mailSenderBuilderStub: MailSenderBuilderStub; + it("should build a Email Notifier", function () { + const options = { + email: { + username: "abc", + password: "password", + sender: "admin@example.com", + service: "gmail" + } + }; + mailSenderBuilderStub = new MailSenderBuilderStub(); + assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof EmailNotifier); + }); + + it("should build a SMTP Notifier", function () { + const options = { + smtp: { + username: "user", + password: "pass", + secure: true, + host: "localhost", + port: 25, + sender: "admin@example.com" + } + }; + + mailSenderBuilderStub = new MailSenderBuilderStub(); + assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof SmtpNotifier); + }); +}); diff --git a/themes/triangles/server/src/lib/notifiers/NotifierFactory.ts b/themes/triangles/server/src/lib/notifiers/NotifierFactory.ts new file mode 100644 index 000000000..a89155feb --- /dev/null +++ b/themes/triangles/server/src/lib/notifiers/NotifierFactory.ts @@ -0,0 +1,33 @@ + +import { NotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; +import Nodemailer = require("nodemailer"); +import { INotifier } from "./INotifier"; + +import { FileSystemNotifier } from "./FileSystemNotifier"; +import { EmailNotifier } from "./EmailNotifier"; +import { SmtpNotifier } from "./SmtpNotifier"; +import { IMailSender } from "./IMailSender"; +import { IMailSenderBuilder } from "./IMailSenderBuilder"; + +export class NotifierFactory { + static build(options: NotifierConfiguration, mailSenderBuilder: IMailSenderBuilder): INotifier { + if ("email" in options) { + const mailSender = mailSenderBuilder.buildEmail(options.email); + return new EmailNotifier(options.email, mailSender); + } + else if ("smtp" in options) { + const mailSender = mailSenderBuilder.buildSmtp(options.smtp); + return new SmtpNotifier(options.smtp, mailSender); + } + else if ("filesystem" in options) { + return new FileSystemNotifier(options.filesystem); + } + else { + throw new Error("No available notifier option detected."); + } + } +} + + + + diff --git a/themes/triangles/server/src/lib/notifiers/NotifierStub.spec.ts b/themes/triangles/server/src/lib/notifiers/NotifierStub.spec.ts new file mode 100644 index 000000000..f99231b50 --- /dev/null +++ b/themes/triangles/server/src/lib/notifiers/NotifierStub.spec.ts @@ -0,0 +1,16 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); + +import { INotifier } from "./INotifier"; + +export class NotifierStub implements INotifier { + notifyStub: Sinon.SinonStub; + + constructor() { + this.notifyStub = Sinon.stub(); + } + + notify(to: string, subject: string, link: string): BluebirdPromise { + return this.notifyStub(to, subject, link); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/SmtpNotifier.ts b/themes/triangles/server/src/lib/notifiers/SmtpNotifier.ts new file mode 100644 index 000000000..f93a6d4a0 --- /dev/null +++ b/themes/triangles/server/src/lib/notifiers/SmtpNotifier.ts @@ -0,0 +1,30 @@ + + +import * as BluebirdPromise from "bluebird"; + +import { IMailSender } from "./IMailSender"; +import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; +import { SmtpNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; + +export class SmtpNotifier extends AbstractEmailNotifier { + private mailSender: IMailSender; + private sender: string; + + constructor(options: SmtpNotifierConfiguration, + mailSender: IMailSender) { + super(); + this.mailSender = mailSender; + this.sender = options.sender; + } + + sendEmail(to: string, subject: string, content: string) { + const mailOptions = { + from: this.sender, + to: to, + subject: subject, + html: content + }; + const that = this; + return this.mailSender.send(mailOptions); + } +} diff --git a/themes/triangles/server/src/lib/regulation/IRegulator.ts b/themes/triangles/server/src/lib/regulation/IRegulator.ts new file mode 100644 index 000000000..c49425b24 --- /dev/null +++ b/themes/triangles/server/src/lib/regulation/IRegulator.ts @@ -0,0 +1,6 @@ +import BluebirdPromise = require("bluebird"); + +export interface IRegulator { + mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; + regulate(userId: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/regulation/Regulator.spec.ts b/themes/triangles/server/src/lib/regulation/Regulator.spec.ts new file mode 100644 index 000000000..f9c6e6086 --- /dev/null +++ b/themes/triangles/server/src/lib/regulation/Regulator.spec.ts @@ -0,0 +1,186 @@ + +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import Assert = require("assert"); + +import { Regulator } from "./Regulator"; +import MockDate = require("mockdate"); +import exceptions = require("../Exceptions"); +import { UserDataStoreStub } from "../storage/UserDataStoreStub.spec"; + +describe("regulation/Regulator", function () { + const USER1 = "USER1"; + const USER2 = "USER2"; + let userDataStoreStub: UserDataStoreStub; + + beforeEach(function () { + userDataStoreStub = new UserDataStoreStub(); + const dataStore: { [userId: string]: { userId: string, date: Date, isAuthenticationSuccessful: boolean }[] } = { + [USER1]: [], + [USER2]: [] + }; + + userDataStoreStub.saveAuthenticationTraceStub.callsFake(function (userId, isAuthenticationSuccessful) { + dataStore[userId].unshift({ + userId: userId, + date: new Date(), + isAuthenticationSuccessful: isAuthenticationSuccessful, + }); + return BluebirdPromise.resolve(); + }); + + userDataStoreStub.retrieveLatestAuthenticationTracesStub.callsFake(function (userId, count) { + const ret = (dataStore[userId].length <= count) ? dataStore[userId] : dataStore[userId].slice(0, 3); + return BluebirdPromise.resolve(ret); + }); + }); + + afterEach(function () { + MockDate.reset(); + }); + + function markAuthenticationAt(regulator: Regulator, user: string, time: string, success: boolean) { + MockDate.set(time); + return regulator.mark(user, success); + } + + it("should mark 2 authentication and regulate (accept)", function () { + const regulator = new Regulator(userDataStoreStub, 3, 10, 10); + + return regulator.mark(USER1, false) + .then(function () { + return regulator.mark(USER1, true); + }) + .then(function () { + return regulator.regulate(USER1); + }); + }); + + it("should mark 3 authentications and regulate (reject)", function () { + const regulator = new Regulator(userDataStoreStub, 3, 10, 10); + + return regulator.mark(USER1, false) + .then(function () { + return regulator.mark(USER1, false); + }) + .then(function () { + return regulator.mark(USER1, false); + }) + .then(function () { + return regulator.regulate(USER1); + }) + .then(function () { return BluebirdPromise.reject(new Error("should not be here!")); }) + .catch(exceptions.AuthenticationRegulationError, function () { + return BluebirdPromise.resolve(); + }); + }); + + it("should mark 1 failed, 1 successful and 1 failed authentications within minimum time and regulate (accept)", function () { + const regulator = new Regulator(userDataStoreStub, 3, 60, 30); + + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", true); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:20", false); + }) + .then(function () { + return regulator.regulate(USER1); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:30", false); + }) + .then(function () { + return regulator.regulate(USER1); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:39", false); + }) + .then(function () { + return regulator.regulate(USER1); + }) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here!")); + }, + function () { + return BluebirdPromise.resolve(); + }); + }); + + it("should regulate user if number of failures is greater than 3 in allowed time lapse", function () { + function markAuthentications(regulator: Regulator, user: string) { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:45", false); + }) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:01:05", false); + }) + .then(function () { + return regulator.regulate(user); + }); + } + + const regulator1 = new Regulator(userDataStoreStub, 3, 60, 60); + const regulator2 = new Regulator(userDataStoreStub, 3, 2 * 60, 60); + + const p1 = markAuthentications(regulator1, USER1); + const p2 = markAuthentications(regulator2, USER2); + + return BluebirdPromise.join(p1, p2) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here...")); + }, function () { + Assert(p1.isFulfilled()); + Assert(p2.isRejected()); + }); + }); + + it("should user wait after regulation to authenticate again", function () { + function markAuthentications(regulator: Regulator, user: string) { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:10", false); + }) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:15", false); + }) + .then(function () { + return markAuthenticationAt(regulator, user, "1/2/2000 00:00:25", false); + }) + .then(function () { + MockDate.set("1/2/2000 00:00:54"); + return regulator.regulate(user); + }) + .then(function () { + return BluebirdPromise.reject(new Error("should fail at this time")); + }, function () { + MockDate.set("1/2/2000 00:00:56"); + return regulator.regulate(user); + }); + } + + const regulator = new Regulator(userDataStoreStub, 4, 30, 30); + return markAuthentications(regulator, USER1); + }); + + it("should disable regulation when max_retries is set to 0", function () { + const maxRetries = 0; + const regulator = new Regulator(userDataStoreStub, maxRetries, 60, 30); + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", false); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:15", false); + }) + .then(function () { + return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:25", false); + }) + .then(function () { + MockDate.set("1/2/2000 00:00:26"); + return regulator.regulate(USER1); + }); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/regulation/Regulator.ts b/themes/triangles/server/src/lib/regulation/Regulator.ts new file mode 100644 index 000000000..1037a6a17 --- /dev/null +++ b/themes/triangles/server/src/lib/regulation/Regulator.ts @@ -0,0 +1,55 @@ + +import * as BluebirdPromise from "bluebird"; +import exceptions = require("../Exceptions"); +import { IUserDataStore } from "../storage/IUserDataStore"; +import { AuthenticationTraceDocument } from "../storage/AuthenticationTraceDocument"; +import { IRegulator } from "./IRegulator"; + +export class Regulator implements IRegulator { + private userDataStore: IUserDataStore; + private banTime: number; + private findTime: number; + private maxRetries: number; + + constructor(userDataStore: any, maxRetries: number, findTime: number, banTime: number) { + this.userDataStore = userDataStore; + this.banTime = banTime; + this.findTime = findTime; + this.maxRetries = maxRetries; + } + + // Mark authentication + mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { + return this.userDataStore.saveAuthenticationTrace(userId, isAuthenticationSuccessful); + } + + regulate(userId: string): BluebirdPromise { + const that = this; + + if (that.maxRetries <= 0) return BluebirdPromise.resolve(); + + return this.userDataStore.retrieveLatestAuthenticationTraces(userId, that.maxRetries) + .then((docs: AuthenticationTraceDocument[]) => { + // less than the max authorized number of authentication in time range, thus authorizing access + if (docs.length < that.maxRetries) return BluebirdPromise.resolve(); + + const numberOfFailedAuth = docs + .map(function (d: AuthenticationTraceDocument) { return d.isAuthenticationSuccessful == false ? 1 : 0; }) + .reduce(function (acc, v) { return acc + v; }, 0); + + if (numberOfFailedAuth < this.maxRetries) return BluebirdPromise.resolve(); + + const newestDocument = docs[0]; + const oldestDocument = docs[that.maxRetries - 1]; + + const authenticationsTimeRangeInSeconds = (newestDocument.date.getTime() - oldestDocument.date.getTime()) / 1000; + const tooManyAuthInTimelapse = (authenticationsTimeRangeInSeconds < this.findTime); + const stillInBannedTimeRange = (new Date(new Date().getTime() - this.banTime * 1000) < newestDocument.date); + + if (tooManyAuthInTimelapse && stillInBannedTimeRange) + throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes."); + + return BluebirdPromise.resolve(); + }); + } +} diff --git a/themes/triangles/server/src/lib/regulation/RegulatorStub.spec.ts b/themes/triangles/server/src/lib/regulation/RegulatorStub.spec.ts new file mode 100644 index 000000000..ca8a00fb1 --- /dev/null +++ b/themes/triangles/server/src/lib/regulation/RegulatorStub.spec.ts @@ -0,0 +1,22 @@ +import Bluebird = require("bluebird"); +import Sinon = require("sinon"); +import { IRegulator } from "./IRegulator"; + + +export class RegulatorStub implements IRegulator { + markStub: Sinon.SinonStub; + regulateStub: Sinon.SinonStub; + + constructor() { + this.markStub = Sinon.stub(); + this.regulateStub = Sinon.stub(); + } + + mark(userId: string, isAuthenticationSuccessful: boolean): Bluebird { + return this.markStub(userId, isAuthenticationSuccessful); + } + + regulate(userId: string): Bluebird { + return this.regulateStub(userId); + } +} diff --git a/themes/triangles/server/src/lib/routes/error/401/get.spec.ts b/themes/triangles/server/src/lib/routes/error/401/get.spec.ts new file mode 100644 index 000000000..9fdac9c3c --- /dev/null +++ b/themes/triangles/server/src/lib/routes/error/401/get.spec.ts @@ -0,0 +1,61 @@ +import Sinon = require("sinon"); +import Express = require("express"); +import Assert = require("assert"); +import Get401 from "./get"; +import { ServerVariables } from "../../../ServerVariables"; +import { ServerVariablesMockBuilder, ServerVariablesMock } + from "../../../ServerVariablesMockBuilder.spec"; + +describe("routes/error/401/get", function () { + let vars: ServerVariables; + let mocks: ServerVariablesMock; + let req: any; + let res: any; + let renderSpy: Sinon.SinonSpy; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + vars = s.variables; + mocks = s.mocks; + + renderSpy = Sinon.spy(); + req = { + headers: {} + }; + res = { + render: renderSpy + }; + }); + + it("should set redirection url to the default redirection url", function () { + vars.config.default_redirection_url = "http://default-redirection"; + return Get401(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/401", { + redirection_url: "http://default-redirection" + })); + }); + }); + + it("should set redirection url to the referer", function () { + req.headers["referer"] = "http://redirection"; + return Get401(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/401", { + redirection_url: "http://redirection" + })); + }); + }); + + it("should render without redirecting the user", function () { + return Get401(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/401", { + redirection_url: undefined + })); + }); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/error/401/get.ts b/themes/triangles/server/src/lib/routes/error/401/get.ts new file mode 100644 index 000000000..ca4a3963d --- /dev/null +++ b/themes/triangles/server/src/lib/routes/error/401/get.ts @@ -0,0 +1,15 @@ + +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import redirector from "../redirector"; +import { ServerVariables } from "../../../ServerVariables"; + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response): BluebirdPromise { + const redirectionUrl = redirector(req, vars); + res.render("errors/401", { + redirection_url: redirectionUrl + }); + return BluebirdPromise.resolve(); + }; +} diff --git a/themes/triangles/server/src/lib/routes/error/403/get.spec.ts b/themes/triangles/server/src/lib/routes/error/403/get.spec.ts new file mode 100644 index 000000000..22eb84853 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/error/403/get.spec.ts @@ -0,0 +1,61 @@ +import Sinon = require("sinon"); +import Express = require("express"); +import Assert = require("assert"); +import Get403 from "./get"; +import { ServerVariables } from "../../../ServerVariables"; +import { ServerVariablesMockBuilder, ServerVariablesMock } + from "../../../ServerVariablesMockBuilder.spec"; + +describe("routes/error/403/get", function () { + let vars: ServerVariables; + let mocks: ServerVariablesMock; + let req: any; + let res: any; + let renderSpy: Sinon.SinonSpy; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + vars = s.variables; + mocks = s.mocks; + + renderSpy = Sinon.spy(); + req = { + headers: {} + }; + res = { + render: renderSpy + }; + }); + + it("should set redirection url to the default redirection url", function () { + vars.config.default_redirection_url = "http://default-redirection"; + return Get403(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/403", { + redirection_url: "http://default-redirection" + })); + }); + }); + + it("should set redirection url to the referer", function () { + req.headers["referer"] = "http://redirection"; + return Get403(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/403", { + redirection_url: "http://redirection" + })); + }); + }); + + it("should render without redirecting the user", function () { + return Get403(vars)(req, res as any) + .then(function () { + Assert(renderSpy.calledOnce); + Assert(renderSpy.calledWithExactly("errors/403", { + redirection_url: undefined + })); + }); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/error/403/get.ts b/themes/triangles/server/src/lib/routes/error/403/get.ts new file mode 100644 index 000000000..3ab0319e5 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/error/403/get.ts @@ -0,0 +1,15 @@ + +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import redirector from "../redirector"; +import { ServerVariables } from "../../../ServerVariables"; + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response): BluebirdPromise { + const redirectionUrl = redirector(req, vars); + res.render("errors/403", { + redirection_url: redirectionUrl + }); + return BluebirdPromise.resolve(); + }; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/error/404/get.spec.ts b/themes/triangles/server/src/lib/routes/error/404/get.spec.ts new file mode 100644 index 000000000..73e4e6cef --- /dev/null +++ b/themes/triangles/server/src/lib/routes/error/404/get.spec.ts @@ -0,0 +1,19 @@ +import Sinon = require("sinon"); +import Express = require("express"); +import Assert = require("assert"); +import Get404 from "./get"; + +describe("routes/error/404/get", function () { + it("should render the page", function () { + const req = {} as Express.Request; + const res = { + render: Sinon.stub() + }; + + return Get404(req, res as any) + .then(function () { + Assert(res.render.calledOnce); + Assert(res.render.calledWith("errors/404")); + }); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/error/404/get.ts b/themes/triangles/server/src/lib/routes/error/404/get.ts new file mode 100644 index 000000000..6693b6fc8 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/error/404/get.ts @@ -0,0 +1,8 @@ + +import BluebirdPromise = require("bluebird"); +import express = require("express"); + +export default function (req: express.Request, res: express.Response): BluebirdPromise { + res.render("errors/404"); + return BluebirdPromise.resolve(); +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/error/redirector.ts b/themes/triangles/server/src/lib/routes/error/redirector.ts new file mode 100644 index 000000000..b1a3ccc11 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/error/redirector.ts @@ -0,0 +1,13 @@ +import Express = require("express"); +import { ServerVariables } from "../../ServerVariables"; + +export default function (req: Express.Request, vars: ServerVariables): string { + let redirectionUrl: string; + + if (req.headers && req.headers["referer"]) + redirectionUrl = "" + req.headers["referer"]; + else if (vars.config.default_redirection_url) + redirectionUrl = vars.config.default_redirection_url; + + return redirectionUrl; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/firstfactor/get.ts b/themes/triangles/server/src/lib/routes/firstfactor/get.ts new file mode 100644 index 000000000..d94f656c8 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/firstfactor/get.ts @@ -0,0 +1,72 @@ + +import express = require("express"); +import Endpoints = require("../../../../../shared/api"); +import BluebirdPromise = require("bluebird"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Constants = require("../../../../../shared/constants"); +import Util = require("util"); +import { ServerVariables } from "../../ServerVariables"; +import { SafeRedirector } from "../../utils/SafeRedirection"; +import { Level } from "../../authentication/Level"; + +function getRedirectParam( + req: express.Request) { + return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; +} + +function redirectToSecondFactorPage( + req: express.Request, + res: express.Response) { + + const redirectUrl = getRedirectParam(req); + if (!redirectUrl) + res.redirect(Endpoints.SECOND_FACTOR_GET); + else + res.redirect( + Util.format("%s?%s=%s", + Endpoints.SECOND_FACTOR_GET, + Constants.REDIRECT_QUERY_PARAM, + redirectUrl)); +} + +function redirectToService( + req: express.Request, + res: express.Response, + redirector: SafeRedirector) { + const redirectUrl = getRedirectParam(req); + if (!redirectUrl) { + res.redirect(Endpoints.LOGGED_IN); + } else { + redirector.redirectOrElse(res, redirectUrl, Endpoints.LOGGED_IN); + } +} + +function renderFirstFactor( + res: express.Response) { + + res.render("firstfactor", { + first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST, + reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET + }); +} + +export default function ( + vars: ServerVariables) { + + const redirector = new SafeRedirector(vars.config.session.domain); + return function (req: express.Request, res: express.Response): BluebirdPromise { + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (authSession.authentication_level == Level.ONE_FACTOR) { + redirectToSecondFactorPage(req, res); + } else if (authSession.authentication_level == Level.TWO_FACTOR) { + redirectToService(req, res, redirector); + } else { + renderFirstFactor(res); + } + resolve(); + }); + }; +} diff --git a/themes/triangles/server/src/lib/routes/firstfactor/post.spec.ts b/themes/triangles/server/src/lib/routes/firstfactor/post.spec.ts new file mode 100644 index 000000000..e1d078cdd --- /dev/null +++ b/themes/triangles/server/src/lib/routes/firstfactor/post.spec.ts @@ -0,0 +1,136 @@ + +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import Assert = require("assert"); +import FirstFactorPost = require("./post"); +import exceptions = require("../../Exceptions"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../types/AuthenticationSession"; +import Endpoints = require("../../../../../shared/api"); +import AuthenticationRegulatorMock = require("../../regulation/RegulatorStub.spec"); +import ExpressMock = require("../../stubs/express.spec"); +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../ServerVariables"; + +describe("routes/firstfactor/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let emails: string[]; + let groups: string[]; + let vars: ServerVariables; + let mocks: ServerVariablesMock; + let authSession: AuthenticationSession; + + beforeEach(function () { + emails = ["test_ok@example.com"]; + groups = ["group1", "group2" ]; + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + mocks.authorizer.authorizationMock.returns(true); + mocks.regulator.regulateStub.returns(BluebirdPromise.resolve()); + mocks.regulator.markStub.returns(BluebirdPromise.resolve()); + + req = { + originalUrl: "/api/firstfactor", + body: { + username: "username", + password: "password" + }, + query: { + redirect: "http://redirect.url" + }, + session: { + cookie: {} + }, + headers: { + host: "home.example.com" + } + }; + + res = ExpressMock.ResponseMock(); + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + }); + + it("should reply with 204 if success", function () { + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.resolve({ + emails: emails, + groups: groups + })); + return FirstFactorPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal("username", authSession.userid); + Assert(res.send.calledOnce); + }); + }); + + describe("keep me logged in", () => { + beforeEach(() => { + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.resolve({ + emails: emails, + groups: groups + })); + req.body.keepMeLoggedIn = "true"; + return FirstFactorPost.default(vars)(req as any, res as any); + }); + + it("should set keep_me_logged_in session variable to true", function () { + Assert.equal(authSession.keep_me_logged_in, true); + }); + + it("should set cookie maxAge to one year", function () { + Assert.equal(req.session.cookie.maxAge, 365 * 24 * 60 * 60 * 1000); + }); + }); + + it("should retrieve email from LDAP", function () { + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.resolve([{ mail: ["test@example.com"] }])); + return FirstFactorPost.default(vars)(req as any, res as any); + }); + + it("should set first email address as user session variable", function () { + const emails = ["test_ok@example.com"]; + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.resolve({ + emails: emails, + groups: groups + })); + + return FirstFactorPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal("test_ok@example.com", authSession.email); + }); + }); + + it("should return error message when LDAP authenticator throws", function () { + mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") + .returns(BluebirdPromise.reject(new exceptions.LdapBindError("Bad credentials"))); + + return FirstFactorPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.equal(mocks.regulator.markStub.getCall(0).args[0], "username"); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + }); + }); + + it("should return error message when regulator rejects authentication", function () { + const err = new exceptions.AuthenticationRegulationError("Authentication regulation..."); + mocks.regulator.regulateStub.returns(BluebirdPromise.reject(err)); + return FirstFactorPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + }); + }); +}); + + diff --git a/themes/triangles/server/src/lib/routes/firstfactor/post.ts b/themes/triangles/server/src/lib/routes/firstfactor/post.ts new file mode 100644 index 000000000..565681d6a --- /dev/null +++ b/themes/triangles/server/src/lib/routes/firstfactor/post.ts @@ -0,0 +1,101 @@ + +import Exceptions = require("../../Exceptions"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import Endpoint = require("../../../../../shared/api"); +import ErrorReplies = require("../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Constants = require("../../../../../shared/constants"); +import UserMessages = require("../../../../../shared/UserMessages"); +import { ServerVariables } from "../../ServerVariables"; +import { AuthenticationSession } from "../../../../types/AuthenticationSession"; +import { GroupsAndEmails } from "../../authentication/backends/GroupsAndEmails"; +import { Level as AuthenticationLevel } from "../../authentication/Level"; +import { Level as AuthorizationLevel } from "../../authorization/Level"; +import { URLDecomposer } from "../../utils/URLDecomposer"; + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response) + : BluebirdPromise { + const username: string = req.body.username; + const password: string = req.body.password; + const keepMeLoggedIn: boolean = req.body.keepMeLoggedIn && + req.body.keepMeLoggedIn === "true"; + let authSession: AuthenticationSession; + + if (keepMeLoggedIn) { + // Stay connected for 1 year. + vars.logger.debug(req, "User requested to stay logged in for one year."); + req.session.cookie.maxAge = 365 * 24 * 60 * 60 * 1000; + } + + return BluebirdPromise.resolve() + .then(function () { + if (!username || !password) { + return BluebirdPromise.reject(new Error("No username or password.")); + } + vars.logger.info(req, "Starting authentication of user \"%s\"", username); + authSession = AuthenticationSessionHandler.get(req, vars.logger); + return vars.regulator.regulate(username); + }) + .then(function () { + vars.logger.info(req, "No regulation applied."); + return vars.usersDatabase.checkUserPassword(username, password); + }) + .then(function (groupsAndEmails: GroupsAndEmails) { + vars.logger.info(req, + "LDAP binding successful. Retrieved information about user are %s", + JSON.stringify(groupsAndEmails)); + authSession.userid = username; + authSession.keep_me_logged_in = keepMeLoggedIn; + authSession.authentication_level = AuthenticationLevel.ONE_FACTOR; + const redirectUrl: string = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined" + // Fuck, don't know why it is a string! + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : ""; + + const emails: string[] = groupsAndEmails.emails; + const groups: string[] = groupsAndEmails.groups; + const decomposition = URLDecomposer.fromUrl(redirectUrl); + const authorizationLevel = (decomposition) + ? vars.authorizer.authorization( + {domain: decomposition.domain, resource: decomposition.path}, + {user: username, groups: groups}) + : AuthorizationLevel.TWO_FACTOR; + + if (emails.length > 0) + authSession.email = emails[0]; + authSession.groups = groups; + + vars.logger.debug(req, "Mark successful authentication to regulator."); + vars.regulator.mark(username, true); + + if (authorizationLevel <= AuthorizationLevel.ONE_FACTOR) { + let newRedirectionUrl: string = redirectUrl; + if (!newRedirectionUrl) + newRedirectionUrl = Endpoint.LOGGED_IN; + res.send({ + redirect: newRedirectionUrl + }); + vars.logger.debug(req, "Redirect to '%s'", redirectUrl); + } + else { + let newRedirectUrl = Endpoint.SECOND_FACTOR_GET; + if (redirectUrl) { + newRedirectUrl += "?" + Constants.REDIRECT_QUERY_PARAM + "=" + + redirectUrl; + } + vars.logger.debug(req, "Redirect to '%s'", newRedirectUrl); + res.send({ + redirect: newRedirectUrl + }); + } + return BluebirdPromise.resolve(); + }) + .catch(Exceptions.LdapBindError, function (err: Error) { + vars.regulator.mark(username, false); + return ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)(err); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)); + }; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/loggedin/get.ts b/themes/triangles/server/src/lib/routes/loggedin/get.ts new file mode 100644 index 000000000..283a041b1 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/loggedin/get.ts @@ -0,0 +1,23 @@ +import Express = require("express"); +import Endpoints = require("../../../../../shared/api"); +import BluebirdPromise = require("bluebird"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { ServerVariables } from "../../ServerVariables"; +import ErrorReplies = require("../../ErrorReplies"); + +export default function (vars: ServerVariables) { + function handler(req: Express.Request, res: Express.Response): BluebirdPromise { + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + res.render("already-logged-in", { + logout_endpoint: Endpoints.LOGOUT_GET, + username: authSession.userid, + redirection_url: vars.config.default_redirection_url + }); + resolve(); + }) + .catch(ErrorReplies.replyWithError401(req, res, vars.logger)); + } + + return handler; +} diff --git a/themes/triangles/server/src/lib/routes/logout/get.ts b/themes/triangles/server/src/lib/routes/logout/get.ts new file mode 100644 index 000000000..4d5112146 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/logout/get.ts @@ -0,0 +1,20 @@ + +import express = require("express"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Constants = require("../../../../../shared/constants"); +import { ServerVariables } from "../../ServerVariables"; + +function getRedirectParam(req: express.Request) { + return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; +} + +export default function (vars: ServerVariables) { + return function(req: express.Request, res: express.Response) { + const redirect_param = getRedirectParam(req); + const redirect_url = redirect_param || "/"; + AuthenticationSessionHandler.reset(req); + res.redirect(redirect_url); + }; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/password-reset/constants.ts b/themes/triangles/server/src/lib/routes/password-reset/constants.ts new file mode 100644 index 000000000..5c639e92a --- /dev/null +++ b/themes/triangles/server/src/lib/routes/password-reset/constants.ts @@ -0,0 +1,2 @@ + +export const CHALLENGE = "reset-password"; \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/password-reset/form/post.spec.ts b/themes/triangles/server/src/lib/routes/password-reset/form/post.spec.ts new file mode 100644 index 000000000..ed029c906 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/password-reset/form/post.spec.ts @@ -0,0 +1,122 @@ + +import PasswordResetFormPost = require("./post"); +import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; +import { UserDataStore } from "../../../storage/UserDataStore"; +import Sinon = require("sinon"); +import Assert = require("assert"); +import BluebirdPromise = require("bluebird"); +import ExpressMock = require("../../../stubs/express.spec"); +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../ServerVariables"; +import { Level } from "../../../authentication/Level"; + +describe("routes/password-reset/form/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let vars: ServerVariables; + let mocks: ServerVariablesMock; + let authSession: AuthenticationSession; + + beforeEach(function () { + req = { + originalUrl: "/api/password-reset", + body: { + userid: "user" + }, + session: {}, + headers: { + host: "localhost" + } + }; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + + mocks.config.authentication_backend.ldap = { + url: "ldap://ldapjs", + mail_attribute: "mail", + user: "user", + password: "password", + additional_users_dn: "ou=users", + additional_groups_dn: "ou=groups", + base_dn: "dc=example,dc=com", + users_filter: "user", + group_name_attribute: "cn", + groups_filter: "groups" + }; + + res = ExpressMock.ResponseMock(); + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + authSession.userid = "user"; + authSession.email = "user@example.com"; + authSession.authentication_level = Level.ONE_FACTOR; + }); + + describe("test reset password post", () => { + it("should update the password and reset auth_session for reauthentication", function () { + req.body = {}; + req.body.password = "new-password"; + + mocks.usersDatabase.updatePasswordStub.returns(BluebirdPromise.resolve()); + + authSession.identity_check = { + userid: "user", + challenge: "reset-password" + }; + return PasswordResetFormPost.default(vars)(req as any, res as any) + .then(function () { + return AuthenticationSessionHandler.get(req as any, vars.logger); + }).then(function (_authSession) { + Assert.equal(res.status.getCall(0).args[0], 204); + Assert.equal(_authSession.authentication_level, Level.NOT_AUTHENTICATED); + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if identity_challenge does not exist", function () { + authSession.identity_check = { + userid: "user", + challenge: undefined + }; + return PasswordResetFormPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "An error occurred during password reset. Your password has not been changed." + }); + }); + }); + + it("should fail when ldap fails", function () { + req.body = {}; + req.body.password = "new-password"; + + mocks.usersDatabase.updatePasswordStub + .returns(BluebirdPromise.reject("Internal error with LDAP")); + + authSession.identity_check = { + challenge: "reset-password", + userid: "user" + }; + return PasswordResetFormPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "An error occurred during password reset. Your password has not been changed." + }); + return BluebirdPromise.resolve(); + }); + }); + }); +}); diff --git a/themes/triangles/server/src/lib/routes/password-reset/form/post.ts b/themes/triangles/server/src/lib/routes/password-reset/form/post.ts new file mode 100644 index 000000000..fccd7471b --- /dev/null +++ b/themes/triangles/server/src/lib/routes/password-reset/form/post.ts @@ -0,0 +1,50 @@ + +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); +import exceptions = require("../../../Exceptions"); +import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; +import ErrorReplies = require("../../../ErrorReplies"); +import UserMessages = require("../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../ServerVariables"; + +import Constants = require("./../constants"); + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const newPassword = objectPath.get(req, "body.password"); + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (!authSession.identity_check) { + reject(new Error("No identity check initiated")); + return; + } + + vars.logger.info(req, "User %s wants to reset his/her password.", + authSession.identity_check.userid); + vars.logger.debug(req, "Challenge %s", authSession.identity_check.challenge); + + if (authSession.identity_check.challenge != Constants.CHALLENGE) { + reject(new Error("Bad challenge.")); + return; + } + resolve(); + }) + .then(function () { + return vars.usersDatabase.updatePassword(authSession.identity_check.userid, newPassword); + }) + .then(function () { + vars.logger.info(req, "Password reset for user '%s'", + authSession.identity_check.userid); + AuthenticationSessionHandler.reset(req); + res.status(204); + res.send(); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.RESET_PASSWORD_FAILED)); + }; +} diff --git a/themes/triangles/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts b/themes/triangles/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts new file mode 100644 index 000000000..ac6a41754 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts @@ -0,0 +1,92 @@ + +import PasswordResetHandler + from "./PasswordResetHandler"; +import { UserDataStore } from "../../../storage/UserDataStore"; +import Sinon = require("sinon"); +import winston = require("winston"); +import assert = require("assert"); +import BluebirdPromise = require("bluebird"); +import ExpressMock = require("../../../stubs/express.spec"); +import { ServerVariablesMock, ServerVariablesMockBuilder } + from "../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../ServerVariables"; + +describe("routes/password-reset/identity/PasswordResetHandler", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + req = { + originalUrl: "/non-api/xxx", + query: { + userid: "user" + }, + session: { + auth: { + userid: "user", + email: "user@example.com", + first_factor: true, + second_factor: false + } + }, + headers: { + host: "localhost" + } + }; + + const options = { + inMemoryOnly: true + }; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + mocks.userDataStore.saveU2FRegistrationStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub + .returns(BluebirdPromise.resolve({})); + res = ExpressMock.ResponseMock(); + }); + + describe("test reset password identity pre check", () => { + it("should fail when no userid is provided", function () { + req.query.userid = undefined; + const handler = new PasswordResetHandler(vars.logger, + vars.usersDatabase); + return handler.preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject("It should fail"); + }) + .catch(function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if ldap fail", function () { + mocks.usersDatabase.getEmailsStub + .returns(BluebirdPromise.reject("Internal error")); + new PasswordResetHandler(vars.logger, vars.usersDatabase) + .preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here")); + }, + function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should returns identity when ldap replies", function () { + mocks.usersDatabase.getEmailsStub + .returns(BluebirdPromise.resolve(["test@example.com"])); + return new PasswordResetHandler(vars.logger, vars.usersDatabase) + .preValidationInit(req as any); + }); + }); +}); diff --git a/themes/triangles/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts b/themes/triangles/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts new file mode 100644 index 000000000..42ae92cda --- /dev/null +++ b/themes/triangles/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts @@ -0,0 +1,69 @@ +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); + +import exceptions = require("../../../Exceptions"); +import { Identity } from "../../../../../types/Identity"; +import { IdentityValidable } from "../../../IdentityValidable"; +import { PRE_VALIDATION_TEMPLATE } from "../../../IdentityCheckPreValidationTemplate"; +import Constants = require("../constants"); +import { IRequestLogger } from "../../../logging/IRequestLogger"; +import { IUsersDatabase } from "../../../authentication/backends/IUsersDatabase"; + +export const TEMPLATE_NAME = "password-reset-form"; + +export default class PasswordResetHandler implements IdentityValidable { + private logger: IRequestLogger; + private usersDatabase: IUsersDatabase; + + constructor(logger: IRequestLogger, usersDatabase: IUsersDatabase) { + this.logger = logger; + this.usersDatabase = usersDatabase; + } + + challenge(): string { + return Constants.CHALLENGE; + } + + preValidationInit(req: express.Request): BluebirdPromise { + const that = this; + const userid: string = + objectPath.get(req, "query.userid"); + return BluebirdPromise.resolve() + .then(function () { + that.logger.debug(req, "User '%s' requested a password reset", userid); + if (!userid) + return BluebirdPromise.reject( + new exceptions.AccessDeniedError("No user id provided")); + + return that.usersDatabase.getEmails(userid); + }) + .then(function (emails: string[]) { + if (!emails && emails.length <= 0) throw new Error("No email found"); + const identity = { + email: emails[0], + userid: userid + }; + return BluebirdPromise.resolve(identity); + }) + .catch(function (err: Error) { + return BluebirdPromise.reject(new exceptions.IdentityError(err.message)); + }); + } + + preValidationResponse(req: express.Request, res: express.Response) { + res.render(PRE_VALIDATION_TEMPLATE); + } + + postValidationInit(req: express.Request) { + return BluebirdPromise.resolve(); + } + + postValidationResponse(req: express.Request, res: express.Response) { + res.render(TEMPLATE_NAME); + } + + mailSubject(): string { + return "Reset your password"; + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/password-reset/request/get.ts b/themes/triangles/server/src/lib/routes/password-reset/request/get.ts new file mode 100644 index 000000000..8f3ae2b4b --- /dev/null +++ b/themes/triangles/server/src/lib/routes/password-reset/request/get.ts @@ -0,0 +1,13 @@ + +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); +import exceptions = require("../../../Exceptions"); + +import Constants = require("./../constants"); + +const TEMPLATE_NAME = "password-reset-request"; + +export default function (req: express.Request, res: express.Response) { + res.render(TEMPLATE_NAME); +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/secondfactor/get.spec.ts b/themes/triangles/server/src/lib/routes/secondfactor/get.spec.ts new file mode 100644 index 000000000..6c77e1f69 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/secondfactor/get.spec.ts @@ -0,0 +1,44 @@ +import SecondFactorGet from "./get"; +import { ServerVariablesMockBuilder, ServerVariablesMock } + from "../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../ServerVariables"; +import Sinon = require("sinon"); +import ExpressMock = require("../../stubs/express.spec"); +import Assert = require("assert"); +import Endpoints = require("../../../../../shared/api"); +import BluebirdPromise = require("bluebird"); + +describe("routes/secondfactor/get", function () { + let mocks: ServerVariablesMock; + let vars: ServerVariables; + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false + } + }; + }); + + describe("test rendering", function () { + it("should render second factor page", function () { + req.session.auth.second_factor = false; + return SecondFactorGet(vars)(req as any, res as any) + .then(function () { + Assert(res.render.calledWith("secondfactor")); + return BluebirdPromise.resolve(); + }); + }); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/secondfactor/get.ts b/themes/triangles/server/src/lib/routes/secondfactor/get.ts new file mode 100644 index 000000000..9f6deb4c6 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/secondfactor/get.ts @@ -0,0 +1,28 @@ + +import Express = require("express"); +import Endpoints = require("../../../../../shared/api"); +import BluebirdPromise = require("bluebird"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { ServerVariables } from "../../ServerVariables"; + +const TEMPLATE_NAME = "secondfactor"; + +export default function (vars: ServerVariables) { + function handler(req: Express.Request, res: Express.Response) + : BluebirdPromise { + + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + + res.render(TEMPLATE_NAME, { + username: authSession.userid, + totp_identity_start_endpoint: + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, + u2f_identity_start_endpoint: + Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET + }); + resolve(); + }); + } + return handler; +} diff --git a/themes/triangles/server/src/lib/routes/secondfactor/redirect.spec.ts b/themes/triangles/server/src/lib/routes/secondfactor/redirect.spec.ts new file mode 100644 index 000000000..ea66e6dca --- /dev/null +++ b/themes/triangles/server/src/lib/routes/secondfactor/redirect.spec.ts @@ -0,0 +1,41 @@ +import Redirect from "./redirect"; +import ExpressMock = require("../../stubs/express.spec"); +import { ServerVariablesMockBuilder, ServerVariablesMock } +from "../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../ServerVariables"; +import Assert = require("assert"); + +describe("routes/secondfactor/redirect", function() { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + }); + + it("should redirect to default_redirection_url", function() { + vars.config.default_redirection_url = "http://default_redirection_url"; + Redirect(vars)(req as any, res as any) + .then(function() { + Assert(res.json.calledWith({ + redirect: "http://default_redirection_url" + })); + }); + }); + + it("should redirect to /", function() { + Redirect(vars)(req as any, res as any) + .then(function() { + Assert(res.json.calledWith({ + redirect: "/" + })); + }); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/secondfactor/redirect.ts b/themes/triangles/server/src/lib/routes/secondfactor/redirect.ts new file mode 100644 index 000000000..5d84d9ebf --- /dev/null +++ b/themes/triangles/server/src/lib/routes/secondfactor/redirect.ts @@ -0,0 +1,30 @@ + +import express = require("express"); +import objectPath = require("object-path"); +import Endpoints = require("../../../../../shared/api"); +import { ServerVariables } from "../../ServerVariables"; +import BluebirdPromise = require("bluebird"); +import ErrorReplies = require("../../ErrorReplies"); +import UserMessages = require("../../../../../shared/UserMessages"); +import { RedirectionMessage } from "../../../../../shared/RedirectionMessage"; +import Constants = require("../../../../../shared/constants"); + +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response) + : BluebirdPromise { + + return new BluebirdPromise(function (resolve, reject) { + let redirectUrl: string = "/"; + if (vars.config.default_redirection_url) { + redirectUrl = vars.config.default_redirection_url; + } + vars.logger.debug(req, "Request redirection to \"%s\".", redirectUrl); + res.json({ + redirect: redirectUrl + } as RedirectionMessage); + return resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + }; +} diff --git a/themes/triangles/server/src/lib/routes/secondfactor/totp/constants.ts b/themes/triangles/server/src/lib/routes/secondfactor/totp/constants.ts new file mode 100644 index 000000000..7b5a1dcfe --- /dev/null +++ b/themes/triangles/server/src/lib/routes/secondfactor/totp/constants.ts @@ -0,0 +1,4 @@ + +export const CHALLENGE = "totp-register"; +export const TEMPLATE_NAME = "totp-register"; + diff --git a/themes/triangles/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts b/themes/triangles/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts new file mode 100644 index 000000000..78b8ea3ea --- /dev/null +++ b/themes/triangles/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts @@ -0,0 +1,116 @@ +import Sinon = require("sinon"); +import RegistrationHandler from "./RegistrationHandler"; +import { Identity } from "../../../../../../types/Identity"; +import { UserDataStore } from "../../../../storage/UserDataStore"; +import BluebirdPromise = require("bluebird"); +import ExpressMock = require("../../../../stubs/express.spec"); +import { ServerVariablesMock, ServerVariablesMockBuilder } + from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; +import Assert = require("assert"); + +describe("routes/secondfactor/totp/identity/RegistrationHandler", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + req.session = { + auth: { + userid: "user", + email: "user@example.com", + first_factor: true, + second_factor: false, + identity_check: { + userid: "user", + challenge: "totp-register" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub + .returns(BluebirdPromise.resolve({})); + mocks.userDataStore.saveTOTPSecretStub + .returns(BluebirdPromise.resolve({})); + + res = ExpressMock.ResponseMock(); + }); + + describe("test totp registration pre validation", function () { + it("should fail if first_factor has not been passed", function () { + req.session.auth.first_factor = false; + return new RegistrationHandler(vars.logger, vars.userDataStore, + vars.totpHandler, vars.config.totp) + .preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("It should fail")); + }) + .catch(function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if userid is missing", function (done) { + req.session.auth.first_factor = false; + req.session.auth.userid = undefined; + + new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler, + vars.config.totp) + .preValidationInit(req as any) + .catch(function (err: Error) { + done(); + }); + }); + + it("should fail if email is missing", function (done) { + req.session.auth.first_factor = false; + req.session.auth.email = undefined; + + new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler, + vars.config.totp) + .preValidationInit(req as any) + .catch(function (err: Error) { + done(); + }); + }); + + it("should succeed if first factor passed, userid and email are provided", + function () { + return new RegistrationHandler(vars.logger, vars.userDataStore, + vars.totpHandler, vars.config.totp) + .preValidationInit(req as any); + }); + }); + + describe("test totp registration post validation", function () { + it("should generate a secret using userId as label and issuer defined in config", function () { + vars.config.totp = { + issuer: "issuer" + }; + return new RegistrationHandler(vars.logger, vars.userDataStore, + vars.totpHandler, vars.config.totp) + .postValidationResponse(req as any, res as any) + .then(function() { + Assert(mocks.totpHandler.generateStub.calledWithExactly("user", "issuer")); + }); + }); + }); +}); diff --git a/themes/triangles/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts b/themes/triangles/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts new file mode 100644 index 000000000..b39b6d045 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts @@ -0,0 +1,112 @@ + +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); + +import { Identity } from "../../../../../../types/Identity"; +import { IdentityValidable } from "../../../../IdentityValidable"; +import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; +import Constants = require("../constants"); +import Endpoints = require("../../../../../../../shared/api"); +import ErrorReplies = require("../../../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import FirstFactorValidator = require("../../../../FirstFactorValidator"); +import { IRequestLogger } from "../../../../logging/IRequestLogger"; +import { IUserDataStore } from "../../../../storage/IUserDataStore"; +import { ITotpHandler } from "../../../../authentication/totp/ITotpHandler"; +import { TOTPSecret } from "../../../../../../types/TOTPSecret"; +import { TotpConfiguration } from "../../../../configuration/schema/TotpConfiguration"; + + +export default class RegistrationHandler implements IdentityValidable { + private logger: IRequestLogger; + private userDataStore: IUserDataStore; + private totp: ITotpHandler; + private configuration: TotpConfiguration; + + constructor(logger: IRequestLogger, + userDataStore: IUserDataStore, + totp: ITotpHandler, configuration: TotpConfiguration) { + this.logger = logger; + this.userDataStore = userDataStore; + this.totp = totp; + this.configuration = configuration; + } + + challenge(): string { + return Constants.CHALLENGE; + } + + private retrieveIdentity(req: express.Request): BluebirdPromise { + const that = this; + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, that.logger); + const userid = authSession.userid; + const email = authSession.email; + + if (!(userid && email)) { + return reject(new Error("User ID or email is missing")); + } + + const identity = { + email: email, + userid: userid + }; + return resolve(identity); + }); + } + + preValidationInit(req: express.Request): BluebirdPromise { + const that = this; + return FirstFactorValidator.validate(req, this.logger) + .then(function () { + return that.retrieveIdentity(req); + }); + } + + preValidationResponse(req: express.Request, res: express.Response) { + res.render(PRE_VALIDATION_TEMPLATE); + } + + postValidationInit(req: express.Request) { + return FirstFactorValidator.validate(req, this.logger); + } + + postValidationResponse(req: express.Request, res: express.Response) + : BluebirdPromise { + const that = this; + let secret: TOTPSecret; + let userId: string; + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, that.logger); + userId = authSession.userid; + + if (authSession.identity_check.challenge != Constants.CHALLENGE + || !userId) + return reject(new Error("Bad challenge.")); + + resolve(); + }) + .then(function () { + secret = that.totp.generate(userId, + that.configuration.issuer); + that.logger.debug(req, "Save the TOTP secret in DB"); + return that.userDataStore.saveTOTPSecret(userId, secret); + }) + .then(function () { + AuthenticationSessionHandler.reset(req); + + res.render(Constants.TEMPLATE_NAME, { + base32_secret: secret.base32, + otpauth_url: secret.otpauth_url, + login_endpoint: Endpoints.FIRST_FACTOR_GET + }); + }) + .catch(ErrorReplies.replyWithError200(req, res, that.logger, UserMessages.OPERATION_FAILED)); + } + + mailSubject(): string { + return "Set up Authelia's one-time password"; + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts b/themes/triangles/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts new file mode 100644 index 000000000..70a20d39d --- /dev/null +++ b/themes/triangles/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts @@ -0,0 +1,76 @@ + +import BluebirdPromise = require("bluebird"); +import Sinon = require("sinon"); +import Assert = require("assert"); +import Exceptions = require("../../../../Exceptions"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import SignPost = require("./post"); +import { ServerVariables } from "../../../../ServerVariables"; + +import ExpressMock = require("../../../../stubs/express.spec"); +import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; +import { Level } from "../../../../authentication/Level"; + +describe("routes/secondfactor/totp/sign/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let authSession: AuthenticationSession; + let vars: ServerVariables; + let mocks: ServerVariablesMock; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + vars = s.variables; + mocks = s.mocks; + const app_get = Sinon.stub(); + req = { + originalUrl: "/api/totp-register", + app: {}, + body: { + token: "abc" + }, + session: {}, + query: { + redirect: "http://redirect" + } + }; + res = ExpressMock.ResponseMock(); + + const doc = { + userid: "user", + secret: { + base32: "ABCDEF" + } + }; + mocks.userDataStore.retrieveTOTPSecretStub.returns(BluebirdPromise.resolve(doc)); + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + authSession.userid = "user"; + authSession.authentication_level = Level.ONE_FACTOR; + }); + + + it("should send status code 200 when totp is valid", function () { + mocks.totpHandler.validateStub.returns(true); + return SignPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(authSession.authentication_level, Level.TWO_FACTOR); + return BluebirdPromise.resolve(); + }); + }); + + it("should send error message when totp is not valid", function () { + mocks.totpHandler.validateStub.returns(false); + return SignPost.default(vars)(req as any, res as any) + .then(function () { + Assert.notEqual(authSession.authentication_level, Level.TWO_FACTOR); + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); +}); + diff --git a/themes/triangles/server/src/lib/routes/secondfactor/totp/sign/post.ts b/themes/triangles/server/src/lib/routes/secondfactor/totp/sign/post.ts new file mode 100644 index 000000000..34a276d12 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/secondfactor/totp/sign/post.ts @@ -0,0 +1,42 @@ +import Bluebird = require("bluebird"); +import Express = require("express"); + +import { TOTPSecretDocument } from "../../../../storage/TOTPSecretDocument"; +import Endpoints = require("../../../../../../../shared/api"); +import Redirect from "../../redirect"; +import ErrorReplies = require("../../../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../../ServerVariables"; +import { Level } from "../../../../authentication/Level"; + +const UNAUTHORIZED_MESSAGE = "Unauthorized access"; + +export default function (vars: ServerVariables) { + function handler(req: Express.Request, res: Express.Response): Bluebird { + let authSession: AuthenticationSession; + const token = req.body.token; + + return new Bluebird(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + vars.logger.info(req, "Initiate TOTP validation for user \"%s\".", authSession.userid); + resolve(); + }) + .then(function () { + return vars.userDataStore.retrieveTOTPSecret(authSession.userid); + }) + .then(function (doc: TOTPSecretDocument) { + if (!vars.totpHandler.validate(token, doc.secret.base32)) + return Bluebird.reject(new Error("Invalid TOTP token.")); + + vars.logger.debug(req, "TOTP validation succeeded."); + authSession.authentication_level = Level.TWO_FACTOR; + Redirect(vars)(req, res); + return Bluebird.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + return handler; +} diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts new file mode 100644 index 000000000..7f16c0ee8 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts @@ -0,0 +1,11 @@ + +import util = require("util"); +import express = require("express"); + +function extract_app_id(req: express.Request): string { + return util.format("https://%s", req.headers.host); +} + +export = { + extract_app_id: extract_app_id +}; \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts new file mode 100644 index 000000000..a54bfbfe6 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts @@ -0,0 +1,96 @@ +import Sinon = require("sinon"); +import Assert = require("assert"); +import BluebirdPromise = require("bluebird"); + +import { Identity } from "../../../../../../types/Identity"; +import RegistrationHandler from "./RegistrationHandler"; +import ExpressMock = require("../../../../stubs/express.spec"); +import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; + +describe("routes/secondfactor/u2f/identity/RegistrationHandler", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + req.app = {}; + req.session = { + auth: { + userid: "user", + email: "user@example.com", + first_factor: true, + second_factor: false + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + + res = ExpressMock.ResponseMock(); + res.send = Sinon.spy(); + res.json = Sinon.spy(); + res.status = Sinon.spy(); + }); + + describe("test u2f registration check", test_registration_check); + + function test_registration_check() { + it("should fail if first_factor has not been passed", function () { + req.session.auth.first_factor = false; + return new RegistrationHandler(vars.logger).preValidationInit(req as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if userid is missing", function () { + req.session.auth.first_factor = false; + req.session.auth.userid = undefined; + + return new RegistrationHandler(vars.logger).preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here")); + }, + function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should fail if email is missing", function () { + req.session.auth.first_factor = false; + req.session.auth.email = undefined; + + return new RegistrationHandler(vars.logger).preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here")); + }, + function (err: Error) { + return BluebirdPromise.resolve(); + }); + }); + + it("should succeed if first factor passed, userid and email are provided", function () { + req.session.auth.first_factor = true; + req.session.auth.email = "admin@example.com"; + req.session.auth.userid = "user"; + return new RegistrationHandler(vars.logger).preValidationInit(req as any); + }); + } +}); diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts new file mode 100644 index 000000000..bc4713c77 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts @@ -0,0 +1,73 @@ + +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import objectPath = require("object-path"); + +import { IdentityValidable } from "../../../../IdentityValidable"; +import { Identity } from "../../../../../../types/Identity"; +import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; +import FirstFactorValidator = require("../../../../FirstFactorValidator"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { IRequestLogger } from "../../../../logging/IRequestLogger"; + +const CHALLENGE = "u2f-register"; +const MAIL_SUBJECT = "Register your security key with Authelia"; + +const POST_VALIDATION_TEMPLATE_NAME = "u2f-register"; + + +export default class RegistrationHandler implements IdentityValidable { + private logger: IRequestLogger; + + constructor(logger: IRequestLogger) { + this.logger = logger; + } + + challenge(): string { + return CHALLENGE; + } + + private retrieveIdentity(req: express.Request): BluebirdPromise { + const that = this; + return new BluebirdPromise(function(resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, that.logger); + const userid = authSession.userid; + const email = authSession.email; + + if (!(userid && email)) { + return reject(new Error("User ID or email is missing")); + } + + const identity = { + email: email, + userid: userid + }; + return resolve(identity); + }); + } + + preValidationInit(req: express.Request): BluebirdPromise { + const that = this; + return FirstFactorValidator.validate(req, this.logger) + .then(function () { + return that.retrieveIdentity(req); + }); + } + + preValidationResponse(req: express.Request, res: express.Response) { + res.render(PRE_VALIDATION_TEMPLATE); + } + + postValidationInit(req: express.Request) { + return FirstFactorValidator.validate(req, this.logger); + } + + postValidationResponse(req: express.Request, res: express.Response) { + res.render(POST_VALIDATION_TEMPLATE_NAME); + } + + mailSubject(): string { + return MAIL_SUBJECT; + } +} + diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts new file mode 100644 index 000000000..de3347a21 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts @@ -0,0 +1,146 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import assert = require("assert"); +import U2FRegisterPost = require("./post"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import ExpressMock = require("../../../../stubs/express.spec"); +import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; + + +describe("routes/secondfactor/u2f/register/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + let authSession: AuthenticationSession; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.originalUrl = "/api/xxxx"; + req.app = {}; + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false, + identity_check: { + challenge: "u2f-register", + userid: "user" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + }); + + describe("test registration", test_registration); + + + function test_registration() { + it("should save u2f meta and return status code 200", function () { + const expectedStatus = { + keyHandle: "keyHandle", + publicKey: "pbk", + certificate: "cert" + }; + mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve(expectedStatus)); + + authSession.register_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { + assert.equal("user", mocks.userDataStore.saveU2FRegistrationStub.getCall(0).args[0]); + assert.equal(authSession.identity_check, undefined); + }); + }); + + it("should return error message on finishRegistration error", function () { + mocks.u2f.checkRegistrationStub.returns({ errorCode: 500 }); + + authSession.register_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function () { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); + + it("should return error message when register_request is not provided", function () { + mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); + authSession.register_request = undefined; + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function () { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); + + it("should return error message when no auth request has been initiated", function () { + mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); + authSession.register_request = undefined; + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function () { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); + + it("should return error message when identity has not been verified", function () { + authSession.identity_check = undefined; + return U2FRegisterPost.default(vars)(req as any, res as any) + .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) + .catch(function () { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + return BluebirdPromise.resolve(); + }); + }); + } +}); + diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/register/post.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/register/post.ts new file mode 100644 index 000000000..7296ccbe5 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/secondfactor/u2f/register/post.ts @@ -0,0 +1,64 @@ + +import { UserDataStore } from "../../../../storage/UserDataStore"; +import objectPath = require("object-path"); +import u2f_common = require("../U2FCommon"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import U2f = require("u2f"); +import { U2FRegistration } from "../../../../../../types/U2FRegistration"; +import redirect from "../../redirect"; +import ErrorReplies = require("../../../../ErrorReplies"); +import { ServerVariables } from "../../../../ServerVariables"; +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; + + +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const appid = u2f_common.extract_app_id(req); + const registrationResponse: U2f.RegistrationData = req.body; + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + const registrationRequest = authSession.register_request; + + if (!registrationRequest) { + return reject(new Error("No registration request")); + } + + if (!authSession.identity_check + || authSession.identity_check.challenge != "u2f-register") { + return reject(new Error("Bad challenge for registration request")); + } + + vars.logger.info(req, "Finishing registration"); + vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); + vars.logger.debug(req, "RegistrationResponse = %s", JSON.stringify(registrationResponse)); + + return resolve(vars.u2f.checkRegistration(registrationRequest, registrationResponse)); + }) + .then(function (u2fResult: U2f.RegistrationResult | U2f.Error): BluebirdPromise { + if (objectPath.has(u2fResult, "errorCode")) + return BluebirdPromise.reject(new Error("Error while registering.")); + + const registrationResult: U2f.RegistrationResult = u2fResult as U2f.RegistrationResult; + vars.logger.info(req, "Store registration and reply"); + vars.logger.debug(req, "RegistrationResult = %s", JSON.stringify(registrationResult)); + const registration: U2FRegistration = { + keyHandle: registrationResult.keyHandle, + publicKey: registrationResult.publicKey + }; + return vars.userDataStore.saveU2FRegistration(authSession.userid, appid, registration); + }) + .then(function () { + authSession.identity_check = undefined; + redirect(vars)(req, res); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + return handler; +} diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts new file mode 100644 index 000000000..a207c9109 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts @@ -0,0 +1,86 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import Assert = require("assert"); +import U2FRegisterRequestGet = require("./get"); +import ExpressMock = require("../../../../stubs/express.spec"); +import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; + +describe("routes/secondfactor/u2f/register_request/get", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.originalUrl = "/api/xxxx"; + req.app = {}; + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false, + identity_check: { + challenge: "u2f-register", + userid: "user" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + const options = { + inMemoryOnly: true + }; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + }); + + describe("test registration request", () => { + it("should send back the registration request and save it in the session", function () { + const expectedRequest = { + test: "abc" + }; + mocks.u2f.requestStub.returns(BluebirdPromise.resolve(expectedRequest)); + return U2FRegisterRequestGet.default(vars)(req as any, res as any) + .then(function () { + Assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); + }); + }); + + it("should return internal error on registration request", function () { + res.send = sinon.spy(); + const user_key_container = {}; + mocks.u2f.requestStub.returns(BluebirdPromise.reject("Internal error")); + return U2FRegisterRequestGet.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + }); + }); + + it("should return forbidden if identity has not been verified", function () { + req.session.auth.identity_check = undefined; + return U2FRegisterRequestGet.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(403, res.status.getCall(0).args[0]); + }); + }); + }); +}); + diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/register_request/get.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/register_request/get.ts new file mode 100644 index 000000000..f611af933 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/secondfactor/u2f/register_request/get.ts @@ -0,0 +1,43 @@ + +import { UserDataStore } from "../../../../storage/UserDataStore"; + +import objectPath = require("object-path"); +import u2f_common = require("../U2FCommon"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import U2f = require("u2f"); +import ErrorReplies = require("../../../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../../ServerVariables"; + +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const appid: string = u2f_common.extract_app_id(req); + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (!authSession.identity_check + || authSession.identity_check.challenge != "u2f-register") { + res.status(403); + res.send(); + return reject(new Error("Bad challenge.")); + } + + vars.logger.info(req, "Starting registration for appId '%s'", appid); + return resolve(vars.u2f.request(appid)); + }) + .then(function (registrationRequest: U2f.Request) { + vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); + authSession.register_request = registrationRequest; + res.json(registrationRequest); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + + return handler; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts new file mode 100644 index 000000000..9b137e66d --- /dev/null +++ b/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts @@ -0,0 +1,101 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import Assert = require("assert"); +import U2FSignPost = require("./post"); +import { ServerVariables } from "../../../../ServerVariables"; +import winston = require("winston"); + +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; +import ExpressMock = require("../../../../stubs/express.spec"); +import U2FMock = require("../../../../stubs/u2f.spec"); +import U2f = require("u2f"); +import { Level } from "../../../../authentication/Level"; + +describe("routes/secondfactor/u2f/sign/post", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.app = {}; + req.originalUrl = "/api/xxxx"; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + req.session = { + auth: { + userid: "user", + authentication_level: Level.ONE_FACTOR, + identity_check: { + challenge: "u2f-register", + userid: "user" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const options = { + inMemoryOnly: true + }; + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + }); + + it("should return status code 204", function () { + const expectedStatus = { + keyHandle: "keyHandle", + publicKey: "pbk", + certificate: "cert" + }; + mocks.u2f.checkSignatureStub.returns(expectedStatus); + + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ + registration: { + publicKey: "PUBKEY" + } + })); + + req.session.auth.sign_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + return U2FSignPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(req.session.auth.authentication_level, Level.TWO_FACTOR); + }); + }); + + it("should return unauthorized error on registration request internal error", function () { + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ + registration: { + publicKey: "PUBKEY" + } + })); + mocks.u2f.checkSignatureStub.returns({ errorCode: 500 }); + + req.session.auth.sign_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + return U2FSignPost.default(vars)(req as any, res as any) + .then(function () { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], + { error: "Operation failed." }); + }); + }); +}); + diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign/post.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign/post.ts new file mode 100644 index 000000000..7ee711c2c --- /dev/null +++ b/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign/post.ts @@ -0,0 +1,57 @@ + +import objectPath = require("object-path"); +import u2f_common = require("../U2FCommon"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import { UserDataStore } from "../../../../storage/UserDataStore"; +import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; +import { Winston } from "../../../../../../types/Dependencies"; +import U2f = require("u2f"); +import exceptions = require("../../../../Exceptions"); +import redirect from "../../redirect"; +import ErrorReplies = require("../../../../ErrorReplies"); +import { ServerVariables } from "../../../../ServerVariables"; +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import { Level } from "../../../../authentication/Level"; + +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const appId = u2f_common.extract_app_id(req); + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (!authSession.sign_request) { + const err = new Error("No sign request"); + ErrorReplies.replyWithError401(req, res, vars.logger)(err); + return reject(err); + } + resolve(); + }) + .then(function () { + const userid = authSession.userid; + return vars.userDataStore.retrieveU2FRegistration(userid, appId); + }) + .then(function (doc: U2FRegistrationDocument): BluebirdPromise { + const signRequest = authSession.sign_request; + const signData: U2f.SignatureData = req.body; + vars.logger.info(req, "Finish authentication"); + return BluebirdPromise.resolve(vars.u2f.checkSignature(signRequest, signData, doc.registration.publicKey)); + }) + .then(function (result: U2f.SignatureResult | U2f.Error): BluebirdPromise { + if (objectPath.has(result, "errorCode")) + return BluebirdPromise.reject(new Error("Error while signing")); + vars.logger.info(req, "Successful authentication"); + authSession.authentication_level = Level.TWO_FACTOR; + redirect(vars)(req, res); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + + return handler; +} + diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts new file mode 100644 index 000000000..dd52b27e0 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts @@ -0,0 +1,68 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import assert = require("assert"); +import U2FSignRequestGet = require("./get"); +import ExpressMock = require("../../../../stubs/express.spec"); +import { Request } from "u2f"; +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; +import { ServerVariables } from "../../../../ServerVariables"; + +import { SignMessage } from "../../../../../../../shared/SignMessage"; + +describe("routes/secondfactor/u2f/sign_request/get", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.originalUrl = "/api/xxxx"; + req.app = {}; + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false, + identity_check: { + challenge: "u2f-register", + userid: "user" + } + } + }; + req.headers = {}; + req.headers.host = "localhost"; + + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + }); + + it("should send back the sign request and save it in the session", function () { + const expectedRequest: Request = { + version: "U2F_V2", + appId: 'app', + challenge: 'challenge!' + }; + mocks.u2f.requestStub.returns(expectedRequest); + mocks.userDataStore.retrieveU2FRegistrationStub + .returns(BluebirdPromise.resolve({ + registration: { + keyHandle: "KeyHandle" + } + })); + + return U2FSignRequestGet.default(vars)(req as any, res as any) + .then(() => { + assert.deepEqual(expectedRequest, req.session.auth.sign_request); + assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); + }); + }); +}); + diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts new file mode 100644 index 000000000..9e93dde06 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts @@ -0,0 +1,42 @@ + +import u2f_common = require("../../../secondfactor/u2f/U2FCommon"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; +import exceptions = require("../../../../Exceptions"); +import ErrorReplies = require("../../../../ErrorReplies"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; +import UserMessages = require("../../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../../ServerVariables"; +import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; + +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession; + const appId = u2f_common.extract_app_id(req); + + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + resolve(); + }) + .then(function () { + return vars.userDataStore.retrieveU2FRegistration(authSession.userid, appId); + }) + .then(function (doc: U2FRegistrationDocument): BluebirdPromise { + if (!doc) + return BluebirdPromise.reject(new exceptions.AccessDeniedError("No U2F registration document found.")); + + const appId: string = u2f_common.extract_app_id(req); + vars.logger.info(req, "Start authentication of app '%s'", appId); + vars.logger.debug(req, "AppId = %s, keyHandle = %s", appId, JSON.stringify(doc.registration.keyHandle)); + + const request = vars.u2f.request(appId, doc.registration.keyHandle); + authSession.sign_request = request; + res.json(request); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + return handler; +} diff --git a/themes/triangles/server/src/lib/routes/verify/access_control.ts b/themes/triangles/server/src/lib/routes/verify/access_control.ts new file mode 100644 index 000000000..136239aeb --- /dev/null +++ b/themes/triangles/server/src/lib/routes/verify/access_control.ts @@ -0,0 +1,51 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import Util = require("util"); + +import Exceptions = require("../../Exceptions"); + +import { Level as AuthorizationLevel } from "../../authorization/Level"; +import { Level as AuthenticationLevel } from "../../authentication/Level"; +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { ServerVariables } from "../../ServerVariables"; + +function isAuthorized( + authorization: AuthorizationLevel, + authentication: AuthenticationLevel): boolean { + + if (authorization == AuthorizationLevel.BYPASS) { + return true; + } else if (authorization == AuthorizationLevel.ONE_FACTOR && + authentication >= AuthenticationLevel.ONE_FACTOR) { + return true; + } else if (authorization == AuthorizationLevel.TWO_FACTOR && + authentication >= AuthenticationLevel.TWO_FACTOR) { + return true; + } + return false; +} + +export default function ( + req: Express.Request, + vars: ServerVariables, + domain: string, resource: string, + user: string, groups: string[], + authenticationLevel: AuthenticationLevel) { + + return new BluebirdPromise(function (resolve, reject) { + const authorizationLevel = vars.authorizer + .authorization({domain, resource}, {user, groups}); + + if (!isAuthorized(authorizationLevel, authenticationLevel)) { + if (authorizationLevel == AuthorizationLevel.DENY) { + reject(new Exceptions.NotAuthorizedError( + Util.format("User %s is not authorized to access %s%s", user, domain, resource))); + return; + } + reject(new Exceptions.NotAuthenticatedError(Util.format( + "User '%s' is not sufficiently authorized to access %s%s.", user, domain, resource))); + return; + } + resolve(); + }); +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/verify/get.spec.ts b/themes/triangles/server/src/lib/routes/verify/get.spec.ts new file mode 100644 index 000000000..67cf19fb6 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/verify/get.spec.ts @@ -0,0 +1,320 @@ + +import Assert = require("assert"); +import BluebirdPromise = require("bluebird"); +import Express = require("express"); +import Sinon = require("sinon"); +import winston = require("winston"); + +import VerifyGet = require("./get"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../types/AuthenticationSession"; +import ExpressMock = require("../../stubs/express.spec"); +import { ServerVariables } from "../../ServerVariables"; +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../ServerVariablesMockBuilder.spec"; +import { Level } from "../../authentication/Level"; +import { Level as AuthorizationLevel } from "../../authorization/Level"; + +describe("routes/verify/get", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + let authSession: AuthenticationSession; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + req.originalUrl = "/api/xxxx"; + req.query = { + redirect: "undefined" + }; + AuthenticationSessionHandler.reset(req as any); + req.headers["x-original-url"] = "https://secret.example.com/"; + const s = ServerVariablesMockBuilder.build(false); + mocks = s.mocks; + vars = s.variables; + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + }); + + describe("with session cookie", function () { + it("should be already authenticated", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + authSession.authentication_level = Level.TWO_FACTOR; + authSession.userid = "myuser"; + authSession.groups = ["mygroup", "othergroup"]; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser"); + Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); + Assert.equal(204, res.status.getCall(0).args[0]); + }); + }); + + function test_session(_authSession: AuthenticationSession, status_code: number) { + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert.equal(status_code, res.status.getCall(0).args[0]); + }); + } + + function test_non_authenticated_401(authSession: AuthenticationSession) { + return test_session(authSession, 401); + } + + function test_unauthorized_403(authSession: AuthenticationSession) { + return test_session(authSession, 403); + } + + function test_authorized(authSession: AuthenticationSession) { + return test_session(authSession, 204); + } + + describe("given user tries to access a 2-factor endpoint", function () { + before(function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + }); + + describe("given different cases of session", function () { + it("should not be authenticated when second factor is missing", function () { + return test_non_authenticated_401({ + keep_me_logged_in: false, + userid: "user", + authentication_level: Level.ONE_FACTOR, + email: undefined, + groups: [], + last_activity_datetime: new Date().getTime() + }); + }); + + it("should not be authenticated when userid is missing", function () { + return test_non_authenticated_401({ + keep_me_logged_in: false, + userid: undefined, + authentication_level: Level.TWO_FACTOR, + email: undefined, + groups: [], + last_activity_datetime: new Date().getTime() + }); + }); + + it("should not be authenticated when level is insufficient", function () { + return test_non_authenticated_401({ + keep_me_logged_in: false, + userid: "user", + authentication_level: Level.NOT_AUTHENTICATED, + email: undefined, + groups: [], + last_activity_datetime: new Date().getTime() + }); + }); + + it("should not be authenticated when session has not be initiated", function () { + return test_non_authenticated_401(undefined); + }); + + it("should not be authenticated when domain is not allowed for user", function () { + authSession.authentication_level = Level.TWO_FACTOR; + authSession.userid = "myuser"; + req.headers["x-original-url"] = "https://test.example.com/"; + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.DENY); + + return test_unauthorized_403({ + keep_me_logged_in: false, + authentication_level: Level.TWO_FACTOR, + userid: "user", + groups: ["group1", "group2"], + email: undefined, + last_activity_datetime: new Date().getTime() + }); + }); + }); + }); + + describe("given user tries to access a single factor endpoint", function () { + beforeEach(function () { + req.headers["x-original-url"] = "https://redirect.url/"; + }); + + it("should be authenticated when first factor is validated", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); + authSession.authentication_level = Level.ONE_FACTOR; + authSession.userid = "user1"; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWith(204)); + Assert(res.send.calledOnce); + }); + }); + + it("should be rejected with 401 when not authenticated", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); + authSession.authentication_level = Level.NOT_AUTHENTICATED; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWith(401)); + }); + }); + }); + + describe("inactivity period", function () { + it("should update last inactivity period on requests on /api/verify", function () { + mocks.config.session.inactivity = 200000; + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + const currentTime = new Date().getTime() - 1000; + AuthenticationSessionHandler.reset(req as any); + authSession.authentication_level = Level.TWO_FACTOR; + authSession.userid = "myuser"; + authSession.groups = ["mygroup", "othergroup"]; + authSession.last_activity_datetime = currentTime; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + return AuthenticationSessionHandler.get(req as any, vars.logger); + }) + .then(function (authSession) { + Assert(authSession.last_activity_datetime > currentTime); + }); + }); + + it("should reset session when max inactivity period has been reached", function () { + mocks.config.session.inactivity = 1; + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + const currentTime = new Date().getTime() - 1000; + AuthenticationSessionHandler.reset(req as any); + authSession.authentication_level = Level.TWO_FACTOR; + authSession.userid = "myuser"; + authSession.groups = ["mygroup", "othergroup"]; + authSession.last_activity_datetime = currentTime; + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + return AuthenticationSessionHandler.get(req as any, vars.logger); + }) + .then(function (authSession) { + Assert.equal(authSession.authentication_level, Level.NOT_AUTHENTICATED); + Assert.equal(authSession.userid, undefined); + }); + }); + }); + }); + + describe("response type 401 | 302", function() { + it("should return error code 401", function() { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( + "Invalid credentials")); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should redirect to provided redirection url", function() { + const REDIRECT_URL = "http://redirection_url.com"; + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( + "Invalid credentials")); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + req.query["rd"] = REDIRECT_URL; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.redirect.calledWithExactly(REDIRECT_URL)); + }); + }); + }); + + describe("with basic auth", function () { + it("should authenticate correctly", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.returns({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "john"); + Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); + Assert.equal(204, res.status.getCall(0).args[0]); + }); + }); + + it("should fail when endpoint is protected by two factors", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.config.access_control.rules = [{ + domain: "secret.example.com", + policy: "two_factor" + }]; + mocks.usersDatabase.checkUserPasswordStub.resolves({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should fail when base64 token is not valid", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.resolves({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic i_m*not_a_base64*token"; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should fail when base64 token has not format user:psswd", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.resolves({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzOmJhZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should fail when bad user password is provided", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( + "Invalid credentials")); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should fail when resource is restricted", function () { + mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + mocks.config.access_control.default_policy = "one_factor"; + mocks.usersDatabase.checkUserPasswordStub.resolves({ + groups: ["mygroup", "othergroup"], + }); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as Express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + }); +}); + diff --git a/themes/triangles/server/src/lib/routes/verify/get.ts b/themes/triangles/server/src/lib/routes/verify/get.ts new file mode 100644 index 000000000..f73861696 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/verify/get.ts @@ -0,0 +1,91 @@ +import BluebirdPromise = require("bluebird"); +import Express = require("express"); +import Exceptions = require("../../Exceptions"); +import ErrorReplies = require("../../ErrorReplies"); +import { ServerVariables } from "../../ServerVariables"; +import GetWithSessionCookieMethod from "./get_session_cookie"; +import GetWithBasicAuthMethod from "./get_basic_auth"; +import Constants = require("../../../../../shared/constants"); +import ObjectPath = require("object-path"); + +import { AuthenticationSessionHandler } + from "../../AuthenticationSessionHandler"; +import { AuthenticationSession } + from "../../../../types/AuthenticationSession"; + +const REMOTE_USER = "Remote-User"; +const REMOTE_GROUPS = "Remote-Groups"; + + +function verifyWithSelectedMethod(req: Express.Request, res: Express.Response, + vars: ServerVariables, authSession: AuthenticationSession) + : () => BluebirdPromise<{ username: string, groups: string[] }> { + return function () { + const authorization: string = "" + req.headers["proxy-authorization"]; + if (authorization && authorization.startsWith("Basic ")) + return GetWithBasicAuthMethod(req, res, vars, authorization); + + return GetWithSessionCookieMethod(req, res, vars, authSession); + }; +} + +function setRedirectHeader(req: Express.Request, res: Express.Response) { + return function () { + const originalUrl = ObjectPath.get( + req, "headers.x-original-url"); + res.set("Redirect", originalUrl); + return BluebirdPromise.resolve(); + }; +} + +function setUserAndGroupsHeaders(res: Express.Response) { + return function (u: { username: string, groups: string[] }) { + res.setHeader(REMOTE_USER, u.username); + res.setHeader(REMOTE_GROUPS, u.groups.join(",")); + return BluebirdPromise.resolve(); + }; +} + +function replyWith200(res: Express.Response) { + return function () { + res.status(204); + res.send(); + }; +} + +function getRedirectParam(req: Express.Request) { + return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; +} + +export default function (vars: ServerVariables) { + return function (req: Express.Request, res: Express.Response) + : BluebirdPromise { + let authSession: AuthenticationSession; + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + resolve(); + }) + .then(setRedirectHeader(req, res)) + .then(verifyWithSelectedMethod(req, res, vars, authSession)) + .then(setUserAndGroupsHeaders(res)) + .then(replyWith200(res)) + // The user is authenticated but has restricted access -> 403 + .catch(Exceptions.NotAuthorizedError, + ErrorReplies.replyWithError403(req, res, vars.logger)) + .catch(Exceptions.NotAuthenticatedError, + ErrorReplies.replyWithError401(req, res, vars.logger)) + // The user is not yet authenticated -> 401 + .catch((err) => { + const redirectUrl = getRedirectParam(req); + if (redirectUrl) { + ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err); + } + else { + ErrorReplies.replyWithError401(req, res, vars.logger)(err); + } + }); + }; +} + diff --git a/themes/triangles/server/src/lib/routes/verify/get_basic_auth.ts b/themes/triangles/server/src/lib/routes/verify/get_basic_auth.ts new file mode 100644 index 000000000..af23c76c9 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/verify/get_basic_auth.ts @@ -0,0 +1,55 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import ObjectPath = require("object-path"); +import { ServerVariables } from "../../ServerVariables"; +import { AuthenticationSession } + from "../../../../types/AuthenticationSession"; +import AccessControl from "./access_control"; +import { URLDecomposer } from "../../utils/URLDecomposer"; +import { Level } from "../../authentication/Level"; + +export default function (req: Express.Request, res: Express.Response, + vars: ServerVariables, authorizationHeader: string) + : BluebirdPromise<{ username: string, groups: string[] }> { + let username: string; + const uri = ObjectPath.get(req, "headers.x-original-url"); + const urlDecomposition = URLDecomposer.fromUrl(uri); + + return BluebirdPromise.resolve() + .then(() => { + const base64Re = new RegExp("^Basic ((?:[A-Za-z0-9+/]{4})*" + + "(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)$"); + const isTokenValidBase64 = base64Re.test(authorizationHeader); + + if (!isTokenValidBase64) { + return BluebirdPromise.reject(new Error("No valid base64 token found in the header")); + } + + const tokenMatches = authorizationHeader.match(base64Re); + const base64Token = tokenMatches[1]; + const decodedToken = Buffer.from(base64Token, "base64").toString(); + const splittedToken = decodedToken.split(":"); + + if (splittedToken.length != 2) { + return BluebirdPromise.reject(new Error( + "The authorization token is invalid. Expecting 'userid:password'")); + } + + username = splittedToken[0]; + const password = splittedToken[1]; + return vars.usersDatabase.checkUserPassword(username, password); + }) + .then(function (groupsAndEmails) { + return AccessControl(req, vars, urlDecomposition.domain, urlDecomposition.path, + username, groupsAndEmails.groups, Level.ONE_FACTOR) + .then(() => BluebirdPromise.resolve({ + username: username, + groups: groupsAndEmails.groups + })); + }) + .catch(function (err: Error) { + return BluebirdPromise.reject( + new Error("Unable to authenticate the user with basic auth. Cause: " + + err.message)); + }); +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/verify/get_session_cookie.ts b/themes/triangles/server/src/lib/routes/verify/get_session_cookie.ts new file mode 100644 index 000000000..070344812 --- /dev/null +++ b/themes/triangles/server/src/lib/routes/verify/get_session_cookie.ts @@ -0,0 +1,78 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import Util = require("util"); +import ObjectPath = require("object-path"); + +import Exceptions = require("../../Exceptions"); +import { Configuration } from "../../configuration/schema/Configuration"; +import { ServerVariables } from "../../ServerVariables"; +import { IRequestLogger } from "../../logging/IRequestLogger"; +import { AuthenticationSession } + from "../../../../types/AuthenticationSession"; +import { AuthenticationSessionHandler } + from "../../AuthenticationSessionHandler"; +import AccessControl from "./access_control"; +import { URLDecomposer } from "../../utils/URLDecomposer"; + +function verify_inactivity(req: Express.Request, + authSession: AuthenticationSession, + configuration: Configuration, logger: IRequestLogger) + : BluebirdPromise { + + // If inactivity is not specified, then inactivity timeout does not apply + if (!configuration.session.inactivity || authSession.keep_me_logged_in) { + return BluebirdPromise.resolve(); + } + + const lastActivityTime = authSession.last_activity_datetime; + const currentTime = new Date().getTime(); + authSession.last_activity_datetime = currentTime; + + const inactivityPeriodMs = currentTime - lastActivityTime; + logger.debug(req, "Inactivity period was %s s and max period was %s.", + inactivityPeriodMs / 1000, configuration.session.inactivity / 1000); + if (inactivityPeriodMs < configuration.session.inactivity) { + return BluebirdPromise.resolve(); + } + + logger.debug(req, "Session has been reset after too long inactivity period."); + AuthenticationSessionHandler.reset(req); + return BluebirdPromise.reject(new Error("Inactivity period exceeded.")); +} + +export default function (req: Express.Request, res: Express.Response, + vars: ServerVariables, authSession: AuthenticationSession) + : BluebirdPromise<{ username: string, groups: string[] }> { + + return BluebirdPromise.resolve() + .then(() => { + const username = authSession.userid; + const groups = authSession.groups; + + if (!authSession.userid) { + return BluebirdPromise.reject(new Exceptions.AccessDeniedError( + "userid is missing")); + } + + const originalUrl = ObjectPath.get( + req, "headers.x-original-url"); + const originalUri = + ObjectPath.get(req, "headers.x-original-uri"); + + const d = URLDecomposer.fromUrl(originalUrl); + vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", d.domain, + d.path, username, groups.join(",")); + return AccessControl(req, vars, d.domain, d.path, username, groups, + authSession.authentication_level); + }) + .then(() => { + return verify_inactivity(req, authSession, + vars.config, vars.logger); + }) + .then(() => { + return BluebirdPromise.resolve({ + username: authSession.userid, + groups: authSession.groups + }); + }); +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/AuthenticationTraceDocument.d.ts b/themes/triangles/server/src/lib/storage/AuthenticationTraceDocument.d.ts new file mode 100644 index 000000000..69818c055 --- /dev/null +++ b/themes/triangles/server/src/lib/storage/AuthenticationTraceDocument.d.ts @@ -0,0 +1,6 @@ + +export interface AuthenticationTraceDocument { + userId: string; + date: Date; + isAuthenticationSuccessful: boolean; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/CollectionFactoryFactory.ts b/themes/triangles/server/src/lib/storage/CollectionFactoryFactory.ts new file mode 100644 index 000000000..92b29abfa --- /dev/null +++ b/themes/triangles/server/src/lib/storage/CollectionFactoryFactory.ts @@ -0,0 +1,15 @@ +import { ICollectionFactory } from "./ICollectionFactory"; +import { NedbCollectionFactory } from "./nedb/NedbCollectionFactory"; +import { MongoCollectionFactory } from "./mongo/MongoCollectionFactory"; +import { IMongoClient } from "../connectors/mongo/IMongoClient"; + + +export class CollectionFactoryFactory { + static createNedb(options: Nedb.DataStoreOptions): ICollectionFactory { + return new NedbCollectionFactory(options); + } + + static createMongo(client: IMongoClient): ICollectionFactory { + return new MongoCollectionFactory(client); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/CollectionFactoryStub.spec.ts b/themes/triangles/server/src/lib/storage/CollectionFactoryStub.spec.ts new file mode 100644 index 000000000..17f8bb021 --- /dev/null +++ b/themes/triangles/server/src/lib/storage/CollectionFactoryStub.spec.ts @@ -0,0 +1,16 @@ +import BluebirdPromise = require("bluebird"); +import Sinon = require("sinon"); +import { ICollection } from "./ICollection"; +import { ICollectionFactory } from "./ICollectionFactory"; + +export class CollectionFactoryStub implements ICollectionFactory { + buildStub: Sinon.SinonStub; + + constructor() { + this.buildStub = Sinon.stub(); + } + + build(collectionName: string): ICollection { + return this.buildStub(collectionName); + } +} diff --git a/themes/triangles/server/src/lib/storage/CollectionStub.spec.ts b/themes/triangles/server/src/lib/storage/CollectionStub.spec.ts new file mode 100644 index 000000000..42895d672 --- /dev/null +++ b/themes/triangles/server/src/lib/storage/CollectionStub.spec.ts @@ -0,0 +1,39 @@ +import BluebirdPromise = require("bluebird"); +import Sinon = require("sinon"); +import { ICollection } from "./ICollection"; + +export class CollectionStub implements ICollection { + findStub: Sinon.SinonStub; + findOneStub: Sinon.SinonStub; + updateStub: Sinon.SinonStub; + removeStub: Sinon.SinonStub; + insertStub: Sinon.SinonStub; + + constructor() { + this.findStub = Sinon.stub(); + this.findOneStub = Sinon.stub(); + this.updateStub = Sinon.stub(); + this.removeStub = Sinon.stub(); + this.insertStub = Sinon.stub(); + } + + find(filter: any, sortKeys: any, count: number): BluebirdPromise { + return this.findStub(filter, sortKeys, count); + } + + findOne(filter: any): BluebirdPromise { + return this.findOneStub(filter); + } + + update(filter: any, document: any, options: any): BluebirdPromise { + return this.updateStub(filter, document, options); + } + + remove(filter: any): BluebirdPromise { + return this.removeStub(filter); + } + + insert(document: any): BluebirdPromise { + return this.insertStub(document); + } +} diff --git a/themes/triangles/server/src/lib/storage/ICollection.d.ts b/themes/triangles/server/src/lib/storage/ICollection.d.ts new file mode 100644 index 000000000..caa6c2a87 --- /dev/null +++ b/themes/triangles/server/src/lib/storage/ICollection.d.ts @@ -0,0 +1,11 @@ +/* istanbul ignore next */ +import BluebirdPromise = require("bluebird"); + +/* istanbul ignore next */ +export interface ICollection { + find(query: any, sortKeys: any, count: number): BluebirdPromise; + findOne(query: any): BluebirdPromise; + update(query: any, updateQuery: any, options?: any): BluebirdPromise; + remove(query: any): BluebirdPromise; + insert(document: any): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/ICollectionFactory.d.ts b/themes/triangles/server/src/lib/storage/ICollectionFactory.d.ts new file mode 100644 index 000000000..39eb42c77 --- /dev/null +++ b/themes/triangles/server/src/lib/storage/ICollectionFactory.d.ts @@ -0,0 +1,6 @@ + +import { ICollection } from "./ICollection"; + +export interface ICollectionFactory { + build(collectionName: string): ICollection; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/IUserDataStore.d.ts b/themes/triangles/server/src/lib/storage/IUserDataStore.d.ts new file mode 100644 index 000000000..81df482aa --- /dev/null +++ b/themes/triangles/server/src/lib/storage/IUserDataStore.d.ts @@ -0,0 +1,21 @@ +import BluebirdPromise = require("bluebird"); +import { TOTPSecretDocument } from "./TOTPSecretDocument"; +import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; +import { U2FRegistration } from "../../../types/U2FRegistration"; +import { TOTPSecret } from "../../../types/TOTPSecret"; +import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; +import { IdentityValidationDocument } from "./IdentityValidationDocument"; + +export interface IUserDataStore { + saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise; + retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise; + + saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; + retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise; + + produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise; + consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise; + + saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise; + retrieveTOTPSecret(userId: string): BluebirdPromise; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/IdentityValidationDocument.d.ts b/themes/triangles/server/src/lib/storage/IdentityValidationDocument.d.ts new file mode 100644 index 000000000..e7fd7b3f4 --- /dev/null +++ b/themes/triangles/server/src/lib/storage/IdentityValidationDocument.d.ts @@ -0,0 +1,7 @@ + +export interface IdentityValidationDocument { + userId: string; + token: string; + challenge: string; + maxDate: Date; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/TOTPSecretDocument.d.ts b/themes/triangles/server/src/lib/storage/TOTPSecretDocument.d.ts new file mode 100644 index 000000000..a6c0bf9e6 --- /dev/null +++ b/themes/triangles/server/src/lib/storage/TOTPSecretDocument.d.ts @@ -0,0 +1,6 @@ +import { TOTPSecret } from "../../../types/TOTPSecret"; + +export interface TOTPSecretDocument { + userid: string; + secret: TOTPSecret; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/U2FRegistrationDocument.d.ts b/themes/triangles/server/src/lib/storage/U2FRegistrationDocument.d.ts new file mode 100644 index 000000000..efec6cb1d --- /dev/null +++ b/themes/triangles/server/src/lib/storage/U2FRegistrationDocument.d.ts @@ -0,0 +1,8 @@ + +import { U2FRegistration } from "../../../types/U2FRegistration"; + +export interface U2FRegistrationDocument { + userId: string; + appId: string; + registration: U2FRegistration; +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/UserDataStore.spec.ts b/themes/triangles/server/src/lib/storage/UserDataStore.spec.ts new file mode 100644 index 000000000..66fb85461 --- /dev/null +++ b/themes/triangles/server/src/lib/storage/UserDataStore.spec.ts @@ -0,0 +1,264 @@ + +import * as Assert from "assert"; +import * as Sinon from "sinon"; +import * as MockDate from "mockdate"; +import BluebirdPromise = require("bluebird"); + +import { UserDataStore } from "./UserDataStore"; +import { TOTPSecret } from "../../../types/TOTPSecret"; +import { U2FRegistration } from "../../../types/U2FRegistration"; +import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; +import { CollectionStub } from "./CollectionStub.spec"; +import { CollectionFactoryStub } from "./CollectionFactoryStub.spec"; + +describe("storage/UserDataStore", function () { + let factory: CollectionFactoryStub; + let collection: CollectionStub; + let userId: string; + let appId: string; + let totpSecret: TOTPSecret; + let u2fRegistration: U2FRegistration; + + beforeEach(function () { + factory = new CollectionFactoryStub(); + collection = new CollectionStub(); + + userId = "user"; + appId = "https://myappId"; + + totpSecret = { + ascii: "abc", + base32: "ABCDKZLEFZGREJK", + otpauth_url: "totp://test", + google_auth_qr: "dummy", + hex: "dummy", + qr_code_ascii: "dummy", + qr_code_base32: "dummy", + qr_code_hex: "dummy" + }; + + u2fRegistration = { + keyHandle: "KEY_HANDLE", + publicKey: "publickey" + }; + }); + + it("should correctly creates collections", function () { + new UserDataStore(factory); + + Assert.equal(4, factory.buildStub.callCount); + Assert(factory.buildStub.calledWith("authentication_traces")); + Assert(factory.buildStub.calledWith("identity_validation_tokens")); + Assert(factory.buildStub.calledWith("u2f_registrations")); + Assert(factory.buildStub.calledWith("totp_secrets")); + }); + + describe("TOTP secrets collection", function () { + it("should save a totp secret", function () { + factory.buildStub.returns(collection); + collection.updateStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.saveTOTPSecret(userId, totpSecret) + .then(function (doc) { + Assert(collection.updateStub.calledOnce); + Assert(collection.updateStub.calledWith({ userId: userId }, { + userId: userId, + secret: totpSecret + }, { upsert: true })); + return BluebirdPromise.resolve(); + }); + }); + + it("should retrieve a totp secret", function () { + factory.buildStub.returns(collection); + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.retrieveTOTPSecret(userId) + .then(function (doc) { + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ userId: userId })); + return BluebirdPromise.resolve(); + }); + }); + }); + + describe("U2F secrets collection", function () { + it("should save a U2F secret", function () { + factory.buildStub.returns(collection); + collection.updateStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.saveU2FRegistration(userId, appId, u2fRegistration) + .then(function (doc) { + Assert(collection.updateStub.calledOnce); + Assert(collection.updateStub.calledWith({ + userId: userId, + appId: appId + }, { + userId: userId, + appId: appId, + registration: u2fRegistration + }, { upsert: true })); + return BluebirdPromise.resolve(); + }); + }); + + it("should retrieve a U2F secret", function () { + factory.buildStub.returns(collection); + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.retrieveU2FRegistration(userId, appId) + .then(function (doc) { + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ + userId: userId, + appId: appId + })); + return BluebirdPromise.resolve(); + }); + }); + }); + + + describe("Regulator traces collection", function () { + it("should save a trace", function () { + factory.buildStub.returns(collection); + collection.insertStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.saveAuthenticationTrace(userId, true) + .then(function (doc) { + Assert(collection.insertStub.calledOnce); + Assert(collection.insertStub.calledWith({ + userId: userId, + date: Sinon.match.date, + isAuthenticationSuccessful: true + })); + return BluebirdPromise.resolve(); + }); + }); + + function should_retrieve_latest_authentication_traces(count: number) { + factory.buildStub.returns(collection); + collection.findStub.withArgs().returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.retrieveLatestAuthenticationTraces(userId, count) + .then(function (doc: AuthenticationTraceDocument[]) { + Assert(collection.findStub.calledOnce); + Assert(collection.findStub.calledWith({ + userId: userId, + }, { date: -1 }, count)); + return BluebirdPromise.resolve(); + }); + } + + it("should retrieve 3 latest failed authentication traces", function () { + should_retrieve_latest_authentication_traces(3); + }); + }); + + + describe("Identity validation collection", function () { + it("should save a identity validation token", function () { + factory.buildStub.returns(collection); + collection.insertStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + const maxAge = 400; + const token = "TOKEN"; + const challenge = "CHALLENGE"; + + return dataStore.produceIdentityValidationToken(userId, token, challenge, maxAge) + .then(function (doc) { + Assert(collection.insertStub.calledOnce); + Assert(collection.insertStub.calledWith({ + userId: userId, + token: token, + challenge: challenge, + maxDate: Sinon.match.date + })); + return BluebirdPromise.resolve(); + }); + }); + + it("should consume an identity token successfully", function () { + factory.buildStub.returns(collection); + + MockDate.set(100); + + const token = "TOKEN"; + const challenge = "CHALLENGE"; + + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ + userId: "USER", + token: token, + challenge: challenge, + maxDate: new Date() + })); + collection.removeStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + MockDate.set(80); + + return dataStore.consumeIdentityValidationToken(token, challenge) + .then(function (doc) { + MockDate.reset(); + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ + token: token, + challenge: challenge + })); + + Assert(collection.removeStub.calledOnce); + Assert(collection.removeStub.calledWith({ + token: token, + challenge: challenge + })); + return BluebirdPromise.resolve(); + }); + }); + + it("should consume an expired identity token", function () { + factory.buildStub.returns(collection); + + MockDate.set(0); + + const token = "TOKEN"; + const challenge = "CHALLENGE"; + + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ + userId: "USER", + token: token, + challenge: challenge, + maxDate: new Date() + })); + + const dataStore = new UserDataStore(factory); + + MockDate.set(80000); + + return dataStore.consumeIdentityValidationToken(token, challenge) + .then(function () { return BluebirdPromise.reject(new Error("should not be here")); }) + .catch(function () { + MockDate.reset(); + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ + token: token, + challenge: challenge + })); + return BluebirdPromise.resolve(); + }); + }); + }); +}); diff --git a/themes/triangles/server/src/lib/storage/UserDataStore.ts b/themes/triangles/server/src/lib/storage/UserDataStore.ts new file mode 100644 index 000000000..27b0cddbd --- /dev/null +++ b/themes/triangles/server/src/lib/storage/UserDataStore.ts @@ -0,0 +1,143 @@ +import * as BluebirdPromise from "bluebird"; +import * as path from "path"; +import { IUserDataStore } from "./IUserDataStore"; +import { ICollection } from "./ICollection"; +import { ICollectionFactory } from "./ICollectionFactory"; +import { TOTPSecretDocument } from "./TOTPSecretDocument"; +import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; +import { U2FRegistration } from "../../../types/U2FRegistration"; +import { TOTPSecret } from "../../../types/TOTPSecret"; +import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; +import { IdentityValidationDocument } from "./IdentityValidationDocument"; + +// Constants + +const IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME = "identity_validation_tokens"; +const AUTHENTICATION_TRACES_COLLECTION_NAME = "authentication_traces"; + +const U2F_REGISTRATIONS_COLLECTION_NAME = "u2f_registrations"; +const TOTP_SECRETS_COLLECTION_NAME = "totp_secrets"; + + +export interface U2FRegistrationKey { + userId: string; + appId: string; +} + +// Source + +export class UserDataStore implements IUserDataStore { + private u2fSecretCollection: ICollection; + private identityCheckTokensCollection: ICollection; + private authenticationTracesCollection: ICollection; + private totpSecretCollection: ICollection; + + private collectionFactory: ICollectionFactory; + + constructor(collectionFactory: ICollectionFactory) { + this.collectionFactory = collectionFactory; + + this.u2fSecretCollection = this.collectionFactory.build(U2F_REGISTRATIONS_COLLECTION_NAME); + this.identityCheckTokensCollection = this.collectionFactory.build(IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME); + this.authenticationTracesCollection = this.collectionFactory.build(AUTHENTICATION_TRACES_COLLECTION_NAME); + this.totpSecretCollection = this.collectionFactory.build(TOTP_SECRETS_COLLECTION_NAME); + } + + saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { + const newDocument: U2FRegistrationDocument = { + userId: userId, + appId: appId, + registration: registration + }; + + const filter: U2FRegistrationKey = { + userId: userId, + appId: appId + }; + + return this.u2fSecretCollection.update(filter, newDocument, { upsert: true }); + } + + retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { + const filter: U2FRegistrationKey = { + userId: userId, + appId: appId + }; + return this.u2fSecretCollection.findOne(filter); + } + + saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { + const newDocument: AuthenticationTraceDocument = { + userId: userId, + date: new Date(), + isAuthenticationSuccessful: isAuthenticationSuccessful, + }; + + return this.authenticationTracesCollection.insert(newDocument); + } + + retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { + const q = { + userId: userId + }; + + return this.authenticationTracesCollection.find(q, { date: -1 }, count); + } + + produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { + const newDocument: IdentityValidationDocument = { + userId: userId, + token: token, + challenge: challenge, + maxDate: new Date(new Date().getTime() + maxAge) + }; + + return this.identityCheckTokensCollection.insert(newDocument); + } + + consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { + const that = this; + const filter = { + token: token, + challenge: challenge + }; + + let identityValidationDocument: IdentityValidationDocument; + + return this.identityCheckTokensCollection.findOne(filter) + .then(function (doc: IdentityValidationDocument) { + if (!doc) { + return BluebirdPromise.reject(new Error("Registration token does not exist")); + } + + identityValidationDocument = doc; + const current_date = new Date(); + if (current_date > doc.maxDate) + return BluebirdPromise.reject(new Error("Registration token is not valid anymore")); + + return that.identityCheckTokensCollection.remove(filter); + }) + .then(() => { + return BluebirdPromise.resolve(identityValidationDocument); + }); + } + + saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { + const doc = { + userId: userId, + secret: secret + }; + + const filter = { + userId: userId + }; + return this.totpSecretCollection.update(filter, doc, { upsert: true }); + } + + retrieveTOTPSecret(userId: string): BluebirdPromise { + const filter = { + userId: userId + }; + return this.totpSecretCollection.findOne(filter); + } +} diff --git a/themes/triangles/server/src/lib/storage/UserDataStoreStub.spec.ts b/themes/triangles/server/src/lib/storage/UserDataStoreStub.spec.ts new file mode 100644 index 000000000..5ea27a2de --- /dev/null +++ b/themes/triangles/server/src/lib/storage/UserDataStoreStub.spec.ts @@ -0,0 +1,64 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); + +import { TOTPSecretDocument } from "./TOTPSecretDocument"; +import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; +import { U2FRegistration } from "../../../types/U2FRegistration"; +import { TOTPSecret } from "../../../types/TOTPSecret"; +import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; +import { IdentityValidationDocument } from "./IdentityValidationDocument"; +import { IUserDataStore } from "./IUserDataStore"; + +export class UserDataStoreStub implements IUserDataStore { + saveU2FRegistrationStub: Sinon.SinonStub; + retrieveU2FRegistrationStub: Sinon.SinonStub; + saveAuthenticationTraceStub: Sinon.SinonStub; + retrieveLatestAuthenticationTracesStub: Sinon.SinonStub; + produceIdentityValidationTokenStub: Sinon.SinonStub; + consumeIdentityValidationTokenStub: Sinon.SinonStub; + saveTOTPSecretStub: Sinon.SinonStub; + retrieveTOTPSecretStub: Sinon.SinonStub; + + constructor() { + this.saveU2FRegistrationStub = Sinon.stub(); + this.retrieveU2FRegistrationStub = Sinon.stub(); + this.saveAuthenticationTraceStub = Sinon.stub(); + this.retrieveLatestAuthenticationTracesStub = Sinon.stub(); + this.produceIdentityValidationTokenStub = Sinon.stub(); + this.consumeIdentityValidationTokenStub = Sinon.stub(); + this.saveTOTPSecretStub = Sinon.stub(); + this.retrieveTOTPSecretStub = Sinon.stub(); + } + + saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { + return this.saveU2FRegistrationStub(userId, appId, registration); + } + + retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { + return this.retrieveU2FRegistrationStub(userId, appId); + } + + saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { + return this.saveAuthenticationTraceStub(userId, isAuthenticationSuccessful); + } + + retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { + return this.retrieveLatestAuthenticationTracesStub(userId, count); + } + + produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { + return this.produceIdentityValidationTokenStub(userId, token, challenge, maxAge); + } + + consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { + return this.consumeIdentityValidationTokenStub(token, challenge); + } + + saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { + return this.saveTOTPSecretStub(userId, secret); + } + + retrieveTOTPSecret(userId: string): BluebirdPromise { + return this.retrieveTOTPSecretStub(userId); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/mongo/MongoCollection.spec.ts b/themes/triangles/server/src/lib/storage/mongo/MongoCollection.spec.ts new file mode 100644 index 000000000..74a773a1e --- /dev/null +++ b/themes/triangles/server/src/lib/storage/mongo/MongoCollection.spec.ts @@ -0,0 +1,110 @@ +import Assert = require("assert"); +import Sinon = require("sinon"); +import MongoDB = require("mongodb"); +import BluebirdPromise = require("bluebird"); +import { MongoClientStub } from "../../connectors/mongo/MongoClientStub.spec"; +import { MongoCollection } from "./MongoCollection"; + +describe("storage/mongo/MongoCollection", function () { + let mongoCollectionStub: any; + let mongoClientStub: MongoClientStub; + let findStub: Sinon.SinonStub; + let findOneStub: Sinon.SinonStub; + let insertOneStub: Sinon.SinonStub; + let updateStub: Sinon.SinonStub; + let removeStub: Sinon.SinonStub; + let countStub: Sinon.SinonStub; + const COLLECTION_NAME = "collection"; + + before(function () { + mongoClientStub = new MongoClientStub(); + mongoCollectionStub = Sinon.createStubInstance(require("mongodb").Collection as any); + findStub = mongoCollectionStub.find as Sinon.SinonStub; + findOneStub = mongoCollectionStub.findOne as Sinon.SinonStub; + insertOneStub = mongoCollectionStub.insertOne as Sinon.SinonStub; + updateStub = mongoCollectionStub.update as Sinon.SinonStub; + removeStub = mongoCollectionStub.remove as Sinon.SinonStub; + countStub = mongoCollectionStub.count as Sinon.SinonStub; + mongoClientStub.collectionStub.returns( + BluebirdPromise.resolve(mongoCollectionStub) + ); + }); + + describe("find", function () { + it("should find a document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + findStub.returns({ + sort: Sinon.stub().returns({ + limit: Sinon.stub().returns({ + toArray: Sinon.stub().returns(BluebirdPromise.resolve([])) + }) + }) + }); + + return collection.find({ key: "KEY" }) + .then(function () { + Assert(findStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("findOne", function () { + it("should find one document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + findOneStub.returns(BluebirdPromise.resolve({})); + + return collection.findOne({ key: "KEY" }) + .then(function () { + Assert(findOneStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("insert", function () { + it("should insert a document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + insertOneStub.returns(BluebirdPromise.resolve({})); + + return collection.insert({ key: "KEY" }) + .then(function () { + Assert(insertOneStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("update", function () { + it("should update a document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + updateStub.returns(BluebirdPromise.resolve({})); + + return collection.update({ key: "KEY" }, { key: "KEY", value: 1 }) + .then(function () { + Assert(updateStub.calledWith({ key: "KEY" }, { key: "KEY", value: 1 })); + }); + }); + }); + + describe("remove", function () { + it("should remove a document in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + removeStub.returns(BluebirdPromise.resolve({})); + + return collection.remove({ key: "KEY" }) + .then(function () { + Assert(removeStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("count", function () { + it("should count documents in the collection", function () { + const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); + countStub.returns(BluebirdPromise.resolve({})); + + return collection.count({ key: "KEY" }) + .then(function () { + Assert(countStub.calledWith({ key: "KEY" })); + }); + }); + }); +}); diff --git a/themes/triangles/server/src/lib/storage/mongo/MongoCollection.ts b/themes/triangles/server/src/lib/storage/mongo/MongoCollection.ts new file mode 100644 index 000000000..9771389f1 --- /dev/null +++ b/themes/triangles/server/src/lib/storage/mongo/MongoCollection.ts @@ -0,0 +1,50 @@ +import Bluebird = require("bluebird"); +import { ICollection } from "../ICollection"; +import MongoDB = require("mongodb"); +import { IMongoClient } from "../../connectors/mongo/IMongoClient"; + + +export class MongoCollection implements ICollection { + private mongoClient: IMongoClient; + private collectionName: string; + + constructor(collectionName: string, mongoClient: IMongoClient) { + this.collectionName = collectionName; + this.mongoClient = mongoClient; + } + + private collection(): Bluebird { + return this.mongoClient.collection(this.collectionName); + } + + find(query: any, sortKeys?: any, count?: number): Bluebird { + return this.collection() + .then((collection) => collection.find(query).sort(sortKeys).limit(count)) + .then((query) => query.toArray()); + } + + findOne(query: any): Bluebird { + return this.collection() + .then((collection) => collection.findOne(query)); + } + + update(query: any, updateQuery: any, options?: any): Bluebird { + return this.collection() + .then((collection) => collection.update(query, updateQuery, options)); + } + + remove(query: any): Bluebird { + return this.collection() + .then((collection) => collection.remove(query)); + } + + insert(document: any): Bluebird { + return this.collection() + .then((collection) => collection.insertOne(document)); + } + + count(query: any): Bluebird { + return this.collection() + .then((collection) => collection.count(query)); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts b/themes/triangles/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts new file mode 100644 index 000000000..bd959cacb --- /dev/null +++ b/themes/triangles/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts @@ -0,0 +1,21 @@ +import Assert = require("assert"); +import Sinon = require("sinon"); +import { MongoClientStub } from "../../connectors/mongo/MongoClientStub.spec"; +import { MongoCollectionFactory } from "./MongoCollectionFactory"; + +describe("storage/mongo/MongoCollectionFactory", function () { + let mongoClient: MongoClientStub; + + before(function() { + mongoClient = new MongoClientStub(); + }); + + describe("create", function () { + it("should create a collection", function () { + const COLLECTION_NAME = "COLLECTION_NAME"; + + const factory = new MongoCollectionFactory(mongoClient); + Assert(factory.build(COLLECTION_NAME)); + }); + }); +}); diff --git a/themes/triangles/server/src/lib/storage/mongo/MongoCollectionFactory.ts b/themes/triangles/server/src/lib/storage/mongo/MongoCollectionFactory.ts new file mode 100644 index 000000000..14a8262c9 --- /dev/null +++ b/themes/triangles/server/src/lib/storage/mongo/MongoCollectionFactory.ts @@ -0,0 +1,19 @@ +import BluebirdPromise = require("bluebird"); +import { ICollection } from "../ICollection"; +import { ICollectionFactory } from "../ICollectionFactory"; +import { MongoCollection } from "./MongoCollection"; +import path = require("path"); +import MongoDB = require("mongodb"); +import { IMongoClient } from "../../connectors/mongo/IMongoClient"; + +export class MongoCollectionFactory implements ICollectionFactory { + private mongoClient: IMongoClient; + + constructor(mongoClient: IMongoClient) { + this.mongoClient = mongoClient; + } + + build(collectionName: string): ICollection { + return new MongoCollection(collectionName, this.mongoClient); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/nedb/NedbCollection.spec.ts b/themes/triangles/server/src/lib/storage/nedb/NedbCollection.spec.ts new file mode 100644 index 000000000..a69962b67 --- /dev/null +++ b/themes/triangles/server/src/lib/storage/nedb/NedbCollection.spec.ts @@ -0,0 +1,136 @@ +import Sinon = require("sinon"); +import Assert = require("assert"); + +import { NedbCollection } from "./NedbCollection"; + +describe("storage/nedb/NedbCollection", function () { + describe("insert", function () { + it("should insert one entry", function () { + const nedbOptions = { + inMemoryOnly: true + }; + const collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou" }); + + return collection.count({}).then(function (count: number) { + Assert.equal(1, count); + }); + }); + + it("should insert three entries", function () { + const nedbOptions = { + inMemoryOnly: true + }; + const collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou" }); + collection.insert({ key: "hello" }); + collection.insert({ key: "hey" }); + + return collection.count({}).then(function (count: number) { + Assert.equal(3, count); + }); + }); + }); + + describe("find", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "hello" }); + collection.insert({ key: "hey" }); + collection.insert({ key: "coucou", value: 2 }); + }); + + it("should find one hello", function () { + return collection.find({ key: "hello" }, { key: 1 }) + .then(function (docs: { key: string }[]) { + Assert.equal(1, docs.length); + Assert(docs[0].key == "hello"); + }); + }); + + it("should find two coucou", function () { + return collection.find({ key: "coucou" }, { value: 1 }) + .then(function (docs: { value: number }[]) { + Assert.equal(2, docs.length); + }); + }); + }); + + describe("findOne", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "coucou", value: 1 }); + }); + + it("should find two coucou", function () { + const doc = { key: "coucou", value: 1 }; + return collection.count(doc) + .then(function (count: number) { + Assert.equal(4, count); + return collection.findOne(doc); + }); + }); + }); + + describe("update", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou", value: 1 }); + }); + + it("should update the value", function () { + return collection.update({ key: "coucou" }, { key: "coucou", value: 2 }, { multi: true }) + .then(function () { + return collection.find({ key: "coucou" }); + }) + .then(function (docs: { key: string, value: number }[]) { + Assert.equal(1, docs.length); + Assert.equal(2, docs[0].value); + }); + }); + }); + + describe("update", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou" }); + collection.insert({ key: "hello" }); + }); + + it("should update the value", function () { + return collection.remove({ key: "coucou" }) + .then(function () { + return collection.count({}); + }) + .then(function (count: number) { + Assert.equal(1, count); + }); + }); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/nedb/NedbCollection.ts b/themes/triangles/server/src/lib/storage/nedb/NedbCollection.ts new file mode 100644 index 000000000..88a93ad05 --- /dev/null +++ b/themes/triangles/server/src/lib/storage/nedb/NedbCollection.ts @@ -0,0 +1,47 @@ +import BluebirdPromise = require("bluebird"); +import { ICollection } from "../ICollection"; +import Nedb = require("nedb"); + +declare module "nedb" { + export class NedbAsync extends Nedb { + constructor(pathOrOptions?: string | Nedb.DataStoreOptions); + updateAsync(query: any, updateQuery: any, options?: Nedb.UpdateOptions): BluebirdPromise; + findOneAsync(query: any): BluebirdPromise; + insertAsync(newDoc: T): BluebirdPromise; + removeAsync(query: any): BluebirdPromise; + countAsync(query: any): BluebirdPromise; + } +} + +export class NedbCollection implements ICollection { + private collection: Nedb.NedbAsync; + + constructor(options: Nedb.DataStoreOptions) { + this.collection = BluebirdPromise.promisifyAll(new Nedb(options)) as Nedb.NedbAsync; + } + + find(query: any, sortKeys?: any, count?: number): BluebirdPromise { + const q = this.collection.find(query).sort(sortKeys).limit(count); + return BluebirdPromise.promisify(q.exec, { context: q })(); + } + + findOne(query: any): BluebirdPromise { + return this.collection.findOneAsync(query); + } + + update(query: any, updateQuery: any, options?: any): BluebirdPromise { + return this.collection.updateAsync(query, updateQuery, options); + } + + remove(query: any): BluebirdPromise { + return this.collection.removeAsync(query); + } + + insert(document: any): BluebirdPromise { + return this.collection.insertAsync(document); + } + + count(query: any): BluebirdPromise { + return this.collection.countAsync(query); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts b/themes/triangles/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts new file mode 100644 index 000000000..da90c661f --- /dev/null +++ b/themes/triangles/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts @@ -0,0 +1,16 @@ +import Sinon = require("sinon"); +import Assert = require("assert"); + +import { NedbCollectionFactory } from "./NedbCollectionFactory"; + +describe("storage/nedb/NedbCollectionFactory", function() { + it("should create a nedb collection", function() { + const nedbOptions = { + inMemoryOnly: true + }; + const factory = new NedbCollectionFactory(nedbOptions); + + const collection = factory.build("mycollection"); + Assert(collection); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/nedb/NedbCollectionFactory.ts b/themes/triangles/server/src/lib/storage/nedb/NedbCollectionFactory.ts new file mode 100644 index 000000000..49c4dc853 --- /dev/null +++ b/themes/triangles/server/src/lib/storage/nedb/NedbCollectionFactory.ts @@ -0,0 +1,28 @@ +import { ICollection } from "../ICollection"; +import { ICollectionFactory } from "../ICollectionFactory"; +import { NedbCollection } from "./NedbCollection"; +import path = require("path"); +import Nedb = require("nedb"); + +export interface NedbOptions { + inMemoryOnly?: boolean; + directory?: string; +} + +export class NedbCollectionFactory implements ICollectionFactory { + private options: Nedb.DataStoreOptions; + + constructor(options: Nedb.DataStoreOptions) { + this.options = options; + } + + build(collectionName: string): ICollection { + const datastoreOptions: Nedb.DataStoreOptions = { + inMemoryOnly: this.options.inMemoryOnly || false, + autoload: true, + filename: (this.options.filename) ? path.resolve(this.options.filename, collectionName) : undefined + }; + + return new NedbCollection(datastoreOptions); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/stubs/express.spec.ts b/themes/triangles/server/src/lib/stubs/express.spec.ts new file mode 100644 index 000000000..48f15d7e1 --- /dev/null +++ b/themes/triangles/server/src/lib/stubs/express.spec.ts @@ -0,0 +1,103 @@ + +import sinon = require("sinon"); +import express = require("express"); + +export interface RequestMock { + app?: any; + body?: any; + session?: any; + headers?: any; + get?: any; + query?: any; + originalUrl: string; +} + +export interface ResponseMock { + send: sinon.SinonStub | sinon.SinonSpy; + sendStatus: sinon.SinonStub; + sendFile: sinon.SinonStub; + sendfile: sinon.SinonStub; + status: sinon.SinonStub | sinon.SinonSpy; + json: sinon.SinonStub | sinon.SinonSpy; + links: sinon.SinonStub; + jsonp: sinon.SinonStub; + download: sinon.SinonStub; + contentType: sinon.SinonStub; + type: sinon.SinonStub; + format: sinon.SinonStub; + attachment: sinon.SinonStub; + set: sinon.SinonStub; + header: sinon.SinonStub; + headersSent: boolean; + get: sinon.SinonStub; + clearCookie: sinon.SinonStub; + cookie: sinon.SinonStub; + location: sinon.SinonStub; + redirect: sinon.SinonStub | sinon.SinonSpy; + render: sinon.SinonStub | sinon.SinonSpy; + locals: sinon.SinonStub; + charset: string; + vary: sinon.SinonStub; + app: any; + write: sinon.SinonStub; + writeContinue: sinon.SinonStub; + writeHead: sinon.SinonStub; + statusCode: number; + statusMessage: string; + setHeader: sinon.SinonStub; + setTimeout: sinon.SinonStub; + sendDate: boolean; + getHeader: sinon.SinonStub; +} + +export function RequestMock(): RequestMock { + return { + originalUrl: "/non-api/xxx", + app: { + get: sinon.stub() + }, + headers: { + "x-forwarded-for": "127.0.0.1" + }, + session: {} + }; +} +export function ResponseMock(): ResponseMock { + return { + send: sinon.stub(), + status: sinon.stub(), + json: sinon.stub(), + sendStatus: sinon.stub(), + links: sinon.stub(), + jsonp: sinon.stub(), + sendFile: sinon.stub(), + sendfile: sinon.stub(), + download: sinon.stub(), + contentType: sinon.stub(), + type: sinon.stub(), + format: sinon.stub(), + attachment: sinon.stub(), + set: sinon.stub(), + header: sinon.stub(), + headersSent: true, + get: sinon.stub(), + clearCookie: sinon.stub(), + cookie: sinon.stub(), + location: sinon.stub(), + redirect: sinon.stub(), + render: sinon.stub(), + locals: sinon.stub(), + charset: "utf-8", + vary: sinon.stub(), + app: sinon.stub(), + write: sinon.stub(), + writeContinue: sinon.stub(), + writeHead: sinon.stub(), + statusCode: 200, + statusMessage: "message", + setHeader: sinon.stub(), + setTimeout: sinon.stub(), + sendDate: true, + getHeader: sinon.stub() + }; +} diff --git a/themes/triangles/server/src/lib/stubs/ldapjs.spec.ts b/themes/triangles/server/src/lib/stubs/ldapjs.spec.ts new file mode 100644 index 000000000..045c0e11b --- /dev/null +++ b/themes/triangles/server/src/lib/stubs/ldapjs.spec.ts @@ -0,0 +1,50 @@ + +import Sinon = require("sinon"); + +export class LdapjsMock { + createClientStub: sinon.SinonStub; + + constructor() { + this.createClientStub = Sinon.stub(); + } + + createClient(params: any) { + return this.createClientStub(params); + } +} + +export class LdapjsClientMock { + bindStub: sinon.SinonStub; + unbindStub: sinon.SinonStub; + searchStub: sinon.SinonStub; + modifyStub: sinon.SinonStub; + onStub: sinon.SinonStub; + + constructor() { + this.bindStub = Sinon.stub(); + this.unbindStub = Sinon.stub(); + this.searchStub = Sinon.stub(); + this.modifyStub = Sinon.stub(); + this.onStub = Sinon.stub(); + } + + bind() { + return this.bindStub(); + } + + unbind() { + return this.unbindStub(); + } + + search() { + return this.searchStub(); + } + + modify() { + return this.modifyStub(); + } + + on() { + return this.onStub(); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/stubs/speakeasy.spec.ts b/themes/triangles/server/src/lib/stubs/speakeasy.spec.ts new file mode 100644 index 000000000..023614dcd --- /dev/null +++ b/themes/triangles/server/src/lib/stubs/speakeasy.spec.ts @@ -0,0 +1,7 @@ + +import sinon = require("sinon"); + +export = { + totp: sinon.stub(), + generateSecret: sinon.stub() +}; diff --git a/themes/triangles/server/src/lib/stubs/u2f.spec.ts b/themes/triangles/server/src/lib/stubs/u2f.spec.ts new file mode 100644 index 000000000..234b28c11 --- /dev/null +++ b/themes/triangles/server/src/lib/stubs/u2f.spec.ts @@ -0,0 +1,16 @@ + +import sinon = require("sinon"); + +export interface U2FMock { + request: sinon.SinonStub; + checkSignature: sinon.SinonStub; + checkRegistration: sinon.SinonStub; +} + +export function U2FMock(): U2FMock { + return { + request: sinon.stub(), + checkSignature: sinon.stub(), + checkRegistration: sinon.stub() + }; +} diff --git a/themes/triangles/server/src/lib/utils/HashGenerator.spec.ts b/themes/triangles/server/src/lib/utils/HashGenerator.spec.ts new file mode 100644 index 000000000..f19619a65 --- /dev/null +++ b/themes/triangles/server/src/lib/utils/HashGenerator.spec.ts @@ -0,0 +1,18 @@ +import Assert = require("assert"); +import { HashGenerator } from "./HashGenerator"; + +describe("utils/HashGenerator", function () { + it("should compute correct ssha512 (password)", function () { + return HashGenerator.ssha512("password", 500000, "jgiCMRyGXzoqpxS3") + .then(function (hash: string) { + Assert.equal(hash, "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"); + }); + }); + + it("should compute correct ssha512 (test)", function () { + return HashGenerator.ssha512("test", 500000, "abcdefghijklmnop") + .then(function (hash: string) { + Assert.equal(hash, "{CRYPT}$6$rounds=500000$abcdefghijklmnop$sTlNGf0VO/HTQIOXemmaBbV28HUch/qhWOA1/4dsDj6CDQYhUgXbYSPL6gccAsWMr2zD5fFWwhKmPdG.yxphs."); + }); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/utils/HashGenerator.ts b/themes/triangles/server/src/lib/utils/HashGenerator.ts new file mode 100644 index 000000000..e67de32b7 --- /dev/null +++ b/themes/triangles/server/src/lib/utils/HashGenerator.ts @@ -0,0 +1,23 @@ +import BluebirdPromise = require("bluebird"); +import RandomString = require("randomstring"); +import Util = require("util"); +const crypt = require("crypt3"); + +export class HashGenerator { + static ssha512( + password: string, + rounds: number = 500000, + salt?: string): BluebirdPromise { + const saltSize = 16; + // $6 means SHA512 + const _salt = Util.format("$6$rounds=%d$%s", rounds, + (salt) ? salt : RandomString.generate(16)); + + const cryptAsync = BluebirdPromise.promisify(crypt); + + return cryptAsync(password, _salt) + .then(function (hash: string) { + return BluebirdPromise.resolve(Util.format("{CRYPT}%s", hash)); + }); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/utils/ObjectCloner.ts b/themes/triangles/server/src/lib/utils/ObjectCloner.ts new file mode 100644 index 000000000..3e125d749 --- /dev/null +++ b/themes/triangles/server/src/lib/utils/ObjectCloner.ts @@ -0,0 +1,6 @@ + +export class ObjectCloner { + static clone(obj: any): any { + return JSON.parse(JSON.stringify(obj)); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/utils/SafeRedirection.spec.ts b/themes/triangles/server/src/lib/utils/SafeRedirection.spec.ts new file mode 100644 index 000000000..4126949fd --- /dev/null +++ b/themes/triangles/server/src/lib/utils/SafeRedirection.spec.ts @@ -0,0 +1,33 @@ +import Assert = require("assert"); +import Sinon = require("sinon"); +import { SafeRedirector } from "./SafeRedirection"; + +describe("web_server/middlewares/SafeRedirection", () => { + describe("Url is in protected domain", () => { + before(() => { + this.redirector = new SafeRedirector("example.com"); + this.res = {redirect: Sinon.stub()}; + }); + + it("should redirect to provided url", () => { + this.redirector.redirectOrElse(this.res, + "https://mysubdomain.example.com:8080/abc", + "https://authelia.example.com"); + Assert(this.res.redirect.calledWith("https://mysubdomain.example.com:8080/abc")); + }); + + it("should redirect to default url when wrong domain", () => { + this.redirector.redirectOrElse(this.res, + "https://mysubdomain.domain.rtf:8080/abc", + "https://authelia.example.com"); + Assert(this.res.redirect.calledWith("https://authelia.example.com")); + }); + + it("should redirect to default url when not terminating by domain", () => { + this.redirector.redirectOrElse(this.res, + "https://mysubdomain.example.com.rtf:8080/abc", + "https://authelia.example.com"); + Assert(this.res.redirect.calledWith("https://authelia.example.com")); + }); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/utils/SafeRedirection.ts b/themes/triangles/server/src/lib/utils/SafeRedirection.ts new file mode 100644 index 000000000..9e6a32e0c --- /dev/null +++ b/themes/triangles/server/src/lib/utils/SafeRedirection.ts @@ -0,0 +1,22 @@ +import Express = require("express"); +import { DomainExtractor } from "../../../../shared/DomainExtractor"; +import { BelongToDomain } from "../../../../shared/BelongToDomain"; + + +export class SafeRedirector { + private domain: string; + + constructor(domain: string) { + this.domain = domain; + } + + redirectOrElse( + res: Express.Response, + url: string, + defaultUrl: string): void { + if (BelongToDomain(url, this.domain)) { + res.redirect(url); + } + res.redirect(defaultUrl); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/utils/URLDecomposer.spec.ts b/themes/triangles/server/src/lib/utils/URLDecomposer.spec.ts new file mode 100644 index 000000000..cbb038738 --- /dev/null +++ b/themes/triangles/server/src/lib/utils/URLDecomposer.spec.ts @@ -0,0 +1,46 @@ +import { URLDecomposer } from "./URLDecomposer"; +import Assert = require("assert"); + +describe("utils/URLDecomposer", function () { + describe("test fromUrl", function () { + it("should return domain from https url", function () { + const d = URLDecomposer.fromUrl("https://www.example.com/test/abc"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/test/abc"); + }); + + it("should return domain from http url", function () { + const d = URLDecomposer.fromUrl("http://www.example.com/test/abc"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/test/abc"); + }); + + it("should return domain when url contains port", function () { + const d = URLDecomposer.fromUrl("https://www.example.com:8080/test/abc"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/test/abc"); + }); + + it("should return default path when no path provided", function () { + const d = URLDecomposer.fromUrl("https://www.example.com:8080"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/"); + }); + + it("should return default path when provided", function () { + const d = URLDecomposer.fromUrl("https://www.example.com:8080/"); + Assert.equal(d.domain, "www.example.com"); + Assert.equal(d.path, "/"); + }); + + it("should return undefined when does not match", function () { + const d = URLDecomposer.fromUrl("https:///abc/test"); + Assert.equal(d, undefined); + }); + + it("should return undefined when does not match", function () { + const d = URLDecomposer.fromUrl("https:///abc/test"); + Assert.equal(d, undefined); + }); + }); +}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/utils/URLDecomposer.ts b/themes/triangles/server/src/lib/utils/URLDecomposer.ts new file mode 100644 index 000000000..9bdf2e9d2 --- /dev/null +++ b/themes/triangles/server/src/lib/utils/URLDecomposer.ts @@ -0,0 +1,15 @@ +export class URLDecomposer { + static fromUrl(url: string): {domain: string, path: string} { + if (!url) return; + const match = url.match(/https?:\/\/([a-z0-9_.-]+)(:[0-9]+)?(.*)/); + + if (!match) return; + + if (match[1] && !match[3]) { + return {domain: match[1], path: "/"}; + } else if (match[1] && match[3]) { + return {domain: match[1], path: match[3]}; + } + return; + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/web_server/Configurator.ts b/themes/triangles/server/src/lib/web_server/Configurator.ts new file mode 100644 index 000000000..6e404874a --- /dev/null +++ b/themes/triangles/server/src/lib/web_server/Configurator.ts @@ -0,0 +1,47 @@ +import { Configuration } from "../configuration/schema/Configuration"; +import { GlobalDependencies } from "../../../types/Dependencies"; +import { SessionConfigurationBuilder } from + "../configuration/SessionConfigurationBuilder"; +import Path = require("path"); +import Express = require("express"); +import * as BodyParser from "body-parser"; +import { RestApi } from "./RestApi"; +import { WithHeadersLogged } from "./middlewares/WithHeadersLogged"; +import { ServerVariables } from "../ServerVariables"; +import Helmet = require("helmet"); + +const addRequestId = require("express-request-id")(); + +// Constants +const TRUST_PROXY = "trust proxy"; +const X_POWERED_BY = "x-powered-by"; +const VIEWS = "views"; +const VIEW_ENGINE = "view engine"; +const PUG = "pug"; + +export class Configurator { + static configure(config: Configuration, + app: Express.Application, + vars: ServerVariables, + deps: GlobalDependencies): void { + const viewsDirectory = Path.resolve(__dirname, "../../views"); + const publicHtmlDirectory = Path.resolve(__dirname, "../../public_html"); + + const expressSessionOptions = SessionConfigurationBuilder.build(config, deps); + + app.use(Express.static(publicHtmlDirectory)); + app.use(BodyParser.urlencoded({ extended: false })); + app.use(BodyParser.json()); + app.use(deps.session(expressSessionOptions)); + app.use(addRequestId); + app.use(WithHeadersLogged.middleware(vars.logger)); + app.disable(X_POWERED_BY); + app.enable(TRUST_PROXY); + app.use(Helmet()); + + app.set(VIEWS, viewsDirectory); + app.set(VIEW_ENGINE, PUG); + + RestApi.setup(app, vars); + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/web_server/RestApi.ts b/themes/triangles/server/src/lib/web_server/RestApi.ts new file mode 100644 index 000000000..9144a15b9 --- /dev/null +++ b/themes/triangles/server/src/lib/web_server/RestApi.ts @@ -0,0 +1,125 @@ +import Express = require("express"); + +import FirstFactorGet = require("../routes/firstfactor/get"); +import SecondFactorGet = require("../routes/secondfactor/get"); + +import FirstFactorPost = require("../routes/firstfactor/post"); +import LogoutGet = require("../routes/logout/get"); +import VerifyGet = require("../routes/verify/get"); +import TOTPSignGet = require("../routes/secondfactor/totp/sign/post"); + +import IdentityCheckMiddleware = require("../IdentityCheckMiddleware"); + +import TOTPRegistrationIdentityHandler from "../routes/secondfactor/totp/identity/RegistrationHandler"; +import U2FRegistrationIdentityHandler from "../routes/secondfactor/u2f/identity/RegistrationHandler"; +import ResetPasswordIdentityHandler from "../routes/password-reset/identity/PasswordResetHandler"; + +import U2FSignPost = require("../routes/secondfactor/u2f/sign/post"); +import U2FSignRequestGet = require("../routes/secondfactor/u2f/sign_request/get"); + +import U2FRegisterPost = require("../routes/secondfactor/u2f/register/post"); +import U2FRegisterRequestGet = require("../routes/secondfactor/u2f/register_request/get"); + +import ResetPasswordFormPost = require("../routes/password-reset/form/post"); +import ResetPasswordRequestPost = require("../routes/password-reset/request/get"); + +import Error401Get = require("../routes/error/401/get"); +import Error403Get = require("../routes/error/403/get"); +import Error404Get = require("../routes/error/404/get"); + +import LoggedIn = require("../routes/loggedin/get"); + +import { ServerVariables } from "../ServerVariables"; +import Endpoints = require("../../../../shared/api"); +import { RequireValidatedFirstFactor } from "./middlewares/RequireValidatedFirstFactor"; + +function setupTotp(app: Express.Application, vars: ServerVariables) { + app.post(Endpoints.SECOND_FACTOR_TOTP_POST, + RequireValidatedFirstFactor.middleware(vars.logger), + TOTPSignGet.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, + RequireValidatedFirstFactor.middleware(vars.logger)); + + app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, + RequireValidatedFirstFactor.middleware(vars.logger)); + + IdentityCheckMiddleware.register(app, + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, + new TOTPRegistrationIdentityHandler(vars.logger, + vars.userDataStore, vars.totpHandler, vars.config.totp), + vars); +} + +function setupU2f(app: Express.Application, vars: ServerVariables) { + app.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, + RequireValidatedFirstFactor.middleware(vars.logger), + U2FSignRequestGet.default(vars)); + + app.post(Endpoints.SECOND_FACTOR_U2F_SIGN_POST, + RequireValidatedFirstFactor.middleware(vars.logger), + U2FSignPost.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, + RequireValidatedFirstFactor.middleware(vars.logger), + U2FRegisterRequestGet.default(vars)); + + app.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, + RequireValidatedFirstFactor.middleware(vars.logger), + U2FRegisterPost.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, + RequireValidatedFirstFactor.middleware(vars.logger)); + + app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, + RequireValidatedFirstFactor.middleware(vars.logger)); + + IdentityCheckMiddleware.register(app, + Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, + Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, + new U2FRegistrationIdentityHandler(vars.logger), vars); +} + +function setupResetPassword(app: Express.Application, vars: ServerVariables) { + IdentityCheckMiddleware.register(app, + Endpoints.RESET_PASSWORD_IDENTITY_START_GET, + Endpoints.RESET_PASSWORD_IDENTITY_FINISH_GET, + new ResetPasswordIdentityHandler(vars.logger, vars.usersDatabase), + vars); + + app.get(Endpoints.RESET_PASSWORD_REQUEST_GET, + ResetPasswordRequestPost.default); + app.post(Endpoints.RESET_PASSWORD_FORM_POST, + ResetPasswordFormPost.default(vars)); +} + +function setupErrors(app: Express.Application, vars: ServerVariables) { + app.get(Endpoints.ERROR_401_GET, Error401Get.default(vars)); + app.get(Endpoints.ERROR_403_GET, Error403Get.default(vars)); + app.get(Endpoints.ERROR_404_GET, Error404Get.default); +} + +export class RestApi { + static setup(app: Express.Application, vars: ServerVariables): void { + app.get(Endpoints.FIRST_FACTOR_GET, FirstFactorGet.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_GET, + RequireValidatedFirstFactor.middleware(vars.logger), + SecondFactorGet.default(vars)); + + app.get(Endpoints.LOGOUT_GET, LogoutGet.default(vars)); + + app.get(Endpoints.VERIFY_GET, VerifyGet.default(vars)); + app.post(Endpoints.FIRST_FACTOR_POST, FirstFactorPost.default(vars)); + + setupTotp(app, vars); + setupU2f(app, vars); + setupResetPassword(app, vars); + setupErrors(app, vars); + + app.get(Endpoints.LOGGED_IN, + RequireValidatedFirstFactor.middleware(vars.logger), + LoggedIn.default(vars)); + } +} diff --git a/themes/triangles/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts b/themes/triangles/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts new file mode 100644 index 000000000..ecfd75765 --- /dev/null +++ b/themes/triangles/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts @@ -0,0 +1,27 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import ErrorReplies = require("../../ErrorReplies"); +import { IRequestLogger } from "../../logging/IRequestLogger"; +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Exceptions = require("../../Exceptions"); +import { Level } from "../../authentication/Level"; + +export class RequireValidatedFirstFactor { + static middleware(logger: IRequestLogger) { + return function (req: Express.Request, res: Express.Response, + next: Express.NextFunction): BluebirdPromise { + + 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.")); + + next(); + resolve(); + }) + .catch(ErrorReplies.replyWithError401(req, res, logger)); + }; + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/web_server/middlewares/WithHeadersLogged.ts b/themes/triangles/server/src/lib/web_server/middlewares/WithHeadersLogged.ts new file mode 100644 index 000000000..139db1141 --- /dev/null +++ b/themes/triangles/server/src/lib/web_server/middlewares/WithHeadersLogged.ts @@ -0,0 +1,12 @@ +import Express = require("express"); +import { IRequestLogger } from "../../logging/IRequestLogger"; + +export class WithHeadersLogged { + static middleware(logger: IRequestLogger) { + return function (req: Express.Request, res: Express.Response, + next: Express.NextFunction): void { + logger.debug(req, "Headers = %s", JSON.stringify(req.headers)); + next(); + }; + } +} \ No newline at end of file diff --git a/themes/triangles/server/src/resources/email-template.ejs b/themes/triangles/server/src/resources/email-template.ejs new file mode 100644 index 000000000..f59c2f944 --- /dev/null +++ b/themes/triangles/server/src/resources/email-template.ejs @@ -0,0 +1,254 @@ + + + + + + Simples-Minimalistic Responsive Template + + + + + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + +
 
+ + + + + + + +
+

<%= title %>

+
+ +
 
+
+
+ + + + + + + + +
+ + + + + + +
 
+
+ + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + +
 
+ + + + + + + + + + + + + + + + + + +
+ This email has been sent to you in order to validate your identity. Please ignore it if you do not know why you received it. +
 
+ <%= button_title %> +
+
 
+
+
+ + + + + + + + +
+ + + + + + + + + + + + +
 
 
 
+
+ + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + +
+ Please ignore this email if you did not initiate the process. +
+
+
+ + + + + diff --git a/themes/triangles/server/src/views/already-logged-in.pug b/themes/triangles/server/src/views/already-logged-in.pug new file mode 100644 index 000000000..137bbea30 --- /dev/null +++ b/themes/triangles/server/src/views/already-logged-in.pug @@ -0,0 +1,14 @@ +extends layout/layout.pug + +block form-header + h1 Sign in + +block content + img(class="header-img" src="/img/success.png" alt="success") + if redirection_url + p You are already logged in as #{ username }.

+ | If you are not redirected in few seconds, click here.

+ | Otherwise, click here to log off. + else + p You are already logged in as #{ username }.

+ | Click here to log off. diff --git a/themes/triangles/server/src/views/errors/.directory b/themes/triangles/server/src/views/errors/.directory new file mode 100644 index 000000000..33f71bea4 --- /dev/null +++ b/themes/triangles/server/src/views/errors/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,59,57 +Version=3 +ViewMode=1 diff --git a/themes/triangles/server/src/views/errors/401.pug b/themes/triangles/server/src/views/errors/401.pug new file mode 100644 index 000000000..b7a222ad0 --- /dev/null +++ b/themes/triangles/server/src/views/errors/401.pug @@ -0,0 +1,16 @@ +extends ../layout/layout.pug + +block variables + - page_classname = "error-401"; + +block form-header + h1 Error 401 + +block content + img(class="header-img" src="/img/warning.png" alt="warning") + if redirection_url + p You are not authorized to access this resource.

+ | Please click here if you are not + | redirected in few seconds. + else + p You are not authorized to access this resource. \ No newline at end of file diff --git a/themes/triangles/server/src/views/errors/403.pug b/themes/triangles/server/src/views/errors/403.pug new file mode 100644 index 000000000..f4b5ca8a2 --- /dev/null +++ b/themes/triangles/server/src/views/errors/403.pug @@ -0,0 +1,16 @@ +extends ../layout/layout.pug + +block variables + - page_classname = "error-403"; + +block form-header + h1 Error 403 + +block content + img(class="header-img" src="/img/warning.png" alt="warning") + if redirection_url + p You don't have enough privileges to access this resource.

+ | Please click here if you are not + | redirected in few seconds. + else + p You don't have enough privileges to access this resource. diff --git a/themes/triangles/server/src/views/errors/404.pug b/themes/triangles/server/src/views/errors/404.pug new file mode 100644 index 000000000..06d6375fc --- /dev/null +++ b/themes/triangles/server/src/views/errors/404.pug @@ -0,0 +1,11 @@ +extends ../layout/layout.pug + +block variables + - page_classname = "error-404"; + +block form-header +

Error 404

+ +block content + img(class="header-img" src="/img/warning.png" alt="warning") + p Page not found. diff --git a/themes/triangles/server/src/views/firstfactor.pug b/themes/triangles/server/src/views/firstfactor.pug new file mode 100644 index 000000000..574470710 --- /dev/null +++ b/themes/triangles/server/src/views/firstfactor.pug @@ -0,0 +1,23 @@ +extends layout/layout.pug + +block variables + - page_classname = "firstfactor"; + +block form-header + h1 Sign in + +block content + div(class="notification") + img(class="header-img" src="/img/sharingan.png" alt="user profile") + p Enter your credentials to sign in + form(class="form-signin") + div(class="form-inputs") + input(type="text" class="form-control" id="username" placeholder="Username" required autofocus) + input(type="password" class="form-control" id="password" placeholder="Password" required) + button(id="signin" class="btn btn-lg btn-primary btn-block" type="submit") Sign in + div(class="keep-me-logged-in pull-left") + input(type="checkbox" id="keep_me_logged_in" name="keep_me_logged_in" value="true") + label(for="keep_me_logged_in") Keep me logged in + div(class="bottom-right-links pull-right") + a(href=reset_password_request_endpoint, class="link forgot-password") Forgot password? + span(class="clearfix") diff --git a/themes/triangles/server/src/views/layout/layout.pug b/themes/triangles/server/src/views/layout/layout.pug new file mode 100644 index 000000000..43247436d --- /dev/null +++ b/themes/triangles/server/src/views/layout/layout.pug @@ -0,0 +1,28 @@ +block variables + +doctype html +html + head + title Authelia - 2FA + meta(name="viewport", content="width=device-width, initial-scale=1.0") + meta(name="robots", content="noindex, nofollow, nosnippet, noarchive") + meta(http-equiv="Content-Security-Policy", content="default-src 'self'; img-src 'self' data:;") + link(rel="icon", href="/img/icon.png" type="image/png" sizes="32x32") + link(rel="stylesheet", type="text/css", href="/css/authelia.css") + if redirection_url + meta(http-equiv="refresh" content="4;url=" + redirection_url) + body + div(class="container") + div(class="row") + div(class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-6 col-lg-offset-3") + div(class="account-wall " + page_classname) + div(class="row header") + block form-header + div(class="row body") + div(class="form col-xs-10 col-xs-offset-1 col-sm-8 col-sm-offset-2 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2") + block content + div(class="row footer poweredby-block") + div(class="poweredby col-xs-6 col-xs-offset-4 col-sm-6 col-sm-offset-4 col-md-6 col-md-offset-4") + | Powered by Authelia + block entrypoint + script(src="/js/authelia.js", type="text/javascript") diff --git a/themes/triangles/server/src/views/need-identity-validation.pug b/themes/triangles/server/src/views/need-identity-validation.pug new file mode 100644 index 000000000..4cfd62717 --- /dev/null +++ b/themes/triangles/server/src/views/need-identity-validation.pug @@ -0,0 +1,12 @@ +extends layout/layout.pug + +block variables + - page_classname = "identity-validation"; + +block form-header + h1 Registration + +block content + img(class="header-img" src="/img/mail.png" alt="mail") + p A confirmation email has been sent to your mailbox. + | Please open it and click on the link within 15 minutes to confirm the registration. diff --git a/themes/triangles/server/src/views/password-reset-form.pug b/themes/triangles/server/src/views/password-reset-form.pug new file mode 100644 index 000000000..fd9311892 --- /dev/null +++ b/themes/triangles/server/src/views/password-reset-form.pug @@ -0,0 +1,18 @@ +extends layout/layout.pug + +block variables + - page_classname = "password-reset-form"; + +block form-header + h1 Reset password + +block content + div(class="notification") + img(class="header-img" src="/img/password_white.png" alt="password") + p Set your new password and confirm it. + form(class="form-signin") + div(class="form-inputs") + input(class="form-control" type="password" name="password1" id="password1" placeholder="New password" required="required") + input(class="form-control" type="password" name="password2" id="password2" placeholder="Password confirmation" required="required") + button(id="reset-password-button" class="btn btn-lg btn-primary btn-block" type="submit") Reset Password + span(class="clearfix") diff --git a/themes/triangles/server/src/views/password-reset-request.pug b/themes/triangles/server/src/views/password-reset-request.pug new file mode 100644 index 000000000..855b5998b --- /dev/null +++ b/themes/triangles/server/src/views/password-reset-request.pug @@ -0,0 +1,18 @@ +extends layout/layout.pug + +block variables + - page_classname = "password-reset-request"; + +block form-header + h1 Reset password + +block content + div(class="notification") + div + img(class="header-img" src="/img/password_white.png" alt="password") + p After giving your username, you will receive an email to change your password. + form(class="form-signin") + div(class="form-inputs") + input(type="text" class="form-control" name="username" id="username" placeholder="Your username" required="required") + button(id="reset-password-button" class="btn btn-lg btn-primary btn-block" type="submit") Reset Password + span(class="clearfix") diff --git a/themes/triangles/server/src/views/secondfactor.pug b/themes/triangles/server/src/views/secondfactor.pug new file mode 100644 index 000000000..87b578185 --- /dev/null +++ b/themes/triangles/server/src/views/secondfactor.pug @@ -0,0 +1,31 @@ +extends layout/layout.pug + +block variables + - page_classname = "secondfactor"; + +block form-header + h1 Sign in + +block content + div + div(class="notification notification-totp") + h3 Hi #{username} + div(class="row") + div(class="u2f-token") + img(src="/img/pendrive.png", alt="security key") + p + | Please, touch your security key
+ b Or
+ | Get a one-time password + form(class="form-signin totp") + div(class="form-inputs") + input(type="text" autocomplete="off" class="form-control" id="token" placeholder="Token" required autofocus) + button(class="btn btn-lg btn-primary btn-block totp-button" type="submit") Sign in + div(class="pull-right bottom-right-links") + div Need to register? + div + a(href=u2f_identity_start_endpoint, class="link register-u2f", data-toggle="tooltip", title="A security key is required to register.") Security key + | | + a(href=totp_identity_start_endpoint, class="link register-totp") Google Authenticator + span(class="clearfix") + script(src="/js/u2f-api.js", type="text/javascript") diff --git a/themes/triangles/server/src/views/totp-register.pug b/themes/triangles/server/src/views/totp-register.pug new file mode 100644 index 000000000..1b4d98354 --- /dev/null +++ b/themes/triangles/server/src/views/totp-register.pug @@ -0,0 +1,25 @@ +extends layout/layout.pug + +block variables + - page_classname = "totp-register"; + +block form-header + h1 One-time passwords + +block content + p Open Google Authenticator and add this entry + p(id="secret") #{ base32_secret } + p or scan this barcode + div(id="qrcode") #{ otpauth_url } + p + a(href=login_endpoint, id="login-button") Login + div(class="need-google-authenticator") + | Need Google Authenticator? + div(class="store-badges") + a(href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1', target="_blank") + img(alt='Get it on Google Play', src='/img/stores/googleplay-badge.svg', class="store-badge") + a(href='https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8', target="_blank") + img(alt='Get it on Apple Store' src='/img/stores/applestore-badge.svg' class="store-badge") + +block entrypoint + script(src="/js/qrcode.min.js", type="text/javascript" ) diff --git a/themes/triangles/server/src/views/u2f-register.pug b/themes/triangles/server/src/views/u2f-register.pug new file mode 100644 index 000000000..d52eba6ca --- /dev/null +++ b/themes/triangles/server/src/views/u2f-register.pug @@ -0,0 +1,12 @@ +extends layout/layout.pug + +block variables + - page_classname = "u2f-register"; + +block form-header + h1 Register your security key + +block content + p Touch the token to register your security key. + img(src="/img/pendrive.png" alt="pendrive") + script(src="/js/u2f-api.js", type="text/javascript") \ No newline at end of file diff --git a/themes/triangles/server/test/requests.ts b/themes/triangles/server/test/requests.ts new file mode 100644 index 000000000..93fa0de47 --- /dev/null +++ b/themes/triangles/server/test/requests.ts @@ -0,0 +1,94 @@ + +import BluebirdPromise = require("bluebird"); +import request = require("request"); +import assert = require("assert"); +import express = require("express"); +import nodemailer = require("nodemailer"); +import Endpoints = require("../../shared/api"); + +declare module "request" { + export interface RequestAPI { + getAsync(uri: string, options?: RequiredUriUrl): BluebirdPromise; + getAsync(uri: string): BluebirdPromise; + getAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; + + postAsync(uri: string, options?: CoreOptions): BluebirdPromise; + postAsync(uri: string): BluebirdPromise; + postAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; + } +} + +const requestAsync: typeof request = BluebirdPromise.promisifyAll(request) as typeof request; + +export = function (port: number) { + const PORT = port; + const BASE_URL = "http://localhost:" + PORT; + + function execute_totp(jar: request.CookieJar, token: string) { + return requestAsync.postAsync({ + url: BASE_URL + Endpoints.SECOND_FACTOR_TOTP_POST, + jar: jar, + form: { + token: token + } + }); + } + + function execute_u2f_authentication(jar: request.CookieJar) { + return requestAsync.getAsync({ + url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, + jar: jar + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200); + return requestAsync.postAsync({ + url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_POST, + jar: jar, + form: { + } + }); + }); + } + + function execute_verification(jar: request.CookieJar) { + return requestAsync.getAsync({ url: BASE_URL + Endpoints.VERIFY_GET, jar: jar }); + } + + function execute_login(jar: request.CookieJar) { + return requestAsync.getAsync({ url: BASE_URL + Endpoints.FIRST_FACTOR_GET, jar: jar }); + } + + function execute_first_factor(jar: request.CookieJar) { + return requestAsync.postAsync({ + url: BASE_URL + Endpoints.FIRST_FACTOR_POST, + jar: jar, + form: { + username: "test_ok", + password: "password" + } + }); + } + + function execute_failing_first_factor(jar: request.CookieJar) { + return requestAsync.postAsync({ + url: BASE_URL + Endpoints.FIRST_FACTOR_POST, + jar: jar, + form: { + username: "test_nok", + password: "password" + } + }); + } + + return { + login: execute_login, + verify: execute_verification, + u2f_authentication: execute_u2f_authentication, + first_factor: execute_first_factor, + failing_first_factor: execute_failing_first_factor, + totp: execute_totp, + }; +}; + diff --git a/themes/triangles/server/tsconfig.json b/themes/triangles/server/tsconfig.json new file mode 100644 index 000000000..ebe98c5ed --- /dev/null +++ b/themes/triangles/server/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "moduleResolution": "node", + "noImplicitAny": true, + "sourceMap": true, + "removeComments": true, + "outDir": "../dist", + "baseUrl": ".", + "paths": { + "*": [ + "./types/*", + "../shared/types/*" + ] + } + }, + "exclude": [ + "src/**/*.spec.ts" + ] +} diff --git a/themes/triangles/server/tslint.json b/themes/triangles/server/tslint.json new file mode 100644 index 000000000..c2c1b7501 --- /dev/null +++ b/themes/triangles/server/tslint.json @@ -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 + } +} diff --git a/themes/triangles/server/types/.directory b/themes/triangles/server/types/.directory new file mode 100644 index 000000000..1e65000ea --- /dev/null +++ b/themes/triangles/server/types/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2018,12,17,20,58,27 +Version=3 +ViewMode=1 diff --git a/themes/triangles/server/types/AuthenticationSession.ts b/themes/triangles/server/types/AuthenticationSession.ts new file mode 100644 index 000000000..bbed0e715 --- /dev/null +++ b/themes/triangles/server/types/AuthenticationSession.ts @@ -0,0 +1,18 @@ +import U2f = require("u2f"); +import { Level } from "../src/lib/authentication/Level"; + +export interface AuthenticationSession { + userid: string; + authentication_level: Level; + keep_me_logged_in: boolean; + last_activity_datetime: number; + identity_check?: { + challenge: string; + userid: string; + }; + register_request?: U2f.Request; + sign_request?: U2f.Request; + email: string; + groups: string[]; + redirect?: string; +} \ No newline at end of file diff --git a/themes/triangles/server/types/Dependencies.ts b/themes/triangles/server/types/Dependencies.ts new file mode 100644 index 000000000..f20404dbc --- /dev/null +++ b/themes/triangles/server/types/Dependencies.ts @@ -0,0 +1,29 @@ +import winston = require("winston"); +import speakeasy = require("speakeasy"); +import nodemailer = require("nodemailer"); +import session = require("express-session"); +import nedb = require("nedb"); +import ldapjs = require("ldapjs"); +import u2f = require("u2f"); +import RedisSession = require("connect-redis"); +import Redis = require("redis"); + +export type Speakeasy = typeof speakeasy; +export type Winston = typeof winston; +export type Session = typeof session; +export type Nedb = typeof nedb; +export type Ldapjs = typeof ldapjs; +export type U2f = typeof u2f; +export type ConnectRedis = typeof RedisSession; +export type Redis = typeof Redis; + +export interface GlobalDependencies { + u2f: U2f; + ldapjs: Ldapjs; + session: Session; + Redis: Redis; + ConnectRedis: ConnectRedis; + winston: Winston; + speakeasy: Speakeasy; + nedb: Nedb; +} \ No newline at end of file diff --git a/themes/triangles/server/types/Identity.ts b/themes/triangles/server/types/Identity.ts new file mode 100644 index 000000000..e985984e2 --- /dev/null +++ b/themes/triangles/server/types/Identity.ts @@ -0,0 +1,6 @@ + + +export interface Identity { + userid: string; + email: string; +} \ No newline at end of file diff --git a/themes/triangles/server/types/TOTPSecret.ts b/themes/triangles/server/types/TOTPSecret.ts new file mode 100644 index 000000000..d6775f2f0 --- /dev/null +++ b/themes/triangles/server/types/TOTPSecret.ts @@ -0,0 +1,11 @@ + +export interface TOTPSecret { + ascii: string; + hex: string; + base32: string; + qr_code_ascii: string; + qr_code_hex: string; + qr_code_base32: string; + google_auth_qr: string; + otpauth_url: string; + } \ No newline at end of file diff --git a/themes/triangles/server/types/U2FRegistration.ts b/themes/triangles/server/types/U2FRegistration.ts new file mode 100644 index 000000000..b6080af07 --- /dev/null +++ b/themes/triangles/server/types/U2FRegistration.ts @@ -0,0 +1,5 @@ + +export interface U2FRegistration { + keyHandle: string; + publicKey: string; +} \ No newline at end of file diff --git a/themes/triangles/server/types/dovehash.d.ts b/themes/triangles/server/types/dovehash.d.ts new file mode 100644 index 000000000..c354609c0 --- /dev/null +++ b/themes/triangles/server/types/dovehash.d.ts @@ -0,0 +1,4 @@ + +declare module "dovehash" { + function encode(algo: string, text: string): string; +} \ No newline at end of file diff --git a/themes/triangles/server/types/speakeasy.d.ts b/themes/triangles/server/types/speakeasy.d.ts new file mode 100644 index 000000000..6ea06948b --- /dev/null +++ b/themes/triangles/server/types/speakeasy.d.ts @@ -0,0 +1,96 @@ +declare module "speakeasy" { + export = speakeasy + + interface SharedOptions { + encoding?: string + algorithm?: string + } + + interface DigestOptions extends SharedOptions { + secret: string + counter: number + } + + interface HOTPOptions extends SharedOptions { + secret: string + counter: number + digest?: Buffer + digits?: number + } + + interface HOTPVerifyOptions extends SharedOptions { + secret: string + token: string + counter: number + digits?: number + window?: number + } + + interface TOTPOptions extends SharedOptions { + secret: string + time?: number + step?: number + epoch?: number + counter?: number + digits?: number + } + + interface TOTPVerifyOptions extends SharedOptions { + secret: string + token: string + time?: number + step?: number + epoch?: number + counter?: number + digits?: number + window?: number + } + + interface GenerateSecretOptions { + length?: number + symbols?: boolean + otpauth_url?: boolean + name?: string + issuer?: string + } + + interface GeneratedSecret { + ascii: string + hex: string + base32: string + qr_code_ascii: string + qr_code_hex: string + qr_code_base32: string + google_auth_qr: string + otpauth_url: string + } + + interface OTPAuthURLOptions extends SharedOptions { + secret: string + label: string + type?: string + counter?: number + issuer?: string + digits?: number + period?: number + } + + interface Speakeasy { + digest: (options: DigestOptions) => Buffer + hotp: { + (options: HOTPOptions): string, + verifyDelta: (options: HOTPVerifyOptions) => boolean, + verify: (options: HOTPVerifyOptions) => boolean, + } + totp: { + (options: TOTPOptions): string + verifyDelta: (options: TOTPVerifyOptions) => boolean, + verify: (options: TOTPVerifyOptions) => boolean, + } + generateSecret: (options?: GenerateSecretOptions) => GeneratedSecret + generateSecretASCII: (length?: number, symbols?: boolean) => string + otpauthURL: (options: OTPAuthURLOptions) => string + } + + const speakeasy: Speakeasy +} \ No newline at end of file From 48c204fc6824f7c8a00c13b8915bce11cb5077db Mon Sep 17 00:00:00 2001 From: BankaiNoJutsu Date: Tue, 18 Dec 2018 16:30:23 +0100 Subject: [PATCH 05/11] added all themes, clean and backup of dist, --theme value check --- .gitignore | 0 .npmignore | 0 .travis.yml | 0 CHANGELOG.md | 0 CONTRIBUTORS.md | 0 Dockerfile | 0 Dockerfile.dev | 0 Gruntfile.js | 186 ++------- LICENSE | 0 README.md | 0 client/src/css/00-bootstrap.min.css | 0 client/src/css/01-main.css | 0 client/src/css/02-login.css | 0 client/src/css/03-errors.css | 0 client/src/css/03-password-reset-form.css | 0 client/src/css/03-password-reset-request.css | 0 client/src/css/03-totp-register.css | 0 client/src/css/03-u2f-register.css | 0 client/src/img/background.svg | 0 client/src/img/icon.png | Bin client/src/img/mail.png | Bin client/src/img/notifications/error.png | Bin client/src/img/notifications/info.png | Bin client/src/img/notifications/success.png | Bin client/src/img/notifications/warning.png | Bin client/src/img/padlock.png | Bin client/src/img/password.png | Bin client/src/img/pendrive.png | Bin client/src/img/stores/applestore-badge.svg | 0 client/src/img/stores/googleplay-badge.svg | 0 client/src/img/success.png | Bin client/src/img/user.png | Bin client/src/img/warning.png | Bin client/src/index.ts | 0 client/src/lib/GetPromised.ts | 0 client/src/lib/INotifier.ts | 0 client/src/lib/Notifier.ts | 0 client/src/lib/QueryParametersRetriever.ts | 0 client/src/lib/SafeRedirect.ts | 0 .../lib/firstfactor/FirstFactorValidator.ts | 0 client/src/lib/firstfactor/UISelectors.ts | 0 client/src/lib/firstfactor/index.ts | 0 client/src/lib/reset-password/constants.ts | 0 .../lib/reset-password/reset-password-form.ts | 0 .../reset-password/reset-password-request.ts | 0 client/src/lib/secondfactor/TOTPValidator.ts | 0 client/src/lib/secondfactor/U2FValidator.ts | 0 client/src/lib/secondfactor/constants.ts | 0 client/src/lib/secondfactor/index.ts | 0 client/src/lib/totp-register/totp-register.ts | 0 client/src/lib/totp-register/ui-selector.ts | 0 client/src/lib/u2f-register/u2f-register.ts | 0 client/src/thirdparties/qrcode.min.js | 0 client/test/Notifier.test.ts | 0 .../firstfactor/FirstFactorValidator.test.ts | 0 client/test/mocks/NotifierStub.ts | 0 client/test/mocks/jquery.ts | 0 client/test/mocks/u2f-api.ts | 0 .../test/secondfactor/TOTPValidator.test.ts | 0 .../test/totp-register/totp-register.test.ts | 0 client/tsconfig.json | 0 client/tslint.json | 0 config.minimal.yml | 2 +- config.template.yml | 0 docker-compose.dev.yml | 0 docker-compose.dockerhub.yml | 0 docker-compose.minimal.dev.yml | 0 docker-compose.minimal.yml | 0 docker-compose.swarm.minimal.yml | 0 docker-compose.test.yml | 0 docker-compose.yml | 0 docs/build.md | 0 docs/configuration.md | 0 docs/deployment-dev.md | 0 docs/deployment-production.md | 0 docs/features.md | 0 docs/getting-started.md | 0 docs/security.md | 0 .../compose/authelia/docker-compose.test.yml | 0 example/compose/docker-compose.base.yml | 0 example/compose/httpbin/docker-compose.yml | 0 example/compose/ldap/access.rules | 0 example/compose/ldap/base.ldif | 0 example/compose/ldap/docker-compose.admin.yml | 0 example/compose/ldap/docker-compose.yml | 0 example/compose/mongo/docker-compose.yml | 0 .../compose/nginx/backend/docker-compose.yml | 0 .../nginx/backend/html/admin/secret.html | 0 .../backend/html/dev/groups/admin/secret.html | 0 .../backend/html/dev/groups/dev/secret.html | 0 .../backend/html/dev/users/bob/secret.html | 0 .../backend/html/dev/users/harry/secret.html | 0 .../backend/html/dev/users/john/secret.html | 0 .../nginx/backend/html/home/index.html | 0 example/compose/nginx/backend/html/icon.png | Bin .../nginx/backend/html/mail/secret.html | 0 .../nginx/backend/html/public/index.html | 0 .../nginx/backend/html/public/secret.html | 0 .../backend/html/single_factor/secret.html | 0 example/compose/nginx/backend/nginx.conf | 0 .../compose/nginx/minimal/docker-compose.yml | 0 .../nginx/minimal/html/admin/secret.html | 0 .../nginx/minimal/html/home/index.html | 0 example/compose/nginx/minimal/nginx.conf | 0 example/compose/nginx/minimal/ssl/server.crt | 0 example/compose/nginx/minimal/ssl/server.csr | 0 example/compose/nginx/minimal/ssl/server.key | 0 .../compose/nginx/portal/docker-compose.yml | 0 example/compose/nginx/portal/nginx.conf | 0 example/compose/nginx/portal/ssl/server.crt | 0 example/compose/nginx/portal/ssl/server.csr | 0 example/compose/nginx/portal/ssl/server.key | 0 example/compose/redis/docker-compose.yml | 0 example/compose/smtp/docker-compose.yml | 0 example/kube/README.md | 0 example/kube/apps/app-home/deployment.yml | 0 example/kube/apps/app-home/index.html | 0 example/kube/apps/app-home/service.yml | 0 example/kube/apps/app1/deployment.yml | 0 example/kube/apps/app1/index.html | 0 example/kube/apps/app1/service.yml | 0 example/kube/apps/app1/ssl/tls.crt | 0 example/kube/apps/app1/ssl/tls.csr | 0 example/kube/apps/app1/ssl/tls.key | 0 example/kube/apps/app2/deployment.yml | 0 example/kube/apps/app2/index.html | 0 example/kube/apps/app2/service.yml | 0 example/kube/apps/app2/ssl/tls.crt | 0 example/kube/apps/app2/ssl/tls.csr | 0 example/kube/apps/app2/ssl/tls.key | 0 example/kube/apps/insecure-ingress.yml | 0 example/kube/apps/secure-ingress.yml | 0 example/kube/authelia/configs/config.yml | 0 example/kube/authelia/deployment.yml | 0 example/kube/authelia/ingress.yml | 0 example/kube/authelia/service.yml | 0 example/kube/authelia/ssl/tls.crt | 0 example/kube/authelia/ssl/tls.csr | 0 example/kube/authelia/ssl/tls.key | 0 example/kube/docker-registry/daemonset.yml | 0 example/kube/docker-registry/ingress.yml | 0 .../docker-registry/replicationcontroller.yml | 0 example/kube/docker-registry/service.yml | 0 .../ingress-controller/default-backend.yml | 0 .../kube/ingress-controller/deployment.yml | 0 example/kube/ingress-controller/service.yml | 0 example/kube/ldap/Dockerfile | 0 example/kube/ldap/deployment.yml | 0 example/kube/ldap/service.yml | 0 example/kube/mailcatcher/deployment.yml | 0 example/kube/mailcatcher/ingress.yml | 0 example/kube/mailcatcher/service.yml | 0 example/kube/namespace.yml | 0 example/kube/storage/mongo.yml | 0 example/kube/storage/redis.yml | 0 images/authelia-title-white.png | Bin images/authelia-title.png | Bin images/email_confirmation.png | Bin images/first_factor.png | Bin images/icon.png | Bin images/kube-logo.png | Bin images/reset_password.png | Bin images/second_factor.png | Bin images/totp.png | Bin images/u2f.png | Bin package-lock.json | 28 +- package.json | 0 .../src/lib/AuthenticationSessionHandler.ts | 0 server/src/lib/ErrorReplies.ts | 0 server/src/lib/Exceptions.ts | 0 server/src/lib/FirstFactorValidator.ts | 0 .../src/lib/IdentityCheckMiddleware.spec.ts | 0 server/src/lib/IdentityCheckMiddleware.ts | 0 .../lib/IdentityCheckPreValidationTemplate.ts | 0 server/src/lib/IdentityValidable.ts | 0 server/src/lib/IdentityValidableStub.spec.ts | 0 server/src/lib/Server.spec.ts | 0 server/src/lib/Server.ts | 0 server/src/lib/ServerVariables.ts | 0 server/src/lib/ServerVariablesInitializer.ts | 0 .../lib/ServerVariablesMockBuilder.spec.ts | 0 server/src/lib/authentication/Level.ts | 0 .../backends/GroupsAndEmails.ts | 0 .../authentication/backends/IUsersDatabase.ts | 0 .../backends/IUsersDatabaseStub.spec.ts | 0 .../backends/file/FileUsersDatabase.spec.ts | 0 .../backends/file/FileUsersDatabase.ts | 0 .../backends/file/ReadWriteQueue.ts | 0 .../authentication/backends/ldap/ISession.ts | 0 .../backends/ldap/ISessionFactory.ts | 0 .../backends/ldap/LdapUsersDatabase.spec.ts | 0 .../backends/ldap/LdapUsersDatabase.ts | 0 .../backends/ldap/SafeSession.spec.ts | 0 .../backends/ldap/SafeSession.ts | 0 .../backends/ldap/Sanitizer.spec.ts | 0 .../authentication/backends/ldap/Sanitizer.ts | 0 .../backends/ldap/Session.spec.ts | 0 .../authentication/backends/ldap/Session.ts | 0 .../backends/ldap/SessionFactory.ts | 0 .../backends/ldap/SessionFactoryStub.spec.ts | 0 .../backends/ldap/SessionStub.spec.ts | 0 .../backends/ldap/connector/Connector.ts | 0 .../ldap/connector/ConnectorFactory.ts | 0 .../connector/ConnectorFactoryStub.spec.ts | 0 .../ldap/connector/ConnectorStub.spec.ts | 0 .../backends/ldap/connector/IConnector.ts | 0 .../ldap/connector/IConnectorFactory.ts | 0 .../lib/authentication/totp/ITotpHandler.ts | 0 .../authentication/totp/TotpHandler.spec.ts | 0 .../lib/authentication/totp/TotpHandler.ts | 0 .../totp/TotpHandlerStub.spec.ts | 0 .../src/lib/authentication/u2f/IU2fHandler.ts | 0 .../src/lib/authentication/u2f/U2fHandler.ts | 0 .../authentication/u2f/U2fHandlerStub.spec.ts | 0 .../src/lib/authorization/Authorizer.spec.ts | 0 server/src/lib/authorization/Authorizer.ts | 0 .../lib/authorization/AuthorizerStub.spec.ts | 0 server/src/lib/authorization/IAuthorizer.ts | 0 server/src/lib/authorization/Level.ts | 0 .../authorization/MultipleDomainMatcher.ts | 0 server/src/lib/authorization/Object.ts | 0 server/src/lib/authorization/Subject.ts | 0 .../configuration/ConfigurationParser.spec.ts | 0 .../lib/configuration/ConfigurationParser.ts | 0 .../SessionConfigurationBuilder.spec.ts | 0 .../SessionConfigurationBuilder.ts | 0 .../schema/AclConfiguration.spec.ts | 0 .../configuration/schema/AclConfiguration.ts | 0 ...AuthenticationBackendConfiguration.spec.ts | 0 .../AuthenticationBackendConfiguration.ts | 0 .../lib/configuration/schema/Configuration.ts | 0 .../schema/FileUsersDatabaseConfiguration.ts | 0 .../schema/LdapConfiguration.spec.ts | 0 .../configuration/schema/LdapConfiguration.ts | 0 .../schema/NotifierConfiguration.spec.ts | 0 .../schema/NotifierConfiguration.ts | 0 .../schema/RegulationConfiguration.spec.ts | 0 .../schema/RegulationConfiguration.ts | 0 .../schema/SessionConfiguration.spec.ts | 0 .../schema/SessionConfiguration.ts | 0 .../schema/StorageConfiguration.spec.ts | 0 .../schema/StorageConfiguration.ts | 0 .../configuration/schema/TotpConfiguration.ts | 0 .../schema/UserDatabaseConfiguration.ts | 0 .../lib/connectors/mongo/IMongoClient.d.ts | 0 .../lib/connectors/mongo/MongoClient.spec.ts | 0 .../src/lib/connectors/mongo/MongoClient.ts | 0 .../connectors/mongo/MongoClientStub.spec.ts | 0 server/src/lib/logging/GlobalLogger.ts | 0 .../src/lib/logging/GlobalLoggerStub.spec.ts | 0 server/src/lib/logging/IGlobalLogger.ts | 0 server/src/lib/logging/IRequestLogger.ts | 0 server/src/lib/logging/RequestLogger.ts | 0 .../src/lib/logging/RequestLoggerStub.spec.ts | 0 .../lib/notifiers/AbstractEmailNotifier.ts | 0 .../src/lib/notifiers/EmailNotifier.spec.ts | 0 server/src/lib/notifiers/EmailNotifier.ts | 0 .../src/lib/notifiers/FileSystemNotifier.ts | 0 server/src/lib/notifiers/IMailSender.ts | 0 .../src/lib/notifiers/IMailSenderBuilder.ts | 0 server/src/lib/notifiers/INotifier.ts | 0 server/src/lib/notifiers/MailSender.ts | 0 .../lib/notifiers/MailSenderBuilder.spec.ts | 0 server/src/lib/notifiers/MailSenderBuilder.ts | 0 .../notifiers/MailSenderBuilderStub.spec.ts | 0 .../src/lib/notifiers/MailSenderStub.spec.ts | 0 .../src/lib/notifiers/NotifierFactory.spec.ts | 0 server/src/lib/notifiers/NotifierFactory.ts | 0 server/src/lib/notifiers/NotifierStub.spec.ts | 0 server/src/lib/notifiers/SmtpNotifier.ts | 0 server/src/lib/regulation/IRegulator.ts | 0 server/src/lib/regulation/Regulator.spec.ts | 0 server/src/lib/regulation/Regulator.ts | 0 .../src/lib/regulation/RegulatorStub.spec.ts | 0 server/src/lib/routes/error/401/get.spec.ts | 0 server/src/lib/routes/error/401/get.ts | 0 server/src/lib/routes/error/403/get.spec.ts | 0 server/src/lib/routes/error/403/get.ts | 0 server/src/lib/routes/error/404/get.spec.ts | 0 server/src/lib/routes/error/404/get.ts | 0 server/src/lib/routes/error/redirector.ts | 0 server/src/lib/routes/firstfactor/get.ts | 0 .../src/lib/routes/firstfactor/post.spec.ts | 0 server/src/lib/routes/firstfactor/post.ts | 0 server/src/lib/routes/loggedin/get.ts | 0 server/src/lib/routes/logout/get.ts | 0 .../lib/routes/password-reset/constants.ts | 0 .../routes/password-reset/form/post.spec.ts | 0 .../lib/routes/password-reset/form/post.ts | 0 .../identity/PasswordResetHandler.spec.ts | 0 .../identity/PasswordResetHandler.ts | 0 .../lib/routes/password-reset/request/get.ts | 0 .../src/lib/routes/secondfactor/get.spec.ts | 0 server/src/lib/routes/secondfactor/get.ts | 0 .../lib/routes/secondfactor/redirect.spec.ts | 0 .../src/lib/routes/secondfactor/redirect.ts | 0 .../lib/routes/secondfactor/totp/constants.ts | 0 .../totp/identity/RegistrationHandler.spec.ts | 0 .../totp/identity/RegistrationHandler.ts | 0 .../secondfactor/totp/sign/post.spec.ts | 0 .../lib/routes/secondfactor/totp/sign/post.ts | 0 .../lib/routes/secondfactor/u2f/U2FCommon.ts | 0 .../u2f/identity/RegistrationHandler.spec.ts | 0 .../u2f/identity/RegistrationHandler.ts | 0 .../secondfactor/u2f/register/post.spec.ts | 0 .../routes/secondfactor/u2f/register/post.ts | 0 .../u2f/register_request/get.spec.ts | 0 .../secondfactor/u2f/register_request/get.ts | 0 .../routes/secondfactor/u2f/sign/post.spec.ts | 0 .../lib/routes/secondfactor/u2f/sign/post.ts | 0 .../secondfactor/u2f/sign_request/get.spec.ts | 0 .../secondfactor/u2f/sign_request/get.ts | 0 .../src/lib/routes/verify/access_control.ts | 0 server/src/lib/routes/verify/get.spec.ts | 0 server/src/lib/routes/verify/get.ts | 0 .../src/lib/routes/verify/get_basic_auth.ts | 0 .../lib/routes/verify/get_session_cookie.ts | 0 .../storage/AuthenticationTraceDocument.d.ts | 0 .../lib/storage/CollectionFactoryFactory.ts | 0 .../lib/storage/CollectionFactoryStub.spec.ts | 0 server/src/lib/storage/CollectionStub.spec.ts | 0 server/src/lib/storage/ICollection.d.ts | 0 .../src/lib/storage/ICollectionFactory.d.ts | 0 server/src/lib/storage/IUserDataStore.d.ts | 0 .../storage/IdentityValidationDocument.d.ts | 0 .../src/lib/storage/TOTPSecretDocument.d.ts | 0 .../lib/storage/U2FRegistrationDocument.d.ts | 0 server/src/lib/storage/UserDataStore.spec.ts | 0 server/src/lib/storage/UserDataStore.ts | 0 .../src/lib/storage/UserDataStoreStub.spec.ts | 0 .../lib/storage/mongo/MongoCollection.spec.ts | 0 .../src/lib/storage/mongo/MongoCollection.ts | 0 .../mongo/MongoCollectionFactory.spec.ts | 0 .../storage/mongo/MongoCollectionFactory.ts | 0 .../lib/storage/nedb/NedbCollection.spec.ts | 0 server/src/lib/storage/nedb/NedbCollection.ts | 0 .../nedb/NedbCollectionFactory.spec.ts | 0 .../lib/storage/nedb/NedbCollectionFactory.ts | 0 server/src/lib/stubs/express.spec.ts | 0 server/src/lib/stubs/ldapjs.spec.ts | 0 server/src/lib/stubs/speakeasy.spec.ts | 0 server/src/lib/stubs/u2f.spec.ts | 0 server/src/lib/utils/HashGenerator.spec.ts | 0 server/src/lib/utils/HashGenerator.ts | 0 server/src/lib/utils/ObjectCloner.ts | 0 server/src/lib/utils/SafeRedirection.spec.ts | 0 server/src/lib/utils/SafeRedirection.ts | 0 server/src/lib/utils/URLDecomposer.spec.ts | 0 server/src/lib/utils/URLDecomposer.ts | 0 server/src/lib/web_server/Configurator.ts | 0 server/src/lib/web_server/RestApi.ts | 0 .../RequireValidatedFirstFactor.ts | 0 .../middlewares/WithHeadersLogged.ts | 0 server/src/resources/email-template.ejs | 0 server/src/views/already-logged-in.pug | 0 server/src/views/errors/401.pug | 0 server/src/views/errors/403.pug | 0 server/src/views/errors/404.pug | 0 server/src/views/firstfactor.pug | 0 server/src/views/layout/layout.pug | 0 server/src/views/need-identity-validation.pug | 0 server/src/views/password-reset-form.pug | 0 server/src/views/password-reset-request.pug | 0 server/src/views/secondfactor.pug | 0 server/src/views/totp-register.pug | 0 server/src/views/u2f-register.pug | 0 server/test/requests.ts | 0 server/tsconfig.json | 0 server/tslint.json | 0 server/types/AuthenticationSession.ts | 0 server/types/Dependencies.ts | 0 server/types/Identity.ts | 0 server/types/TOTPSecret.ts | 0 server/types/U2FRegistration.ts | 0 server/types/dovehash.d.ts | 0 server/types/speakeasy.d.ts | 0 shared/BelongToDomain.ts | 0 shared/DomainExtractor.spec.ts | 0 shared/DomainExtractor.ts | 0 shared/ErrorMessage.ts | 0 shared/RedirectionMessage.ts | 0 shared/SignMessage.ts | 0 shared/UserMessages.ts | 0 shared/api.ts | 0 shared/constants.ts | 0 shared/types/u2f.d.ts | 0 test/complete-config/00-suite.ts | 0 test/complete-config/closed-redirection.ts | 0 .../mongo-broken-connection.ts | 0 test/configuration.ts | 0 test/environment.ts | 0 test/features/access-control.feature | 0 test/features/auth-portal-redirection.feature | 0 test/features/authelia.feature | 0 test/features/authentication.feature | 0 test/features/forward-headers.feature | 0 test/features/redirection.feature | 0 test/features/registration.feature | 0 test/features/regulation.feature | 0 test/features/reset-password.feature | 0 test/features/resilience.feature | 0 test/features/restrictions.feature | 0 test/features/session-timeout.feature | 0 test/features/single-factor-domain.feature | 0 .../step_definitions/access-control.ts | 0 test/features/step_definitions/authelia.ts | 0 .../step_definitions/authentication.ts | 0 .../step_definitions/forward-headers.ts | 0 test/features/step_definitions/hooks.ts | 0 .../step_definitions/notifications.ts | 0 test/features/step_definitions/redirection.ts | 0 .../features/step_definitions/registration.ts | 0 test/features/step_definitions/regulation.ts | 0 .../step_definitions/reset-password.ts | 0 test/features/step_definitions/resilience.ts | 0 .../features/step_definitions/restrictions.ts | 0 .../step_definitions/session-timeout.ts | 0 .../step_definitions/single-factor.ts | 0 test/features/support/world.ts | 0 test/helpers/access-secret.ts | 0 test/helpers/click-on-button.ts | 0 test/helpers/click-on-link.ts | 0 test/helpers/fill-field.ts | 0 test/helpers/fill-login-page-and-click.ts | 0 test/helpers/full-login.ts | 0 test/helpers/get-identity-link.ts | 0 test/helpers/login-and-register-totp.ts | 0 test/helpers/login-as.ts | 0 test/helpers/register-totp.ts | 0 test/helpers/see-notification.ts | 0 test/helpers/validate-totp.ts | 0 test/helpers/visit-page.ts | 0 test/helpers/wait-redirected.ts | 0 test/helpers/with-driver.ts | 0 test/inactivity/00-suite.ts | 0 test/inactivity/keep_me_logged_in.ts | 0 test/minimal-config/00-suite.ts | 0 test/minimal-config/bad_password.ts | 0 test/minimal-config/fail_totp.ts | 0 test/minimal-config/register_totp.ts | 0 test/minimal-config/reset_password.ts | 0 test/minimal-config/validate_totp.ts | 0 themes/black/client/src/css/.directory | 0 .../black/client/src/css/00-bootstrap.min.css | 0 themes/black/client/src/css/01-main.css | 2 +- themes/black/client/src/css/02-login.css | 0 themes/black/client/src/css/03-errors.css | 0 .../client/src/css/03-password-reset-form.css | 0 .../src/css/03-password-reset-request.css | 0 .../black/client/src/css/03-totp-register.css | 0 .../black/client/src/css/03-u2f-register.css | 0 .../client/src/img/RandomizedPattern.svg | 0 themes/black/client/src/img/background.jpg | Bin themes/black/client/src/img/icon.png | Bin themes/black/client/src/img/mail.png | Bin .../client/src/img/notifications/.directory | 0 .../client/src/img/notifications/error.png | Bin .../client/src/img/notifications/info.png | Bin .../client/src/img/notifications/success.png | Bin .../client/src/img/notifications/warning.png | Bin themes/black/client/src/img/padlock.png | Bin .../black/client/src/img/password_white.png | Bin themes/black/client/src/img/pendrive.png | Bin themes/black/client/src/img/sharingan.png | Bin themes/black/client/src/img/stores/.directory | 0 .../src/img/stores/applestore-badge.svg | 0 .../src/img/stores/googleplay-badge.svg | 0 themes/black/client/src/img/success.png | Bin themes/black/client/src/img/user.png | Bin themes/black/client/src/img/warning.png | Bin themes/black/client/src/index.ts | 34 -- themes/black/client/src/lib/GetPromised.ts | 14 - themes/black/client/src/lib/INotifier.ts | 14 - themes/black/client/src/lib/Notifier.ts | 83 ---- .../src/lib/QueryParametersRetriever.ts | 12 - themes/black/client/src/lib/SafeRedirect.ts | 10 - .../lib/firstfactor/FirstFactorValidator.ts | 46 --- .../client/src/lib/firstfactor/UISelectors.ts | 5 - .../black/client/src/lib/firstfactor/index.ts | 49 --- .../src/lib/reset-password/constants.ts | 2 - .../lib/reset-password/reset-password-form.ts | 57 --- .../reset-password/reset-password-request.ts | 56 --- .../src/lib/secondfactor/TOTPValidator.ts | 28 -- .../src/lib/secondfactor/U2FValidator.ts | 42 -- .../client/src/lib/secondfactor/constants.ts | 3 - .../client/src/lib/secondfactor/index.ts | 59 --- .../src/lib/totp-register/totp-register.ts | 11 - .../src/lib/totp-register/ui-selector.ts | 2 - .../src/lib/u2f-register/u2f-register.ts | 56 --- .../client/src/thirdparties/qrcode.min.js | 0 .../black/client/src/thirdparties/u2f-api.js | 0 themes/black/client/test/Notifier.test.ts | 71 ---- .../firstfactor/FirstFactorValidator.test.ts | 44 -- .../black/client/test/mocks/NotifierStub.ts | 33 -- themes/black/client/test/mocks/jquery.ts | 59 --- themes/black/client/test/mocks/u2f-api.ts | 14 - .../test/secondfactor/TOTPValidator.test.ts | 37 -- .../test/totp-register/totp-register.test.ts | 31 -- themes/black/client/tsconfig.json | 24 -- themes/black/client/tslint.json | 60 --- themes/black/server/.directory | 0 themes/black/server/src/index.ts | 28 -- themes/black/server/src/lib/.directory | 4 - .../src/lib/AuthenticationSessionHandler.ts | 45 -- themes/black/server/src/lib/ErrorReplies.ts | 49 --- themes/black/server/src/lib/Exceptions.ts | 88 ---- .../server/src/lib/FirstFactorValidator.ts | 20 - .../src/lib/IdentityCheckMiddleware.spec.ts | 176 -------- .../server/src/lib/IdentityCheckMiddleware.ts | 138 ------- .../lib/IdentityCheckPreValidationTemplate.ts | 3 - .../black/server/src/lib/IdentityValidable.ts | 19 - .../src/lib/IdentityValidableStub.spec.ts | 52 --- themes/black/server/src/lib/Server.spec.ts | 81 ---- themes/black/server/src/lib/Server.ts | 93 ----- .../black/server/src/lib/ServerVariables.ts | 21 - .../src/lib/ServerVariablesInitializer.ts | 116 ------ .../lib/ServerVariablesMockBuilder.spec.ts | 87 ---- .../server/src/lib/authentication/Level.ts | 5 - .../backends/GroupsAndEmails.ts | 5 - .../authentication/backends/IUsersDatabase.ts | 10 - .../backends/IUsersDatabaseStub.spec.ts | 35 -- .../backends/file/FileUsersDatabase.spec.ts | 224 ---------- .../backends/file/FileUsersDatabase.ts | 182 --------- .../backends/file/ReadWriteQueue.ts | 60 --- .../authentication/backends/ldap/ISession.ts | 12 - .../backends/ldap/ISessionFactory.ts | 6 - .../backends/ldap/LdapUsersDatabase.spec.ts | 386 ------------------ .../backends/ldap/LdapUsersDatabase.ts | 107 ----- .../backends/ldap/SafeSession.spec.ts | 76 ---- .../backends/ldap/SafeSession.ts | 62 --- .../backends/ldap/Sanitizer.spec.ts | 25 -- .../authentication/backends/ldap/Sanitizer.ts | 25 -- .../backends/ldap/Session.spec.ts | 127 ------ .../authentication/backends/ldap/Session.ts | 156 ------- .../backends/ldap/SessionFactory.ts | 37 -- .../backends/ldap/SessionFactoryStub.spec.ts | 16 - .../backends/ldap/SessionStub.spec.ts | 46 --- .../backends/ldap/connector/Connector.ts | 69 ---- .../ldap/connector/ConnectorFactory.ts | 18 - .../connector/ConnectorFactoryStub.spec.ts | 17 - .../ldap/connector/ConnectorStub.spec.ts | 34 -- .../backends/ldap/connector/IConnector.ts | 9 - .../ldap/connector/IConnectorFactory.ts | 5 - .../lib/authentication/totp/ITotpHandler.ts | 6 - .../authentication/totp/TotpHandler.spec.ts | 39 -- .../lib/authentication/totp/TotpHandler.ts | 36 -- .../totp/TotpHandlerStub.spec.ts | 22 - .../src/lib/authentication/u2f/IU2fHandler.ts | 9 - .../src/lib/authentication/u2f/U2fHandler.ts | 24 -- .../authentication/u2f/U2fHandlerStub.spec.ts | 31 -- .../src/lib/authorization/Authorizer.spec.ts | 372 ----------------- .../src/lib/authorization/Authorizer.ts | 85 ---- .../lib/authorization/AuthorizerStub.spec.ts | 17 - .../src/lib/authorization/IAuthorizer.ts | 7 - .../server/src/lib/authorization/Level.ts | 6 - .../authorization/MultipleDomainMatcher.ts | 12 - .../server/src/lib/authorization/Object.ts | 5 - .../server/src/lib/authorization/Subject.ts | 5 - .../configuration/ConfigurationParser.spec.ts | 171 -------- .../lib/configuration/ConfigurationParser.ts | 39 -- .../SessionConfigurationBuilder.spec.ts | 149 ------- .../SessionConfigurationBuilder.ts | 52 --- .../schema/AclConfiguration.spec.ts | 34 -- .../configuration/schema/AclConfiguration.ts | 41 -- ...AuthenticationBackendConfiguration.spec.ts | 11 - .../AuthenticationBackendConfiguration.ts | 25 -- .../lib/configuration/schema/Configuration.ts | 68 --- .../schema/FileUsersDatabaseConfiguration.ts | 4 - .../schema/LdapConfiguration.spec.ts | 25 -- .../configuration/schema/LdapConfiguration.ts | 40 -- .../schema/NotifierConfiguration.spec.ts | 40 -- .../schema/NotifierConfiguration.ts | 45 -- .../schema/RegulationConfiguration.spec.ts | 13 - .../schema/RegulationConfiguration.ts | 23 -- .../schema/SessionConfiguration.spec.ts | 16 - .../schema/SessionConfiguration.ts | 32 -- .../schema/StorageConfiguration.spec.ts | 15 - .../schema/StorageConfiguration.ts | 30 -- .../configuration/schema/TotpConfiguration.ts | 13 - .../schema/UserDatabaseConfiguration.ts | 9 - .../lib/connectors/mongo/IMongoClient.d.ts | 6 - .../lib/connectors/mongo/MongoClient.spec.ts | 119 ------ .../src/lib/connectors/mongo/MongoClient.ts | 76 ---- .../connectors/mongo/MongoClientStub.spec.ts | 16 - .../server/src/lib/logging/GlobalLogger.ts | 34 -- .../src/lib/logging/GlobalLoggerStub.spec.ts | 38 -- .../server/src/lib/logging/IGlobalLogger.ts | 5 - .../server/src/lib/logging/IRequestLogger.ts | 7 - .../server/src/lib/logging/RequestLogger.ts | 45 -- .../src/lib/logging/RequestLoggerStub.spec.ts | 38 -- .../lib/notifiers/AbstractEmailNotifier.ts | 23 -- .../src/lib/notifiers/EmailNotifier.spec.ts | 54 --- .../server/src/lib/notifiers/EmailNotifier.ts | 27 -- .../src/lib/notifiers/FileSystemNotifier.ts | 22 - .../server/src/lib/notifiers/IMailSender.ts | 6 - .../src/lib/notifiers/IMailSenderBuilder.ts | 7 - .../server/src/lib/notifiers/INotifier.ts | 5 - .../server/src/lib/notifiers/MailSender.ts | 42 -- .../lib/notifiers/MailSenderBuilder.spec.ts | 67 --- .../src/lib/notifiers/MailSenderBuilder.ts | 42 -- .../notifiers/MailSenderBuilderStub.spec.ts | 25 -- .../src/lib/notifiers/MailSenderStub.spec.ts | 16 - .../src/lib/notifiers/NotifierFactory.spec.ts | 42 -- .../src/lib/notifiers/NotifierFactory.ts | 33 -- .../src/lib/notifiers/NotifierStub.spec.ts | 16 - .../server/src/lib/notifiers/SmtpNotifier.ts | 30 -- .../server/src/lib/regulation/IRegulator.ts | 6 - .../src/lib/regulation/Regulator.spec.ts | 186 --------- .../server/src/lib/regulation/Regulator.ts | 55 --- .../src/lib/regulation/RegulatorStub.spec.ts | 22 - .../src/lib/routes/error/401/get.spec.ts | 61 --- .../server/src/lib/routes/error/401/get.ts | 15 - .../src/lib/routes/error/403/get.spec.ts | 61 --- .../server/src/lib/routes/error/403/get.ts | 15 - .../src/lib/routes/error/404/get.spec.ts | 19 - .../server/src/lib/routes/error/404/get.ts | 8 - .../server/src/lib/routes/error/redirector.ts | 13 - .../server/src/lib/routes/firstfactor/get.ts | 72 ---- .../src/lib/routes/firstfactor/post.spec.ts | 136 ------ .../server/src/lib/routes/firstfactor/post.ts | 101 ----- .../server/src/lib/routes/loggedin/get.ts | 23 -- .../black/server/src/lib/routes/logout/get.ts | 20 - .../lib/routes/password-reset/constants.ts | 2 - .../routes/password-reset/form/post.spec.ts | 122 ------ .../lib/routes/password-reset/form/post.ts | 50 --- .../identity/PasswordResetHandler.spec.ts | 92 ----- .../identity/PasswordResetHandler.ts | 69 ---- .../lib/routes/password-reset/request/get.ts | 13 - .../src/lib/routes/secondfactor/get.spec.ts | 44 -- .../server/src/lib/routes/secondfactor/get.ts | 28 -- .../lib/routes/secondfactor/redirect.spec.ts | 41 -- .../src/lib/routes/secondfactor/redirect.ts | 30 -- .../lib/routes/secondfactor/totp/constants.ts | 4 - .../totp/identity/RegistrationHandler.spec.ts | 116 ------ .../totp/identity/RegistrationHandler.ts | 112 ----- .../secondfactor/totp/sign/post.spec.ts | 76 ---- .../lib/routes/secondfactor/totp/sign/post.ts | 42 -- .../lib/routes/secondfactor/u2f/U2FCommon.ts | 11 - .../u2f/identity/RegistrationHandler.spec.ts | 96 ----- .../u2f/identity/RegistrationHandler.ts | 73 ---- .../secondfactor/u2f/register/post.spec.ts | 146 ------- .../routes/secondfactor/u2f/register/post.ts | 64 --- .../u2f/register_request/get.spec.ts | 86 ---- .../secondfactor/u2f/register_request/get.ts | 43 -- .../routes/secondfactor/u2f/sign/post.spec.ts | 101 ----- .../lib/routes/secondfactor/u2f/sign/post.ts | 57 --- .../secondfactor/u2f/sign_request/get.spec.ts | 68 --- .../secondfactor/u2f/sign_request/get.ts | 42 -- .../src/lib/routes/verify/access_control.ts | 51 --- .../server/src/lib/routes/verify/get.spec.ts | 320 --------------- .../black/server/src/lib/routes/verify/get.ts | 91 ----- .../src/lib/routes/verify/get_basic_auth.ts | 55 --- .../lib/routes/verify/get_session_cookie.ts | 78 ---- .../storage/AuthenticationTraceDocument.d.ts | 6 - .../lib/storage/CollectionFactoryFactory.ts | 15 - .../lib/storage/CollectionFactoryStub.spec.ts | 16 - .../src/lib/storage/CollectionStub.spec.ts | 39 -- .../server/src/lib/storage/ICollection.d.ts | 11 - .../src/lib/storage/ICollectionFactory.d.ts | 6 - .../src/lib/storage/IUserDataStore.d.ts | 21 - .../storage/IdentityValidationDocument.d.ts | 7 - .../src/lib/storage/TOTPSecretDocument.d.ts | 6 - .../lib/storage/U2FRegistrationDocument.d.ts | 8 - .../src/lib/storage/UserDataStore.spec.ts | 264 ------------ .../server/src/lib/storage/UserDataStore.ts | 143 ------- .../src/lib/storage/UserDataStoreStub.spec.ts | 64 --- .../lib/storage/mongo/MongoCollection.spec.ts | 110 ----- .../src/lib/storage/mongo/MongoCollection.ts | 50 --- .../mongo/MongoCollectionFactory.spec.ts | 21 - .../storage/mongo/MongoCollectionFactory.ts | 19 - .../lib/storage/nedb/NedbCollection.spec.ts | 136 ------ .../src/lib/storage/nedb/NedbCollection.ts | 47 --- .../nedb/NedbCollectionFactory.spec.ts | 16 - .../lib/storage/nedb/NedbCollectionFactory.ts | 28 -- .../server/src/lib/stubs/express.spec.ts | 103 ----- .../black/server/src/lib/stubs/ldapjs.spec.ts | 50 --- .../server/src/lib/stubs/speakeasy.spec.ts | 7 - themes/black/server/src/lib/stubs/u2f.spec.ts | 16 - .../src/lib/utils/HashGenerator.spec.ts | 18 - .../server/src/lib/utils/HashGenerator.ts | 23 -- .../server/src/lib/utils/ObjectCloner.ts | 6 - .../src/lib/utils/SafeRedirection.spec.ts | 33 -- .../server/src/lib/utils/SafeRedirection.ts | 22 - .../src/lib/utils/URLDecomposer.spec.ts | 46 --- .../server/src/lib/utils/URLDecomposer.ts | 15 - .../server/src/lib/web_server/Configurator.ts | 47 --- .../server/src/lib/web_server/RestApi.ts | 125 ------ .../RequireValidatedFirstFactor.ts | 27 -- .../middlewares/WithHeadersLogged.ts | 12 - .../server/src/resources/email-template.ejs | 0 .../server/src/views/already-logged-in.pug | 0 .../black/server/src/views/errors/.directory | 0 themes/black/server/src/views/errors/401.pug | 0 themes/black/server/src/views/errors/403.pug | 0 themes/black/server/src/views/errors/404.pug | 0 themes/black/server/src/views/firstfactor.pug | 0 .../black/server/src/views/layout/layout.pug | 0 .../src/views/need-identity-validation.pug | 0 .../server/src/views/password-reset-form.pug | 0 .../src/views/password-reset-request.pug | 0 .../black/server/src/views/secondfactor.pug | 0 .../black/server/src/views/totp-register.pug | 0 .../black/server/src/views/u2f-register.pug | 0 themes/black/server/test/requests.ts | 94 ----- themes/black/server/tsconfig.json | 21 - themes/black/server/tslint.json | 60 --- themes/black/server/types/.directory | 4 - .../server/types/AuthenticationSession.ts | 18 - themes/black/server/types/Dependencies.ts | 29 -- themes/black/server/types/Identity.ts | 6 - themes/black/server/types/TOTPSecret.ts | 11 - themes/black/server/types/U2FRegistration.ts | 5 - themes/black/server/types/dovehash.d.ts | 4 - themes/black/server/types/speakeasy.d.ts | 96 ----- .../client/src/css/.directory | 0 .../client/src/css/00-bootstrap.min.css | 0 .../client/src/css/01-main.css | 0 .../client/src/css/02-login.css | 0 .../client/src/css/03-errors.css | 0 .../client/src/css/03-password-reset-form.css | 0 .../src/css/03-password-reset-request.css | 0 .../client/src/css/03-totp-register.css | 0 .../client/src/css/03-u2f-register.css | 0 .../client/src/img/background.svg | 0 .../{main => default}/client/src/img/icon.png | Bin .../{main => default}/client/src/img/mail.png | Bin .../client/src/img/notifications/.directory | 0 .../client/src/img/notifications/error.png | Bin .../client/src/img/notifications/info.png | Bin .../client/src/img/notifications/success.png | Bin .../client/src/img/notifications/warning.png | Bin .../client/src/img/padlock.png | Bin .../client/src/img/password.png | Bin .../client/src/img/pendrive.png | Bin .../client/src/img/stores/.directory | 0 .../src/img/stores/applestore-badge.svg | 0 .../src/img/stores/googleplay-badge.svg | 0 .../client/src/img/success.png | Bin .../{main => default}/client/src/img/user.png | Bin .../client/src/img/warning.png | Bin .../client/src/thirdparties/qrcode.min.js | 0 themes/{main => default}/server/.directory | 0 .../server/src/resources/email-template.ejs | 0 .../server/src/views/already-logged-in.pug | 0 .../server/src/views/errors/.directory | 0 .../server/src/views/errors/401.pug | 0 .../server/src/views/errors/403.pug | 0 .../server/src/views/errors/404.pug | 0 .../server/src/views/firstfactor.pug | 0 .../server/src/views/layout/layout.pug | 0 .../src/views/need-identity-validation.pug | 0 .../server/src/views/password-reset-form.pug | 0 .../src/views/password-reset-request.pug | 0 .../server/src/views/secondfactor.pug | 0 .../server/src/views/totp-register.pug | 0 .../server/src/views/u2f-register.pug | 0 themes/main/client/src/index.ts | 34 -- themes/main/client/src/lib/GetPromised.ts | 14 - themes/main/client/src/lib/INotifier.ts | 14 - themes/main/client/src/lib/Notifier.ts | 83 ---- .../src/lib/QueryParametersRetriever.ts | 12 - themes/main/client/src/lib/SafeRedirect.ts | 10 - .../lib/firstfactor/FirstFactorValidator.ts | 46 --- .../client/src/lib/firstfactor/UISelectors.ts | 5 - .../main/client/src/lib/firstfactor/index.ts | 49 --- .../src/lib/reset-password/constants.ts | 2 - .../lib/reset-password/reset-password-form.ts | 57 --- .../reset-password/reset-password-request.ts | 56 --- .../src/lib/secondfactor/TOTPValidator.ts | 28 -- .../src/lib/secondfactor/U2FValidator.ts | 42 -- .../client/src/lib/secondfactor/constants.ts | 3 - .../main/client/src/lib/secondfactor/index.ts | 59 --- .../src/lib/totp-register/totp-register.ts | 11 - .../src/lib/totp-register/ui-selector.ts | 2 - .../src/lib/u2f-register/u2f-register.ts | 56 --- themes/main/client/test/Notifier.test.ts | 71 ---- .../firstfactor/FirstFactorValidator.test.ts | 46 --- themes/main/client/test/mocks/NotifierStub.ts | 33 -- themes/main/client/test/mocks/jquery.ts | 59 --- themes/main/client/test/mocks/u2f-api.ts | 14 - .../test/secondfactor/TOTPValidator.test.ts | 37 -- .../test/totp-register/totp-register.test.ts | 31 -- themes/main/client/tsconfig.json | 24 -- themes/main/client/tslint.json | 60 --- themes/main/server/src/index.ts | 28 -- themes/main/server/src/lib/.directory | 4 - .../src/lib/AuthenticationSessionHandler.ts | 45 -- themes/main/server/src/lib/ErrorReplies.ts | 49 --- themes/main/server/src/lib/Exceptions.ts | 88 ---- .../server/src/lib/FirstFactorValidator.ts | 20 - .../src/lib/IdentityCheckMiddleware.spec.ts | 176 -------- .../server/src/lib/IdentityCheckMiddleware.ts | 138 ------- .../lib/IdentityCheckPreValidationTemplate.ts | 3 - .../main/server/src/lib/IdentityValidable.ts | 19 - .../src/lib/IdentityValidableStub.spec.ts | 52 --- themes/main/server/src/lib/Server.spec.ts | 81 ---- themes/main/server/src/lib/Server.ts | 93 ----- themes/main/server/src/lib/ServerVariables.ts | 21 - .../src/lib/ServerVariablesInitializer.ts | 116 ------ .../lib/ServerVariablesMockBuilder.spec.ts | 87 ---- .../server/src/lib/authentication/Level.ts | 5 - .../backends/GroupsAndEmails.ts | 5 - .../authentication/backends/IUsersDatabase.ts | 10 - .../backends/IUsersDatabaseStub.spec.ts | 35 -- .../backends/file/FileUsersDatabase.spec.ts | 224 ---------- .../backends/file/FileUsersDatabase.ts | 182 --------- .../backends/file/ReadWriteQueue.ts | 60 --- .../authentication/backends/ldap/ISession.ts | 12 - .../backends/ldap/ISessionFactory.ts | 6 - .../backends/ldap/LdapUsersDatabase.spec.ts | 386 ------------------ .../backends/ldap/LdapUsersDatabase.ts | 107 ----- .../backends/ldap/SafeSession.spec.ts | 76 ---- .../backends/ldap/SafeSession.ts | 62 --- .../backends/ldap/Sanitizer.spec.ts | 25 -- .../authentication/backends/ldap/Sanitizer.ts | 25 -- .../backends/ldap/Session.spec.ts | 127 ------ .../authentication/backends/ldap/Session.ts | 156 ------- .../backends/ldap/SessionFactory.ts | 37 -- .../backends/ldap/SessionFactoryStub.spec.ts | 16 - .../backends/ldap/SessionStub.spec.ts | 46 --- .../backends/ldap/connector/Connector.ts | 69 ---- .../ldap/connector/ConnectorFactory.ts | 18 - .../connector/ConnectorFactoryStub.spec.ts | 17 - .../ldap/connector/ConnectorStub.spec.ts | 34 -- .../backends/ldap/connector/IConnector.ts | 9 - .../ldap/connector/IConnectorFactory.ts | 5 - .../lib/authentication/totp/ITotpHandler.ts | 6 - .../authentication/totp/TotpHandler.spec.ts | 39 -- .../lib/authentication/totp/TotpHandler.ts | 36 -- .../totp/TotpHandlerStub.spec.ts | 22 - .../src/lib/authentication/u2f/IU2fHandler.ts | 9 - .../src/lib/authentication/u2f/U2fHandler.ts | 24 -- .../authentication/u2f/U2fHandlerStub.spec.ts | 31 -- .../src/lib/authorization/Authorizer.spec.ts | 372 ----------------- .../src/lib/authorization/Authorizer.ts | 85 ---- .../lib/authorization/AuthorizerStub.spec.ts | 17 - .../src/lib/authorization/IAuthorizer.ts | 7 - .../server/src/lib/authorization/Level.ts | 6 - .../authorization/MultipleDomainMatcher.ts | 12 - .../server/src/lib/authorization/Object.ts | 5 - .../server/src/lib/authorization/Subject.ts | 5 - .../configuration/ConfigurationParser.spec.ts | 171 -------- .../lib/configuration/ConfigurationParser.ts | 39 -- .../SessionConfigurationBuilder.spec.ts | 149 ------- .../SessionConfigurationBuilder.ts | 52 --- .../schema/AclConfiguration.spec.ts | 34 -- .../configuration/schema/AclConfiguration.ts | 41 -- ...AuthenticationBackendConfiguration.spec.ts | 11 - .../AuthenticationBackendConfiguration.ts | 25 -- .../lib/configuration/schema/Configuration.ts | 68 --- .../schema/FileUsersDatabaseConfiguration.ts | 4 - .../schema/LdapConfiguration.spec.ts | 25 -- .../configuration/schema/LdapConfiguration.ts | 40 -- .../schema/NotifierConfiguration.spec.ts | 40 -- .../schema/NotifierConfiguration.ts | 45 -- .../schema/RegulationConfiguration.spec.ts | 13 - .../schema/RegulationConfiguration.ts | 23 -- .../schema/SessionConfiguration.spec.ts | 16 - .../schema/SessionConfiguration.ts | 32 -- .../schema/StorageConfiguration.spec.ts | 15 - .../schema/StorageConfiguration.ts | 30 -- .../configuration/schema/TotpConfiguration.ts | 13 - .../schema/UserDatabaseConfiguration.ts | 9 - .../lib/connectors/mongo/IMongoClient.d.ts | 6 - .../lib/connectors/mongo/MongoClient.spec.ts | 119 ------ .../src/lib/connectors/mongo/MongoClient.ts | 76 ---- .../connectors/mongo/MongoClientStub.spec.ts | 16 - .../server/src/lib/logging/GlobalLogger.ts | 34 -- .../src/lib/logging/GlobalLoggerStub.spec.ts | 38 -- .../server/src/lib/logging/IGlobalLogger.ts | 5 - .../server/src/lib/logging/IRequestLogger.ts | 7 - .../server/src/lib/logging/RequestLogger.ts | 45 -- .../src/lib/logging/RequestLoggerStub.spec.ts | 38 -- .../lib/notifiers/AbstractEmailNotifier.ts | 23 -- .../src/lib/notifiers/EmailNotifier.spec.ts | 54 --- .../server/src/lib/notifiers/EmailNotifier.ts | 27 -- .../src/lib/notifiers/FileSystemNotifier.ts | 22 - .../server/src/lib/notifiers/IMailSender.ts | 6 - .../src/lib/notifiers/IMailSenderBuilder.ts | 7 - .../server/src/lib/notifiers/INotifier.ts | 5 - .../server/src/lib/notifiers/MailSender.ts | 42 -- .../lib/notifiers/MailSenderBuilder.spec.ts | 67 --- .../src/lib/notifiers/MailSenderBuilder.ts | 42 -- .../notifiers/MailSenderBuilderStub.spec.ts | 25 -- .../src/lib/notifiers/MailSenderStub.spec.ts | 16 - .../src/lib/notifiers/NotifierFactory.spec.ts | 42 -- .../src/lib/notifiers/NotifierFactory.ts | 33 -- .../src/lib/notifiers/NotifierStub.spec.ts | 16 - .../server/src/lib/notifiers/SmtpNotifier.ts | 30 -- .../server/src/lib/regulation/IRegulator.ts | 6 - .../src/lib/regulation/Regulator.spec.ts | 186 --------- .../server/src/lib/regulation/Regulator.ts | 55 --- .../src/lib/regulation/RegulatorStub.spec.ts | 22 - .../src/lib/routes/error/401/get.spec.ts | 61 --- .../server/src/lib/routes/error/401/get.ts | 15 - .../src/lib/routes/error/403/get.spec.ts | 61 --- .../server/src/lib/routes/error/403/get.ts | 15 - .../src/lib/routes/error/404/get.spec.ts | 19 - .../server/src/lib/routes/error/404/get.ts | 8 - .../server/src/lib/routes/error/redirector.ts | 13 - .../server/src/lib/routes/firstfactor/get.ts | 72 ---- .../src/lib/routes/firstfactor/post.spec.ts | 136 ------ .../server/src/lib/routes/firstfactor/post.ts | 101 ----- .../server/src/lib/routes/loggedin/get.ts | 23 -- .../main/server/src/lib/routes/logout/get.ts | 20 - .../lib/routes/password-reset/constants.ts | 2 - .../routes/password-reset/form/post.spec.ts | 122 ------ .../lib/routes/password-reset/form/post.ts | 50 --- .../identity/PasswordResetHandler.spec.ts | 92 ----- .../identity/PasswordResetHandler.ts | 69 ---- .../lib/routes/password-reset/request/get.ts | 13 - .../src/lib/routes/secondfactor/get.spec.ts | 44 -- .../server/src/lib/routes/secondfactor/get.ts | 28 -- .../lib/routes/secondfactor/redirect.spec.ts | 41 -- .../src/lib/routes/secondfactor/redirect.ts | 30 -- .../lib/routes/secondfactor/totp/constants.ts | 4 - .../totp/identity/RegistrationHandler.spec.ts | 116 ------ .../totp/identity/RegistrationHandler.ts | 112 ----- .../secondfactor/totp/sign/post.spec.ts | 76 ---- .../lib/routes/secondfactor/totp/sign/post.ts | 42 -- .../lib/routes/secondfactor/u2f/U2FCommon.ts | 11 - .../u2f/identity/RegistrationHandler.spec.ts | 96 ----- .../u2f/identity/RegistrationHandler.ts | 73 ---- .../secondfactor/u2f/register/post.spec.ts | 146 ------- .../routes/secondfactor/u2f/register/post.ts | 64 --- .../u2f/register_request/get.spec.ts | 86 ---- .../secondfactor/u2f/register_request/get.ts | 43 -- .../routes/secondfactor/u2f/sign/post.spec.ts | 101 ----- .../lib/routes/secondfactor/u2f/sign/post.ts | 57 --- .../secondfactor/u2f/sign_request/get.spec.ts | 68 --- .../secondfactor/u2f/sign_request/get.ts | 42 -- .../src/lib/routes/verify/access_control.ts | 51 --- .../server/src/lib/routes/verify/get.spec.ts | 320 --------------- .../main/server/src/lib/routes/verify/get.ts | 91 ----- .../src/lib/routes/verify/get_basic_auth.ts | 55 --- .../lib/routes/verify/get_session_cookie.ts | 78 ---- .../storage/AuthenticationTraceDocument.d.ts | 6 - .../lib/storage/CollectionFactoryFactory.ts | 15 - .../lib/storage/CollectionFactoryStub.spec.ts | 16 - .../src/lib/storage/CollectionStub.spec.ts | 39 -- .../server/src/lib/storage/ICollection.d.ts | 11 - .../src/lib/storage/ICollectionFactory.d.ts | 6 - .../src/lib/storage/IUserDataStore.d.ts | 21 - .../storage/IdentityValidationDocument.d.ts | 7 - .../src/lib/storage/TOTPSecretDocument.d.ts | 6 - .../lib/storage/U2FRegistrationDocument.d.ts | 8 - .../src/lib/storage/UserDataStore.spec.ts | 264 ------------ .../server/src/lib/storage/UserDataStore.ts | 143 ------- .../src/lib/storage/UserDataStoreStub.spec.ts | 64 --- .../lib/storage/mongo/MongoCollection.spec.ts | 110 ----- .../src/lib/storage/mongo/MongoCollection.ts | 50 --- .../mongo/MongoCollectionFactory.spec.ts | 21 - .../storage/mongo/MongoCollectionFactory.ts | 19 - .../lib/storage/nedb/NedbCollection.spec.ts | 136 ------ .../src/lib/storage/nedb/NedbCollection.ts | 47 --- .../nedb/NedbCollectionFactory.spec.ts | 16 - .../lib/storage/nedb/NedbCollectionFactory.ts | 28 -- .../main/server/src/lib/stubs/express.spec.ts | 103 ----- .../main/server/src/lib/stubs/ldapjs.spec.ts | 50 --- .../server/src/lib/stubs/speakeasy.spec.ts | 7 - themes/main/server/src/lib/stubs/u2f.spec.ts | 16 - .../src/lib/utils/HashGenerator.spec.ts | 18 - .../server/src/lib/utils/HashGenerator.ts | 23 -- .../main/server/src/lib/utils/ObjectCloner.ts | 6 - .../src/lib/utils/SafeRedirection.spec.ts | 33 -- .../server/src/lib/utils/SafeRedirection.ts | 22 - .../src/lib/utils/URLDecomposer.spec.ts | 46 --- .../server/src/lib/utils/URLDecomposer.ts | 15 - .../server/src/lib/web_server/Configurator.ts | 47 --- .../main/server/src/lib/web_server/RestApi.ts | 125 ------ .../RequireValidatedFirstFactor.ts | 27 -- .../middlewares/WithHeadersLogged.ts | 12 - themes/main/server/test/requests.ts | 94 ----- themes/main/server/tsconfig.json | 21 - themes/main/server/tslint.json | 60 --- themes/main/server/types/.directory | 4 - .../server/types/AuthenticationSession.ts | 18 - themes/main/server/types/Dependencies.ts | 29 -- themes/main/server/types/Identity.ts | 6 - themes/main/server/types/TOTPSecret.ts | 11 - themes/main/server/types/U2FRegistration.ts | 5 - themes/main/server/types/dovehash.d.ts | 4 - themes/main/server/types/speakeasy.d.ts | 96 ----- themes/matrix/client/src/css/.directory | 0 .../client/src/css/00-bootstrap.min.css | 0 themes/matrix/client/src/css/01-main.css | 2 +- themes/matrix/client/src/css/02-login.css | 0 themes/matrix/client/src/css/03-errors.css | 0 .../client/src/css/03-password-reset-form.css | 0 .../src/css/03-password-reset-request.css | 0 .../client/src/css/03-totp-register.css | 0 .../matrix/client/src/css/03-u2f-register.css | 0 themes/matrix/client/src/img/background.jpg | Bin themes/matrix/client/src/img/icon.png | Bin themes/matrix/client/src/img/mail.png | Bin .../client/src/img/matrix_circle_128x128.png | Bin .../client/src/img/notifications/.directory | 0 .../client/src/img/notifications/error.png | Bin .../client/src/img/notifications/info.png | Bin .../client/src/img/notifications/success.png | Bin .../client/src/img/notifications/warning.png | Bin themes/matrix/client/src/img/padlock.png | Bin .../matrix/client/src/img/password_white.png | Bin themes/matrix/client/src/img/pendrive.png | Bin .../matrix/client/src/img/stores/.directory | 0 .../src/img/stores/applestore-badge.svg | 0 .../src/img/stores/googleplay-badge.svg | 0 themes/matrix/client/src/img/success.png | Bin themes/matrix/client/src/img/user.png | Bin themes/matrix/client/src/img/warning.png | Bin themes/matrix/client/src/index.ts | 34 -- themes/matrix/client/src/lib/GetPromised.ts | 14 - themes/matrix/client/src/lib/INotifier.ts | 14 - themes/matrix/client/src/lib/Notifier.ts | 83 ---- .../src/lib/QueryParametersRetriever.ts | 12 - themes/matrix/client/src/lib/SafeRedirect.ts | 10 - .../lib/firstfactor/FirstFactorValidator.ts | 46 --- .../client/src/lib/firstfactor/UISelectors.ts | 5 - .../client/src/lib/firstfactor/index.ts | 49 --- .../src/lib/reset-password/constants.ts | 2 - .../lib/reset-password/reset-password-form.ts | 57 --- .../reset-password/reset-password-request.ts | 56 --- .../src/lib/secondfactor/TOTPValidator.ts | 28 -- .../src/lib/secondfactor/U2FValidator.ts | 42 -- .../client/src/lib/secondfactor/constants.ts | 3 - .../client/src/lib/secondfactor/index.ts | 59 --- .../src/lib/totp-register/totp-register.ts | 11 - .../src/lib/totp-register/ui-selector.ts | 2 - .../src/lib/u2f-register/u2f-register.ts | 56 --- .../matrix/client/src/thirdparties/matrix.js | 0 .../client/src/thirdparties/qrcode.min.js | 0 .../matrix/client/src/thirdparties/u2f-api.js | 0 themes/matrix/client/test/Notifier.test.ts | 71 ---- .../firstfactor/FirstFactorValidator.test.ts | 44 -- .../matrix/client/test/mocks/NotifierStub.ts | 33 -- themes/matrix/client/test/mocks/jquery.ts | 59 --- themes/matrix/client/test/mocks/u2f-api.ts | 14 - .../test/secondfactor/TOTPValidator.test.ts | 37 -- .../test/totp-register/totp-register.test.ts | 31 -- themes/matrix/client/tsconfig.json | 24 -- themes/matrix/client/tslint.json | 60 --- themes/matrix/server/.directory | 0 themes/matrix/server/src/index.ts | 28 -- themes/matrix/server/src/lib/.directory | 4 - .../src/lib/AuthenticationSessionHandler.ts | 45 -- themes/matrix/server/src/lib/ErrorReplies.ts | 49 --- themes/matrix/server/src/lib/Exceptions.ts | 88 ---- .../server/src/lib/FirstFactorValidator.ts | 20 - .../src/lib/IdentityCheckMiddleware.spec.ts | 176 -------- .../server/src/lib/IdentityCheckMiddleware.ts | 138 ------- .../lib/IdentityCheckPreValidationTemplate.ts | 3 - .../server/src/lib/IdentityValidable.ts | 19 - .../src/lib/IdentityValidableStub.spec.ts | 52 --- themes/matrix/server/src/lib/Server.spec.ts | 81 ---- themes/matrix/server/src/lib/Server.ts | 93 ----- .../matrix/server/src/lib/ServerVariables.ts | 21 - .../src/lib/ServerVariablesInitializer.ts | 116 ------ .../lib/ServerVariablesMockBuilder.spec.ts | 87 ---- .../server/src/lib/authentication/Level.ts | 5 - .../backends/GroupsAndEmails.ts | 5 - .../authentication/backends/IUsersDatabase.ts | 10 - .../backends/IUsersDatabaseStub.spec.ts | 35 -- .../backends/file/FileUsersDatabase.spec.ts | 224 ---------- .../backends/file/FileUsersDatabase.ts | 182 --------- .../backends/file/ReadWriteQueue.ts | 60 --- .../authentication/backends/ldap/ISession.ts | 12 - .../backends/ldap/ISessionFactory.ts | 6 - .../backends/ldap/LdapUsersDatabase.spec.ts | 386 ------------------ .../backends/ldap/LdapUsersDatabase.ts | 107 ----- .../backends/ldap/SafeSession.spec.ts | 76 ---- .../backends/ldap/SafeSession.ts | 62 --- .../backends/ldap/Sanitizer.spec.ts | 25 -- .../authentication/backends/ldap/Sanitizer.ts | 25 -- .../backends/ldap/Session.spec.ts | 127 ------ .../authentication/backends/ldap/Session.ts | 156 ------- .../backends/ldap/SessionFactory.ts | 37 -- .../backends/ldap/SessionFactoryStub.spec.ts | 16 - .../backends/ldap/SessionStub.spec.ts | 46 --- .../backends/ldap/connector/Connector.ts | 69 ---- .../ldap/connector/ConnectorFactory.ts | 18 - .../connector/ConnectorFactoryStub.spec.ts | 17 - .../ldap/connector/ConnectorStub.spec.ts | 34 -- .../backends/ldap/connector/IConnector.ts | 9 - .../ldap/connector/IConnectorFactory.ts | 5 - .../lib/authentication/totp/ITotpHandler.ts | 6 - .../authentication/totp/TotpHandler.spec.ts | 39 -- .../lib/authentication/totp/TotpHandler.ts | 36 -- .../totp/TotpHandlerStub.spec.ts | 22 - .../src/lib/authentication/u2f/IU2fHandler.ts | 9 - .../src/lib/authentication/u2f/U2fHandler.ts | 24 -- .../authentication/u2f/U2fHandlerStub.spec.ts | 31 -- .../src/lib/authorization/Authorizer.spec.ts | 372 ----------------- .../src/lib/authorization/Authorizer.ts | 85 ---- .../lib/authorization/AuthorizerStub.spec.ts | 17 - .../src/lib/authorization/IAuthorizer.ts | 7 - .../server/src/lib/authorization/Level.ts | 6 - .../authorization/MultipleDomainMatcher.ts | 12 - .../server/src/lib/authorization/Object.ts | 5 - .../server/src/lib/authorization/Subject.ts | 5 - .../configuration/ConfigurationParser.spec.ts | 171 -------- .../lib/configuration/ConfigurationParser.ts | 39 -- .../SessionConfigurationBuilder.spec.ts | 149 ------- .../SessionConfigurationBuilder.ts | 52 --- .../schema/AclConfiguration.spec.ts | 34 -- .../configuration/schema/AclConfiguration.ts | 41 -- ...AuthenticationBackendConfiguration.spec.ts | 11 - .../AuthenticationBackendConfiguration.ts | 25 -- .../lib/configuration/schema/Configuration.ts | 68 --- .../schema/FileUsersDatabaseConfiguration.ts | 4 - .../schema/LdapConfiguration.spec.ts | 25 -- .../configuration/schema/LdapConfiguration.ts | 40 -- .../schema/NotifierConfiguration.spec.ts | 40 -- .../schema/NotifierConfiguration.ts | 45 -- .../schema/RegulationConfiguration.spec.ts | 13 - .../schema/RegulationConfiguration.ts | 23 -- .../schema/SessionConfiguration.spec.ts | 16 - .../schema/SessionConfiguration.ts | 32 -- .../schema/StorageConfiguration.spec.ts | 15 - .../schema/StorageConfiguration.ts | 30 -- .../configuration/schema/TotpConfiguration.ts | 13 - .../schema/UserDatabaseConfiguration.ts | 9 - .../lib/connectors/mongo/IMongoClient.d.ts | 6 - .../lib/connectors/mongo/MongoClient.spec.ts | 119 ------ .../src/lib/connectors/mongo/MongoClient.ts | 76 ---- .../connectors/mongo/MongoClientStub.spec.ts | 16 - .../server/src/lib/logging/GlobalLogger.ts | 34 -- .../src/lib/logging/GlobalLoggerStub.spec.ts | 38 -- .../server/src/lib/logging/IGlobalLogger.ts | 5 - .../server/src/lib/logging/IRequestLogger.ts | 7 - .../server/src/lib/logging/RequestLogger.ts | 45 -- .../src/lib/logging/RequestLoggerStub.spec.ts | 38 -- .../lib/notifiers/AbstractEmailNotifier.ts | 23 -- .../src/lib/notifiers/EmailNotifier.spec.ts | 54 --- .../server/src/lib/notifiers/EmailNotifier.ts | 27 -- .../src/lib/notifiers/FileSystemNotifier.ts | 22 - .../server/src/lib/notifiers/IMailSender.ts | 6 - .../src/lib/notifiers/IMailSenderBuilder.ts | 7 - .../server/src/lib/notifiers/INotifier.ts | 5 - .../server/src/lib/notifiers/MailSender.ts | 42 -- .../lib/notifiers/MailSenderBuilder.spec.ts | 67 --- .../src/lib/notifiers/MailSenderBuilder.ts | 42 -- .../notifiers/MailSenderBuilderStub.spec.ts | 25 -- .../src/lib/notifiers/MailSenderStub.spec.ts | 16 - .../src/lib/notifiers/NotifierFactory.spec.ts | 42 -- .../src/lib/notifiers/NotifierFactory.ts | 33 -- .../src/lib/notifiers/NotifierStub.spec.ts | 16 - .../server/src/lib/notifiers/SmtpNotifier.ts | 30 -- .../server/src/lib/regulation/IRegulator.ts | 6 - .../src/lib/regulation/Regulator.spec.ts | 186 --------- .../server/src/lib/regulation/Regulator.ts | 55 --- .../src/lib/regulation/RegulatorStub.spec.ts | 22 - .../src/lib/routes/error/401/get.spec.ts | 61 --- .../server/src/lib/routes/error/401/get.ts | 15 - .../src/lib/routes/error/403/get.spec.ts | 61 --- .../server/src/lib/routes/error/403/get.ts | 15 - .../src/lib/routes/error/404/get.spec.ts | 19 - .../server/src/lib/routes/error/404/get.ts | 8 - .../server/src/lib/routes/error/redirector.ts | 13 - .../server/src/lib/routes/firstfactor/get.ts | 72 ---- .../src/lib/routes/firstfactor/post.spec.ts | 136 ------ .../server/src/lib/routes/firstfactor/post.ts | 101 ----- .../server/src/lib/routes/loggedin/get.ts | 23 -- .../server/src/lib/routes/logout/get.ts | 20 - .../lib/routes/password-reset/constants.ts | 2 - .../routes/password-reset/form/post.spec.ts | 122 ------ .../lib/routes/password-reset/form/post.ts | 50 --- .../identity/PasswordResetHandler.spec.ts | 92 ----- .../identity/PasswordResetHandler.ts | 69 ---- .../lib/routes/password-reset/request/get.ts | 13 - .../src/lib/routes/secondfactor/get.spec.ts | 44 -- .../server/src/lib/routes/secondfactor/get.ts | 28 -- .../lib/routes/secondfactor/redirect.spec.ts | 41 -- .../src/lib/routes/secondfactor/redirect.ts | 30 -- .../lib/routes/secondfactor/totp/constants.ts | 4 - .../totp/identity/RegistrationHandler.spec.ts | 116 ------ .../totp/identity/RegistrationHandler.ts | 112 ----- .../secondfactor/totp/sign/post.spec.ts | 76 ---- .../lib/routes/secondfactor/totp/sign/post.ts | 42 -- .../lib/routes/secondfactor/u2f/U2FCommon.ts | 11 - .../u2f/identity/RegistrationHandler.spec.ts | 96 ----- .../u2f/identity/RegistrationHandler.ts | 73 ---- .../secondfactor/u2f/register/post.spec.ts | 146 ------- .../routes/secondfactor/u2f/register/post.ts | 64 --- .../u2f/register_request/get.spec.ts | 86 ---- .../secondfactor/u2f/register_request/get.ts | 43 -- .../routes/secondfactor/u2f/sign/post.spec.ts | 101 ----- .../lib/routes/secondfactor/u2f/sign/post.ts | 57 --- .../secondfactor/u2f/sign_request/get.spec.ts | 68 --- .../secondfactor/u2f/sign_request/get.ts | 42 -- .../src/lib/routes/verify/access_control.ts | 51 --- .../server/src/lib/routes/verify/get.spec.ts | 320 --------------- .../server/src/lib/routes/verify/get.ts | 91 ----- .../src/lib/routes/verify/get_basic_auth.ts | 55 --- .../lib/routes/verify/get_session_cookie.ts | 78 ---- .../storage/AuthenticationTraceDocument.d.ts | 6 - .../lib/storage/CollectionFactoryFactory.ts | 15 - .../lib/storage/CollectionFactoryStub.spec.ts | 16 - .../src/lib/storage/CollectionStub.spec.ts | 39 -- .../server/src/lib/storage/ICollection.d.ts | 11 - .../src/lib/storage/ICollectionFactory.d.ts | 6 - .../src/lib/storage/IUserDataStore.d.ts | 21 - .../storage/IdentityValidationDocument.d.ts | 7 - .../src/lib/storage/TOTPSecretDocument.d.ts | 6 - .../lib/storage/U2FRegistrationDocument.d.ts | 8 - .../src/lib/storage/UserDataStore.spec.ts | 264 ------------ .../server/src/lib/storage/UserDataStore.ts | 143 ------- .../src/lib/storage/UserDataStoreStub.spec.ts | 64 --- .../lib/storage/mongo/MongoCollection.spec.ts | 110 ----- .../src/lib/storage/mongo/MongoCollection.ts | 50 --- .../mongo/MongoCollectionFactory.spec.ts | 21 - .../storage/mongo/MongoCollectionFactory.ts | 19 - .../lib/storage/nedb/NedbCollection.spec.ts | 136 ------ .../src/lib/storage/nedb/NedbCollection.ts | 47 --- .../nedb/NedbCollectionFactory.spec.ts | 16 - .../lib/storage/nedb/NedbCollectionFactory.ts | 28 -- .../server/src/lib/stubs/express.spec.ts | 103 ----- .../server/src/lib/stubs/ldapjs.spec.ts | 50 --- .../server/src/lib/stubs/speakeasy.spec.ts | 7 - .../matrix/server/src/lib/stubs/u2f.spec.ts | 16 - .../src/lib/utils/HashGenerator.spec.ts | 18 - .../server/src/lib/utils/HashGenerator.ts | 23 -- .../server/src/lib/utils/ObjectCloner.ts | 6 - .../src/lib/utils/SafeRedirection.spec.ts | 33 -- .../server/src/lib/utils/SafeRedirection.ts | 22 - .../src/lib/utils/URLDecomposer.spec.ts | 46 --- .../server/src/lib/utils/URLDecomposer.ts | 15 - .../server/src/lib/web_server/Configurator.ts | 47 --- .../server/src/lib/web_server/RestApi.ts | 125 ------ .../RequireValidatedFirstFactor.ts | 27 -- .../middlewares/WithHeadersLogged.ts | 12 - .../server/src/resources/email-template.ejs | 0 .../server/src/views/already-logged-in.pug | 0 .../matrix/server/src/views/errors/.directory | 0 themes/matrix/server/src/views/errors/401.pug | 0 themes/matrix/server/src/views/errors/403.pug | 0 themes/matrix/server/src/views/errors/404.pug | 0 .../matrix/server/src/views/firstfactor.pug | 0 .../matrix/server/src/views/layout/layout.pug | 0 .../src/views/need-identity-validation.pug | 0 .../server/src/views/password-reset-form.pug | 0 .../src/views/password-reset-request.pug | 0 .../matrix/server/src/views/secondfactor.pug | 0 .../matrix/server/src/views/totp-register.pug | 0 .../matrix/server/src/views/u2f-register.pug | 0 themes/matrix/server/test/requests.ts | 94 ----- themes/matrix/server/tsconfig.json | 21 - themes/matrix/server/tslint.json | 60 --- themes/matrix/server/types/.directory | 4 - .../server/types/AuthenticationSession.ts | 18 - themes/matrix/server/types/Dependencies.ts | 29 -- themes/matrix/server/types/Identity.ts | 6 - themes/matrix/server/types/TOTPSecret.ts | 11 - themes/matrix/server/types/U2FRegistration.ts | 5 - themes/matrix/server/types/dovehash.d.ts | 4 - themes/matrix/server/types/speakeasy.d.ts | 96 ----- themes/squares/client/src/css/.directory | 0 .../client/src/css/00-bootstrap.min.css | 0 themes/squares/client/src/css/01-main.css | 0 themes/squares/client/src/css/02-login.css | 0 themes/squares/client/src/css/03-errors.css | 0 .../client/src/css/03-password-reset-form.css | 0 .../src/css/03-password-reset-request.css | 0 .../client/src/css/03-totp-register.css | 0 .../client/src/css/03-u2f-register.css | 0 .../squares/client/src/img/LargeTriangles.svg | 0 .../client/src/img/RandomizedPattern.svg | 0 themes/squares/client/src/img/background.jpg | Bin themes/squares/client/src/img/background.svg | 0 themes/squares/client/src/img/icon.png | Bin themes/squares/client/src/img/mail.png | Bin .../client/src/img/matrix_circle_128x128.png | Bin .../client/src/img/notifications/.directory | 0 .../client/src/img/notifications/error.png | Bin .../client/src/img/notifications/info.png | Bin .../client/src/img/notifications/success.png | Bin .../client/src/img/notifications/warning.png | Bin themes/squares/client/src/img/padlock.png | Bin .../squares/client/src/img/password_white.png | Bin themes/squares/client/src/img/pendrive.png | Bin themes/squares/client/src/img/sharingan.png | Bin .../squares/client/src/img/stores/.directory | 0 .../src/img/stores/applestore-badge.svg | 0 .../src/img/stores/googleplay-badge.svg | 0 themes/squares/client/src/img/success.png | Bin themes/squares/client/src/img/user.png | Bin themes/squares/client/src/img/warning.png | Bin themes/squares/client/src/index.ts | 34 -- themes/squares/client/src/lib/GetPromised.ts | 14 - themes/squares/client/src/lib/INotifier.ts | 14 - themes/squares/client/src/lib/Notifier.ts | 83 ---- .../src/lib/QueryParametersRetriever.ts | 12 - themes/squares/client/src/lib/SafeRedirect.ts | 10 - .../lib/firstfactor/FirstFactorValidator.ts | 46 --- .../client/src/lib/firstfactor/UISelectors.ts | 5 - .../client/src/lib/firstfactor/index.ts | 49 --- .../src/lib/reset-password/constants.ts | 2 - .../lib/reset-password/reset-password-form.ts | 57 --- .../reset-password/reset-password-request.ts | 56 --- .../src/lib/secondfactor/TOTPValidator.ts | 28 -- .../src/lib/secondfactor/U2FValidator.ts | 42 -- .../client/src/lib/secondfactor/constants.ts | 3 - .../client/src/lib/secondfactor/index.ts | 59 --- .../src/lib/totp-register/totp-register.ts | 11 - .../src/lib/totp-register/ui-selector.ts | 2 - .../src/lib/u2f-register/u2f-register.ts | 56 --- .../client/src/thirdparties/qrcode.min.js | 0 .../client/src/thirdparties/u2f-api.js | 0 themes/squares/client/test/Notifier.test.ts | 71 ---- .../firstfactor/FirstFactorValidator.test.ts | 44 -- .../squares/client/test/mocks/NotifierStub.ts | 33 -- themes/squares/client/test/mocks/jquery.ts | 59 --- themes/squares/client/test/mocks/u2f-api.ts | 14 - .../test/secondfactor/TOTPValidator.test.ts | 37 -- .../test/totp-register/totp-register.test.ts | 31 -- themes/squares/client/tsconfig.json | 24 -- themes/squares/client/tslint.json | 60 --- themes/squares/server/.directory | 0 themes/squares/server/src/index.ts | 28 -- themes/squares/server/src/lib/.directory | 4 - .../src/lib/AuthenticationSessionHandler.ts | 45 -- themes/squares/server/src/lib/ErrorReplies.ts | 49 --- themes/squares/server/src/lib/Exceptions.ts | 88 ---- .../server/src/lib/FirstFactorValidator.ts | 20 - .../src/lib/IdentityCheckMiddleware.spec.ts | 176 -------- .../server/src/lib/IdentityCheckMiddleware.ts | 138 ------- .../lib/IdentityCheckPreValidationTemplate.ts | 3 - .../server/src/lib/IdentityValidable.ts | 19 - .../src/lib/IdentityValidableStub.spec.ts | 52 --- themes/squares/server/src/lib/Server.spec.ts | 81 ---- themes/squares/server/src/lib/Server.ts | 93 ----- .../squares/server/src/lib/ServerVariables.ts | 21 - .../src/lib/ServerVariablesInitializer.ts | 116 ------ .../lib/ServerVariablesMockBuilder.spec.ts | 87 ---- .../server/src/lib/authentication/Level.ts | 5 - .../backends/GroupsAndEmails.ts | 5 - .../authentication/backends/IUsersDatabase.ts | 10 - .../backends/IUsersDatabaseStub.spec.ts | 35 -- .../backends/file/FileUsersDatabase.spec.ts | 224 ---------- .../backends/file/FileUsersDatabase.ts | 182 --------- .../backends/file/ReadWriteQueue.ts | 60 --- .../authentication/backends/ldap/ISession.ts | 12 - .../backends/ldap/ISessionFactory.ts | 6 - .../backends/ldap/LdapUsersDatabase.spec.ts | 386 ------------------ .../backends/ldap/LdapUsersDatabase.ts | 107 ----- .../backends/ldap/SafeSession.spec.ts | 76 ---- .../backends/ldap/SafeSession.ts | 62 --- .../backends/ldap/Sanitizer.spec.ts | 25 -- .../authentication/backends/ldap/Sanitizer.ts | 25 -- .../backends/ldap/Session.spec.ts | 127 ------ .../authentication/backends/ldap/Session.ts | 156 ------- .../backends/ldap/SessionFactory.ts | 37 -- .../backends/ldap/SessionFactoryStub.spec.ts | 16 - .../backends/ldap/SessionStub.spec.ts | 46 --- .../backends/ldap/connector/Connector.ts | 69 ---- .../ldap/connector/ConnectorFactory.ts | 18 - .../connector/ConnectorFactoryStub.spec.ts | 17 - .../ldap/connector/ConnectorStub.spec.ts | 34 -- .../backends/ldap/connector/IConnector.ts | 9 - .../ldap/connector/IConnectorFactory.ts | 5 - .../lib/authentication/totp/ITotpHandler.ts | 6 - .../authentication/totp/TotpHandler.spec.ts | 39 -- .../lib/authentication/totp/TotpHandler.ts | 36 -- .../totp/TotpHandlerStub.spec.ts | 22 - .../src/lib/authentication/u2f/IU2fHandler.ts | 9 - .../src/lib/authentication/u2f/U2fHandler.ts | 24 -- .../authentication/u2f/U2fHandlerStub.spec.ts | 31 -- .../src/lib/authorization/Authorizer.spec.ts | 372 ----------------- .../src/lib/authorization/Authorizer.ts | 85 ---- .../lib/authorization/AuthorizerStub.spec.ts | 17 - .../src/lib/authorization/IAuthorizer.ts | 7 - .../server/src/lib/authorization/Level.ts | 6 - .../authorization/MultipleDomainMatcher.ts | 12 - .../server/src/lib/authorization/Object.ts | 5 - .../server/src/lib/authorization/Subject.ts | 5 - .../configuration/ConfigurationParser.spec.ts | 171 -------- .../lib/configuration/ConfigurationParser.ts | 39 -- .../SessionConfigurationBuilder.spec.ts | 149 ------- .../SessionConfigurationBuilder.ts | 52 --- .../schema/AclConfiguration.spec.ts | 34 -- .../configuration/schema/AclConfiguration.ts | 41 -- ...AuthenticationBackendConfiguration.spec.ts | 11 - .../AuthenticationBackendConfiguration.ts | 25 -- .../lib/configuration/schema/Configuration.ts | 68 --- .../schema/FileUsersDatabaseConfiguration.ts | 4 - .../schema/LdapConfiguration.spec.ts | 25 -- .../configuration/schema/LdapConfiguration.ts | 40 -- .../schema/NotifierConfiguration.spec.ts | 40 -- .../schema/NotifierConfiguration.ts | 45 -- .../schema/RegulationConfiguration.spec.ts | 13 - .../schema/RegulationConfiguration.ts | 23 -- .../schema/SessionConfiguration.spec.ts | 16 - .../schema/SessionConfiguration.ts | 32 -- .../schema/StorageConfiguration.spec.ts | 15 - .../schema/StorageConfiguration.ts | 30 -- .../configuration/schema/TotpConfiguration.ts | 13 - .../schema/UserDatabaseConfiguration.ts | 9 - .../lib/connectors/mongo/IMongoClient.d.ts | 6 - .../lib/connectors/mongo/MongoClient.spec.ts | 119 ------ .../src/lib/connectors/mongo/MongoClient.ts | 76 ---- .../connectors/mongo/MongoClientStub.spec.ts | 16 - .../server/src/lib/logging/GlobalLogger.ts | 34 -- .../src/lib/logging/GlobalLoggerStub.spec.ts | 38 -- .../server/src/lib/logging/IGlobalLogger.ts | 5 - .../server/src/lib/logging/IRequestLogger.ts | 7 - .../server/src/lib/logging/RequestLogger.ts | 45 -- .../src/lib/logging/RequestLoggerStub.spec.ts | 38 -- .../lib/notifiers/AbstractEmailNotifier.ts | 23 -- .../src/lib/notifiers/EmailNotifier.spec.ts | 54 --- .../server/src/lib/notifiers/EmailNotifier.ts | 27 -- .../src/lib/notifiers/FileSystemNotifier.ts | 22 - .../server/src/lib/notifiers/IMailSender.ts | 6 - .../src/lib/notifiers/IMailSenderBuilder.ts | 7 - .../server/src/lib/notifiers/INotifier.ts | 5 - .../server/src/lib/notifiers/MailSender.ts | 42 -- .../lib/notifiers/MailSenderBuilder.spec.ts | 67 --- .../src/lib/notifiers/MailSenderBuilder.ts | 42 -- .../notifiers/MailSenderBuilderStub.spec.ts | 25 -- .../src/lib/notifiers/MailSenderStub.spec.ts | 16 - .../src/lib/notifiers/NotifierFactory.spec.ts | 42 -- .../src/lib/notifiers/NotifierFactory.ts | 33 -- .../src/lib/notifiers/NotifierStub.spec.ts | 16 - .../server/src/lib/notifiers/SmtpNotifier.ts | 30 -- .../server/src/lib/regulation/IRegulator.ts | 6 - .../src/lib/regulation/Regulator.spec.ts | 186 --------- .../server/src/lib/regulation/Regulator.ts | 55 --- .../src/lib/regulation/RegulatorStub.spec.ts | 22 - .../src/lib/routes/error/401/get.spec.ts | 61 --- .../server/src/lib/routes/error/401/get.ts | 15 - .../src/lib/routes/error/403/get.spec.ts | 61 --- .../server/src/lib/routes/error/403/get.ts | 15 - .../src/lib/routes/error/404/get.spec.ts | 19 - .../server/src/lib/routes/error/404/get.ts | 8 - .../server/src/lib/routes/error/redirector.ts | 13 - .../server/src/lib/routes/firstfactor/get.ts | 72 ---- .../src/lib/routes/firstfactor/post.spec.ts | 136 ------ .../server/src/lib/routes/firstfactor/post.ts | 101 ----- .../server/src/lib/routes/loggedin/get.ts | 23 -- .../server/src/lib/routes/logout/get.ts | 20 - .../lib/routes/password-reset/constants.ts | 2 - .../routes/password-reset/form/post.spec.ts | 122 ------ .../lib/routes/password-reset/form/post.ts | 50 --- .../identity/PasswordResetHandler.spec.ts | 92 ----- .../identity/PasswordResetHandler.ts | 69 ---- .../lib/routes/password-reset/request/get.ts | 13 - .../src/lib/routes/secondfactor/get.spec.ts | 44 -- .../server/src/lib/routes/secondfactor/get.ts | 28 -- .../lib/routes/secondfactor/redirect.spec.ts | 41 -- .../src/lib/routes/secondfactor/redirect.ts | 30 -- .../lib/routes/secondfactor/totp/constants.ts | 4 - .../totp/identity/RegistrationHandler.spec.ts | 116 ------ .../totp/identity/RegistrationHandler.ts | 112 ----- .../secondfactor/totp/sign/post.spec.ts | 76 ---- .../lib/routes/secondfactor/totp/sign/post.ts | 42 -- .../lib/routes/secondfactor/u2f/U2FCommon.ts | 11 - .../u2f/identity/RegistrationHandler.spec.ts | 96 ----- .../u2f/identity/RegistrationHandler.ts | 73 ---- .../secondfactor/u2f/register/post.spec.ts | 146 ------- .../routes/secondfactor/u2f/register/post.ts | 64 --- .../u2f/register_request/get.spec.ts | 86 ---- .../secondfactor/u2f/register_request/get.ts | 43 -- .../routes/secondfactor/u2f/sign/post.spec.ts | 101 ----- .../lib/routes/secondfactor/u2f/sign/post.ts | 57 --- .../secondfactor/u2f/sign_request/get.spec.ts | 68 --- .../secondfactor/u2f/sign_request/get.ts | 42 -- .../src/lib/routes/verify/access_control.ts | 51 --- .../server/src/lib/routes/verify/get.spec.ts | 320 --------------- .../server/src/lib/routes/verify/get.ts | 91 ----- .../src/lib/routes/verify/get_basic_auth.ts | 55 --- .../lib/routes/verify/get_session_cookie.ts | 78 ---- .../storage/AuthenticationTraceDocument.d.ts | 6 - .../lib/storage/CollectionFactoryFactory.ts | 15 - .../lib/storage/CollectionFactoryStub.spec.ts | 16 - .../src/lib/storage/CollectionStub.spec.ts | 39 -- .../server/src/lib/storage/ICollection.d.ts | 11 - .../src/lib/storage/ICollectionFactory.d.ts | 6 - .../src/lib/storage/IUserDataStore.d.ts | 21 - .../storage/IdentityValidationDocument.d.ts | 7 - .../src/lib/storage/TOTPSecretDocument.d.ts | 6 - .../lib/storage/U2FRegistrationDocument.d.ts | 8 - .../src/lib/storage/UserDataStore.spec.ts | 264 ------------ .../server/src/lib/storage/UserDataStore.ts | 143 ------- .../src/lib/storage/UserDataStoreStub.spec.ts | 64 --- .../lib/storage/mongo/MongoCollection.spec.ts | 110 ----- .../src/lib/storage/mongo/MongoCollection.ts | 50 --- .../mongo/MongoCollectionFactory.spec.ts | 21 - .../storage/mongo/MongoCollectionFactory.ts | 19 - .../lib/storage/nedb/NedbCollection.spec.ts | 136 ------ .../src/lib/storage/nedb/NedbCollection.ts | 47 --- .../nedb/NedbCollectionFactory.spec.ts | 16 - .../lib/storage/nedb/NedbCollectionFactory.ts | 28 -- .../server/src/lib/stubs/express.spec.ts | 103 ----- .../server/src/lib/stubs/ldapjs.spec.ts | 50 --- .../server/src/lib/stubs/speakeasy.spec.ts | 7 - .../squares/server/src/lib/stubs/u2f.spec.ts | 16 - .../src/lib/utils/HashGenerator.spec.ts | 18 - .../server/src/lib/utils/HashGenerator.ts | 23 -- .../server/src/lib/utils/ObjectCloner.ts | 6 - .../src/lib/utils/SafeRedirection.spec.ts | 33 -- .../server/src/lib/utils/SafeRedirection.ts | 22 - .../src/lib/utils/URLDecomposer.spec.ts | 46 --- .../server/src/lib/utils/URLDecomposer.ts | 15 - .../server/src/lib/web_server/Configurator.ts | 47 --- .../server/src/lib/web_server/RestApi.ts | 125 ------ .../RequireValidatedFirstFactor.ts | 27 -- .../middlewares/WithHeadersLogged.ts | 12 - .../server/src/resources/email-template.ejs | 0 .../server/src/views/already-logged-in.pug | 0 .../server/src/views/errors/.directory | 0 .../squares/server/src/views/errors/401.pug | 0 .../squares/server/src/views/errors/403.pug | 0 .../squares/server/src/views/errors/404.pug | 0 .../squares/server/src/views/firstfactor.pug | 0 .../server/src/views/layout/layout.pug | 0 .../src/views/need-identity-validation.pug | 0 .../server/src/views/password-reset-form.pug | 0 .../src/views/password-reset-request.pug | 0 .../squares/server/src/views/secondfactor.pug | 0 .../server/src/views/totp-register.pug | 0 .../squares/server/src/views/u2f-register.pug | 0 themes/squares/server/test/requests.ts | 94 ----- themes/squares/server/tsconfig.json | 21 - themes/squares/server/tslint.json | 60 --- themes/squares/server/types/.directory | 4 - .../server/types/AuthenticationSession.ts | 18 - themes/squares/server/types/Dependencies.ts | 29 -- themes/squares/server/types/Identity.ts | 6 - themes/squares/server/types/TOTPSecret.ts | 11 - .../squares/server/types/U2FRegistration.ts | 5 - themes/squares/server/types/dovehash.d.ts | 4 - themes/squares/server/types/speakeasy.d.ts | 96 ----- themes/triangles/client/src/.directory | 0 themes/triangles/client/src/css/.directory | 0 .../client/src/css/00-bootstrap.min.css | 0 themes/triangles/client/src/css/01-main.css | 0 themes/triangles/client/src/css/02-login.css | 0 themes/triangles/client/src/css/03-errors.css | 0 .../client/src/css/03-password-reset-form.css | 0 .../src/css/03-password-reset-request.css | 0 .../client/src/css/03-totp-register.css | 0 .../client/src/css/03-u2f-register.css | 0 .../client/src/img/LargeTriangles.svg | 0 .../triangles/client/src/img/background.jpg | Bin themes/triangles/client/src/img/icon.png | Bin themes/triangles/client/src/img/mail.png | Bin .../client/src/img/matrix_circle_128x128.png | Bin .../client/src/img/notifications/.directory | 0 .../client/src/img/notifications/error.png | Bin .../client/src/img/notifications/info.png | Bin .../client/src/img/notifications/success.png | Bin .../client/src/img/notifications/warning.png | Bin themes/triangles/client/src/img/padlock.png | Bin .../client/src/img/password_white.png | Bin themes/triangles/client/src/img/pendrive.png | Bin themes/triangles/client/src/img/sharingan.png | Bin .../client/src/img/stores/.directory | 0 .../src/img/stores/applestore-badge.svg | 0 .../src/img/stores/googleplay-badge.svg | 0 themes/triangles/client/src/img/success.png | Bin themes/triangles/client/src/img/user.png | Bin themes/triangles/client/src/img/warning.png | Bin themes/triangles/client/src/index.ts | 34 -- .../triangles/client/src/lib/GetPromised.ts | 14 - themes/triangles/client/src/lib/INotifier.ts | 14 - themes/triangles/client/src/lib/Notifier.ts | 83 ---- .../src/lib/QueryParametersRetriever.ts | 12 - .../triangles/client/src/lib/SafeRedirect.ts | 10 - .../lib/firstfactor/FirstFactorValidator.ts | 46 --- .../client/src/lib/firstfactor/UISelectors.ts | 5 - .../client/src/lib/firstfactor/index.ts | 49 --- .../src/lib/reset-password/constants.ts | 2 - .../lib/reset-password/reset-password-form.ts | 57 --- .../reset-password/reset-password-request.ts | 56 --- .../src/lib/secondfactor/TOTPValidator.ts | 28 -- .../src/lib/secondfactor/U2FValidator.ts | 42 -- .../client/src/lib/secondfactor/constants.ts | 3 - .../client/src/lib/secondfactor/index.ts | 59 --- .../src/lib/totp-register/totp-register.ts | 11 - .../src/lib/totp-register/ui-selector.ts | 2 - .../src/lib/u2f-register/u2f-register.ts | 56 --- .../client/src/thirdparties/qrcode.min.js | 0 .../client/src/thirdparties/u2f-api.js | 0 themes/triangles/client/test/Notifier.test.ts | 71 ---- .../firstfactor/FirstFactorValidator.test.ts | 44 -- .../client/test/mocks/NotifierStub.ts | 33 -- themes/triangles/client/test/mocks/jquery.ts | 59 --- themes/triangles/client/test/mocks/u2f-api.ts | 14 - .../test/secondfactor/TOTPValidator.test.ts | 37 -- .../test/totp-register/totp-register.test.ts | 31 -- themes/triangles/client/tsconfig.json | 24 -- themes/triangles/client/tslint.json | 60 --- themes/triangles/server/.directory | 0 themes/triangles/server/src/index.ts | 28 -- themes/triangles/server/src/lib/.directory | 4 - .../src/lib/AuthenticationSessionHandler.ts | 45 -- .../triangles/server/src/lib/ErrorReplies.ts | 49 --- themes/triangles/server/src/lib/Exceptions.ts | 88 ---- .../server/src/lib/FirstFactorValidator.ts | 20 - .../src/lib/IdentityCheckMiddleware.spec.ts | 176 -------- .../server/src/lib/IdentityCheckMiddleware.ts | 138 ------- .../lib/IdentityCheckPreValidationTemplate.ts | 3 - .../server/src/lib/IdentityValidable.ts | 19 - .../src/lib/IdentityValidableStub.spec.ts | 52 --- .../triangles/server/src/lib/Server.spec.ts | 81 ---- themes/triangles/server/src/lib/Server.ts | 93 ----- .../server/src/lib/ServerVariables.ts | 21 - .../src/lib/ServerVariablesInitializer.ts | 116 ------ .../lib/ServerVariablesMockBuilder.spec.ts | 87 ---- .../server/src/lib/authentication/Level.ts | 5 - .../backends/GroupsAndEmails.ts | 5 - .../authentication/backends/IUsersDatabase.ts | 10 - .../backends/IUsersDatabaseStub.spec.ts | 35 -- .../backends/file/FileUsersDatabase.spec.ts | 224 ---------- .../backends/file/FileUsersDatabase.ts | 182 --------- .../backends/file/ReadWriteQueue.ts | 60 --- .../authentication/backends/ldap/ISession.ts | 12 - .../backends/ldap/ISessionFactory.ts | 6 - .../backends/ldap/LdapUsersDatabase.spec.ts | 386 ------------------ .../backends/ldap/LdapUsersDatabase.ts | 107 ----- .../backends/ldap/SafeSession.spec.ts | 76 ---- .../backends/ldap/SafeSession.ts | 62 --- .../backends/ldap/Sanitizer.spec.ts | 25 -- .../authentication/backends/ldap/Sanitizer.ts | 25 -- .../backends/ldap/Session.spec.ts | 127 ------ .../authentication/backends/ldap/Session.ts | 156 ------- .../backends/ldap/SessionFactory.ts | 37 -- .../backends/ldap/SessionFactoryStub.spec.ts | 16 - .../backends/ldap/SessionStub.spec.ts | 46 --- .../backends/ldap/connector/Connector.ts | 69 ---- .../ldap/connector/ConnectorFactory.ts | 18 - .../connector/ConnectorFactoryStub.spec.ts | 17 - .../ldap/connector/ConnectorStub.spec.ts | 34 -- .../backends/ldap/connector/IConnector.ts | 9 - .../ldap/connector/IConnectorFactory.ts | 5 - .../lib/authentication/totp/ITotpHandler.ts | 6 - .../authentication/totp/TotpHandler.spec.ts | 39 -- .../lib/authentication/totp/TotpHandler.ts | 36 -- .../totp/TotpHandlerStub.spec.ts | 22 - .../src/lib/authentication/u2f/IU2fHandler.ts | 9 - .../src/lib/authentication/u2f/U2fHandler.ts | 24 -- .../authentication/u2f/U2fHandlerStub.spec.ts | 31 -- .../src/lib/authorization/Authorizer.spec.ts | 372 ----------------- .../src/lib/authorization/Authorizer.ts | 85 ---- .../lib/authorization/AuthorizerStub.spec.ts | 17 - .../src/lib/authorization/IAuthorizer.ts | 7 - .../server/src/lib/authorization/Level.ts | 6 - .../authorization/MultipleDomainMatcher.ts | 12 - .../server/src/lib/authorization/Object.ts | 5 - .../server/src/lib/authorization/Subject.ts | 5 - .../configuration/ConfigurationParser.spec.ts | 171 -------- .../lib/configuration/ConfigurationParser.ts | 39 -- .../SessionConfigurationBuilder.spec.ts | 149 ------- .../SessionConfigurationBuilder.ts | 52 --- .../schema/AclConfiguration.spec.ts | 34 -- .../configuration/schema/AclConfiguration.ts | 41 -- ...AuthenticationBackendConfiguration.spec.ts | 11 - .../AuthenticationBackendConfiguration.ts | 25 -- .../lib/configuration/schema/Configuration.ts | 68 --- .../schema/FileUsersDatabaseConfiguration.ts | 4 - .../schema/LdapConfiguration.spec.ts | 25 -- .../configuration/schema/LdapConfiguration.ts | 40 -- .../schema/NotifierConfiguration.spec.ts | 40 -- .../schema/NotifierConfiguration.ts | 45 -- .../schema/RegulationConfiguration.spec.ts | 13 - .../schema/RegulationConfiguration.ts | 23 -- .../schema/SessionConfiguration.spec.ts | 16 - .../schema/SessionConfiguration.ts | 32 -- .../schema/StorageConfiguration.spec.ts | 15 - .../schema/StorageConfiguration.ts | 30 -- .../configuration/schema/TotpConfiguration.ts | 13 - .../schema/UserDatabaseConfiguration.ts | 9 - .../lib/connectors/mongo/IMongoClient.d.ts | 6 - .../lib/connectors/mongo/MongoClient.spec.ts | 119 ------ .../src/lib/connectors/mongo/MongoClient.ts | 76 ---- .../connectors/mongo/MongoClientStub.spec.ts | 16 - .../server/src/lib/logging/GlobalLogger.ts | 34 -- .../src/lib/logging/GlobalLoggerStub.spec.ts | 38 -- .../server/src/lib/logging/IGlobalLogger.ts | 5 - .../server/src/lib/logging/IRequestLogger.ts | 7 - .../server/src/lib/logging/RequestLogger.ts | 45 -- .../src/lib/logging/RequestLoggerStub.spec.ts | 38 -- .../lib/notifiers/AbstractEmailNotifier.ts | 23 -- .../src/lib/notifiers/EmailNotifier.spec.ts | 54 --- .../server/src/lib/notifiers/EmailNotifier.ts | 27 -- .../src/lib/notifiers/FileSystemNotifier.ts | 22 - .../server/src/lib/notifiers/IMailSender.ts | 6 - .../src/lib/notifiers/IMailSenderBuilder.ts | 7 - .../server/src/lib/notifiers/INotifier.ts | 5 - .../server/src/lib/notifiers/MailSender.ts | 42 -- .../lib/notifiers/MailSenderBuilder.spec.ts | 67 --- .../src/lib/notifiers/MailSenderBuilder.ts | 42 -- .../notifiers/MailSenderBuilderStub.spec.ts | 25 -- .../src/lib/notifiers/MailSenderStub.spec.ts | 16 - .../src/lib/notifiers/NotifierFactory.spec.ts | 42 -- .../src/lib/notifiers/NotifierFactory.ts | 33 -- .../src/lib/notifiers/NotifierStub.spec.ts | 16 - .../server/src/lib/notifiers/SmtpNotifier.ts | 30 -- .../server/src/lib/regulation/IRegulator.ts | 6 - .../src/lib/regulation/Regulator.spec.ts | 186 --------- .../server/src/lib/regulation/Regulator.ts | 55 --- .../src/lib/regulation/RegulatorStub.spec.ts | 22 - .../src/lib/routes/error/401/get.spec.ts | 61 --- .../server/src/lib/routes/error/401/get.ts | 15 - .../src/lib/routes/error/403/get.spec.ts | 61 --- .../server/src/lib/routes/error/403/get.ts | 15 - .../src/lib/routes/error/404/get.spec.ts | 19 - .../server/src/lib/routes/error/404/get.ts | 8 - .../server/src/lib/routes/error/redirector.ts | 13 - .../server/src/lib/routes/firstfactor/get.ts | 72 ---- .../src/lib/routes/firstfactor/post.spec.ts | 136 ------ .../server/src/lib/routes/firstfactor/post.ts | 101 ----- .../server/src/lib/routes/loggedin/get.ts | 23 -- .../server/src/lib/routes/logout/get.ts | 20 - .../lib/routes/password-reset/constants.ts | 2 - .../routes/password-reset/form/post.spec.ts | 122 ------ .../lib/routes/password-reset/form/post.ts | 50 --- .../identity/PasswordResetHandler.spec.ts | 92 ----- .../identity/PasswordResetHandler.ts | 69 ---- .../lib/routes/password-reset/request/get.ts | 13 - .../src/lib/routes/secondfactor/get.spec.ts | 44 -- .../server/src/lib/routes/secondfactor/get.ts | 28 -- .../lib/routes/secondfactor/redirect.spec.ts | 41 -- .../src/lib/routes/secondfactor/redirect.ts | 30 -- .../lib/routes/secondfactor/totp/constants.ts | 4 - .../totp/identity/RegistrationHandler.spec.ts | 116 ------ .../totp/identity/RegistrationHandler.ts | 112 ----- .../secondfactor/totp/sign/post.spec.ts | 76 ---- .../lib/routes/secondfactor/totp/sign/post.ts | 42 -- .../lib/routes/secondfactor/u2f/U2FCommon.ts | 11 - .../u2f/identity/RegistrationHandler.spec.ts | 96 ----- .../u2f/identity/RegistrationHandler.ts | 73 ---- .../secondfactor/u2f/register/post.spec.ts | 146 ------- .../routes/secondfactor/u2f/register/post.ts | 64 --- .../u2f/register_request/get.spec.ts | 86 ---- .../secondfactor/u2f/register_request/get.ts | 43 -- .../routes/secondfactor/u2f/sign/post.spec.ts | 101 ----- .../lib/routes/secondfactor/u2f/sign/post.ts | 57 --- .../secondfactor/u2f/sign_request/get.spec.ts | 68 --- .../secondfactor/u2f/sign_request/get.ts | 42 -- .../src/lib/routes/verify/access_control.ts | 51 --- .../server/src/lib/routes/verify/get.spec.ts | 320 --------------- .../server/src/lib/routes/verify/get.ts | 91 ----- .../src/lib/routes/verify/get_basic_auth.ts | 55 --- .../lib/routes/verify/get_session_cookie.ts | 78 ---- .../storage/AuthenticationTraceDocument.d.ts | 6 - .../lib/storage/CollectionFactoryFactory.ts | 15 - .../lib/storage/CollectionFactoryStub.spec.ts | 16 - .../src/lib/storage/CollectionStub.spec.ts | 39 -- .../server/src/lib/storage/ICollection.d.ts | 11 - .../src/lib/storage/ICollectionFactory.d.ts | 6 - .../src/lib/storage/IUserDataStore.d.ts | 21 - .../storage/IdentityValidationDocument.d.ts | 7 - .../src/lib/storage/TOTPSecretDocument.d.ts | 6 - .../lib/storage/U2FRegistrationDocument.d.ts | 8 - .../src/lib/storage/UserDataStore.spec.ts | 264 ------------ .../server/src/lib/storage/UserDataStore.ts | 143 ------- .../src/lib/storage/UserDataStoreStub.spec.ts | 64 --- .../lib/storage/mongo/MongoCollection.spec.ts | 110 ----- .../src/lib/storage/mongo/MongoCollection.ts | 50 --- .../mongo/MongoCollectionFactory.spec.ts | 21 - .../storage/mongo/MongoCollectionFactory.ts | 19 - .../lib/storage/nedb/NedbCollection.spec.ts | 136 ------ .../src/lib/storage/nedb/NedbCollection.ts | 47 --- .../nedb/NedbCollectionFactory.spec.ts | 16 - .../lib/storage/nedb/NedbCollectionFactory.ts | 28 -- .../server/src/lib/stubs/express.spec.ts | 103 ----- .../server/src/lib/stubs/ldapjs.spec.ts | 50 --- .../server/src/lib/stubs/speakeasy.spec.ts | 7 - .../server/src/lib/stubs/u2f.spec.ts | 16 - .../src/lib/utils/HashGenerator.spec.ts | 18 - .../server/src/lib/utils/HashGenerator.ts | 23 -- .../server/src/lib/utils/ObjectCloner.ts | 6 - .../src/lib/utils/SafeRedirection.spec.ts | 33 -- .../server/src/lib/utils/SafeRedirection.ts | 22 - .../src/lib/utils/URLDecomposer.spec.ts | 46 --- .../server/src/lib/utils/URLDecomposer.ts | 15 - .../server/src/lib/web_server/Configurator.ts | 47 --- .../server/src/lib/web_server/RestApi.ts | 125 ------ .../RequireValidatedFirstFactor.ts | 27 -- .../middlewares/WithHeadersLogged.ts | 12 - .../server/src/resources/email-template.ejs | 0 .../server/src/views/already-logged-in.pug | 0 .../server/src/views/errors/.directory | 0 .../triangles/server/src/views/errors/401.pug | 0 .../triangles/server/src/views/errors/403.pug | 0 .../triangles/server/src/views/errors/404.pug | 0 .../server/src/views/firstfactor.pug | 0 .../server/src/views/layout/layout.pug | 0 .../src/views/need-identity-validation.pug | 0 .../server/src/views/password-reset-form.pug | 0 .../src/views/password-reset-request.pug | 0 .../server/src/views/secondfactor.pug | 0 .../server/src/views/totp-register.pug | 0 .../server/src/views/u2f-register.pug | 0 themes/triangles/server/test/requests.ts | 94 ----- themes/triangles/server/tsconfig.json | 21 - themes/triangles/server/tslint.json | 60 --- themes/triangles/server/types/.directory | 4 - .../server/types/AuthenticationSession.ts | 18 - themes/triangles/server/types/Dependencies.ts | 29 -- themes/triangles/server/types/Identity.ts | 6 - themes/triangles/server/types/TOTPSecret.ts | 11 - .../triangles/server/types/U2FRegistration.ts | 5 - themes/triangles/server/types/dovehash.d.ts | 4 - themes/triangles/server/types/speakeasy.d.ts | 96 ----- users_database.yml | 0 1805 files changed, 66 insertions(+), 55701 deletions(-) mode change 100644 => 100755 .gitignore mode change 100644 => 100755 .npmignore mode change 100644 => 100755 .travis.yml mode change 100644 => 100755 CHANGELOG.md mode change 100644 => 100755 CONTRIBUTORS.md mode change 100644 => 100755 Dockerfile mode change 100644 => 100755 Dockerfile.dev mode change 100644 => 100755 Gruntfile.js mode change 100644 => 100755 LICENSE mode change 100644 => 100755 README.md mode change 100644 => 100755 client/src/css/00-bootstrap.min.css mode change 100644 => 100755 client/src/css/01-main.css mode change 100644 => 100755 client/src/css/02-login.css mode change 100644 => 100755 client/src/css/03-errors.css mode change 100644 => 100755 client/src/css/03-password-reset-form.css mode change 100644 => 100755 client/src/css/03-password-reset-request.css mode change 100644 => 100755 client/src/css/03-totp-register.css mode change 100644 => 100755 client/src/css/03-u2f-register.css mode change 100644 => 100755 client/src/img/background.svg mode change 100644 => 100755 client/src/img/icon.png mode change 100644 => 100755 client/src/img/mail.png mode change 100644 => 100755 client/src/img/notifications/error.png mode change 100644 => 100755 client/src/img/notifications/info.png mode change 100644 => 100755 client/src/img/notifications/success.png mode change 100644 => 100755 client/src/img/notifications/warning.png mode change 100644 => 100755 client/src/img/padlock.png mode change 100644 => 100755 client/src/img/password.png mode change 100644 => 100755 client/src/img/pendrive.png mode change 100644 => 100755 client/src/img/stores/applestore-badge.svg mode change 100644 => 100755 client/src/img/stores/googleplay-badge.svg mode change 100644 => 100755 client/src/img/success.png mode change 100644 => 100755 client/src/img/user.png mode change 100644 => 100755 client/src/img/warning.png mode change 100644 => 100755 client/src/index.ts mode change 100644 => 100755 client/src/lib/GetPromised.ts mode change 100644 => 100755 client/src/lib/INotifier.ts mode change 100644 => 100755 client/src/lib/Notifier.ts mode change 100644 => 100755 client/src/lib/QueryParametersRetriever.ts mode change 100644 => 100755 client/src/lib/SafeRedirect.ts mode change 100644 => 100755 client/src/lib/firstfactor/FirstFactorValidator.ts mode change 100644 => 100755 client/src/lib/firstfactor/UISelectors.ts mode change 100644 => 100755 client/src/lib/firstfactor/index.ts mode change 100644 => 100755 client/src/lib/reset-password/constants.ts mode change 100644 => 100755 client/src/lib/reset-password/reset-password-form.ts mode change 100644 => 100755 client/src/lib/reset-password/reset-password-request.ts mode change 100644 => 100755 client/src/lib/secondfactor/TOTPValidator.ts mode change 100644 => 100755 client/src/lib/secondfactor/U2FValidator.ts mode change 100644 => 100755 client/src/lib/secondfactor/constants.ts mode change 100644 => 100755 client/src/lib/secondfactor/index.ts mode change 100644 => 100755 client/src/lib/totp-register/totp-register.ts mode change 100644 => 100755 client/src/lib/totp-register/ui-selector.ts mode change 100644 => 100755 client/src/lib/u2f-register/u2f-register.ts mode change 100644 => 100755 client/src/thirdparties/qrcode.min.js mode change 100644 => 100755 client/test/Notifier.test.ts mode change 100644 => 100755 client/test/firstfactor/FirstFactorValidator.test.ts mode change 100644 => 100755 client/test/mocks/NotifierStub.ts mode change 100644 => 100755 client/test/mocks/jquery.ts mode change 100644 => 100755 client/test/mocks/u2f-api.ts mode change 100644 => 100755 client/test/secondfactor/TOTPValidator.test.ts mode change 100644 => 100755 client/test/totp-register/totp-register.test.ts mode change 100644 => 100755 client/tsconfig.json mode change 100644 => 100755 client/tslint.json mode change 100644 => 100755 config.minimal.yml mode change 100644 => 100755 config.template.yml mode change 100644 => 100755 docker-compose.dev.yml mode change 100644 => 100755 docker-compose.dockerhub.yml mode change 100644 => 100755 docker-compose.minimal.dev.yml mode change 100644 => 100755 docker-compose.minimal.yml mode change 100644 => 100755 docker-compose.swarm.minimal.yml mode change 100644 => 100755 docker-compose.test.yml mode change 100644 => 100755 docker-compose.yml mode change 100644 => 100755 docs/build.md mode change 100644 => 100755 docs/configuration.md mode change 100644 => 100755 docs/deployment-dev.md mode change 100644 => 100755 docs/deployment-production.md mode change 100644 => 100755 docs/features.md mode change 100644 => 100755 docs/getting-started.md mode change 100644 => 100755 docs/security.md mode change 100644 => 100755 example/compose/authelia/docker-compose.test.yml mode change 100644 => 100755 example/compose/docker-compose.base.yml mode change 100644 => 100755 example/compose/httpbin/docker-compose.yml mode change 100644 => 100755 example/compose/ldap/access.rules mode change 100644 => 100755 example/compose/ldap/base.ldif mode change 100644 => 100755 example/compose/ldap/docker-compose.admin.yml mode change 100644 => 100755 example/compose/ldap/docker-compose.yml mode change 100644 => 100755 example/compose/mongo/docker-compose.yml mode change 100644 => 100755 example/compose/nginx/backend/docker-compose.yml mode change 100644 => 100755 example/compose/nginx/backend/html/admin/secret.html mode change 100644 => 100755 example/compose/nginx/backend/html/dev/groups/admin/secret.html mode change 100644 => 100755 example/compose/nginx/backend/html/dev/groups/dev/secret.html mode change 100644 => 100755 example/compose/nginx/backend/html/dev/users/bob/secret.html mode change 100644 => 100755 example/compose/nginx/backend/html/dev/users/harry/secret.html mode change 100644 => 100755 example/compose/nginx/backend/html/dev/users/john/secret.html mode change 100644 => 100755 example/compose/nginx/backend/html/home/index.html mode change 100644 => 100755 example/compose/nginx/backend/html/icon.png mode change 100644 => 100755 example/compose/nginx/backend/html/mail/secret.html mode change 100644 => 100755 example/compose/nginx/backend/html/public/index.html mode change 100644 => 100755 example/compose/nginx/backend/html/public/secret.html mode change 100644 => 100755 example/compose/nginx/backend/html/single_factor/secret.html mode change 100644 => 100755 example/compose/nginx/backend/nginx.conf mode change 100644 => 100755 example/compose/nginx/minimal/docker-compose.yml mode change 100644 => 100755 example/compose/nginx/minimal/html/admin/secret.html mode change 100644 => 100755 example/compose/nginx/minimal/html/home/index.html mode change 100644 => 100755 example/compose/nginx/minimal/nginx.conf mode change 100644 => 100755 example/compose/nginx/minimal/ssl/server.crt mode change 100644 => 100755 example/compose/nginx/minimal/ssl/server.csr mode change 100644 => 100755 example/compose/nginx/minimal/ssl/server.key mode change 100644 => 100755 example/compose/nginx/portal/docker-compose.yml mode change 100644 => 100755 example/compose/nginx/portal/nginx.conf mode change 100644 => 100755 example/compose/nginx/portal/ssl/server.crt mode change 100644 => 100755 example/compose/nginx/portal/ssl/server.csr mode change 100644 => 100755 example/compose/nginx/portal/ssl/server.key mode change 100644 => 100755 example/compose/redis/docker-compose.yml mode change 100644 => 100755 example/compose/smtp/docker-compose.yml mode change 100644 => 100755 example/kube/README.md mode change 100644 => 100755 example/kube/apps/app-home/deployment.yml mode change 100644 => 100755 example/kube/apps/app-home/index.html mode change 100644 => 100755 example/kube/apps/app-home/service.yml mode change 100644 => 100755 example/kube/apps/app1/deployment.yml mode change 100644 => 100755 example/kube/apps/app1/index.html mode change 100644 => 100755 example/kube/apps/app1/service.yml mode change 100644 => 100755 example/kube/apps/app1/ssl/tls.crt mode change 100644 => 100755 example/kube/apps/app1/ssl/tls.csr mode change 100644 => 100755 example/kube/apps/app1/ssl/tls.key mode change 100644 => 100755 example/kube/apps/app2/deployment.yml mode change 100644 => 100755 example/kube/apps/app2/index.html mode change 100644 => 100755 example/kube/apps/app2/service.yml mode change 100644 => 100755 example/kube/apps/app2/ssl/tls.crt mode change 100644 => 100755 example/kube/apps/app2/ssl/tls.csr mode change 100644 => 100755 example/kube/apps/app2/ssl/tls.key mode change 100644 => 100755 example/kube/apps/insecure-ingress.yml mode change 100644 => 100755 example/kube/apps/secure-ingress.yml mode change 100644 => 100755 example/kube/authelia/configs/config.yml mode change 100644 => 100755 example/kube/authelia/deployment.yml mode change 100644 => 100755 example/kube/authelia/ingress.yml mode change 100644 => 100755 example/kube/authelia/service.yml mode change 100644 => 100755 example/kube/authelia/ssl/tls.crt mode change 100644 => 100755 example/kube/authelia/ssl/tls.csr mode change 100644 => 100755 example/kube/authelia/ssl/tls.key mode change 100644 => 100755 example/kube/docker-registry/daemonset.yml mode change 100644 => 100755 example/kube/docker-registry/ingress.yml mode change 100644 => 100755 example/kube/docker-registry/replicationcontroller.yml mode change 100644 => 100755 example/kube/docker-registry/service.yml mode change 100644 => 100755 example/kube/ingress-controller/default-backend.yml mode change 100644 => 100755 example/kube/ingress-controller/deployment.yml mode change 100644 => 100755 example/kube/ingress-controller/service.yml mode change 100644 => 100755 example/kube/ldap/Dockerfile mode change 100644 => 100755 example/kube/ldap/deployment.yml mode change 100644 => 100755 example/kube/ldap/service.yml mode change 100644 => 100755 example/kube/mailcatcher/deployment.yml mode change 100644 => 100755 example/kube/mailcatcher/ingress.yml mode change 100644 => 100755 example/kube/mailcatcher/service.yml mode change 100644 => 100755 example/kube/namespace.yml mode change 100644 => 100755 example/kube/storage/mongo.yml mode change 100644 => 100755 example/kube/storage/redis.yml mode change 100644 => 100755 images/authelia-title-white.png mode change 100644 => 100755 images/authelia-title.png mode change 100644 => 100755 images/email_confirmation.png mode change 100644 => 100755 images/first_factor.png mode change 100644 => 100755 images/icon.png mode change 100644 => 100755 images/kube-logo.png mode change 100644 => 100755 images/reset_password.png mode change 100644 => 100755 images/second_factor.png mode change 100644 => 100755 images/totp.png mode change 100644 => 100755 images/u2f.png mode change 100644 => 100755 package-lock.json mode change 100644 => 100755 package.json mode change 100644 => 100755 server/src/lib/AuthenticationSessionHandler.ts mode change 100644 => 100755 server/src/lib/ErrorReplies.ts mode change 100644 => 100755 server/src/lib/Exceptions.ts mode change 100644 => 100755 server/src/lib/FirstFactorValidator.ts mode change 100644 => 100755 server/src/lib/IdentityCheckMiddleware.spec.ts mode change 100644 => 100755 server/src/lib/IdentityCheckMiddleware.ts mode change 100644 => 100755 server/src/lib/IdentityCheckPreValidationTemplate.ts mode change 100644 => 100755 server/src/lib/IdentityValidable.ts mode change 100644 => 100755 server/src/lib/IdentityValidableStub.spec.ts mode change 100644 => 100755 server/src/lib/Server.spec.ts mode change 100644 => 100755 server/src/lib/Server.ts mode change 100644 => 100755 server/src/lib/ServerVariables.ts mode change 100644 => 100755 server/src/lib/ServerVariablesInitializer.ts mode change 100644 => 100755 server/src/lib/ServerVariablesMockBuilder.spec.ts mode change 100644 => 100755 server/src/lib/authentication/Level.ts mode change 100644 => 100755 server/src/lib/authentication/backends/GroupsAndEmails.ts mode change 100644 => 100755 server/src/lib/authentication/backends/IUsersDatabase.ts mode change 100644 => 100755 server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts mode change 100644 => 100755 server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts mode change 100644 => 100755 server/src/lib/authentication/backends/file/FileUsersDatabase.ts mode change 100644 => 100755 server/src/lib/authentication/backends/file/ReadWriteQueue.ts mode change 100644 => 100755 server/src/lib/authentication/backends/ldap/ISession.ts mode change 100644 => 100755 server/src/lib/authentication/backends/ldap/ISessionFactory.ts mode change 100644 => 100755 server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts mode change 100644 => 100755 server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts mode change 100644 => 100755 server/src/lib/authentication/backends/ldap/SafeSession.spec.ts mode change 100644 => 100755 server/src/lib/authentication/backends/ldap/SafeSession.ts mode change 100644 => 100755 server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts mode change 100644 => 100755 server/src/lib/authentication/backends/ldap/Sanitizer.ts mode change 100644 => 100755 server/src/lib/authentication/backends/ldap/Session.spec.ts mode change 100644 => 100755 server/src/lib/authentication/backends/ldap/Session.ts mode change 100644 => 100755 server/src/lib/authentication/backends/ldap/SessionFactory.ts mode change 100644 => 100755 server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts mode change 100644 => 100755 server/src/lib/authentication/backends/ldap/SessionStub.spec.ts mode change 100644 => 100755 server/src/lib/authentication/backends/ldap/connector/Connector.ts mode change 100644 => 100755 server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts mode change 100644 => 100755 server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts mode change 100644 => 100755 server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts mode change 100644 => 100755 server/src/lib/authentication/backends/ldap/connector/IConnector.ts mode change 100644 => 100755 server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts mode change 100644 => 100755 server/src/lib/authentication/totp/ITotpHandler.ts mode change 100644 => 100755 server/src/lib/authentication/totp/TotpHandler.spec.ts mode change 100644 => 100755 server/src/lib/authentication/totp/TotpHandler.ts mode change 100644 => 100755 server/src/lib/authentication/totp/TotpHandlerStub.spec.ts mode change 100644 => 100755 server/src/lib/authentication/u2f/IU2fHandler.ts mode change 100644 => 100755 server/src/lib/authentication/u2f/U2fHandler.ts mode change 100644 => 100755 server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts mode change 100644 => 100755 server/src/lib/authorization/Authorizer.spec.ts mode change 100644 => 100755 server/src/lib/authorization/Authorizer.ts mode change 100644 => 100755 server/src/lib/authorization/AuthorizerStub.spec.ts mode change 100644 => 100755 server/src/lib/authorization/IAuthorizer.ts mode change 100644 => 100755 server/src/lib/authorization/Level.ts mode change 100644 => 100755 server/src/lib/authorization/MultipleDomainMatcher.ts mode change 100644 => 100755 server/src/lib/authorization/Object.ts mode change 100644 => 100755 server/src/lib/authorization/Subject.ts mode change 100644 => 100755 server/src/lib/configuration/ConfigurationParser.spec.ts mode change 100644 => 100755 server/src/lib/configuration/ConfigurationParser.ts mode change 100644 => 100755 server/src/lib/configuration/SessionConfigurationBuilder.spec.ts mode change 100644 => 100755 server/src/lib/configuration/SessionConfigurationBuilder.ts mode change 100644 => 100755 server/src/lib/configuration/schema/AclConfiguration.spec.ts mode change 100644 => 100755 server/src/lib/configuration/schema/AclConfiguration.ts mode change 100644 => 100755 server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts mode change 100644 => 100755 server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts mode change 100644 => 100755 server/src/lib/configuration/schema/Configuration.ts mode change 100644 => 100755 server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts mode change 100644 => 100755 server/src/lib/configuration/schema/LdapConfiguration.spec.ts mode change 100644 => 100755 server/src/lib/configuration/schema/LdapConfiguration.ts mode change 100644 => 100755 server/src/lib/configuration/schema/NotifierConfiguration.spec.ts mode change 100644 => 100755 server/src/lib/configuration/schema/NotifierConfiguration.ts mode change 100644 => 100755 server/src/lib/configuration/schema/RegulationConfiguration.spec.ts mode change 100644 => 100755 server/src/lib/configuration/schema/RegulationConfiguration.ts mode change 100644 => 100755 server/src/lib/configuration/schema/SessionConfiguration.spec.ts mode change 100644 => 100755 server/src/lib/configuration/schema/SessionConfiguration.ts mode change 100644 => 100755 server/src/lib/configuration/schema/StorageConfiguration.spec.ts mode change 100644 => 100755 server/src/lib/configuration/schema/StorageConfiguration.ts mode change 100644 => 100755 server/src/lib/configuration/schema/TotpConfiguration.ts mode change 100644 => 100755 server/src/lib/configuration/schema/UserDatabaseConfiguration.ts mode change 100644 => 100755 server/src/lib/connectors/mongo/IMongoClient.d.ts mode change 100644 => 100755 server/src/lib/connectors/mongo/MongoClient.spec.ts mode change 100644 => 100755 server/src/lib/connectors/mongo/MongoClient.ts mode change 100644 => 100755 server/src/lib/connectors/mongo/MongoClientStub.spec.ts mode change 100644 => 100755 server/src/lib/logging/GlobalLogger.ts mode change 100644 => 100755 server/src/lib/logging/GlobalLoggerStub.spec.ts mode change 100644 => 100755 server/src/lib/logging/IGlobalLogger.ts mode change 100644 => 100755 server/src/lib/logging/IRequestLogger.ts mode change 100644 => 100755 server/src/lib/logging/RequestLogger.ts mode change 100644 => 100755 server/src/lib/logging/RequestLoggerStub.spec.ts mode change 100644 => 100755 server/src/lib/notifiers/AbstractEmailNotifier.ts mode change 100644 => 100755 server/src/lib/notifiers/EmailNotifier.spec.ts mode change 100644 => 100755 server/src/lib/notifiers/EmailNotifier.ts mode change 100644 => 100755 server/src/lib/notifiers/FileSystemNotifier.ts mode change 100644 => 100755 server/src/lib/notifiers/IMailSender.ts mode change 100644 => 100755 server/src/lib/notifiers/IMailSenderBuilder.ts mode change 100644 => 100755 server/src/lib/notifiers/INotifier.ts mode change 100644 => 100755 server/src/lib/notifiers/MailSender.ts mode change 100644 => 100755 server/src/lib/notifiers/MailSenderBuilder.spec.ts mode change 100644 => 100755 server/src/lib/notifiers/MailSenderBuilder.ts mode change 100644 => 100755 server/src/lib/notifiers/MailSenderBuilderStub.spec.ts mode change 100644 => 100755 server/src/lib/notifiers/MailSenderStub.spec.ts mode change 100644 => 100755 server/src/lib/notifiers/NotifierFactory.spec.ts mode change 100644 => 100755 server/src/lib/notifiers/NotifierFactory.ts mode change 100644 => 100755 server/src/lib/notifiers/NotifierStub.spec.ts mode change 100644 => 100755 server/src/lib/notifiers/SmtpNotifier.ts mode change 100644 => 100755 server/src/lib/regulation/IRegulator.ts mode change 100644 => 100755 server/src/lib/regulation/Regulator.spec.ts mode change 100644 => 100755 server/src/lib/regulation/Regulator.ts mode change 100644 => 100755 server/src/lib/regulation/RegulatorStub.spec.ts mode change 100644 => 100755 server/src/lib/routes/error/401/get.spec.ts mode change 100644 => 100755 server/src/lib/routes/error/401/get.ts mode change 100644 => 100755 server/src/lib/routes/error/403/get.spec.ts mode change 100644 => 100755 server/src/lib/routes/error/403/get.ts mode change 100644 => 100755 server/src/lib/routes/error/404/get.spec.ts mode change 100644 => 100755 server/src/lib/routes/error/404/get.ts mode change 100644 => 100755 server/src/lib/routes/error/redirector.ts mode change 100644 => 100755 server/src/lib/routes/firstfactor/get.ts mode change 100644 => 100755 server/src/lib/routes/firstfactor/post.spec.ts mode change 100644 => 100755 server/src/lib/routes/firstfactor/post.ts mode change 100644 => 100755 server/src/lib/routes/loggedin/get.ts mode change 100644 => 100755 server/src/lib/routes/logout/get.ts mode change 100644 => 100755 server/src/lib/routes/password-reset/constants.ts mode change 100644 => 100755 server/src/lib/routes/password-reset/form/post.spec.ts mode change 100644 => 100755 server/src/lib/routes/password-reset/form/post.ts mode change 100644 => 100755 server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts mode change 100644 => 100755 server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts mode change 100644 => 100755 server/src/lib/routes/password-reset/request/get.ts mode change 100644 => 100755 server/src/lib/routes/secondfactor/get.spec.ts mode change 100644 => 100755 server/src/lib/routes/secondfactor/get.ts mode change 100644 => 100755 server/src/lib/routes/secondfactor/redirect.spec.ts mode change 100644 => 100755 server/src/lib/routes/secondfactor/redirect.ts mode change 100644 => 100755 server/src/lib/routes/secondfactor/totp/constants.ts mode change 100644 => 100755 server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts mode change 100644 => 100755 server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts mode change 100644 => 100755 server/src/lib/routes/secondfactor/totp/sign/post.spec.ts mode change 100644 => 100755 server/src/lib/routes/secondfactor/totp/sign/post.ts mode change 100644 => 100755 server/src/lib/routes/secondfactor/u2f/U2FCommon.ts mode change 100644 => 100755 server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts mode change 100644 => 100755 server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts mode change 100644 => 100755 server/src/lib/routes/secondfactor/u2f/register/post.spec.ts mode change 100644 => 100755 server/src/lib/routes/secondfactor/u2f/register/post.ts mode change 100644 => 100755 server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts mode change 100644 => 100755 server/src/lib/routes/secondfactor/u2f/register_request/get.ts mode change 100644 => 100755 server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts mode change 100644 => 100755 server/src/lib/routes/secondfactor/u2f/sign/post.ts mode change 100644 => 100755 server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts mode change 100644 => 100755 server/src/lib/routes/secondfactor/u2f/sign_request/get.ts mode change 100644 => 100755 server/src/lib/routes/verify/access_control.ts mode change 100644 => 100755 server/src/lib/routes/verify/get.spec.ts mode change 100644 => 100755 server/src/lib/routes/verify/get.ts mode change 100644 => 100755 server/src/lib/routes/verify/get_basic_auth.ts mode change 100644 => 100755 server/src/lib/routes/verify/get_session_cookie.ts mode change 100644 => 100755 server/src/lib/storage/AuthenticationTraceDocument.d.ts mode change 100644 => 100755 server/src/lib/storage/CollectionFactoryFactory.ts mode change 100644 => 100755 server/src/lib/storage/CollectionFactoryStub.spec.ts mode change 100644 => 100755 server/src/lib/storage/CollectionStub.spec.ts mode change 100644 => 100755 server/src/lib/storage/ICollection.d.ts mode change 100644 => 100755 server/src/lib/storage/ICollectionFactory.d.ts mode change 100644 => 100755 server/src/lib/storage/IUserDataStore.d.ts mode change 100644 => 100755 server/src/lib/storage/IdentityValidationDocument.d.ts mode change 100644 => 100755 server/src/lib/storage/TOTPSecretDocument.d.ts mode change 100644 => 100755 server/src/lib/storage/U2FRegistrationDocument.d.ts mode change 100644 => 100755 server/src/lib/storage/UserDataStore.spec.ts mode change 100644 => 100755 server/src/lib/storage/UserDataStore.ts mode change 100644 => 100755 server/src/lib/storage/UserDataStoreStub.spec.ts mode change 100644 => 100755 server/src/lib/storage/mongo/MongoCollection.spec.ts mode change 100644 => 100755 server/src/lib/storage/mongo/MongoCollection.ts mode change 100644 => 100755 server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts mode change 100644 => 100755 server/src/lib/storage/mongo/MongoCollectionFactory.ts mode change 100644 => 100755 server/src/lib/storage/nedb/NedbCollection.spec.ts mode change 100644 => 100755 server/src/lib/storage/nedb/NedbCollection.ts mode change 100644 => 100755 server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts mode change 100644 => 100755 server/src/lib/storage/nedb/NedbCollectionFactory.ts mode change 100644 => 100755 server/src/lib/stubs/express.spec.ts mode change 100644 => 100755 server/src/lib/stubs/ldapjs.spec.ts mode change 100644 => 100755 server/src/lib/stubs/speakeasy.spec.ts mode change 100644 => 100755 server/src/lib/stubs/u2f.spec.ts mode change 100644 => 100755 server/src/lib/utils/HashGenerator.spec.ts mode change 100644 => 100755 server/src/lib/utils/HashGenerator.ts mode change 100644 => 100755 server/src/lib/utils/ObjectCloner.ts mode change 100644 => 100755 server/src/lib/utils/SafeRedirection.spec.ts mode change 100644 => 100755 server/src/lib/utils/SafeRedirection.ts mode change 100644 => 100755 server/src/lib/utils/URLDecomposer.spec.ts mode change 100644 => 100755 server/src/lib/utils/URLDecomposer.ts mode change 100644 => 100755 server/src/lib/web_server/Configurator.ts mode change 100644 => 100755 server/src/lib/web_server/RestApi.ts mode change 100644 => 100755 server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts mode change 100644 => 100755 server/src/lib/web_server/middlewares/WithHeadersLogged.ts mode change 100644 => 100755 server/src/resources/email-template.ejs mode change 100644 => 100755 server/src/views/already-logged-in.pug mode change 100644 => 100755 server/src/views/errors/401.pug mode change 100644 => 100755 server/src/views/errors/403.pug mode change 100644 => 100755 server/src/views/errors/404.pug mode change 100644 => 100755 server/src/views/firstfactor.pug mode change 100644 => 100755 server/src/views/layout/layout.pug mode change 100644 => 100755 server/src/views/need-identity-validation.pug mode change 100644 => 100755 server/src/views/password-reset-form.pug mode change 100644 => 100755 server/src/views/password-reset-request.pug mode change 100644 => 100755 server/src/views/secondfactor.pug mode change 100644 => 100755 server/src/views/totp-register.pug mode change 100644 => 100755 server/src/views/u2f-register.pug mode change 100644 => 100755 server/test/requests.ts mode change 100644 => 100755 server/tsconfig.json mode change 100644 => 100755 server/tslint.json mode change 100644 => 100755 server/types/AuthenticationSession.ts mode change 100644 => 100755 server/types/Dependencies.ts mode change 100644 => 100755 server/types/Identity.ts mode change 100644 => 100755 server/types/TOTPSecret.ts mode change 100644 => 100755 server/types/U2FRegistration.ts mode change 100644 => 100755 server/types/dovehash.d.ts mode change 100644 => 100755 server/types/speakeasy.d.ts mode change 100644 => 100755 shared/BelongToDomain.ts mode change 100644 => 100755 shared/DomainExtractor.spec.ts mode change 100644 => 100755 shared/DomainExtractor.ts mode change 100644 => 100755 shared/ErrorMessage.ts mode change 100644 => 100755 shared/RedirectionMessage.ts mode change 100644 => 100755 shared/SignMessage.ts mode change 100644 => 100755 shared/UserMessages.ts mode change 100644 => 100755 shared/api.ts mode change 100644 => 100755 shared/constants.ts mode change 100644 => 100755 shared/types/u2f.d.ts mode change 100644 => 100755 test/complete-config/00-suite.ts mode change 100644 => 100755 test/complete-config/closed-redirection.ts mode change 100644 => 100755 test/complete-config/mongo-broken-connection.ts mode change 100644 => 100755 test/configuration.ts mode change 100644 => 100755 test/environment.ts mode change 100644 => 100755 test/features/access-control.feature mode change 100644 => 100755 test/features/auth-portal-redirection.feature mode change 100644 => 100755 test/features/authelia.feature mode change 100644 => 100755 test/features/authentication.feature mode change 100644 => 100755 test/features/forward-headers.feature mode change 100644 => 100755 test/features/redirection.feature mode change 100644 => 100755 test/features/registration.feature mode change 100644 => 100755 test/features/regulation.feature mode change 100644 => 100755 test/features/reset-password.feature mode change 100644 => 100755 test/features/resilience.feature mode change 100644 => 100755 test/features/restrictions.feature mode change 100644 => 100755 test/features/session-timeout.feature mode change 100644 => 100755 test/features/single-factor-domain.feature mode change 100644 => 100755 test/features/step_definitions/access-control.ts mode change 100644 => 100755 test/features/step_definitions/authelia.ts mode change 100644 => 100755 test/features/step_definitions/authentication.ts mode change 100644 => 100755 test/features/step_definitions/forward-headers.ts mode change 100644 => 100755 test/features/step_definitions/hooks.ts mode change 100644 => 100755 test/features/step_definitions/notifications.ts mode change 100644 => 100755 test/features/step_definitions/redirection.ts mode change 100644 => 100755 test/features/step_definitions/registration.ts mode change 100644 => 100755 test/features/step_definitions/regulation.ts mode change 100644 => 100755 test/features/step_definitions/reset-password.ts mode change 100644 => 100755 test/features/step_definitions/resilience.ts mode change 100644 => 100755 test/features/step_definitions/restrictions.ts mode change 100644 => 100755 test/features/step_definitions/session-timeout.ts mode change 100644 => 100755 test/features/step_definitions/single-factor.ts mode change 100644 => 100755 test/features/support/world.ts mode change 100644 => 100755 test/helpers/access-secret.ts mode change 100644 => 100755 test/helpers/click-on-button.ts mode change 100644 => 100755 test/helpers/click-on-link.ts mode change 100644 => 100755 test/helpers/fill-field.ts mode change 100644 => 100755 test/helpers/fill-login-page-and-click.ts mode change 100644 => 100755 test/helpers/full-login.ts mode change 100644 => 100755 test/helpers/get-identity-link.ts mode change 100644 => 100755 test/helpers/login-and-register-totp.ts mode change 100644 => 100755 test/helpers/login-as.ts mode change 100644 => 100755 test/helpers/register-totp.ts mode change 100644 => 100755 test/helpers/see-notification.ts mode change 100644 => 100755 test/helpers/validate-totp.ts mode change 100644 => 100755 test/helpers/visit-page.ts mode change 100644 => 100755 test/helpers/wait-redirected.ts mode change 100644 => 100755 test/helpers/with-driver.ts mode change 100644 => 100755 test/inactivity/00-suite.ts mode change 100644 => 100755 test/inactivity/keep_me_logged_in.ts mode change 100644 => 100755 test/minimal-config/00-suite.ts mode change 100644 => 100755 test/minimal-config/bad_password.ts mode change 100644 => 100755 test/minimal-config/fail_totp.ts mode change 100644 => 100755 test/minimal-config/register_totp.ts mode change 100644 => 100755 test/minimal-config/reset_password.ts mode change 100644 => 100755 test/minimal-config/validate_totp.ts mode change 100644 => 100755 themes/black/client/src/css/.directory mode change 100644 => 100755 themes/black/client/src/css/00-bootstrap.min.css mode change 100644 => 100755 themes/black/client/src/css/01-main.css mode change 100644 => 100755 themes/black/client/src/css/02-login.css mode change 100644 => 100755 themes/black/client/src/css/03-errors.css mode change 100644 => 100755 themes/black/client/src/css/03-password-reset-form.css mode change 100644 => 100755 themes/black/client/src/css/03-password-reset-request.css mode change 100644 => 100755 themes/black/client/src/css/03-totp-register.css mode change 100644 => 100755 themes/black/client/src/css/03-u2f-register.css mode change 100644 => 100755 themes/black/client/src/img/RandomizedPattern.svg mode change 100644 => 100755 themes/black/client/src/img/background.jpg mode change 100644 => 100755 themes/black/client/src/img/icon.png mode change 100644 => 100755 themes/black/client/src/img/mail.png mode change 100644 => 100755 themes/black/client/src/img/notifications/.directory mode change 100644 => 100755 themes/black/client/src/img/notifications/error.png mode change 100644 => 100755 themes/black/client/src/img/notifications/info.png mode change 100644 => 100755 themes/black/client/src/img/notifications/success.png mode change 100644 => 100755 themes/black/client/src/img/notifications/warning.png mode change 100644 => 100755 themes/black/client/src/img/padlock.png mode change 100644 => 100755 themes/black/client/src/img/password_white.png mode change 100644 => 100755 themes/black/client/src/img/pendrive.png mode change 100644 => 100755 themes/black/client/src/img/sharingan.png mode change 100644 => 100755 themes/black/client/src/img/stores/.directory mode change 100644 => 100755 themes/black/client/src/img/stores/applestore-badge.svg mode change 100644 => 100755 themes/black/client/src/img/stores/googleplay-badge.svg mode change 100644 => 100755 themes/black/client/src/img/success.png mode change 100644 => 100755 themes/black/client/src/img/user.png mode change 100644 => 100755 themes/black/client/src/img/warning.png delete mode 100644 themes/black/client/src/index.ts delete mode 100644 themes/black/client/src/lib/GetPromised.ts delete mode 100644 themes/black/client/src/lib/INotifier.ts delete mode 100644 themes/black/client/src/lib/Notifier.ts delete mode 100644 themes/black/client/src/lib/QueryParametersRetriever.ts delete mode 100644 themes/black/client/src/lib/SafeRedirect.ts delete mode 100644 themes/black/client/src/lib/firstfactor/FirstFactorValidator.ts delete mode 100644 themes/black/client/src/lib/firstfactor/UISelectors.ts delete mode 100644 themes/black/client/src/lib/firstfactor/index.ts delete mode 100644 themes/black/client/src/lib/reset-password/constants.ts delete mode 100644 themes/black/client/src/lib/reset-password/reset-password-form.ts delete mode 100644 themes/black/client/src/lib/reset-password/reset-password-request.ts delete mode 100644 themes/black/client/src/lib/secondfactor/TOTPValidator.ts delete mode 100644 themes/black/client/src/lib/secondfactor/U2FValidator.ts delete mode 100644 themes/black/client/src/lib/secondfactor/constants.ts delete mode 100644 themes/black/client/src/lib/secondfactor/index.ts delete mode 100644 themes/black/client/src/lib/totp-register/totp-register.ts delete mode 100644 themes/black/client/src/lib/totp-register/ui-selector.ts delete mode 100644 themes/black/client/src/lib/u2f-register/u2f-register.ts mode change 100644 => 100755 themes/black/client/src/thirdparties/qrcode.min.js mode change 100644 => 100755 themes/black/client/src/thirdparties/u2f-api.js delete mode 100644 themes/black/client/test/Notifier.test.ts delete mode 100644 themes/black/client/test/firstfactor/FirstFactorValidator.test.ts delete mode 100644 themes/black/client/test/mocks/NotifierStub.ts delete mode 100644 themes/black/client/test/mocks/jquery.ts delete mode 100644 themes/black/client/test/mocks/u2f-api.ts delete mode 100644 themes/black/client/test/secondfactor/TOTPValidator.test.ts delete mode 100644 themes/black/client/test/totp-register/totp-register.test.ts delete mode 100644 themes/black/client/tsconfig.json delete mode 100644 themes/black/client/tslint.json mode change 100644 => 100755 themes/black/server/.directory delete mode 100755 themes/black/server/src/index.ts delete mode 100644 themes/black/server/src/lib/.directory delete mode 100644 themes/black/server/src/lib/AuthenticationSessionHandler.ts delete mode 100644 themes/black/server/src/lib/ErrorReplies.ts delete mode 100644 themes/black/server/src/lib/Exceptions.ts delete mode 100644 themes/black/server/src/lib/FirstFactorValidator.ts delete mode 100644 themes/black/server/src/lib/IdentityCheckMiddleware.spec.ts delete mode 100644 themes/black/server/src/lib/IdentityCheckMiddleware.ts delete mode 100644 themes/black/server/src/lib/IdentityCheckPreValidationTemplate.ts delete mode 100644 themes/black/server/src/lib/IdentityValidable.ts delete mode 100644 themes/black/server/src/lib/IdentityValidableStub.spec.ts delete mode 100644 themes/black/server/src/lib/Server.spec.ts delete mode 100644 themes/black/server/src/lib/Server.ts delete mode 100644 themes/black/server/src/lib/ServerVariables.ts delete mode 100644 themes/black/server/src/lib/ServerVariablesInitializer.ts delete mode 100644 themes/black/server/src/lib/ServerVariablesMockBuilder.spec.ts delete mode 100644 themes/black/server/src/lib/authentication/Level.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/GroupsAndEmails.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/IUsersDatabase.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/file/FileUsersDatabase.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/file/ReadWriteQueue.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/ldap/ISession.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/ldap/ISessionFactory.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/ldap/SafeSession.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/ldap/Sanitizer.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/ldap/Session.spec.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/ldap/Session.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/ldap/SessionFactory.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/ldap/connector/Connector.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/ldap/connector/IConnector.ts delete mode 100644 themes/black/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts delete mode 100644 themes/black/server/src/lib/authentication/totp/ITotpHandler.ts delete mode 100644 themes/black/server/src/lib/authentication/totp/TotpHandler.spec.ts delete mode 100644 themes/black/server/src/lib/authentication/totp/TotpHandler.ts delete mode 100644 themes/black/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts delete mode 100644 themes/black/server/src/lib/authentication/u2f/IU2fHandler.ts delete mode 100644 themes/black/server/src/lib/authentication/u2f/U2fHandler.ts delete mode 100644 themes/black/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts delete mode 100644 themes/black/server/src/lib/authorization/Authorizer.spec.ts delete mode 100644 themes/black/server/src/lib/authorization/Authorizer.ts delete mode 100644 themes/black/server/src/lib/authorization/AuthorizerStub.spec.ts delete mode 100644 themes/black/server/src/lib/authorization/IAuthorizer.ts delete mode 100644 themes/black/server/src/lib/authorization/Level.ts delete mode 100644 themes/black/server/src/lib/authorization/MultipleDomainMatcher.ts delete mode 100644 themes/black/server/src/lib/authorization/Object.ts delete mode 100644 themes/black/server/src/lib/authorization/Subject.ts delete mode 100644 themes/black/server/src/lib/configuration/ConfigurationParser.spec.ts delete mode 100644 themes/black/server/src/lib/configuration/ConfigurationParser.ts delete mode 100644 themes/black/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts delete mode 100644 themes/black/server/src/lib/configuration/SessionConfigurationBuilder.ts delete mode 100644 themes/black/server/src/lib/configuration/schema/AclConfiguration.spec.ts delete mode 100644 themes/black/server/src/lib/configuration/schema/AclConfiguration.ts delete mode 100644 themes/black/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts delete mode 100644 themes/black/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts delete mode 100644 themes/black/server/src/lib/configuration/schema/Configuration.ts delete mode 100644 themes/black/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts delete mode 100644 themes/black/server/src/lib/configuration/schema/LdapConfiguration.spec.ts delete mode 100644 themes/black/server/src/lib/configuration/schema/LdapConfiguration.ts delete mode 100644 themes/black/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts delete mode 100644 themes/black/server/src/lib/configuration/schema/NotifierConfiguration.ts delete mode 100644 themes/black/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts delete mode 100644 themes/black/server/src/lib/configuration/schema/RegulationConfiguration.ts delete mode 100644 themes/black/server/src/lib/configuration/schema/SessionConfiguration.spec.ts delete mode 100644 themes/black/server/src/lib/configuration/schema/SessionConfiguration.ts delete mode 100644 themes/black/server/src/lib/configuration/schema/StorageConfiguration.spec.ts delete mode 100644 themes/black/server/src/lib/configuration/schema/StorageConfiguration.ts delete mode 100644 themes/black/server/src/lib/configuration/schema/TotpConfiguration.ts delete mode 100644 themes/black/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts delete mode 100644 themes/black/server/src/lib/connectors/mongo/IMongoClient.d.ts delete mode 100644 themes/black/server/src/lib/connectors/mongo/MongoClient.spec.ts delete mode 100644 themes/black/server/src/lib/connectors/mongo/MongoClient.ts delete mode 100644 themes/black/server/src/lib/connectors/mongo/MongoClientStub.spec.ts delete mode 100644 themes/black/server/src/lib/logging/GlobalLogger.ts delete mode 100644 themes/black/server/src/lib/logging/GlobalLoggerStub.spec.ts delete mode 100644 themes/black/server/src/lib/logging/IGlobalLogger.ts delete mode 100644 themes/black/server/src/lib/logging/IRequestLogger.ts delete mode 100644 themes/black/server/src/lib/logging/RequestLogger.ts delete mode 100644 themes/black/server/src/lib/logging/RequestLoggerStub.spec.ts delete mode 100644 themes/black/server/src/lib/notifiers/AbstractEmailNotifier.ts delete mode 100644 themes/black/server/src/lib/notifiers/EmailNotifier.spec.ts delete mode 100644 themes/black/server/src/lib/notifiers/EmailNotifier.ts delete mode 100644 themes/black/server/src/lib/notifiers/FileSystemNotifier.ts delete mode 100644 themes/black/server/src/lib/notifiers/IMailSender.ts delete mode 100644 themes/black/server/src/lib/notifiers/IMailSenderBuilder.ts delete mode 100644 themes/black/server/src/lib/notifiers/INotifier.ts delete mode 100644 themes/black/server/src/lib/notifiers/MailSender.ts delete mode 100644 themes/black/server/src/lib/notifiers/MailSenderBuilder.spec.ts delete mode 100644 themes/black/server/src/lib/notifiers/MailSenderBuilder.ts delete mode 100644 themes/black/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts delete mode 100644 themes/black/server/src/lib/notifiers/MailSenderStub.spec.ts delete mode 100644 themes/black/server/src/lib/notifiers/NotifierFactory.spec.ts delete mode 100644 themes/black/server/src/lib/notifiers/NotifierFactory.ts delete mode 100644 themes/black/server/src/lib/notifiers/NotifierStub.spec.ts delete mode 100644 themes/black/server/src/lib/notifiers/SmtpNotifier.ts delete mode 100644 themes/black/server/src/lib/regulation/IRegulator.ts delete mode 100644 themes/black/server/src/lib/regulation/Regulator.spec.ts delete mode 100644 themes/black/server/src/lib/regulation/Regulator.ts delete mode 100644 themes/black/server/src/lib/regulation/RegulatorStub.spec.ts delete mode 100644 themes/black/server/src/lib/routes/error/401/get.spec.ts delete mode 100644 themes/black/server/src/lib/routes/error/401/get.ts delete mode 100644 themes/black/server/src/lib/routes/error/403/get.spec.ts delete mode 100644 themes/black/server/src/lib/routes/error/403/get.ts delete mode 100644 themes/black/server/src/lib/routes/error/404/get.spec.ts delete mode 100644 themes/black/server/src/lib/routes/error/404/get.ts delete mode 100644 themes/black/server/src/lib/routes/error/redirector.ts delete mode 100644 themes/black/server/src/lib/routes/firstfactor/get.ts delete mode 100644 themes/black/server/src/lib/routes/firstfactor/post.spec.ts delete mode 100644 themes/black/server/src/lib/routes/firstfactor/post.ts delete mode 100644 themes/black/server/src/lib/routes/loggedin/get.ts delete mode 100644 themes/black/server/src/lib/routes/logout/get.ts delete mode 100644 themes/black/server/src/lib/routes/password-reset/constants.ts delete mode 100644 themes/black/server/src/lib/routes/password-reset/form/post.spec.ts delete mode 100644 themes/black/server/src/lib/routes/password-reset/form/post.ts delete mode 100644 themes/black/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts delete mode 100644 themes/black/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts delete mode 100644 themes/black/server/src/lib/routes/password-reset/request/get.ts delete mode 100644 themes/black/server/src/lib/routes/secondfactor/get.spec.ts delete mode 100644 themes/black/server/src/lib/routes/secondfactor/get.ts delete mode 100644 themes/black/server/src/lib/routes/secondfactor/redirect.spec.ts delete mode 100644 themes/black/server/src/lib/routes/secondfactor/redirect.ts delete mode 100644 themes/black/server/src/lib/routes/secondfactor/totp/constants.ts delete mode 100644 themes/black/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts delete mode 100644 themes/black/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts delete mode 100644 themes/black/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts delete mode 100644 themes/black/server/src/lib/routes/secondfactor/totp/sign/post.ts delete mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts delete mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts delete mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts delete mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts delete mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/register/post.ts delete mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts delete mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/register_request/get.ts delete mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts delete mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/sign/post.ts delete mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts delete mode 100644 themes/black/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts delete mode 100644 themes/black/server/src/lib/routes/verify/access_control.ts delete mode 100644 themes/black/server/src/lib/routes/verify/get.spec.ts delete mode 100644 themes/black/server/src/lib/routes/verify/get.ts delete mode 100644 themes/black/server/src/lib/routes/verify/get_basic_auth.ts delete mode 100644 themes/black/server/src/lib/routes/verify/get_session_cookie.ts delete mode 100644 themes/black/server/src/lib/storage/AuthenticationTraceDocument.d.ts delete mode 100644 themes/black/server/src/lib/storage/CollectionFactoryFactory.ts delete mode 100644 themes/black/server/src/lib/storage/CollectionFactoryStub.spec.ts delete mode 100644 themes/black/server/src/lib/storage/CollectionStub.spec.ts delete mode 100644 themes/black/server/src/lib/storage/ICollection.d.ts delete mode 100644 themes/black/server/src/lib/storage/ICollectionFactory.d.ts delete mode 100644 themes/black/server/src/lib/storage/IUserDataStore.d.ts delete mode 100644 themes/black/server/src/lib/storage/IdentityValidationDocument.d.ts delete mode 100644 themes/black/server/src/lib/storage/TOTPSecretDocument.d.ts delete mode 100644 themes/black/server/src/lib/storage/U2FRegistrationDocument.d.ts delete mode 100644 themes/black/server/src/lib/storage/UserDataStore.spec.ts delete mode 100644 themes/black/server/src/lib/storage/UserDataStore.ts delete mode 100644 themes/black/server/src/lib/storage/UserDataStoreStub.spec.ts delete mode 100644 themes/black/server/src/lib/storage/mongo/MongoCollection.spec.ts delete mode 100644 themes/black/server/src/lib/storage/mongo/MongoCollection.ts delete mode 100644 themes/black/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts delete mode 100644 themes/black/server/src/lib/storage/mongo/MongoCollectionFactory.ts delete mode 100644 themes/black/server/src/lib/storage/nedb/NedbCollection.spec.ts delete mode 100644 themes/black/server/src/lib/storage/nedb/NedbCollection.ts delete mode 100644 themes/black/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts delete mode 100644 themes/black/server/src/lib/storage/nedb/NedbCollectionFactory.ts delete mode 100644 themes/black/server/src/lib/stubs/express.spec.ts delete mode 100644 themes/black/server/src/lib/stubs/ldapjs.spec.ts delete mode 100644 themes/black/server/src/lib/stubs/speakeasy.spec.ts delete mode 100644 themes/black/server/src/lib/stubs/u2f.spec.ts delete mode 100644 themes/black/server/src/lib/utils/HashGenerator.spec.ts delete mode 100644 themes/black/server/src/lib/utils/HashGenerator.ts delete mode 100644 themes/black/server/src/lib/utils/ObjectCloner.ts delete mode 100644 themes/black/server/src/lib/utils/SafeRedirection.spec.ts delete mode 100644 themes/black/server/src/lib/utils/SafeRedirection.ts delete mode 100644 themes/black/server/src/lib/utils/URLDecomposer.spec.ts delete mode 100644 themes/black/server/src/lib/utils/URLDecomposer.ts delete mode 100644 themes/black/server/src/lib/web_server/Configurator.ts delete mode 100644 themes/black/server/src/lib/web_server/RestApi.ts delete mode 100644 themes/black/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts delete mode 100644 themes/black/server/src/lib/web_server/middlewares/WithHeadersLogged.ts mode change 100644 => 100755 themes/black/server/src/resources/email-template.ejs mode change 100644 => 100755 themes/black/server/src/views/already-logged-in.pug mode change 100644 => 100755 themes/black/server/src/views/errors/.directory mode change 100644 => 100755 themes/black/server/src/views/errors/401.pug mode change 100644 => 100755 themes/black/server/src/views/errors/403.pug mode change 100644 => 100755 themes/black/server/src/views/errors/404.pug mode change 100644 => 100755 themes/black/server/src/views/firstfactor.pug mode change 100644 => 100755 themes/black/server/src/views/layout/layout.pug mode change 100644 => 100755 themes/black/server/src/views/need-identity-validation.pug mode change 100644 => 100755 themes/black/server/src/views/password-reset-form.pug mode change 100644 => 100755 themes/black/server/src/views/password-reset-request.pug mode change 100644 => 100755 themes/black/server/src/views/secondfactor.pug mode change 100644 => 100755 themes/black/server/src/views/totp-register.pug mode change 100644 => 100755 themes/black/server/src/views/u2f-register.pug delete mode 100644 themes/black/server/test/requests.ts delete mode 100644 themes/black/server/tsconfig.json delete mode 100644 themes/black/server/tslint.json delete mode 100644 themes/black/server/types/.directory delete mode 100644 themes/black/server/types/AuthenticationSession.ts delete mode 100644 themes/black/server/types/Dependencies.ts delete mode 100644 themes/black/server/types/Identity.ts delete mode 100644 themes/black/server/types/TOTPSecret.ts delete mode 100644 themes/black/server/types/U2FRegistration.ts delete mode 100644 themes/black/server/types/dovehash.d.ts delete mode 100644 themes/black/server/types/speakeasy.d.ts rename themes/{main => default}/client/src/css/.directory (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/css/00-bootstrap.min.css (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/css/01-main.css (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/css/02-login.css (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/css/03-errors.css (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/css/03-password-reset-form.css (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/css/03-password-reset-request.css (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/css/03-totp-register.css (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/css/03-u2f-register.css (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/img/background.svg (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/img/icon.png (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/img/mail.png (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/img/notifications/.directory (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/img/notifications/error.png (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/img/notifications/info.png (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/img/notifications/success.png (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/img/notifications/warning.png (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/img/padlock.png (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/img/password.png (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/img/pendrive.png (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/img/stores/.directory (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/img/stores/applestore-badge.svg (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/img/stores/googleplay-badge.svg (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/img/success.png (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/img/user.png (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/img/warning.png (100%) mode change 100644 => 100755 rename themes/{main => default}/client/src/thirdparties/qrcode.min.js (100%) mode change 100644 => 100755 rename themes/{main => default}/server/.directory (100%) mode change 100644 => 100755 rename themes/{main => default}/server/src/resources/email-template.ejs (100%) mode change 100644 => 100755 rename themes/{main => default}/server/src/views/already-logged-in.pug (100%) mode change 100644 => 100755 rename themes/{main => default}/server/src/views/errors/.directory (100%) mode change 100644 => 100755 rename themes/{main => default}/server/src/views/errors/401.pug (100%) mode change 100644 => 100755 rename themes/{main => default}/server/src/views/errors/403.pug (100%) mode change 100644 => 100755 rename themes/{main => default}/server/src/views/errors/404.pug (100%) mode change 100644 => 100755 rename themes/{main => default}/server/src/views/firstfactor.pug (100%) mode change 100644 => 100755 rename themes/{main => default}/server/src/views/layout/layout.pug (100%) mode change 100644 => 100755 rename themes/{main => default}/server/src/views/need-identity-validation.pug (100%) mode change 100644 => 100755 rename themes/{main => default}/server/src/views/password-reset-form.pug (100%) mode change 100644 => 100755 rename themes/{main => default}/server/src/views/password-reset-request.pug (100%) mode change 100644 => 100755 rename themes/{main => default}/server/src/views/secondfactor.pug (100%) mode change 100644 => 100755 rename themes/{main => default}/server/src/views/totp-register.pug (100%) mode change 100644 => 100755 rename themes/{main => default}/server/src/views/u2f-register.pug (100%) mode change 100644 => 100755 delete mode 100644 themes/main/client/src/index.ts delete mode 100644 themes/main/client/src/lib/GetPromised.ts delete mode 100644 themes/main/client/src/lib/INotifier.ts delete mode 100644 themes/main/client/src/lib/Notifier.ts delete mode 100644 themes/main/client/src/lib/QueryParametersRetriever.ts delete mode 100644 themes/main/client/src/lib/SafeRedirect.ts delete mode 100644 themes/main/client/src/lib/firstfactor/FirstFactorValidator.ts delete mode 100644 themes/main/client/src/lib/firstfactor/UISelectors.ts delete mode 100644 themes/main/client/src/lib/firstfactor/index.ts delete mode 100644 themes/main/client/src/lib/reset-password/constants.ts delete mode 100644 themes/main/client/src/lib/reset-password/reset-password-form.ts delete mode 100644 themes/main/client/src/lib/reset-password/reset-password-request.ts delete mode 100644 themes/main/client/src/lib/secondfactor/TOTPValidator.ts delete mode 100644 themes/main/client/src/lib/secondfactor/U2FValidator.ts delete mode 100644 themes/main/client/src/lib/secondfactor/constants.ts delete mode 100644 themes/main/client/src/lib/secondfactor/index.ts delete mode 100644 themes/main/client/src/lib/totp-register/totp-register.ts delete mode 100644 themes/main/client/src/lib/totp-register/ui-selector.ts delete mode 100644 themes/main/client/src/lib/u2f-register/u2f-register.ts delete mode 100644 themes/main/client/test/Notifier.test.ts delete mode 100644 themes/main/client/test/firstfactor/FirstFactorValidator.test.ts delete mode 100644 themes/main/client/test/mocks/NotifierStub.ts delete mode 100644 themes/main/client/test/mocks/jquery.ts delete mode 100644 themes/main/client/test/mocks/u2f-api.ts delete mode 100644 themes/main/client/test/secondfactor/TOTPValidator.test.ts delete mode 100644 themes/main/client/test/totp-register/totp-register.test.ts delete mode 100644 themes/main/client/tsconfig.json delete mode 100644 themes/main/client/tslint.json delete mode 100755 themes/main/server/src/index.ts delete mode 100644 themes/main/server/src/lib/.directory delete mode 100644 themes/main/server/src/lib/AuthenticationSessionHandler.ts delete mode 100644 themes/main/server/src/lib/ErrorReplies.ts delete mode 100644 themes/main/server/src/lib/Exceptions.ts delete mode 100644 themes/main/server/src/lib/FirstFactorValidator.ts delete mode 100644 themes/main/server/src/lib/IdentityCheckMiddleware.spec.ts delete mode 100644 themes/main/server/src/lib/IdentityCheckMiddleware.ts delete mode 100644 themes/main/server/src/lib/IdentityCheckPreValidationTemplate.ts delete mode 100644 themes/main/server/src/lib/IdentityValidable.ts delete mode 100644 themes/main/server/src/lib/IdentityValidableStub.spec.ts delete mode 100644 themes/main/server/src/lib/Server.spec.ts delete mode 100644 themes/main/server/src/lib/Server.ts delete mode 100644 themes/main/server/src/lib/ServerVariables.ts delete mode 100644 themes/main/server/src/lib/ServerVariablesInitializer.ts delete mode 100644 themes/main/server/src/lib/ServerVariablesMockBuilder.spec.ts delete mode 100644 themes/main/server/src/lib/authentication/Level.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/GroupsAndEmails.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/IUsersDatabase.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/file/FileUsersDatabase.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/file/ReadWriteQueue.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/ldap/ISession.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/ldap/ISessionFactory.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/ldap/SafeSession.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/ldap/Sanitizer.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/ldap/Session.spec.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/ldap/Session.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/ldap/SessionFactory.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/ldap/connector/Connector.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/ldap/connector/IConnector.ts delete mode 100644 themes/main/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts delete mode 100644 themes/main/server/src/lib/authentication/totp/ITotpHandler.ts delete mode 100644 themes/main/server/src/lib/authentication/totp/TotpHandler.spec.ts delete mode 100644 themes/main/server/src/lib/authentication/totp/TotpHandler.ts delete mode 100644 themes/main/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts delete mode 100644 themes/main/server/src/lib/authentication/u2f/IU2fHandler.ts delete mode 100644 themes/main/server/src/lib/authentication/u2f/U2fHandler.ts delete mode 100644 themes/main/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts delete mode 100644 themes/main/server/src/lib/authorization/Authorizer.spec.ts delete mode 100644 themes/main/server/src/lib/authorization/Authorizer.ts delete mode 100644 themes/main/server/src/lib/authorization/AuthorizerStub.spec.ts delete mode 100644 themes/main/server/src/lib/authorization/IAuthorizer.ts delete mode 100644 themes/main/server/src/lib/authorization/Level.ts delete mode 100644 themes/main/server/src/lib/authorization/MultipleDomainMatcher.ts delete mode 100644 themes/main/server/src/lib/authorization/Object.ts delete mode 100644 themes/main/server/src/lib/authorization/Subject.ts delete mode 100644 themes/main/server/src/lib/configuration/ConfigurationParser.spec.ts delete mode 100644 themes/main/server/src/lib/configuration/ConfigurationParser.ts delete mode 100644 themes/main/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts delete mode 100644 themes/main/server/src/lib/configuration/SessionConfigurationBuilder.ts delete mode 100644 themes/main/server/src/lib/configuration/schema/AclConfiguration.spec.ts delete mode 100644 themes/main/server/src/lib/configuration/schema/AclConfiguration.ts delete mode 100644 themes/main/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts delete mode 100644 themes/main/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts delete mode 100644 themes/main/server/src/lib/configuration/schema/Configuration.ts delete mode 100644 themes/main/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts delete mode 100644 themes/main/server/src/lib/configuration/schema/LdapConfiguration.spec.ts delete mode 100644 themes/main/server/src/lib/configuration/schema/LdapConfiguration.ts delete mode 100644 themes/main/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts delete mode 100644 themes/main/server/src/lib/configuration/schema/NotifierConfiguration.ts delete mode 100644 themes/main/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts delete mode 100644 themes/main/server/src/lib/configuration/schema/RegulationConfiguration.ts delete mode 100644 themes/main/server/src/lib/configuration/schema/SessionConfiguration.spec.ts delete mode 100644 themes/main/server/src/lib/configuration/schema/SessionConfiguration.ts delete mode 100644 themes/main/server/src/lib/configuration/schema/StorageConfiguration.spec.ts delete mode 100644 themes/main/server/src/lib/configuration/schema/StorageConfiguration.ts delete mode 100644 themes/main/server/src/lib/configuration/schema/TotpConfiguration.ts delete mode 100644 themes/main/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts delete mode 100644 themes/main/server/src/lib/connectors/mongo/IMongoClient.d.ts delete mode 100644 themes/main/server/src/lib/connectors/mongo/MongoClient.spec.ts delete mode 100644 themes/main/server/src/lib/connectors/mongo/MongoClient.ts delete mode 100644 themes/main/server/src/lib/connectors/mongo/MongoClientStub.spec.ts delete mode 100644 themes/main/server/src/lib/logging/GlobalLogger.ts delete mode 100644 themes/main/server/src/lib/logging/GlobalLoggerStub.spec.ts delete mode 100644 themes/main/server/src/lib/logging/IGlobalLogger.ts delete mode 100644 themes/main/server/src/lib/logging/IRequestLogger.ts delete mode 100644 themes/main/server/src/lib/logging/RequestLogger.ts delete mode 100644 themes/main/server/src/lib/logging/RequestLoggerStub.spec.ts delete mode 100644 themes/main/server/src/lib/notifiers/AbstractEmailNotifier.ts delete mode 100644 themes/main/server/src/lib/notifiers/EmailNotifier.spec.ts delete mode 100644 themes/main/server/src/lib/notifiers/EmailNotifier.ts delete mode 100644 themes/main/server/src/lib/notifiers/FileSystemNotifier.ts delete mode 100644 themes/main/server/src/lib/notifiers/IMailSender.ts delete mode 100644 themes/main/server/src/lib/notifiers/IMailSenderBuilder.ts delete mode 100644 themes/main/server/src/lib/notifiers/INotifier.ts delete mode 100644 themes/main/server/src/lib/notifiers/MailSender.ts delete mode 100644 themes/main/server/src/lib/notifiers/MailSenderBuilder.spec.ts delete mode 100644 themes/main/server/src/lib/notifiers/MailSenderBuilder.ts delete mode 100644 themes/main/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts delete mode 100644 themes/main/server/src/lib/notifiers/MailSenderStub.spec.ts delete mode 100644 themes/main/server/src/lib/notifiers/NotifierFactory.spec.ts delete mode 100644 themes/main/server/src/lib/notifiers/NotifierFactory.ts delete mode 100644 themes/main/server/src/lib/notifiers/NotifierStub.spec.ts delete mode 100644 themes/main/server/src/lib/notifiers/SmtpNotifier.ts delete mode 100644 themes/main/server/src/lib/regulation/IRegulator.ts delete mode 100644 themes/main/server/src/lib/regulation/Regulator.spec.ts delete mode 100644 themes/main/server/src/lib/regulation/Regulator.ts delete mode 100644 themes/main/server/src/lib/regulation/RegulatorStub.spec.ts delete mode 100644 themes/main/server/src/lib/routes/error/401/get.spec.ts delete mode 100644 themes/main/server/src/lib/routes/error/401/get.ts delete mode 100644 themes/main/server/src/lib/routes/error/403/get.spec.ts delete mode 100644 themes/main/server/src/lib/routes/error/403/get.ts delete mode 100644 themes/main/server/src/lib/routes/error/404/get.spec.ts delete mode 100644 themes/main/server/src/lib/routes/error/404/get.ts delete mode 100644 themes/main/server/src/lib/routes/error/redirector.ts delete mode 100644 themes/main/server/src/lib/routes/firstfactor/get.ts delete mode 100644 themes/main/server/src/lib/routes/firstfactor/post.spec.ts delete mode 100644 themes/main/server/src/lib/routes/firstfactor/post.ts delete mode 100644 themes/main/server/src/lib/routes/loggedin/get.ts delete mode 100644 themes/main/server/src/lib/routes/logout/get.ts delete mode 100644 themes/main/server/src/lib/routes/password-reset/constants.ts delete mode 100644 themes/main/server/src/lib/routes/password-reset/form/post.spec.ts delete mode 100644 themes/main/server/src/lib/routes/password-reset/form/post.ts delete mode 100644 themes/main/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts delete mode 100644 themes/main/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts delete mode 100644 themes/main/server/src/lib/routes/password-reset/request/get.ts delete mode 100644 themes/main/server/src/lib/routes/secondfactor/get.spec.ts delete mode 100644 themes/main/server/src/lib/routes/secondfactor/get.ts delete mode 100644 themes/main/server/src/lib/routes/secondfactor/redirect.spec.ts delete mode 100644 themes/main/server/src/lib/routes/secondfactor/redirect.ts delete mode 100644 themes/main/server/src/lib/routes/secondfactor/totp/constants.ts delete mode 100644 themes/main/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts delete mode 100644 themes/main/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts delete mode 100644 themes/main/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts delete mode 100644 themes/main/server/src/lib/routes/secondfactor/totp/sign/post.ts delete mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts delete mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts delete mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts delete mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts delete mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/register/post.ts delete mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts delete mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/register_request/get.ts delete mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts delete mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/sign/post.ts delete mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts delete mode 100644 themes/main/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts delete mode 100644 themes/main/server/src/lib/routes/verify/access_control.ts delete mode 100644 themes/main/server/src/lib/routes/verify/get.spec.ts delete mode 100644 themes/main/server/src/lib/routes/verify/get.ts delete mode 100644 themes/main/server/src/lib/routes/verify/get_basic_auth.ts delete mode 100644 themes/main/server/src/lib/routes/verify/get_session_cookie.ts delete mode 100644 themes/main/server/src/lib/storage/AuthenticationTraceDocument.d.ts delete mode 100644 themes/main/server/src/lib/storage/CollectionFactoryFactory.ts delete mode 100644 themes/main/server/src/lib/storage/CollectionFactoryStub.spec.ts delete mode 100644 themes/main/server/src/lib/storage/CollectionStub.spec.ts delete mode 100644 themes/main/server/src/lib/storage/ICollection.d.ts delete mode 100644 themes/main/server/src/lib/storage/ICollectionFactory.d.ts delete mode 100644 themes/main/server/src/lib/storage/IUserDataStore.d.ts delete mode 100644 themes/main/server/src/lib/storage/IdentityValidationDocument.d.ts delete mode 100644 themes/main/server/src/lib/storage/TOTPSecretDocument.d.ts delete mode 100644 themes/main/server/src/lib/storage/U2FRegistrationDocument.d.ts delete mode 100644 themes/main/server/src/lib/storage/UserDataStore.spec.ts delete mode 100644 themes/main/server/src/lib/storage/UserDataStore.ts delete mode 100644 themes/main/server/src/lib/storage/UserDataStoreStub.spec.ts delete mode 100644 themes/main/server/src/lib/storage/mongo/MongoCollection.spec.ts delete mode 100644 themes/main/server/src/lib/storage/mongo/MongoCollection.ts delete mode 100644 themes/main/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts delete mode 100644 themes/main/server/src/lib/storage/mongo/MongoCollectionFactory.ts delete mode 100644 themes/main/server/src/lib/storage/nedb/NedbCollection.spec.ts delete mode 100644 themes/main/server/src/lib/storage/nedb/NedbCollection.ts delete mode 100644 themes/main/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts delete mode 100644 themes/main/server/src/lib/storage/nedb/NedbCollectionFactory.ts delete mode 100644 themes/main/server/src/lib/stubs/express.spec.ts delete mode 100644 themes/main/server/src/lib/stubs/ldapjs.spec.ts delete mode 100644 themes/main/server/src/lib/stubs/speakeasy.spec.ts delete mode 100644 themes/main/server/src/lib/stubs/u2f.spec.ts delete mode 100644 themes/main/server/src/lib/utils/HashGenerator.spec.ts delete mode 100644 themes/main/server/src/lib/utils/HashGenerator.ts delete mode 100644 themes/main/server/src/lib/utils/ObjectCloner.ts delete mode 100644 themes/main/server/src/lib/utils/SafeRedirection.spec.ts delete mode 100644 themes/main/server/src/lib/utils/SafeRedirection.ts delete mode 100644 themes/main/server/src/lib/utils/URLDecomposer.spec.ts delete mode 100644 themes/main/server/src/lib/utils/URLDecomposer.ts delete mode 100644 themes/main/server/src/lib/web_server/Configurator.ts delete mode 100644 themes/main/server/src/lib/web_server/RestApi.ts delete mode 100644 themes/main/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts delete mode 100644 themes/main/server/src/lib/web_server/middlewares/WithHeadersLogged.ts delete mode 100644 themes/main/server/test/requests.ts delete mode 100644 themes/main/server/tsconfig.json delete mode 100644 themes/main/server/tslint.json delete mode 100644 themes/main/server/types/.directory delete mode 100644 themes/main/server/types/AuthenticationSession.ts delete mode 100644 themes/main/server/types/Dependencies.ts delete mode 100644 themes/main/server/types/Identity.ts delete mode 100644 themes/main/server/types/TOTPSecret.ts delete mode 100644 themes/main/server/types/U2FRegistration.ts delete mode 100644 themes/main/server/types/dovehash.d.ts delete mode 100644 themes/main/server/types/speakeasy.d.ts mode change 100644 => 100755 themes/matrix/client/src/css/.directory mode change 100644 => 100755 themes/matrix/client/src/css/00-bootstrap.min.css mode change 100644 => 100755 themes/matrix/client/src/css/01-main.css mode change 100644 => 100755 themes/matrix/client/src/css/02-login.css mode change 100644 => 100755 themes/matrix/client/src/css/03-errors.css mode change 100644 => 100755 themes/matrix/client/src/css/03-password-reset-form.css mode change 100644 => 100755 themes/matrix/client/src/css/03-password-reset-request.css mode change 100644 => 100755 themes/matrix/client/src/css/03-totp-register.css mode change 100644 => 100755 themes/matrix/client/src/css/03-u2f-register.css mode change 100644 => 100755 themes/matrix/client/src/img/background.jpg mode change 100644 => 100755 themes/matrix/client/src/img/icon.png mode change 100644 => 100755 themes/matrix/client/src/img/mail.png mode change 100644 => 100755 themes/matrix/client/src/img/matrix_circle_128x128.png mode change 100644 => 100755 themes/matrix/client/src/img/notifications/.directory mode change 100644 => 100755 themes/matrix/client/src/img/notifications/error.png mode change 100644 => 100755 themes/matrix/client/src/img/notifications/info.png mode change 100644 => 100755 themes/matrix/client/src/img/notifications/success.png mode change 100644 => 100755 themes/matrix/client/src/img/notifications/warning.png mode change 100644 => 100755 themes/matrix/client/src/img/padlock.png mode change 100644 => 100755 themes/matrix/client/src/img/password_white.png mode change 100644 => 100755 themes/matrix/client/src/img/pendrive.png mode change 100644 => 100755 themes/matrix/client/src/img/stores/.directory mode change 100644 => 100755 themes/matrix/client/src/img/stores/applestore-badge.svg mode change 100644 => 100755 themes/matrix/client/src/img/stores/googleplay-badge.svg mode change 100644 => 100755 themes/matrix/client/src/img/success.png mode change 100644 => 100755 themes/matrix/client/src/img/user.png mode change 100644 => 100755 themes/matrix/client/src/img/warning.png delete mode 100644 themes/matrix/client/src/index.ts delete mode 100644 themes/matrix/client/src/lib/GetPromised.ts delete mode 100644 themes/matrix/client/src/lib/INotifier.ts delete mode 100644 themes/matrix/client/src/lib/Notifier.ts delete mode 100644 themes/matrix/client/src/lib/QueryParametersRetriever.ts delete mode 100644 themes/matrix/client/src/lib/SafeRedirect.ts delete mode 100644 themes/matrix/client/src/lib/firstfactor/FirstFactorValidator.ts delete mode 100644 themes/matrix/client/src/lib/firstfactor/UISelectors.ts delete mode 100644 themes/matrix/client/src/lib/firstfactor/index.ts delete mode 100644 themes/matrix/client/src/lib/reset-password/constants.ts delete mode 100644 themes/matrix/client/src/lib/reset-password/reset-password-form.ts delete mode 100644 themes/matrix/client/src/lib/reset-password/reset-password-request.ts delete mode 100644 themes/matrix/client/src/lib/secondfactor/TOTPValidator.ts delete mode 100644 themes/matrix/client/src/lib/secondfactor/U2FValidator.ts delete mode 100644 themes/matrix/client/src/lib/secondfactor/constants.ts delete mode 100644 themes/matrix/client/src/lib/secondfactor/index.ts delete mode 100644 themes/matrix/client/src/lib/totp-register/totp-register.ts delete mode 100644 themes/matrix/client/src/lib/totp-register/ui-selector.ts delete mode 100644 themes/matrix/client/src/lib/u2f-register/u2f-register.ts mode change 100644 => 100755 themes/matrix/client/src/thirdparties/matrix.js mode change 100644 => 100755 themes/matrix/client/src/thirdparties/qrcode.min.js mode change 100644 => 100755 themes/matrix/client/src/thirdparties/u2f-api.js delete mode 100644 themes/matrix/client/test/Notifier.test.ts delete mode 100644 themes/matrix/client/test/firstfactor/FirstFactorValidator.test.ts delete mode 100644 themes/matrix/client/test/mocks/NotifierStub.ts delete mode 100644 themes/matrix/client/test/mocks/jquery.ts delete mode 100644 themes/matrix/client/test/mocks/u2f-api.ts delete mode 100644 themes/matrix/client/test/secondfactor/TOTPValidator.test.ts delete mode 100644 themes/matrix/client/test/totp-register/totp-register.test.ts delete mode 100644 themes/matrix/client/tsconfig.json delete mode 100644 themes/matrix/client/tslint.json mode change 100644 => 100755 themes/matrix/server/.directory delete mode 100755 themes/matrix/server/src/index.ts delete mode 100644 themes/matrix/server/src/lib/.directory delete mode 100644 themes/matrix/server/src/lib/AuthenticationSessionHandler.ts delete mode 100644 themes/matrix/server/src/lib/ErrorReplies.ts delete mode 100644 themes/matrix/server/src/lib/Exceptions.ts delete mode 100644 themes/matrix/server/src/lib/FirstFactorValidator.ts delete mode 100644 themes/matrix/server/src/lib/IdentityCheckMiddleware.spec.ts delete mode 100644 themes/matrix/server/src/lib/IdentityCheckMiddleware.ts delete mode 100644 themes/matrix/server/src/lib/IdentityCheckPreValidationTemplate.ts delete mode 100644 themes/matrix/server/src/lib/IdentityValidable.ts delete mode 100644 themes/matrix/server/src/lib/IdentityValidableStub.spec.ts delete mode 100644 themes/matrix/server/src/lib/Server.spec.ts delete mode 100644 themes/matrix/server/src/lib/Server.ts delete mode 100644 themes/matrix/server/src/lib/ServerVariables.ts delete mode 100644 themes/matrix/server/src/lib/ServerVariablesInitializer.ts delete mode 100644 themes/matrix/server/src/lib/ServerVariablesMockBuilder.spec.ts delete mode 100644 themes/matrix/server/src/lib/authentication/Level.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/GroupsAndEmails.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/IUsersDatabase.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/file/FileUsersDatabase.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/file/ReadWriteQueue.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/ISession.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/ISessionFactory.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/SafeSession.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/Sanitizer.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/Session.spec.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/Session.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/SessionFactory.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/connector/Connector.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/connector/IConnector.ts delete mode 100644 themes/matrix/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts delete mode 100644 themes/matrix/server/src/lib/authentication/totp/ITotpHandler.ts delete mode 100644 themes/matrix/server/src/lib/authentication/totp/TotpHandler.spec.ts delete mode 100644 themes/matrix/server/src/lib/authentication/totp/TotpHandler.ts delete mode 100644 themes/matrix/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts delete mode 100644 themes/matrix/server/src/lib/authentication/u2f/IU2fHandler.ts delete mode 100644 themes/matrix/server/src/lib/authentication/u2f/U2fHandler.ts delete mode 100644 themes/matrix/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts delete mode 100644 themes/matrix/server/src/lib/authorization/Authorizer.spec.ts delete mode 100644 themes/matrix/server/src/lib/authorization/Authorizer.ts delete mode 100644 themes/matrix/server/src/lib/authorization/AuthorizerStub.spec.ts delete mode 100644 themes/matrix/server/src/lib/authorization/IAuthorizer.ts delete mode 100644 themes/matrix/server/src/lib/authorization/Level.ts delete mode 100644 themes/matrix/server/src/lib/authorization/MultipleDomainMatcher.ts delete mode 100644 themes/matrix/server/src/lib/authorization/Object.ts delete mode 100644 themes/matrix/server/src/lib/authorization/Subject.ts delete mode 100644 themes/matrix/server/src/lib/configuration/ConfigurationParser.spec.ts delete mode 100644 themes/matrix/server/src/lib/configuration/ConfigurationParser.ts delete mode 100644 themes/matrix/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts delete mode 100644 themes/matrix/server/src/lib/configuration/SessionConfigurationBuilder.ts delete mode 100644 themes/matrix/server/src/lib/configuration/schema/AclConfiguration.spec.ts delete mode 100644 themes/matrix/server/src/lib/configuration/schema/AclConfiguration.ts delete mode 100644 themes/matrix/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts delete mode 100644 themes/matrix/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts delete mode 100644 themes/matrix/server/src/lib/configuration/schema/Configuration.ts delete mode 100644 themes/matrix/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts delete mode 100644 themes/matrix/server/src/lib/configuration/schema/LdapConfiguration.spec.ts delete mode 100644 themes/matrix/server/src/lib/configuration/schema/LdapConfiguration.ts delete mode 100644 themes/matrix/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts delete mode 100644 themes/matrix/server/src/lib/configuration/schema/NotifierConfiguration.ts delete mode 100644 themes/matrix/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts delete mode 100644 themes/matrix/server/src/lib/configuration/schema/RegulationConfiguration.ts delete mode 100644 themes/matrix/server/src/lib/configuration/schema/SessionConfiguration.spec.ts delete mode 100644 themes/matrix/server/src/lib/configuration/schema/SessionConfiguration.ts delete mode 100644 themes/matrix/server/src/lib/configuration/schema/StorageConfiguration.spec.ts delete mode 100644 themes/matrix/server/src/lib/configuration/schema/StorageConfiguration.ts delete mode 100644 themes/matrix/server/src/lib/configuration/schema/TotpConfiguration.ts delete mode 100644 themes/matrix/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts delete mode 100644 themes/matrix/server/src/lib/connectors/mongo/IMongoClient.d.ts delete mode 100644 themes/matrix/server/src/lib/connectors/mongo/MongoClient.spec.ts delete mode 100644 themes/matrix/server/src/lib/connectors/mongo/MongoClient.ts delete mode 100644 themes/matrix/server/src/lib/connectors/mongo/MongoClientStub.spec.ts delete mode 100644 themes/matrix/server/src/lib/logging/GlobalLogger.ts delete mode 100644 themes/matrix/server/src/lib/logging/GlobalLoggerStub.spec.ts delete mode 100644 themes/matrix/server/src/lib/logging/IGlobalLogger.ts delete mode 100644 themes/matrix/server/src/lib/logging/IRequestLogger.ts delete mode 100644 themes/matrix/server/src/lib/logging/RequestLogger.ts delete mode 100644 themes/matrix/server/src/lib/logging/RequestLoggerStub.spec.ts delete mode 100644 themes/matrix/server/src/lib/notifiers/AbstractEmailNotifier.ts delete mode 100644 themes/matrix/server/src/lib/notifiers/EmailNotifier.spec.ts delete mode 100644 themes/matrix/server/src/lib/notifiers/EmailNotifier.ts delete mode 100644 themes/matrix/server/src/lib/notifiers/FileSystemNotifier.ts delete mode 100644 themes/matrix/server/src/lib/notifiers/IMailSender.ts delete mode 100644 themes/matrix/server/src/lib/notifiers/IMailSenderBuilder.ts delete mode 100644 themes/matrix/server/src/lib/notifiers/INotifier.ts delete mode 100644 themes/matrix/server/src/lib/notifiers/MailSender.ts delete mode 100644 themes/matrix/server/src/lib/notifiers/MailSenderBuilder.spec.ts delete mode 100644 themes/matrix/server/src/lib/notifiers/MailSenderBuilder.ts delete mode 100644 themes/matrix/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts delete mode 100644 themes/matrix/server/src/lib/notifiers/MailSenderStub.spec.ts delete mode 100644 themes/matrix/server/src/lib/notifiers/NotifierFactory.spec.ts delete mode 100644 themes/matrix/server/src/lib/notifiers/NotifierFactory.ts delete mode 100644 themes/matrix/server/src/lib/notifiers/NotifierStub.spec.ts delete mode 100644 themes/matrix/server/src/lib/notifiers/SmtpNotifier.ts delete mode 100644 themes/matrix/server/src/lib/regulation/IRegulator.ts delete mode 100644 themes/matrix/server/src/lib/regulation/Regulator.spec.ts delete mode 100644 themes/matrix/server/src/lib/regulation/Regulator.ts delete mode 100644 themes/matrix/server/src/lib/regulation/RegulatorStub.spec.ts delete mode 100644 themes/matrix/server/src/lib/routes/error/401/get.spec.ts delete mode 100644 themes/matrix/server/src/lib/routes/error/401/get.ts delete mode 100644 themes/matrix/server/src/lib/routes/error/403/get.spec.ts delete mode 100644 themes/matrix/server/src/lib/routes/error/403/get.ts delete mode 100644 themes/matrix/server/src/lib/routes/error/404/get.spec.ts delete mode 100644 themes/matrix/server/src/lib/routes/error/404/get.ts delete mode 100644 themes/matrix/server/src/lib/routes/error/redirector.ts delete mode 100644 themes/matrix/server/src/lib/routes/firstfactor/get.ts delete mode 100644 themes/matrix/server/src/lib/routes/firstfactor/post.spec.ts delete mode 100644 themes/matrix/server/src/lib/routes/firstfactor/post.ts delete mode 100644 themes/matrix/server/src/lib/routes/loggedin/get.ts delete mode 100644 themes/matrix/server/src/lib/routes/logout/get.ts delete mode 100644 themes/matrix/server/src/lib/routes/password-reset/constants.ts delete mode 100644 themes/matrix/server/src/lib/routes/password-reset/form/post.spec.ts delete mode 100644 themes/matrix/server/src/lib/routes/password-reset/form/post.ts delete mode 100644 themes/matrix/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts delete mode 100644 themes/matrix/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts delete mode 100644 themes/matrix/server/src/lib/routes/password-reset/request/get.ts delete mode 100644 themes/matrix/server/src/lib/routes/secondfactor/get.spec.ts delete mode 100644 themes/matrix/server/src/lib/routes/secondfactor/get.ts delete mode 100644 themes/matrix/server/src/lib/routes/secondfactor/redirect.spec.ts delete mode 100644 themes/matrix/server/src/lib/routes/secondfactor/redirect.ts delete mode 100644 themes/matrix/server/src/lib/routes/secondfactor/totp/constants.ts delete mode 100644 themes/matrix/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts delete mode 100644 themes/matrix/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts delete mode 100644 themes/matrix/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts delete mode 100644 themes/matrix/server/src/lib/routes/secondfactor/totp/sign/post.ts delete mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts delete mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts delete mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts delete mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts delete mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/register/post.ts delete mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts delete mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/register_request/get.ts delete mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts delete mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/sign/post.ts delete mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts delete mode 100644 themes/matrix/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts delete mode 100644 themes/matrix/server/src/lib/routes/verify/access_control.ts delete mode 100644 themes/matrix/server/src/lib/routes/verify/get.spec.ts delete mode 100644 themes/matrix/server/src/lib/routes/verify/get.ts delete mode 100644 themes/matrix/server/src/lib/routes/verify/get_basic_auth.ts delete mode 100644 themes/matrix/server/src/lib/routes/verify/get_session_cookie.ts delete mode 100644 themes/matrix/server/src/lib/storage/AuthenticationTraceDocument.d.ts delete mode 100644 themes/matrix/server/src/lib/storage/CollectionFactoryFactory.ts delete mode 100644 themes/matrix/server/src/lib/storage/CollectionFactoryStub.spec.ts delete mode 100644 themes/matrix/server/src/lib/storage/CollectionStub.spec.ts delete mode 100644 themes/matrix/server/src/lib/storage/ICollection.d.ts delete mode 100644 themes/matrix/server/src/lib/storage/ICollectionFactory.d.ts delete mode 100644 themes/matrix/server/src/lib/storage/IUserDataStore.d.ts delete mode 100644 themes/matrix/server/src/lib/storage/IdentityValidationDocument.d.ts delete mode 100644 themes/matrix/server/src/lib/storage/TOTPSecretDocument.d.ts delete mode 100644 themes/matrix/server/src/lib/storage/U2FRegistrationDocument.d.ts delete mode 100644 themes/matrix/server/src/lib/storage/UserDataStore.spec.ts delete mode 100644 themes/matrix/server/src/lib/storage/UserDataStore.ts delete mode 100644 themes/matrix/server/src/lib/storage/UserDataStoreStub.spec.ts delete mode 100644 themes/matrix/server/src/lib/storage/mongo/MongoCollection.spec.ts delete mode 100644 themes/matrix/server/src/lib/storage/mongo/MongoCollection.ts delete mode 100644 themes/matrix/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts delete mode 100644 themes/matrix/server/src/lib/storage/mongo/MongoCollectionFactory.ts delete mode 100644 themes/matrix/server/src/lib/storage/nedb/NedbCollection.spec.ts delete mode 100644 themes/matrix/server/src/lib/storage/nedb/NedbCollection.ts delete mode 100644 themes/matrix/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts delete mode 100644 themes/matrix/server/src/lib/storage/nedb/NedbCollectionFactory.ts delete mode 100644 themes/matrix/server/src/lib/stubs/express.spec.ts delete mode 100644 themes/matrix/server/src/lib/stubs/ldapjs.spec.ts delete mode 100644 themes/matrix/server/src/lib/stubs/speakeasy.spec.ts delete mode 100644 themes/matrix/server/src/lib/stubs/u2f.spec.ts delete mode 100644 themes/matrix/server/src/lib/utils/HashGenerator.spec.ts delete mode 100644 themes/matrix/server/src/lib/utils/HashGenerator.ts delete mode 100644 themes/matrix/server/src/lib/utils/ObjectCloner.ts delete mode 100644 themes/matrix/server/src/lib/utils/SafeRedirection.spec.ts delete mode 100644 themes/matrix/server/src/lib/utils/SafeRedirection.ts delete mode 100644 themes/matrix/server/src/lib/utils/URLDecomposer.spec.ts delete mode 100644 themes/matrix/server/src/lib/utils/URLDecomposer.ts delete mode 100644 themes/matrix/server/src/lib/web_server/Configurator.ts delete mode 100644 themes/matrix/server/src/lib/web_server/RestApi.ts delete mode 100644 themes/matrix/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts delete mode 100644 themes/matrix/server/src/lib/web_server/middlewares/WithHeadersLogged.ts mode change 100644 => 100755 themes/matrix/server/src/resources/email-template.ejs mode change 100644 => 100755 themes/matrix/server/src/views/already-logged-in.pug mode change 100644 => 100755 themes/matrix/server/src/views/errors/.directory mode change 100644 => 100755 themes/matrix/server/src/views/errors/401.pug mode change 100644 => 100755 themes/matrix/server/src/views/errors/403.pug mode change 100644 => 100755 themes/matrix/server/src/views/errors/404.pug mode change 100644 => 100755 themes/matrix/server/src/views/firstfactor.pug mode change 100644 => 100755 themes/matrix/server/src/views/layout/layout.pug mode change 100644 => 100755 themes/matrix/server/src/views/need-identity-validation.pug mode change 100644 => 100755 themes/matrix/server/src/views/password-reset-form.pug mode change 100644 => 100755 themes/matrix/server/src/views/password-reset-request.pug mode change 100644 => 100755 themes/matrix/server/src/views/secondfactor.pug mode change 100644 => 100755 themes/matrix/server/src/views/totp-register.pug mode change 100644 => 100755 themes/matrix/server/src/views/u2f-register.pug delete mode 100644 themes/matrix/server/test/requests.ts delete mode 100644 themes/matrix/server/tsconfig.json delete mode 100644 themes/matrix/server/tslint.json delete mode 100644 themes/matrix/server/types/.directory delete mode 100644 themes/matrix/server/types/AuthenticationSession.ts delete mode 100644 themes/matrix/server/types/Dependencies.ts delete mode 100644 themes/matrix/server/types/Identity.ts delete mode 100644 themes/matrix/server/types/TOTPSecret.ts delete mode 100644 themes/matrix/server/types/U2FRegistration.ts delete mode 100644 themes/matrix/server/types/dovehash.d.ts delete mode 100644 themes/matrix/server/types/speakeasy.d.ts mode change 100644 => 100755 themes/squares/client/src/css/.directory mode change 100644 => 100755 themes/squares/client/src/css/00-bootstrap.min.css mode change 100644 => 100755 themes/squares/client/src/css/01-main.css mode change 100644 => 100755 themes/squares/client/src/css/02-login.css mode change 100644 => 100755 themes/squares/client/src/css/03-errors.css mode change 100644 => 100755 themes/squares/client/src/css/03-password-reset-form.css mode change 100644 => 100755 themes/squares/client/src/css/03-password-reset-request.css mode change 100644 => 100755 themes/squares/client/src/css/03-totp-register.css mode change 100644 => 100755 themes/squares/client/src/css/03-u2f-register.css mode change 100644 => 100755 themes/squares/client/src/img/LargeTriangles.svg mode change 100644 => 100755 themes/squares/client/src/img/RandomizedPattern.svg mode change 100644 => 100755 themes/squares/client/src/img/background.jpg mode change 100644 => 100755 themes/squares/client/src/img/background.svg mode change 100644 => 100755 themes/squares/client/src/img/icon.png mode change 100644 => 100755 themes/squares/client/src/img/mail.png mode change 100644 => 100755 themes/squares/client/src/img/matrix_circle_128x128.png mode change 100644 => 100755 themes/squares/client/src/img/notifications/.directory mode change 100644 => 100755 themes/squares/client/src/img/notifications/error.png mode change 100644 => 100755 themes/squares/client/src/img/notifications/info.png mode change 100644 => 100755 themes/squares/client/src/img/notifications/success.png mode change 100644 => 100755 themes/squares/client/src/img/notifications/warning.png mode change 100644 => 100755 themes/squares/client/src/img/padlock.png mode change 100644 => 100755 themes/squares/client/src/img/password_white.png mode change 100644 => 100755 themes/squares/client/src/img/pendrive.png mode change 100644 => 100755 themes/squares/client/src/img/sharingan.png mode change 100644 => 100755 themes/squares/client/src/img/stores/.directory mode change 100644 => 100755 themes/squares/client/src/img/stores/applestore-badge.svg mode change 100644 => 100755 themes/squares/client/src/img/stores/googleplay-badge.svg mode change 100644 => 100755 themes/squares/client/src/img/success.png mode change 100644 => 100755 themes/squares/client/src/img/user.png mode change 100644 => 100755 themes/squares/client/src/img/warning.png delete mode 100644 themes/squares/client/src/index.ts delete mode 100644 themes/squares/client/src/lib/GetPromised.ts delete mode 100644 themes/squares/client/src/lib/INotifier.ts delete mode 100644 themes/squares/client/src/lib/Notifier.ts delete mode 100644 themes/squares/client/src/lib/QueryParametersRetriever.ts delete mode 100644 themes/squares/client/src/lib/SafeRedirect.ts delete mode 100644 themes/squares/client/src/lib/firstfactor/FirstFactorValidator.ts delete mode 100644 themes/squares/client/src/lib/firstfactor/UISelectors.ts delete mode 100644 themes/squares/client/src/lib/firstfactor/index.ts delete mode 100644 themes/squares/client/src/lib/reset-password/constants.ts delete mode 100644 themes/squares/client/src/lib/reset-password/reset-password-form.ts delete mode 100644 themes/squares/client/src/lib/reset-password/reset-password-request.ts delete mode 100644 themes/squares/client/src/lib/secondfactor/TOTPValidator.ts delete mode 100644 themes/squares/client/src/lib/secondfactor/U2FValidator.ts delete mode 100644 themes/squares/client/src/lib/secondfactor/constants.ts delete mode 100644 themes/squares/client/src/lib/secondfactor/index.ts delete mode 100644 themes/squares/client/src/lib/totp-register/totp-register.ts delete mode 100644 themes/squares/client/src/lib/totp-register/ui-selector.ts delete mode 100644 themes/squares/client/src/lib/u2f-register/u2f-register.ts mode change 100644 => 100755 themes/squares/client/src/thirdparties/qrcode.min.js mode change 100644 => 100755 themes/squares/client/src/thirdparties/u2f-api.js delete mode 100644 themes/squares/client/test/Notifier.test.ts delete mode 100644 themes/squares/client/test/firstfactor/FirstFactorValidator.test.ts delete mode 100644 themes/squares/client/test/mocks/NotifierStub.ts delete mode 100644 themes/squares/client/test/mocks/jquery.ts delete mode 100644 themes/squares/client/test/mocks/u2f-api.ts delete mode 100644 themes/squares/client/test/secondfactor/TOTPValidator.test.ts delete mode 100644 themes/squares/client/test/totp-register/totp-register.test.ts delete mode 100644 themes/squares/client/tsconfig.json delete mode 100644 themes/squares/client/tslint.json mode change 100644 => 100755 themes/squares/server/.directory delete mode 100755 themes/squares/server/src/index.ts delete mode 100644 themes/squares/server/src/lib/.directory delete mode 100644 themes/squares/server/src/lib/AuthenticationSessionHandler.ts delete mode 100644 themes/squares/server/src/lib/ErrorReplies.ts delete mode 100644 themes/squares/server/src/lib/Exceptions.ts delete mode 100644 themes/squares/server/src/lib/FirstFactorValidator.ts delete mode 100644 themes/squares/server/src/lib/IdentityCheckMiddleware.spec.ts delete mode 100644 themes/squares/server/src/lib/IdentityCheckMiddleware.ts delete mode 100644 themes/squares/server/src/lib/IdentityCheckPreValidationTemplate.ts delete mode 100644 themes/squares/server/src/lib/IdentityValidable.ts delete mode 100644 themes/squares/server/src/lib/IdentityValidableStub.spec.ts delete mode 100644 themes/squares/server/src/lib/Server.spec.ts delete mode 100644 themes/squares/server/src/lib/Server.ts delete mode 100644 themes/squares/server/src/lib/ServerVariables.ts delete mode 100644 themes/squares/server/src/lib/ServerVariablesInitializer.ts delete mode 100644 themes/squares/server/src/lib/ServerVariablesMockBuilder.spec.ts delete mode 100644 themes/squares/server/src/lib/authentication/Level.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/GroupsAndEmails.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/IUsersDatabase.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/file/FileUsersDatabase.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/file/ReadWriteQueue.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/ISession.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/ISessionFactory.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/SafeSession.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/Sanitizer.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/Session.spec.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/Session.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/SessionFactory.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/connector/Connector.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/connector/IConnector.ts delete mode 100644 themes/squares/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts delete mode 100644 themes/squares/server/src/lib/authentication/totp/ITotpHandler.ts delete mode 100644 themes/squares/server/src/lib/authentication/totp/TotpHandler.spec.ts delete mode 100644 themes/squares/server/src/lib/authentication/totp/TotpHandler.ts delete mode 100644 themes/squares/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts delete mode 100644 themes/squares/server/src/lib/authentication/u2f/IU2fHandler.ts delete mode 100644 themes/squares/server/src/lib/authentication/u2f/U2fHandler.ts delete mode 100644 themes/squares/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts delete mode 100644 themes/squares/server/src/lib/authorization/Authorizer.spec.ts delete mode 100644 themes/squares/server/src/lib/authorization/Authorizer.ts delete mode 100644 themes/squares/server/src/lib/authorization/AuthorizerStub.spec.ts delete mode 100644 themes/squares/server/src/lib/authorization/IAuthorizer.ts delete mode 100644 themes/squares/server/src/lib/authorization/Level.ts delete mode 100644 themes/squares/server/src/lib/authorization/MultipleDomainMatcher.ts delete mode 100644 themes/squares/server/src/lib/authorization/Object.ts delete mode 100644 themes/squares/server/src/lib/authorization/Subject.ts delete mode 100644 themes/squares/server/src/lib/configuration/ConfigurationParser.spec.ts delete mode 100644 themes/squares/server/src/lib/configuration/ConfigurationParser.ts delete mode 100644 themes/squares/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts delete mode 100644 themes/squares/server/src/lib/configuration/SessionConfigurationBuilder.ts delete mode 100644 themes/squares/server/src/lib/configuration/schema/AclConfiguration.spec.ts delete mode 100644 themes/squares/server/src/lib/configuration/schema/AclConfiguration.ts delete mode 100644 themes/squares/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts delete mode 100644 themes/squares/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts delete mode 100644 themes/squares/server/src/lib/configuration/schema/Configuration.ts delete mode 100644 themes/squares/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts delete mode 100644 themes/squares/server/src/lib/configuration/schema/LdapConfiguration.spec.ts delete mode 100644 themes/squares/server/src/lib/configuration/schema/LdapConfiguration.ts delete mode 100644 themes/squares/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts delete mode 100644 themes/squares/server/src/lib/configuration/schema/NotifierConfiguration.ts delete mode 100644 themes/squares/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts delete mode 100644 themes/squares/server/src/lib/configuration/schema/RegulationConfiguration.ts delete mode 100644 themes/squares/server/src/lib/configuration/schema/SessionConfiguration.spec.ts delete mode 100644 themes/squares/server/src/lib/configuration/schema/SessionConfiguration.ts delete mode 100644 themes/squares/server/src/lib/configuration/schema/StorageConfiguration.spec.ts delete mode 100644 themes/squares/server/src/lib/configuration/schema/StorageConfiguration.ts delete mode 100644 themes/squares/server/src/lib/configuration/schema/TotpConfiguration.ts delete mode 100644 themes/squares/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts delete mode 100644 themes/squares/server/src/lib/connectors/mongo/IMongoClient.d.ts delete mode 100644 themes/squares/server/src/lib/connectors/mongo/MongoClient.spec.ts delete mode 100644 themes/squares/server/src/lib/connectors/mongo/MongoClient.ts delete mode 100644 themes/squares/server/src/lib/connectors/mongo/MongoClientStub.spec.ts delete mode 100644 themes/squares/server/src/lib/logging/GlobalLogger.ts delete mode 100644 themes/squares/server/src/lib/logging/GlobalLoggerStub.spec.ts delete mode 100644 themes/squares/server/src/lib/logging/IGlobalLogger.ts delete mode 100644 themes/squares/server/src/lib/logging/IRequestLogger.ts delete mode 100644 themes/squares/server/src/lib/logging/RequestLogger.ts delete mode 100644 themes/squares/server/src/lib/logging/RequestLoggerStub.spec.ts delete mode 100644 themes/squares/server/src/lib/notifiers/AbstractEmailNotifier.ts delete mode 100644 themes/squares/server/src/lib/notifiers/EmailNotifier.spec.ts delete mode 100644 themes/squares/server/src/lib/notifiers/EmailNotifier.ts delete mode 100644 themes/squares/server/src/lib/notifiers/FileSystemNotifier.ts delete mode 100644 themes/squares/server/src/lib/notifiers/IMailSender.ts delete mode 100644 themes/squares/server/src/lib/notifiers/IMailSenderBuilder.ts delete mode 100644 themes/squares/server/src/lib/notifiers/INotifier.ts delete mode 100644 themes/squares/server/src/lib/notifiers/MailSender.ts delete mode 100644 themes/squares/server/src/lib/notifiers/MailSenderBuilder.spec.ts delete mode 100644 themes/squares/server/src/lib/notifiers/MailSenderBuilder.ts delete mode 100644 themes/squares/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts delete mode 100644 themes/squares/server/src/lib/notifiers/MailSenderStub.spec.ts delete mode 100644 themes/squares/server/src/lib/notifiers/NotifierFactory.spec.ts delete mode 100644 themes/squares/server/src/lib/notifiers/NotifierFactory.ts delete mode 100644 themes/squares/server/src/lib/notifiers/NotifierStub.spec.ts delete mode 100644 themes/squares/server/src/lib/notifiers/SmtpNotifier.ts delete mode 100644 themes/squares/server/src/lib/regulation/IRegulator.ts delete mode 100644 themes/squares/server/src/lib/regulation/Regulator.spec.ts delete mode 100644 themes/squares/server/src/lib/regulation/Regulator.ts delete mode 100644 themes/squares/server/src/lib/regulation/RegulatorStub.spec.ts delete mode 100644 themes/squares/server/src/lib/routes/error/401/get.spec.ts delete mode 100644 themes/squares/server/src/lib/routes/error/401/get.ts delete mode 100644 themes/squares/server/src/lib/routes/error/403/get.spec.ts delete mode 100644 themes/squares/server/src/lib/routes/error/403/get.ts delete mode 100644 themes/squares/server/src/lib/routes/error/404/get.spec.ts delete mode 100644 themes/squares/server/src/lib/routes/error/404/get.ts delete mode 100644 themes/squares/server/src/lib/routes/error/redirector.ts delete mode 100644 themes/squares/server/src/lib/routes/firstfactor/get.ts delete mode 100644 themes/squares/server/src/lib/routes/firstfactor/post.spec.ts delete mode 100644 themes/squares/server/src/lib/routes/firstfactor/post.ts delete mode 100644 themes/squares/server/src/lib/routes/loggedin/get.ts delete mode 100644 themes/squares/server/src/lib/routes/logout/get.ts delete mode 100644 themes/squares/server/src/lib/routes/password-reset/constants.ts delete mode 100644 themes/squares/server/src/lib/routes/password-reset/form/post.spec.ts delete mode 100644 themes/squares/server/src/lib/routes/password-reset/form/post.ts delete mode 100644 themes/squares/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts delete mode 100644 themes/squares/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts delete mode 100644 themes/squares/server/src/lib/routes/password-reset/request/get.ts delete mode 100644 themes/squares/server/src/lib/routes/secondfactor/get.spec.ts delete mode 100644 themes/squares/server/src/lib/routes/secondfactor/get.ts delete mode 100644 themes/squares/server/src/lib/routes/secondfactor/redirect.spec.ts delete mode 100644 themes/squares/server/src/lib/routes/secondfactor/redirect.ts delete mode 100644 themes/squares/server/src/lib/routes/secondfactor/totp/constants.ts delete mode 100644 themes/squares/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts delete mode 100644 themes/squares/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts delete mode 100644 themes/squares/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts delete mode 100644 themes/squares/server/src/lib/routes/secondfactor/totp/sign/post.ts delete mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts delete mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts delete mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts delete mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts delete mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/register/post.ts delete mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts delete mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/register_request/get.ts delete mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts delete mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/sign/post.ts delete mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts delete mode 100644 themes/squares/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts delete mode 100644 themes/squares/server/src/lib/routes/verify/access_control.ts delete mode 100644 themes/squares/server/src/lib/routes/verify/get.spec.ts delete mode 100644 themes/squares/server/src/lib/routes/verify/get.ts delete mode 100644 themes/squares/server/src/lib/routes/verify/get_basic_auth.ts delete mode 100644 themes/squares/server/src/lib/routes/verify/get_session_cookie.ts delete mode 100644 themes/squares/server/src/lib/storage/AuthenticationTraceDocument.d.ts delete mode 100644 themes/squares/server/src/lib/storage/CollectionFactoryFactory.ts delete mode 100644 themes/squares/server/src/lib/storage/CollectionFactoryStub.spec.ts delete mode 100644 themes/squares/server/src/lib/storage/CollectionStub.spec.ts delete mode 100644 themes/squares/server/src/lib/storage/ICollection.d.ts delete mode 100644 themes/squares/server/src/lib/storage/ICollectionFactory.d.ts delete mode 100644 themes/squares/server/src/lib/storage/IUserDataStore.d.ts delete mode 100644 themes/squares/server/src/lib/storage/IdentityValidationDocument.d.ts delete mode 100644 themes/squares/server/src/lib/storage/TOTPSecretDocument.d.ts delete mode 100644 themes/squares/server/src/lib/storage/U2FRegistrationDocument.d.ts delete mode 100644 themes/squares/server/src/lib/storage/UserDataStore.spec.ts delete mode 100644 themes/squares/server/src/lib/storage/UserDataStore.ts delete mode 100644 themes/squares/server/src/lib/storage/UserDataStoreStub.spec.ts delete mode 100644 themes/squares/server/src/lib/storage/mongo/MongoCollection.spec.ts delete mode 100644 themes/squares/server/src/lib/storage/mongo/MongoCollection.ts delete mode 100644 themes/squares/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts delete mode 100644 themes/squares/server/src/lib/storage/mongo/MongoCollectionFactory.ts delete mode 100644 themes/squares/server/src/lib/storage/nedb/NedbCollection.spec.ts delete mode 100644 themes/squares/server/src/lib/storage/nedb/NedbCollection.ts delete mode 100644 themes/squares/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts delete mode 100644 themes/squares/server/src/lib/storage/nedb/NedbCollectionFactory.ts delete mode 100644 themes/squares/server/src/lib/stubs/express.spec.ts delete mode 100644 themes/squares/server/src/lib/stubs/ldapjs.spec.ts delete mode 100644 themes/squares/server/src/lib/stubs/speakeasy.spec.ts delete mode 100644 themes/squares/server/src/lib/stubs/u2f.spec.ts delete mode 100644 themes/squares/server/src/lib/utils/HashGenerator.spec.ts delete mode 100644 themes/squares/server/src/lib/utils/HashGenerator.ts delete mode 100644 themes/squares/server/src/lib/utils/ObjectCloner.ts delete mode 100644 themes/squares/server/src/lib/utils/SafeRedirection.spec.ts delete mode 100644 themes/squares/server/src/lib/utils/SafeRedirection.ts delete mode 100644 themes/squares/server/src/lib/utils/URLDecomposer.spec.ts delete mode 100644 themes/squares/server/src/lib/utils/URLDecomposer.ts delete mode 100644 themes/squares/server/src/lib/web_server/Configurator.ts delete mode 100644 themes/squares/server/src/lib/web_server/RestApi.ts delete mode 100644 themes/squares/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts delete mode 100644 themes/squares/server/src/lib/web_server/middlewares/WithHeadersLogged.ts mode change 100644 => 100755 themes/squares/server/src/resources/email-template.ejs mode change 100644 => 100755 themes/squares/server/src/views/already-logged-in.pug mode change 100644 => 100755 themes/squares/server/src/views/errors/.directory mode change 100644 => 100755 themes/squares/server/src/views/errors/401.pug mode change 100644 => 100755 themes/squares/server/src/views/errors/403.pug mode change 100644 => 100755 themes/squares/server/src/views/errors/404.pug mode change 100644 => 100755 themes/squares/server/src/views/firstfactor.pug mode change 100644 => 100755 themes/squares/server/src/views/layout/layout.pug mode change 100644 => 100755 themes/squares/server/src/views/need-identity-validation.pug mode change 100644 => 100755 themes/squares/server/src/views/password-reset-form.pug mode change 100644 => 100755 themes/squares/server/src/views/password-reset-request.pug mode change 100644 => 100755 themes/squares/server/src/views/secondfactor.pug mode change 100644 => 100755 themes/squares/server/src/views/totp-register.pug mode change 100644 => 100755 themes/squares/server/src/views/u2f-register.pug delete mode 100644 themes/squares/server/test/requests.ts delete mode 100644 themes/squares/server/tsconfig.json delete mode 100644 themes/squares/server/tslint.json delete mode 100644 themes/squares/server/types/.directory delete mode 100644 themes/squares/server/types/AuthenticationSession.ts delete mode 100644 themes/squares/server/types/Dependencies.ts delete mode 100644 themes/squares/server/types/Identity.ts delete mode 100644 themes/squares/server/types/TOTPSecret.ts delete mode 100644 themes/squares/server/types/U2FRegistration.ts delete mode 100644 themes/squares/server/types/dovehash.d.ts delete mode 100644 themes/squares/server/types/speakeasy.d.ts mode change 100644 => 100755 themes/triangles/client/src/.directory mode change 100644 => 100755 themes/triangles/client/src/css/.directory mode change 100644 => 100755 themes/triangles/client/src/css/00-bootstrap.min.css mode change 100644 => 100755 themes/triangles/client/src/css/01-main.css mode change 100644 => 100755 themes/triangles/client/src/css/02-login.css mode change 100644 => 100755 themes/triangles/client/src/css/03-errors.css mode change 100644 => 100755 themes/triangles/client/src/css/03-password-reset-form.css mode change 100644 => 100755 themes/triangles/client/src/css/03-password-reset-request.css mode change 100644 => 100755 themes/triangles/client/src/css/03-totp-register.css mode change 100644 => 100755 themes/triangles/client/src/css/03-u2f-register.css mode change 100644 => 100755 themes/triangles/client/src/img/LargeTriangles.svg mode change 100644 => 100755 themes/triangles/client/src/img/background.jpg mode change 100644 => 100755 themes/triangles/client/src/img/icon.png mode change 100644 => 100755 themes/triangles/client/src/img/mail.png mode change 100644 => 100755 themes/triangles/client/src/img/matrix_circle_128x128.png mode change 100644 => 100755 themes/triangles/client/src/img/notifications/.directory mode change 100644 => 100755 themes/triangles/client/src/img/notifications/error.png mode change 100644 => 100755 themes/triangles/client/src/img/notifications/info.png mode change 100644 => 100755 themes/triangles/client/src/img/notifications/success.png mode change 100644 => 100755 themes/triangles/client/src/img/notifications/warning.png mode change 100644 => 100755 themes/triangles/client/src/img/padlock.png mode change 100644 => 100755 themes/triangles/client/src/img/password_white.png mode change 100644 => 100755 themes/triangles/client/src/img/pendrive.png mode change 100644 => 100755 themes/triangles/client/src/img/sharingan.png mode change 100644 => 100755 themes/triangles/client/src/img/stores/.directory mode change 100644 => 100755 themes/triangles/client/src/img/stores/applestore-badge.svg mode change 100644 => 100755 themes/triangles/client/src/img/stores/googleplay-badge.svg mode change 100644 => 100755 themes/triangles/client/src/img/success.png mode change 100644 => 100755 themes/triangles/client/src/img/user.png mode change 100644 => 100755 themes/triangles/client/src/img/warning.png delete mode 100644 themes/triangles/client/src/index.ts delete mode 100644 themes/triangles/client/src/lib/GetPromised.ts delete mode 100644 themes/triangles/client/src/lib/INotifier.ts delete mode 100644 themes/triangles/client/src/lib/Notifier.ts delete mode 100644 themes/triangles/client/src/lib/QueryParametersRetriever.ts delete mode 100644 themes/triangles/client/src/lib/SafeRedirect.ts delete mode 100644 themes/triangles/client/src/lib/firstfactor/FirstFactorValidator.ts delete mode 100644 themes/triangles/client/src/lib/firstfactor/UISelectors.ts delete mode 100644 themes/triangles/client/src/lib/firstfactor/index.ts delete mode 100644 themes/triangles/client/src/lib/reset-password/constants.ts delete mode 100644 themes/triangles/client/src/lib/reset-password/reset-password-form.ts delete mode 100644 themes/triangles/client/src/lib/reset-password/reset-password-request.ts delete mode 100644 themes/triangles/client/src/lib/secondfactor/TOTPValidator.ts delete mode 100644 themes/triangles/client/src/lib/secondfactor/U2FValidator.ts delete mode 100644 themes/triangles/client/src/lib/secondfactor/constants.ts delete mode 100644 themes/triangles/client/src/lib/secondfactor/index.ts delete mode 100644 themes/triangles/client/src/lib/totp-register/totp-register.ts delete mode 100644 themes/triangles/client/src/lib/totp-register/ui-selector.ts delete mode 100644 themes/triangles/client/src/lib/u2f-register/u2f-register.ts mode change 100644 => 100755 themes/triangles/client/src/thirdparties/qrcode.min.js mode change 100644 => 100755 themes/triangles/client/src/thirdparties/u2f-api.js delete mode 100644 themes/triangles/client/test/Notifier.test.ts delete mode 100644 themes/triangles/client/test/firstfactor/FirstFactorValidator.test.ts delete mode 100644 themes/triangles/client/test/mocks/NotifierStub.ts delete mode 100644 themes/triangles/client/test/mocks/jquery.ts delete mode 100644 themes/triangles/client/test/mocks/u2f-api.ts delete mode 100644 themes/triangles/client/test/secondfactor/TOTPValidator.test.ts delete mode 100644 themes/triangles/client/test/totp-register/totp-register.test.ts delete mode 100644 themes/triangles/client/tsconfig.json delete mode 100644 themes/triangles/client/tslint.json mode change 100644 => 100755 themes/triangles/server/.directory delete mode 100755 themes/triangles/server/src/index.ts delete mode 100644 themes/triangles/server/src/lib/.directory delete mode 100644 themes/triangles/server/src/lib/AuthenticationSessionHandler.ts delete mode 100644 themes/triangles/server/src/lib/ErrorReplies.ts delete mode 100644 themes/triangles/server/src/lib/Exceptions.ts delete mode 100644 themes/triangles/server/src/lib/FirstFactorValidator.ts delete mode 100644 themes/triangles/server/src/lib/IdentityCheckMiddleware.spec.ts delete mode 100644 themes/triangles/server/src/lib/IdentityCheckMiddleware.ts delete mode 100644 themes/triangles/server/src/lib/IdentityCheckPreValidationTemplate.ts delete mode 100644 themes/triangles/server/src/lib/IdentityValidable.ts delete mode 100644 themes/triangles/server/src/lib/IdentityValidableStub.spec.ts delete mode 100644 themes/triangles/server/src/lib/Server.spec.ts delete mode 100644 themes/triangles/server/src/lib/Server.ts delete mode 100644 themes/triangles/server/src/lib/ServerVariables.ts delete mode 100644 themes/triangles/server/src/lib/ServerVariablesInitializer.ts delete mode 100644 themes/triangles/server/src/lib/ServerVariablesMockBuilder.spec.ts delete mode 100644 themes/triangles/server/src/lib/authentication/Level.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/GroupsAndEmails.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/IUsersDatabase.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/file/FileUsersDatabase.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/file/ReadWriteQueue.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/ISession.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/ISessionFactory.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/SafeSession.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/Sanitizer.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/Session.spec.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/Session.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/SessionFactory.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/connector/Connector.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/connector/IConnector.ts delete mode 100644 themes/triangles/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts delete mode 100644 themes/triangles/server/src/lib/authentication/totp/ITotpHandler.ts delete mode 100644 themes/triangles/server/src/lib/authentication/totp/TotpHandler.spec.ts delete mode 100644 themes/triangles/server/src/lib/authentication/totp/TotpHandler.ts delete mode 100644 themes/triangles/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts delete mode 100644 themes/triangles/server/src/lib/authentication/u2f/IU2fHandler.ts delete mode 100644 themes/triangles/server/src/lib/authentication/u2f/U2fHandler.ts delete mode 100644 themes/triangles/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts delete mode 100644 themes/triangles/server/src/lib/authorization/Authorizer.spec.ts delete mode 100644 themes/triangles/server/src/lib/authorization/Authorizer.ts delete mode 100644 themes/triangles/server/src/lib/authorization/AuthorizerStub.spec.ts delete mode 100644 themes/triangles/server/src/lib/authorization/IAuthorizer.ts delete mode 100644 themes/triangles/server/src/lib/authorization/Level.ts delete mode 100644 themes/triangles/server/src/lib/authorization/MultipleDomainMatcher.ts delete mode 100644 themes/triangles/server/src/lib/authorization/Object.ts delete mode 100644 themes/triangles/server/src/lib/authorization/Subject.ts delete mode 100644 themes/triangles/server/src/lib/configuration/ConfigurationParser.spec.ts delete mode 100644 themes/triangles/server/src/lib/configuration/ConfigurationParser.ts delete mode 100644 themes/triangles/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts delete mode 100644 themes/triangles/server/src/lib/configuration/SessionConfigurationBuilder.ts delete mode 100644 themes/triangles/server/src/lib/configuration/schema/AclConfiguration.spec.ts delete mode 100644 themes/triangles/server/src/lib/configuration/schema/AclConfiguration.ts delete mode 100644 themes/triangles/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts delete mode 100644 themes/triangles/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts delete mode 100644 themes/triangles/server/src/lib/configuration/schema/Configuration.ts delete mode 100644 themes/triangles/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts delete mode 100644 themes/triangles/server/src/lib/configuration/schema/LdapConfiguration.spec.ts delete mode 100644 themes/triangles/server/src/lib/configuration/schema/LdapConfiguration.ts delete mode 100644 themes/triangles/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts delete mode 100644 themes/triangles/server/src/lib/configuration/schema/NotifierConfiguration.ts delete mode 100644 themes/triangles/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts delete mode 100644 themes/triangles/server/src/lib/configuration/schema/RegulationConfiguration.ts delete mode 100644 themes/triangles/server/src/lib/configuration/schema/SessionConfiguration.spec.ts delete mode 100644 themes/triangles/server/src/lib/configuration/schema/SessionConfiguration.ts delete mode 100644 themes/triangles/server/src/lib/configuration/schema/StorageConfiguration.spec.ts delete mode 100644 themes/triangles/server/src/lib/configuration/schema/StorageConfiguration.ts delete mode 100644 themes/triangles/server/src/lib/configuration/schema/TotpConfiguration.ts delete mode 100644 themes/triangles/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts delete mode 100644 themes/triangles/server/src/lib/connectors/mongo/IMongoClient.d.ts delete mode 100644 themes/triangles/server/src/lib/connectors/mongo/MongoClient.spec.ts delete mode 100644 themes/triangles/server/src/lib/connectors/mongo/MongoClient.ts delete mode 100644 themes/triangles/server/src/lib/connectors/mongo/MongoClientStub.spec.ts delete mode 100644 themes/triangles/server/src/lib/logging/GlobalLogger.ts delete mode 100644 themes/triangles/server/src/lib/logging/GlobalLoggerStub.spec.ts delete mode 100644 themes/triangles/server/src/lib/logging/IGlobalLogger.ts delete mode 100644 themes/triangles/server/src/lib/logging/IRequestLogger.ts delete mode 100644 themes/triangles/server/src/lib/logging/RequestLogger.ts delete mode 100644 themes/triangles/server/src/lib/logging/RequestLoggerStub.spec.ts delete mode 100644 themes/triangles/server/src/lib/notifiers/AbstractEmailNotifier.ts delete mode 100644 themes/triangles/server/src/lib/notifiers/EmailNotifier.spec.ts delete mode 100644 themes/triangles/server/src/lib/notifiers/EmailNotifier.ts delete mode 100644 themes/triangles/server/src/lib/notifiers/FileSystemNotifier.ts delete mode 100644 themes/triangles/server/src/lib/notifiers/IMailSender.ts delete mode 100644 themes/triangles/server/src/lib/notifiers/IMailSenderBuilder.ts delete mode 100644 themes/triangles/server/src/lib/notifiers/INotifier.ts delete mode 100644 themes/triangles/server/src/lib/notifiers/MailSender.ts delete mode 100644 themes/triangles/server/src/lib/notifiers/MailSenderBuilder.spec.ts delete mode 100644 themes/triangles/server/src/lib/notifiers/MailSenderBuilder.ts delete mode 100644 themes/triangles/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts delete mode 100644 themes/triangles/server/src/lib/notifiers/MailSenderStub.spec.ts delete mode 100644 themes/triangles/server/src/lib/notifiers/NotifierFactory.spec.ts delete mode 100644 themes/triangles/server/src/lib/notifiers/NotifierFactory.ts delete mode 100644 themes/triangles/server/src/lib/notifiers/NotifierStub.spec.ts delete mode 100644 themes/triangles/server/src/lib/notifiers/SmtpNotifier.ts delete mode 100644 themes/triangles/server/src/lib/regulation/IRegulator.ts delete mode 100644 themes/triangles/server/src/lib/regulation/Regulator.spec.ts delete mode 100644 themes/triangles/server/src/lib/regulation/Regulator.ts delete mode 100644 themes/triangles/server/src/lib/regulation/RegulatorStub.spec.ts delete mode 100644 themes/triangles/server/src/lib/routes/error/401/get.spec.ts delete mode 100644 themes/triangles/server/src/lib/routes/error/401/get.ts delete mode 100644 themes/triangles/server/src/lib/routes/error/403/get.spec.ts delete mode 100644 themes/triangles/server/src/lib/routes/error/403/get.ts delete mode 100644 themes/triangles/server/src/lib/routes/error/404/get.spec.ts delete mode 100644 themes/triangles/server/src/lib/routes/error/404/get.ts delete mode 100644 themes/triangles/server/src/lib/routes/error/redirector.ts delete mode 100644 themes/triangles/server/src/lib/routes/firstfactor/get.ts delete mode 100644 themes/triangles/server/src/lib/routes/firstfactor/post.spec.ts delete mode 100644 themes/triangles/server/src/lib/routes/firstfactor/post.ts delete mode 100644 themes/triangles/server/src/lib/routes/loggedin/get.ts delete mode 100644 themes/triangles/server/src/lib/routes/logout/get.ts delete mode 100644 themes/triangles/server/src/lib/routes/password-reset/constants.ts delete mode 100644 themes/triangles/server/src/lib/routes/password-reset/form/post.spec.ts delete mode 100644 themes/triangles/server/src/lib/routes/password-reset/form/post.ts delete mode 100644 themes/triangles/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts delete mode 100644 themes/triangles/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts delete mode 100644 themes/triangles/server/src/lib/routes/password-reset/request/get.ts delete mode 100644 themes/triangles/server/src/lib/routes/secondfactor/get.spec.ts delete mode 100644 themes/triangles/server/src/lib/routes/secondfactor/get.ts delete mode 100644 themes/triangles/server/src/lib/routes/secondfactor/redirect.spec.ts delete mode 100644 themes/triangles/server/src/lib/routes/secondfactor/redirect.ts delete mode 100644 themes/triangles/server/src/lib/routes/secondfactor/totp/constants.ts delete mode 100644 themes/triangles/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts delete mode 100644 themes/triangles/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts delete mode 100644 themes/triangles/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts delete mode 100644 themes/triangles/server/src/lib/routes/secondfactor/totp/sign/post.ts delete mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts delete mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts delete mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts delete mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts delete mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/register/post.ts delete mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts delete mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/register_request/get.ts delete mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts delete mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/sign/post.ts delete mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts delete mode 100644 themes/triangles/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts delete mode 100644 themes/triangles/server/src/lib/routes/verify/access_control.ts delete mode 100644 themes/triangles/server/src/lib/routes/verify/get.spec.ts delete mode 100644 themes/triangles/server/src/lib/routes/verify/get.ts delete mode 100644 themes/triangles/server/src/lib/routes/verify/get_basic_auth.ts delete mode 100644 themes/triangles/server/src/lib/routes/verify/get_session_cookie.ts delete mode 100644 themes/triangles/server/src/lib/storage/AuthenticationTraceDocument.d.ts delete mode 100644 themes/triangles/server/src/lib/storage/CollectionFactoryFactory.ts delete mode 100644 themes/triangles/server/src/lib/storage/CollectionFactoryStub.spec.ts delete mode 100644 themes/triangles/server/src/lib/storage/CollectionStub.spec.ts delete mode 100644 themes/triangles/server/src/lib/storage/ICollection.d.ts delete mode 100644 themes/triangles/server/src/lib/storage/ICollectionFactory.d.ts delete mode 100644 themes/triangles/server/src/lib/storage/IUserDataStore.d.ts delete mode 100644 themes/triangles/server/src/lib/storage/IdentityValidationDocument.d.ts delete mode 100644 themes/triangles/server/src/lib/storage/TOTPSecretDocument.d.ts delete mode 100644 themes/triangles/server/src/lib/storage/U2FRegistrationDocument.d.ts delete mode 100644 themes/triangles/server/src/lib/storage/UserDataStore.spec.ts delete mode 100644 themes/triangles/server/src/lib/storage/UserDataStore.ts delete mode 100644 themes/triangles/server/src/lib/storage/UserDataStoreStub.spec.ts delete mode 100644 themes/triangles/server/src/lib/storage/mongo/MongoCollection.spec.ts delete mode 100644 themes/triangles/server/src/lib/storage/mongo/MongoCollection.ts delete mode 100644 themes/triangles/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts delete mode 100644 themes/triangles/server/src/lib/storage/mongo/MongoCollectionFactory.ts delete mode 100644 themes/triangles/server/src/lib/storage/nedb/NedbCollection.spec.ts delete mode 100644 themes/triangles/server/src/lib/storage/nedb/NedbCollection.ts delete mode 100644 themes/triangles/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts delete mode 100644 themes/triangles/server/src/lib/storage/nedb/NedbCollectionFactory.ts delete mode 100644 themes/triangles/server/src/lib/stubs/express.spec.ts delete mode 100644 themes/triangles/server/src/lib/stubs/ldapjs.spec.ts delete mode 100644 themes/triangles/server/src/lib/stubs/speakeasy.spec.ts delete mode 100644 themes/triangles/server/src/lib/stubs/u2f.spec.ts delete mode 100644 themes/triangles/server/src/lib/utils/HashGenerator.spec.ts delete mode 100644 themes/triangles/server/src/lib/utils/HashGenerator.ts delete mode 100644 themes/triangles/server/src/lib/utils/ObjectCloner.ts delete mode 100644 themes/triangles/server/src/lib/utils/SafeRedirection.spec.ts delete mode 100644 themes/triangles/server/src/lib/utils/SafeRedirection.ts delete mode 100644 themes/triangles/server/src/lib/utils/URLDecomposer.spec.ts delete mode 100644 themes/triangles/server/src/lib/utils/URLDecomposer.ts delete mode 100644 themes/triangles/server/src/lib/web_server/Configurator.ts delete mode 100644 themes/triangles/server/src/lib/web_server/RestApi.ts delete mode 100644 themes/triangles/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts delete mode 100644 themes/triangles/server/src/lib/web_server/middlewares/WithHeadersLogged.ts mode change 100644 => 100755 themes/triangles/server/src/resources/email-template.ejs mode change 100644 => 100755 themes/triangles/server/src/views/already-logged-in.pug mode change 100644 => 100755 themes/triangles/server/src/views/errors/.directory mode change 100644 => 100755 themes/triangles/server/src/views/errors/401.pug mode change 100644 => 100755 themes/triangles/server/src/views/errors/403.pug mode change 100644 => 100755 themes/triangles/server/src/views/errors/404.pug mode change 100644 => 100755 themes/triangles/server/src/views/firstfactor.pug mode change 100644 => 100755 themes/triangles/server/src/views/layout/layout.pug mode change 100644 => 100755 themes/triangles/server/src/views/need-identity-validation.pug mode change 100644 => 100755 themes/triangles/server/src/views/password-reset-form.pug mode change 100644 => 100755 themes/triangles/server/src/views/password-reset-request.pug mode change 100644 => 100755 themes/triangles/server/src/views/secondfactor.pug mode change 100644 => 100755 themes/triangles/server/src/views/totp-register.pug mode change 100644 => 100755 themes/triangles/server/src/views/u2f-register.pug delete mode 100644 themes/triangles/server/test/requests.ts delete mode 100644 themes/triangles/server/tsconfig.json delete mode 100644 themes/triangles/server/tslint.json delete mode 100644 themes/triangles/server/types/.directory delete mode 100644 themes/triangles/server/types/AuthenticationSession.ts delete mode 100644 themes/triangles/server/types/Dependencies.ts delete mode 100644 themes/triangles/server/types/Identity.ts delete mode 100644 themes/triangles/server/types/TOTPSecret.ts delete mode 100644 themes/triangles/server/types/U2FRegistration.ts delete mode 100644 themes/triangles/server/types/dovehash.d.ts delete mode 100644 themes/triangles/server/types/speakeasy.d.ts mode change 100644 => 100755 users_database.yml diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/.npmignore b/.npmignore old mode 100644 new mode 100755 diff --git a/.travis.yml b/.travis.yml old mode 100644 new mode 100755 diff --git a/CHANGELOG.md b/CHANGELOG.md old mode 100644 new mode 100755 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md old mode 100644 new mode 100755 diff --git a/Dockerfile b/Dockerfile old mode 100644 new mode 100755 diff --git a/Dockerfile.dev b/Dockerfile.dev old mode 100644 new mode 100755 diff --git a/Gruntfile.js b/Gruntfile.js old mode 100644 new mode 100755 index fcf4a5731..2bcb4f8f7 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,7 +1,8 @@ module.exports = function (grunt) { const buildDir = "dist"; const schemaDir = "server/src/lib/configuration/Configuration.schema.json" - + var theme = grunt.option('theme') || 'default'; + grunt.initConfig({ env: { "env-test-server-unit": { @@ -14,7 +15,10 @@ module.exports = function (grunt) { TS_NODE_PROJECT: "server/tsconfig.json" } }, - clean: ['dist'], + clean: { + dist: ['dist'], + backup: ['backup'], + }, run: { "compile-server": { cmd: "./node_modules/.bin/tsc", @@ -83,123 +87,34 @@ module.exports = function (grunt) { } }, copy: { - main_resources: { + backup: { + files: [{ expand: true, - cwd: 'themes/main/server/src/resources', + src: ['dist/**'], + dest: 'backup' + }] + }, + resources: { + expand: true, + cwd: 'themes/' + theme + '/server/src/resources', src: '**', dest: `${buildDir}/server/src/resources/` }, - main_views: { + views: { expand: true, - cwd: 'themes/main/server/src/views', + cwd: 'themes/' + theme + '/server/src/views', src: '**', dest: `${buildDir}/server/src/views/` }, - main_images: { + images: { expand: true, - cwd: 'themes/main/client/src/img', + cwd: 'themes/' + theme + '/client/src/img', src: '**', dest: `${buildDir}/server/src/public_html/img/` }, - main_thirdparties: { + thirdparties: { expand: true, - 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: '**', - dest: `${buildDir}/server/src/public_html/js/` - }, - black_resources: { - expand: true, - cwd: 'themes/black/server/src/resources', - src: '**', - dest: `${buildDir}/server/src/resources/` - }, - black_views: { - expand: true, - cwd: 'themes/black/server/src/views', - src: '**', - dest: `${buildDir}/server/src/views/` - }, - black_images: { - expand: true, - cwd: 'themes/black/client/src/img', - src: '**', - dest: `${buildDir}/server/src/public_html/img/` - }, - black_thirdparties: { - expand: true, - cwd: 'themes/black/client/src/thirdparties', - src: '**', - dest: `${buildDir}/server/src/public_html/js/` - }, - squares_resources: { - expand: true, - cwd: 'themes/squares/server/src/resources', - src: '**', - dest: `${buildDir}/server/src/resources/` - }, - squares_views: { - expand: true, - cwd: 'themes/squares/server/src/views', - src: '**', - dest: `${buildDir}/server/src/views/` - }, - squares_images: { - expand: true, - cwd: 'themes/squares/client/src/img', - src: '**', - dest: `${buildDir}/server/src/public_html/img/` - }, - squares_thirdparties: { - expand: true, - cwd: 'themes/squares/client/src/thirdparties', - src: '**', - dest: `${buildDir}/server/src/public_html/js/` - }, - triangles_resources: { - expand: true, - cwd: 'themes/triangles/server/src/resources', - src: '**', - dest: `${buildDir}/server/src/resources/` - }, - triangles_views: { - expand: true, - cwd: 'themes/triangles/server/src/views', - src: '**', - dest: `${buildDir}/server/src/views/` - }, - triangles_images: { - expand: true, - cwd: 'themes/triangles/client/src/img', - src: '**', - dest: `${buildDir}/server/src/public_html/img/` - }, - triangles_thirdparties: { - expand: true, - cwd: 'themes/triangles/client/src/thirdparties', + cwd: 'themes/' + theme + '/client/src/thirdparties', src: '**', dest: `${buildDir}/server/src/public_html/js/` }, @@ -270,24 +185,8 @@ module.exports = function (grunt) { } }, concat: { - main_css: { - src: ['themes/main/client/src/css/*.css'], - dest: `${buildDir}/server/src/public_html/css/authelia.css` - }, - matrix_css: { - src: ['themes/matrix/client/src/css/*.css'], - dest: `${buildDir}/server/src/public_html/css/authelia.css` - }, - black_css: { - src: ['themes/black/client/src/css/*.css'], - dest: `${buildDir}/server/src/public_html/css/authelia.css` - }, - squares_css: { - src: ['themes/squares/client/src/css/*.css'], - dest: `${buildDir}/server/src/public_html/css/authelia.css` - }, - triangles_css: { - src: ['themes/triangles/client/src/css/*.css'], + css: { + src: ['themes/' + theme + '/client/src/css/*.css'], dest: `${buildDir}/server/src/public_html/css/authelia.css` }, }, @@ -299,8 +198,6 @@ module.exports = function (grunt) { } } }); - - var target = grunt.option('target') || 'main'; grunt.loadNpmTasks('grunt-browserify'); grunt.loadNpmTasks('grunt-contrib-concat'); @@ -311,7 +208,6 @@ module.exports = function (grunt) { grunt.loadNpmTasks('grunt-run'); grunt.loadNpmTasks('grunt-env'); - grunt.registerTask('compile-server', ['run:lint-server', 'run:compile-server']) grunt.registerTask('compile-client', ['run:lint-client', 'run:compile-client']) @@ -321,31 +217,33 @@ module.exports = function (grunt) { 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('copy-resources-main', ['copy:main_resources', 'copy:main_views', 'copy:main_images', 'copy:main_thirdparties', 'concat:main_css']); - - grunt.registerTask('copy-resources-matrix', ['copy:matrix_resources', 'copy:matrix_views', 'copy:matrix_images', 'copy:matrix_thirdparties', 'concat:matrix_css']); - - grunt.registerTask('copy-resources-black', ['copy:black_resources', 'copy:black_views', 'copy:black_images', 'copy:black_thirdparties', 'concat:black_css']); - - grunt.registerTask('copy-resources-squares', ['copy:squares_resources', 'copy:squares_views', 'copy:squares_images', 'copy:squares_thirdparties', 'concat:squares_css']); - - grunt.registerTask('copy-resources-triangles', ['copy:triangles_resources', 'copy:triangles_views', 'copy:triangles_images', 'copy:triangles_thirdparties', 'concat:triangles_css']); + grunt.registerTask('copy-resources', ['copy:resources', 'copy:views', 'copy:images', 'copy:thirdparties', 'concat:css']); grunt.registerTask('generate-config-schema', ['run:generate-config-schema', 'copy:schema']); 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-server-black', ['compile-server', 'copy-resources-black', 'generate-config-schema']); - grunt.registerTask('build-server-squares', ['compile-server', 'copy-resources-squares', 'generate-config-schema']); - grunt.registerTask('build-server-triangles', ['compile-server', 'copy-resources-triangles', 'generate-config-schema']); + + grunt.registerTask('build-server', ['compile-server', 'copy-resources', 'generate-config-schema']); - grunt.registerTask('build', ['build-client', 'build-server-'+target]); - grunt.registerTask('build-dist', ['clean', 'build', 'run:minify', 'cssmin', 'run:include-minified-script']); + grunt.registerTask('build', ['build-client', 'build-server']); + grunt.registerTask('build-dist', ['clean:backup', 'copy:backup', 'clean:dist', 'build', 'run:minify', 'cssmin', 'run:include-minified-script']); grunt.registerTask('schema', ['run:generate-config-schema']) grunt.registerTask('docker-build', ['run:docker-build']); + + grunt.registerTask('check', function() { + if (grunt.option('theme') == 'undefined') { + grunt.log.writeln('1- Valid argmuents are just "grunt" (will use default) or "grunt --theme=|default|black|matrix|squares|triangles"'); + } + if ((theme != 'default') && (theme != 'black') && (theme != 'matrix') && (theme != 'squares') && (theme != 'triangles')) { + grunt.warn('2- Valid argmuents are just "grunt" (will use default) or "grunt --theme=|default|black|matrix|squares|triangles"'); + } + if (grunt.option('theme') == 'default' || 'black' || 'matrix' || 'squares' || 'triangles') { + grunt.log.ok(); + grunt.log.writeln('Building "'+ theme +'" theme'); + } + }); - grunt.registerTask('default', ['build-dist']); + grunt.registerTask('default', ['check', 'build-dist']); }; diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/client/src/css/00-bootstrap.min.css b/client/src/css/00-bootstrap.min.css old mode 100644 new mode 100755 diff --git a/client/src/css/01-main.css b/client/src/css/01-main.css old mode 100644 new mode 100755 diff --git a/client/src/css/02-login.css b/client/src/css/02-login.css old mode 100644 new mode 100755 diff --git a/client/src/css/03-errors.css b/client/src/css/03-errors.css old mode 100644 new mode 100755 diff --git a/client/src/css/03-password-reset-form.css b/client/src/css/03-password-reset-form.css old mode 100644 new mode 100755 diff --git a/client/src/css/03-password-reset-request.css b/client/src/css/03-password-reset-request.css old mode 100644 new mode 100755 diff --git a/client/src/css/03-totp-register.css b/client/src/css/03-totp-register.css old mode 100644 new mode 100755 diff --git a/client/src/css/03-u2f-register.css b/client/src/css/03-u2f-register.css old mode 100644 new mode 100755 diff --git a/client/src/img/background.svg b/client/src/img/background.svg old mode 100644 new mode 100755 diff --git a/client/src/img/icon.png b/client/src/img/icon.png old mode 100644 new mode 100755 diff --git a/client/src/img/mail.png b/client/src/img/mail.png old mode 100644 new mode 100755 diff --git a/client/src/img/notifications/error.png b/client/src/img/notifications/error.png old mode 100644 new mode 100755 diff --git a/client/src/img/notifications/info.png b/client/src/img/notifications/info.png old mode 100644 new mode 100755 diff --git a/client/src/img/notifications/success.png b/client/src/img/notifications/success.png old mode 100644 new mode 100755 diff --git a/client/src/img/notifications/warning.png b/client/src/img/notifications/warning.png old mode 100644 new mode 100755 diff --git a/client/src/img/padlock.png b/client/src/img/padlock.png old mode 100644 new mode 100755 diff --git a/client/src/img/password.png b/client/src/img/password.png old mode 100644 new mode 100755 diff --git a/client/src/img/pendrive.png b/client/src/img/pendrive.png old mode 100644 new mode 100755 diff --git a/client/src/img/stores/applestore-badge.svg b/client/src/img/stores/applestore-badge.svg old mode 100644 new mode 100755 diff --git a/client/src/img/stores/googleplay-badge.svg b/client/src/img/stores/googleplay-badge.svg old mode 100644 new mode 100755 diff --git a/client/src/img/success.png b/client/src/img/success.png old mode 100644 new mode 100755 diff --git a/client/src/img/user.png b/client/src/img/user.png old mode 100644 new mode 100755 diff --git a/client/src/img/warning.png b/client/src/img/warning.png old mode 100644 new mode 100755 diff --git a/client/src/index.ts b/client/src/index.ts old mode 100644 new mode 100755 diff --git a/client/src/lib/GetPromised.ts b/client/src/lib/GetPromised.ts old mode 100644 new mode 100755 diff --git a/client/src/lib/INotifier.ts b/client/src/lib/INotifier.ts old mode 100644 new mode 100755 diff --git a/client/src/lib/Notifier.ts b/client/src/lib/Notifier.ts old mode 100644 new mode 100755 diff --git a/client/src/lib/QueryParametersRetriever.ts b/client/src/lib/QueryParametersRetriever.ts old mode 100644 new mode 100755 diff --git a/client/src/lib/SafeRedirect.ts b/client/src/lib/SafeRedirect.ts old mode 100644 new mode 100755 diff --git a/client/src/lib/firstfactor/FirstFactorValidator.ts b/client/src/lib/firstfactor/FirstFactorValidator.ts old mode 100644 new mode 100755 diff --git a/client/src/lib/firstfactor/UISelectors.ts b/client/src/lib/firstfactor/UISelectors.ts old mode 100644 new mode 100755 diff --git a/client/src/lib/firstfactor/index.ts b/client/src/lib/firstfactor/index.ts old mode 100644 new mode 100755 diff --git a/client/src/lib/reset-password/constants.ts b/client/src/lib/reset-password/constants.ts old mode 100644 new mode 100755 diff --git a/client/src/lib/reset-password/reset-password-form.ts b/client/src/lib/reset-password/reset-password-form.ts old mode 100644 new mode 100755 diff --git a/client/src/lib/reset-password/reset-password-request.ts b/client/src/lib/reset-password/reset-password-request.ts old mode 100644 new mode 100755 diff --git a/client/src/lib/secondfactor/TOTPValidator.ts b/client/src/lib/secondfactor/TOTPValidator.ts old mode 100644 new mode 100755 diff --git a/client/src/lib/secondfactor/U2FValidator.ts b/client/src/lib/secondfactor/U2FValidator.ts old mode 100644 new mode 100755 diff --git a/client/src/lib/secondfactor/constants.ts b/client/src/lib/secondfactor/constants.ts old mode 100644 new mode 100755 diff --git a/client/src/lib/secondfactor/index.ts b/client/src/lib/secondfactor/index.ts old mode 100644 new mode 100755 diff --git a/client/src/lib/totp-register/totp-register.ts b/client/src/lib/totp-register/totp-register.ts old mode 100644 new mode 100755 diff --git a/client/src/lib/totp-register/ui-selector.ts b/client/src/lib/totp-register/ui-selector.ts old mode 100644 new mode 100755 diff --git a/client/src/lib/u2f-register/u2f-register.ts b/client/src/lib/u2f-register/u2f-register.ts old mode 100644 new mode 100755 diff --git a/client/src/thirdparties/qrcode.min.js b/client/src/thirdparties/qrcode.min.js old mode 100644 new mode 100755 diff --git a/client/test/Notifier.test.ts b/client/test/Notifier.test.ts old mode 100644 new mode 100755 diff --git a/client/test/firstfactor/FirstFactorValidator.test.ts b/client/test/firstfactor/FirstFactorValidator.test.ts old mode 100644 new mode 100755 diff --git a/client/test/mocks/NotifierStub.ts b/client/test/mocks/NotifierStub.ts old mode 100644 new mode 100755 diff --git a/client/test/mocks/jquery.ts b/client/test/mocks/jquery.ts old mode 100644 new mode 100755 diff --git a/client/test/mocks/u2f-api.ts b/client/test/mocks/u2f-api.ts old mode 100644 new mode 100755 diff --git a/client/test/secondfactor/TOTPValidator.test.ts b/client/test/secondfactor/TOTPValidator.test.ts old mode 100644 new mode 100755 diff --git a/client/test/totp-register/totp-register.test.ts b/client/test/totp-register/totp-register.test.ts old mode 100644 new mode 100755 diff --git a/client/tsconfig.json b/client/tsconfig.json old mode 100644 new mode 100755 diff --git a/client/tslint.json b/client/tslint.json old mode 100644 new mode 100755 diff --git a/config.minimal.yml b/config.minimal.yml old mode 100644 new mode 100755 index 8da7bc5e1..c43828f2e --- a/config.minimal.yml +++ b/config.minimal.yml @@ -4,7 +4,7 @@ authentication_backend: file: - path: /etc/authelia/users_database.yml + path: users_database.yml session: secret: unsecure_session_secret diff --git a/config.template.yml b/config.template.yml old mode 100644 new mode 100755 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml old mode 100644 new mode 100755 diff --git a/docker-compose.dockerhub.yml b/docker-compose.dockerhub.yml old mode 100644 new mode 100755 diff --git a/docker-compose.minimal.dev.yml b/docker-compose.minimal.dev.yml old mode 100644 new mode 100755 diff --git a/docker-compose.minimal.yml b/docker-compose.minimal.yml old mode 100644 new mode 100755 diff --git a/docker-compose.swarm.minimal.yml b/docker-compose.swarm.minimal.yml old mode 100644 new mode 100755 diff --git a/docker-compose.test.yml b/docker-compose.test.yml old mode 100644 new mode 100755 diff --git a/docker-compose.yml b/docker-compose.yml old mode 100644 new mode 100755 diff --git a/docs/build.md b/docs/build.md old mode 100644 new mode 100755 diff --git a/docs/configuration.md b/docs/configuration.md old mode 100644 new mode 100755 diff --git a/docs/deployment-dev.md b/docs/deployment-dev.md old mode 100644 new mode 100755 diff --git a/docs/deployment-production.md b/docs/deployment-production.md old mode 100644 new mode 100755 diff --git a/docs/features.md b/docs/features.md old mode 100644 new mode 100755 diff --git a/docs/getting-started.md b/docs/getting-started.md old mode 100644 new mode 100755 diff --git a/docs/security.md b/docs/security.md old mode 100644 new mode 100755 diff --git a/example/compose/authelia/docker-compose.test.yml b/example/compose/authelia/docker-compose.test.yml old mode 100644 new mode 100755 diff --git a/example/compose/docker-compose.base.yml b/example/compose/docker-compose.base.yml old mode 100644 new mode 100755 diff --git a/example/compose/httpbin/docker-compose.yml b/example/compose/httpbin/docker-compose.yml old mode 100644 new mode 100755 diff --git a/example/compose/ldap/access.rules b/example/compose/ldap/access.rules old mode 100644 new mode 100755 diff --git a/example/compose/ldap/base.ldif b/example/compose/ldap/base.ldif old mode 100644 new mode 100755 diff --git a/example/compose/ldap/docker-compose.admin.yml b/example/compose/ldap/docker-compose.admin.yml old mode 100644 new mode 100755 diff --git a/example/compose/ldap/docker-compose.yml b/example/compose/ldap/docker-compose.yml old mode 100644 new mode 100755 diff --git a/example/compose/mongo/docker-compose.yml b/example/compose/mongo/docker-compose.yml old mode 100644 new mode 100755 diff --git a/example/compose/nginx/backend/docker-compose.yml b/example/compose/nginx/backend/docker-compose.yml old mode 100644 new mode 100755 diff --git a/example/compose/nginx/backend/html/admin/secret.html b/example/compose/nginx/backend/html/admin/secret.html old mode 100644 new mode 100755 diff --git a/example/compose/nginx/backend/html/dev/groups/admin/secret.html b/example/compose/nginx/backend/html/dev/groups/admin/secret.html old mode 100644 new mode 100755 diff --git a/example/compose/nginx/backend/html/dev/groups/dev/secret.html b/example/compose/nginx/backend/html/dev/groups/dev/secret.html old mode 100644 new mode 100755 diff --git a/example/compose/nginx/backend/html/dev/users/bob/secret.html b/example/compose/nginx/backend/html/dev/users/bob/secret.html old mode 100644 new mode 100755 diff --git a/example/compose/nginx/backend/html/dev/users/harry/secret.html b/example/compose/nginx/backend/html/dev/users/harry/secret.html old mode 100644 new mode 100755 diff --git a/example/compose/nginx/backend/html/dev/users/john/secret.html b/example/compose/nginx/backend/html/dev/users/john/secret.html old mode 100644 new mode 100755 diff --git a/example/compose/nginx/backend/html/home/index.html b/example/compose/nginx/backend/html/home/index.html old mode 100644 new mode 100755 diff --git a/example/compose/nginx/backend/html/icon.png b/example/compose/nginx/backend/html/icon.png old mode 100644 new mode 100755 diff --git a/example/compose/nginx/backend/html/mail/secret.html b/example/compose/nginx/backend/html/mail/secret.html old mode 100644 new mode 100755 diff --git a/example/compose/nginx/backend/html/public/index.html b/example/compose/nginx/backend/html/public/index.html old mode 100644 new mode 100755 diff --git a/example/compose/nginx/backend/html/public/secret.html b/example/compose/nginx/backend/html/public/secret.html old mode 100644 new mode 100755 diff --git a/example/compose/nginx/backend/html/single_factor/secret.html b/example/compose/nginx/backend/html/single_factor/secret.html old mode 100644 new mode 100755 diff --git a/example/compose/nginx/backend/nginx.conf b/example/compose/nginx/backend/nginx.conf old mode 100644 new mode 100755 diff --git a/example/compose/nginx/minimal/docker-compose.yml b/example/compose/nginx/minimal/docker-compose.yml old mode 100644 new mode 100755 diff --git a/example/compose/nginx/minimal/html/admin/secret.html b/example/compose/nginx/minimal/html/admin/secret.html old mode 100644 new mode 100755 diff --git a/example/compose/nginx/minimal/html/home/index.html b/example/compose/nginx/minimal/html/home/index.html old mode 100644 new mode 100755 diff --git a/example/compose/nginx/minimal/nginx.conf b/example/compose/nginx/minimal/nginx.conf old mode 100644 new mode 100755 diff --git a/example/compose/nginx/minimal/ssl/server.crt b/example/compose/nginx/minimal/ssl/server.crt old mode 100644 new mode 100755 diff --git a/example/compose/nginx/minimal/ssl/server.csr b/example/compose/nginx/minimal/ssl/server.csr old mode 100644 new mode 100755 diff --git a/example/compose/nginx/minimal/ssl/server.key b/example/compose/nginx/minimal/ssl/server.key old mode 100644 new mode 100755 diff --git a/example/compose/nginx/portal/docker-compose.yml b/example/compose/nginx/portal/docker-compose.yml old mode 100644 new mode 100755 diff --git a/example/compose/nginx/portal/nginx.conf b/example/compose/nginx/portal/nginx.conf old mode 100644 new mode 100755 diff --git a/example/compose/nginx/portal/ssl/server.crt b/example/compose/nginx/portal/ssl/server.crt old mode 100644 new mode 100755 diff --git a/example/compose/nginx/portal/ssl/server.csr b/example/compose/nginx/portal/ssl/server.csr old mode 100644 new mode 100755 diff --git a/example/compose/nginx/portal/ssl/server.key b/example/compose/nginx/portal/ssl/server.key old mode 100644 new mode 100755 diff --git a/example/compose/redis/docker-compose.yml b/example/compose/redis/docker-compose.yml old mode 100644 new mode 100755 diff --git a/example/compose/smtp/docker-compose.yml b/example/compose/smtp/docker-compose.yml old mode 100644 new mode 100755 diff --git a/example/kube/README.md b/example/kube/README.md old mode 100644 new mode 100755 diff --git a/example/kube/apps/app-home/deployment.yml b/example/kube/apps/app-home/deployment.yml old mode 100644 new mode 100755 diff --git a/example/kube/apps/app-home/index.html b/example/kube/apps/app-home/index.html old mode 100644 new mode 100755 diff --git a/example/kube/apps/app-home/service.yml b/example/kube/apps/app-home/service.yml old mode 100644 new mode 100755 diff --git a/example/kube/apps/app1/deployment.yml b/example/kube/apps/app1/deployment.yml old mode 100644 new mode 100755 diff --git a/example/kube/apps/app1/index.html b/example/kube/apps/app1/index.html old mode 100644 new mode 100755 diff --git a/example/kube/apps/app1/service.yml b/example/kube/apps/app1/service.yml old mode 100644 new mode 100755 diff --git a/example/kube/apps/app1/ssl/tls.crt b/example/kube/apps/app1/ssl/tls.crt old mode 100644 new mode 100755 diff --git a/example/kube/apps/app1/ssl/tls.csr b/example/kube/apps/app1/ssl/tls.csr old mode 100644 new mode 100755 diff --git a/example/kube/apps/app1/ssl/tls.key b/example/kube/apps/app1/ssl/tls.key old mode 100644 new mode 100755 diff --git a/example/kube/apps/app2/deployment.yml b/example/kube/apps/app2/deployment.yml old mode 100644 new mode 100755 diff --git a/example/kube/apps/app2/index.html b/example/kube/apps/app2/index.html old mode 100644 new mode 100755 diff --git a/example/kube/apps/app2/service.yml b/example/kube/apps/app2/service.yml old mode 100644 new mode 100755 diff --git a/example/kube/apps/app2/ssl/tls.crt b/example/kube/apps/app2/ssl/tls.crt old mode 100644 new mode 100755 diff --git a/example/kube/apps/app2/ssl/tls.csr b/example/kube/apps/app2/ssl/tls.csr old mode 100644 new mode 100755 diff --git a/example/kube/apps/app2/ssl/tls.key b/example/kube/apps/app2/ssl/tls.key old mode 100644 new mode 100755 diff --git a/example/kube/apps/insecure-ingress.yml b/example/kube/apps/insecure-ingress.yml old mode 100644 new mode 100755 diff --git a/example/kube/apps/secure-ingress.yml b/example/kube/apps/secure-ingress.yml old mode 100644 new mode 100755 diff --git a/example/kube/authelia/configs/config.yml b/example/kube/authelia/configs/config.yml old mode 100644 new mode 100755 diff --git a/example/kube/authelia/deployment.yml b/example/kube/authelia/deployment.yml old mode 100644 new mode 100755 diff --git a/example/kube/authelia/ingress.yml b/example/kube/authelia/ingress.yml old mode 100644 new mode 100755 diff --git a/example/kube/authelia/service.yml b/example/kube/authelia/service.yml old mode 100644 new mode 100755 diff --git a/example/kube/authelia/ssl/tls.crt b/example/kube/authelia/ssl/tls.crt old mode 100644 new mode 100755 diff --git a/example/kube/authelia/ssl/tls.csr b/example/kube/authelia/ssl/tls.csr old mode 100644 new mode 100755 diff --git a/example/kube/authelia/ssl/tls.key b/example/kube/authelia/ssl/tls.key old mode 100644 new mode 100755 diff --git a/example/kube/docker-registry/daemonset.yml b/example/kube/docker-registry/daemonset.yml old mode 100644 new mode 100755 diff --git a/example/kube/docker-registry/ingress.yml b/example/kube/docker-registry/ingress.yml old mode 100644 new mode 100755 diff --git a/example/kube/docker-registry/replicationcontroller.yml b/example/kube/docker-registry/replicationcontroller.yml old mode 100644 new mode 100755 diff --git a/example/kube/docker-registry/service.yml b/example/kube/docker-registry/service.yml old mode 100644 new mode 100755 diff --git a/example/kube/ingress-controller/default-backend.yml b/example/kube/ingress-controller/default-backend.yml old mode 100644 new mode 100755 diff --git a/example/kube/ingress-controller/deployment.yml b/example/kube/ingress-controller/deployment.yml old mode 100644 new mode 100755 diff --git a/example/kube/ingress-controller/service.yml b/example/kube/ingress-controller/service.yml old mode 100644 new mode 100755 diff --git a/example/kube/ldap/Dockerfile b/example/kube/ldap/Dockerfile old mode 100644 new mode 100755 diff --git a/example/kube/ldap/deployment.yml b/example/kube/ldap/deployment.yml old mode 100644 new mode 100755 diff --git a/example/kube/ldap/service.yml b/example/kube/ldap/service.yml old mode 100644 new mode 100755 diff --git a/example/kube/mailcatcher/deployment.yml b/example/kube/mailcatcher/deployment.yml old mode 100644 new mode 100755 diff --git a/example/kube/mailcatcher/ingress.yml b/example/kube/mailcatcher/ingress.yml old mode 100644 new mode 100755 diff --git a/example/kube/mailcatcher/service.yml b/example/kube/mailcatcher/service.yml old mode 100644 new mode 100755 diff --git a/example/kube/namespace.yml b/example/kube/namespace.yml old mode 100644 new mode 100755 diff --git a/example/kube/storage/mongo.yml b/example/kube/storage/mongo.yml old mode 100644 new mode 100755 diff --git a/example/kube/storage/redis.yml b/example/kube/storage/redis.yml old mode 100644 new mode 100755 diff --git a/images/authelia-title-white.png b/images/authelia-title-white.png old mode 100644 new mode 100755 diff --git a/images/authelia-title.png b/images/authelia-title.png old mode 100644 new mode 100755 diff --git a/images/email_confirmation.png b/images/email_confirmation.png old mode 100644 new mode 100755 diff --git a/images/first_factor.png b/images/first_factor.png old mode 100644 new mode 100755 diff --git a/images/icon.png b/images/icon.png old mode 100644 new mode 100755 diff --git a/images/kube-logo.png b/images/kube-logo.png old mode 100644 new mode 100755 diff --git a/images/reset_password.png b/images/reset_password.png old mode 100644 new mode 100755 diff --git a/images/second_factor.png b/images/second_factor.png old mode 100644 new mode 100755 diff --git a/images/totp.png b/images/totp.png old mode 100644 new mode 100755 diff --git a/images/u2f.png b/images/u2f.png old mode 100644 new mode 100755 diff --git a/package-lock.json b/package-lock.json old mode 100644 new mode 100755 index 6e9837892..890819932 --- a/package-lock.json +++ b/package-lock.json @@ -2971,12 +2971,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2991,17 +2993,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3118,7 +3123,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3130,6 +3136,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3144,6 +3151,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3151,12 +3159,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -3175,6 +3185,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3255,7 +3266,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3267,6 +3279,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3388,6 +3401,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", diff --git a/package.json b/package.json old mode 100644 new mode 100755 diff --git a/server/src/lib/AuthenticationSessionHandler.ts b/server/src/lib/AuthenticationSessionHandler.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/ErrorReplies.ts b/server/src/lib/ErrorReplies.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/Exceptions.ts b/server/src/lib/Exceptions.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/FirstFactorValidator.ts b/server/src/lib/FirstFactorValidator.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/IdentityCheckMiddleware.spec.ts b/server/src/lib/IdentityCheckMiddleware.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/IdentityCheckMiddleware.ts b/server/src/lib/IdentityCheckMiddleware.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/IdentityCheckPreValidationTemplate.ts b/server/src/lib/IdentityCheckPreValidationTemplate.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/IdentityValidable.ts b/server/src/lib/IdentityValidable.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/IdentityValidableStub.spec.ts b/server/src/lib/IdentityValidableStub.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/Server.spec.ts b/server/src/lib/Server.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/Server.ts b/server/src/lib/Server.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/ServerVariables.ts b/server/src/lib/ServerVariables.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/ServerVariablesInitializer.ts b/server/src/lib/ServerVariablesInitializer.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/ServerVariablesMockBuilder.spec.ts b/server/src/lib/ServerVariablesMockBuilder.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/Level.ts b/server/src/lib/authentication/Level.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/GroupsAndEmails.ts b/server/src/lib/authentication/backends/GroupsAndEmails.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/IUsersDatabase.ts b/server/src/lib/authentication/backends/IUsersDatabase.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts b/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts b/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/file/FileUsersDatabase.ts b/server/src/lib/authentication/backends/file/FileUsersDatabase.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/file/ReadWriteQueue.ts b/server/src/lib/authentication/backends/file/ReadWriteQueue.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/ldap/ISession.ts b/server/src/lib/authentication/backends/ldap/ISession.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/ldap/ISessionFactory.ts b/server/src/lib/authentication/backends/ldap/ISessionFactory.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts b/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts b/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts b/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/ldap/SafeSession.ts b/server/src/lib/authentication/backends/ldap/SafeSession.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts b/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/ldap/Sanitizer.ts b/server/src/lib/authentication/backends/ldap/Sanitizer.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/ldap/Session.spec.ts b/server/src/lib/authentication/backends/ldap/Session.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/ldap/Session.ts b/server/src/lib/authentication/backends/ldap/Session.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/ldap/SessionFactory.ts b/server/src/lib/authentication/backends/ldap/SessionFactory.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts b/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts b/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/ldap/connector/Connector.ts b/server/src/lib/authentication/backends/ldap/connector/Connector.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts b/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts b/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts b/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/ldap/connector/IConnector.ts b/server/src/lib/authentication/backends/ldap/connector/IConnector.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts b/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/totp/ITotpHandler.ts b/server/src/lib/authentication/totp/ITotpHandler.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/totp/TotpHandler.spec.ts b/server/src/lib/authentication/totp/TotpHandler.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/totp/TotpHandler.ts b/server/src/lib/authentication/totp/TotpHandler.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts b/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/u2f/IU2fHandler.ts b/server/src/lib/authentication/u2f/IU2fHandler.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/u2f/U2fHandler.ts b/server/src/lib/authentication/u2f/U2fHandler.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts b/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authorization/Authorizer.spec.ts b/server/src/lib/authorization/Authorizer.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authorization/Authorizer.ts b/server/src/lib/authorization/Authorizer.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authorization/AuthorizerStub.spec.ts b/server/src/lib/authorization/AuthorizerStub.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authorization/IAuthorizer.ts b/server/src/lib/authorization/IAuthorizer.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authorization/Level.ts b/server/src/lib/authorization/Level.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authorization/MultipleDomainMatcher.ts b/server/src/lib/authorization/MultipleDomainMatcher.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authorization/Object.ts b/server/src/lib/authorization/Object.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/authorization/Subject.ts b/server/src/lib/authorization/Subject.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/ConfigurationParser.spec.ts b/server/src/lib/configuration/ConfigurationParser.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/ConfigurationParser.ts b/server/src/lib/configuration/ConfigurationParser.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts b/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/SessionConfigurationBuilder.ts b/server/src/lib/configuration/SessionConfigurationBuilder.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/schema/AclConfiguration.spec.ts b/server/src/lib/configuration/schema/AclConfiguration.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/schema/AclConfiguration.ts b/server/src/lib/configuration/schema/AclConfiguration.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts b/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts b/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/schema/Configuration.ts b/server/src/lib/configuration/schema/Configuration.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts b/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/schema/LdapConfiguration.spec.ts b/server/src/lib/configuration/schema/LdapConfiguration.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/schema/LdapConfiguration.ts b/server/src/lib/configuration/schema/LdapConfiguration.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts b/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/schema/NotifierConfiguration.ts b/server/src/lib/configuration/schema/NotifierConfiguration.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts b/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/schema/RegulationConfiguration.ts b/server/src/lib/configuration/schema/RegulationConfiguration.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/schema/SessionConfiguration.spec.ts b/server/src/lib/configuration/schema/SessionConfiguration.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/schema/SessionConfiguration.ts b/server/src/lib/configuration/schema/SessionConfiguration.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/schema/StorageConfiguration.spec.ts b/server/src/lib/configuration/schema/StorageConfiguration.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/schema/StorageConfiguration.ts b/server/src/lib/configuration/schema/StorageConfiguration.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/schema/TotpConfiguration.ts b/server/src/lib/configuration/schema/TotpConfiguration.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts b/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/connectors/mongo/IMongoClient.d.ts b/server/src/lib/connectors/mongo/IMongoClient.d.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/connectors/mongo/MongoClient.spec.ts b/server/src/lib/connectors/mongo/MongoClient.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/connectors/mongo/MongoClient.ts b/server/src/lib/connectors/mongo/MongoClient.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/connectors/mongo/MongoClientStub.spec.ts b/server/src/lib/connectors/mongo/MongoClientStub.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/logging/GlobalLogger.ts b/server/src/lib/logging/GlobalLogger.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/logging/GlobalLoggerStub.spec.ts b/server/src/lib/logging/GlobalLoggerStub.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/logging/IGlobalLogger.ts b/server/src/lib/logging/IGlobalLogger.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/logging/IRequestLogger.ts b/server/src/lib/logging/IRequestLogger.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/logging/RequestLogger.ts b/server/src/lib/logging/RequestLogger.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/logging/RequestLoggerStub.spec.ts b/server/src/lib/logging/RequestLoggerStub.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/notifiers/AbstractEmailNotifier.ts b/server/src/lib/notifiers/AbstractEmailNotifier.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/notifiers/EmailNotifier.spec.ts b/server/src/lib/notifiers/EmailNotifier.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/notifiers/EmailNotifier.ts b/server/src/lib/notifiers/EmailNotifier.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/notifiers/FileSystemNotifier.ts b/server/src/lib/notifiers/FileSystemNotifier.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/notifiers/IMailSender.ts b/server/src/lib/notifiers/IMailSender.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/notifiers/IMailSenderBuilder.ts b/server/src/lib/notifiers/IMailSenderBuilder.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/notifiers/INotifier.ts b/server/src/lib/notifiers/INotifier.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/notifiers/MailSender.ts b/server/src/lib/notifiers/MailSender.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/notifiers/MailSenderBuilder.spec.ts b/server/src/lib/notifiers/MailSenderBuilder.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/notifiers/MailSenderBuilder.ts b/server/src/lib/notifiers/MailSenderBuilder.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts b/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/notifiers/MailSenderStub.spec.ts b/server/src/lib/notifiers/MailSenderStub.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/notifiers/NotifierFactory.spec.ts b/server/src/lib/notifiers/NotifierFactory.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/notifiers/NotifierFactory.ts b/server/src/lib/notifiers/NotifierFactory.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/notifiers/NotifierStub.spec.ts b/server/src/lib/notifiers/NotifierStub.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/notifiers/SmtpNotifier.ts b/server/src/lib/notifiers/SmtpNotifier.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/regulation/IRegulator.ts b/server/src/lib/regulation/IRegulator.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/regulation/Regulator.spec.ts b/server/src/lib/regulation/Regulator.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/regulation/Regulator.ts b/server/src/lib/regulation/Regulator.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/regulation/RegulatorStub.spec.ts b/server/src/lib/regulation/RegulatorStub.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/error/401/get.spec.ts b/server/src/lib/routes/error/401/get.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/error/401/get.ts b/server/src/lib/routes/error/401/get.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/error/403/get.spec.ts b/server/src/lib/routes/error/403/get.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/error/403/get.ts b/server/src/lib/routes/error/403/get.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/error/404/get.spec.ts b/server/src/lib/routes/error/404/get.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/error/404/get.ts b/server/src/lib/routes/error/404/get.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/error/redirector.ts b/server/src/lib/routes/error/redirector.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/firstfactor/get.ts b/server/src/lib/routes/firstfactor/get.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/firstfactor/post.spec.ts b/server/src/lib/routes/firstfactor/post.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/firstfactor/post.ts b/server/src/lib/routes/firstfactor/post.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/loggedin/get.ts b/server/src/lib/routes/loggedin/get.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/logout/get.ts b/server/src/lib/routes/logout/get.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/password-reset/constants.ts b/server/src/lib/routes/password-reset/constants.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/password-reset/form/post.spec.ts b/server/src/lib/routes/password-reset/form/post.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/password-reset/form/post.ts b/server/src/lib/routes/password-reset/form/post.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts b/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts b/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/password-reset/request/get.ts b/server/src/lib/routes/password-reset/request/get.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/secondfactor/get.spec.ts b/server/src/lib/routes/secondfactor/get.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/secondfactor/get.ts b/server/src/lib/routes/secondfactor/get.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/secondfactor/redirect.spec.ts b/server/src/lib/routes/secondfactor/redirect.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/secondfactor/redirect.ts b/server/src/lib/routes/secondfactor/redirect.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/secondfactor/totp/constants.ts b/server/src/lib/routes/secondfactor/totp/constants.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts b/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts b/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts b/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/secondfactor/totp/sign/post.ts b/server/src/lib/routes/secondfactor/totp/sign/post.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts b/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts b/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts b/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts b/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/secondfactor/u2f/register/post.ts b/server/src/lib/routes/secondfactor/u2f/register/post.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts b/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/secondfactor/u2f/register_request/get.ts b/server/src/lib/routes/secondfactor/u2f/register_request/get.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts b/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/secondfactor/u2f/sign/post.ts b/server/src/lib/routes/secondfactor/u2f/sign/post.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts b/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts b/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/verify/access_control.ts b/server/src/lib/routes/verify/access_control.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/verify/get.spec.ts b/server/src/lib/routes/verify/get.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/verify/get.ts b/server/src/lib/routes/verify/get.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/verify/get_basic_auth.ts b/server/src/lib/routes/verify/get_basic_auth.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/routes/verify/get_session_cookie.ts b/server/src/lib/routes/verify/get_session_cookie.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/AuthenticationTraceDocument.d.ts b/server/src/lib/storage/AuthenticationTraceDocument.d.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/CollectionFactoryFactory.ts b/server/src/lib/storage/CollectionFactoryFactory.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/CollectionFactoryStub.spec.ts b/server/src/lib/storage/CollectionFactoryStub.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/CollectionStub.spec.ts b/server/src/lib/storage/CollectionStub.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/ICollection.d.ts b/server/src/lib/storage/ICollection.d.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/ICollectionFactory.d.ts b/server/src/lib/storage/ICollectionFactory.d.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/IUserDataStore.d.ts b/server/src/lib/storage/IUserDataStore.d.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/IdentityValidationDocument.d.ts b/server/src/lib/storage/IdentityValidationDocument.d.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/TOTPSecretDocument.d.ts b/server/src/lib/storage/TOTPSecretDocument.d.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/U2FRegistrationDocument.d.ts b/server/src/lib/storage/U2FRegistrationDocument.d.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/UserDataStore.spec.ts b/server/src/lib/storage/UserDataStore.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/UserDataStore.ts b/server/src/lib/storage/UserDataStore.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/UserDataStoreStub.spec.ts b/server/src/lib/storage/UserDataStoreStub.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/mongo/MongoCollection.spec.ts b/server/src/lib/storage/mongo/MongoCollection.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/mongo/MongoCollection.ts b/server/src/lib/storage/mongo/MongoCollection.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts b/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/mongo/MongoCollectionFactory.ts b/server/src/lib/storage/mongo/MongoCollectionFactory.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/nedb/NedbCollection.spec.ts b/server/src/lib/storage/nedb/NedbCollection.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/nedb/NedbCollection.ts b/server/src/lib/storage/nedb/NedbCollection.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts b/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/storage/nedb/NedbCollectionFactory.ts b/server/src/lib/storage/nedb/NedbCollectionFactory.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/stubs/express.spec.ts b/server/src/lib/stubs/express.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/stubs/ldapjs.spec.ts b/server/src/lib/stubs/ldapjs.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/stubs/speakeasy.spec.ts b/server/src/lib/stubs/speakeasy.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/stubs/u2f.spec.ts b/server/src/lib/stubs/u2f.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/utils/HashGenerator.spec.ts b/server/src/lib/utils/HashGenerator.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/utils/HashGenerator.ts b/server/src/lib/utils/HashGenerator.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/utils/ObjectCloner.ts b/server/src/lib/utils/ObjectCloner.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/utils/SafeRedirection.spec.ts b/server/src/lib/utils/SafeRedirection.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/utils/SafeRedirection.ts b/server/src/lib/utils/SafeRedirection.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/utils/URLDecomposer.spec.ts b/server/src/lib/utils/URLDecomposer.spec.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/utils/URLDecomposer.ts b/server/src/lib/utils/URLDecomposer.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/web_server/Configurator.ts b/server/src/lib/web_server/Configurator.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/web_server/RestApi.ts b/server/src/lib/web_server/RestApi.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts b/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts old mode 100644 new mode 100755 diff --git a/server/src/lib/web_server/middlewares/WithHeadersLogged.ts b/server/src/lib/web_server/middlewares/WithHeadersLogged.ts old mode 100644 new mode 100755 diff --git a/server/src/resources/email-template.ejs b/server/src/resources/email-template.ejs old mode 100644 new mode 100755 diff --git a/server/src/views/already-logged-in.pug b/server/src/views/already-logged-in.pug old mode 100644 new mode 100755 diff --git a/server/src/views/errors/401.pug b/server/src/views/errors/401.pug old mode 100644 new mode 100755 diff --git a/server/src/views/errors/403.pug b/server/src/views/errors/403.pug old mode 100644 new mode 100755 diff --git a/server/src/views/errors/404.pug b/server/src/views/errors/404.pug old mode 100644 new mode 100755 diff --git a/server/src/views/firstfactor.pug b/server/src/views/firstfactor.pug old mode 100644 new mode 100755 diff --git a/server/src/views/layout/layout.pug b/server/src/views/layout/layout.pug old mode 100644 new mode 100755 diff --git a/server/src/views/need-identity-validation.pug b/server/src/views/need-identity-validation.pug old mode 100644 new mode 100755 diff --git a/server/src/views/password-reset-form.pug b/server/src/views/password-reset-form.pug old mode 100644 new mode 100755 diff --git a/server/src/views/password-reset-request.pug b/server/src/views/password-reset-request.pug old mode 100644 new mode 100755 diff --git a/server/src/views/secondfactor.pug b/server/src/views/secondfactor.pug old mode 100644 new mode 100755 diff --git a/server/src/views/totp-register.pug b/server/src/views/totp-register.pug old mode 100644 new mode 100755 diff --git a/server/src/views/u2f-register.pug b/server/src/views/u2f-register.pug old mode 100644 new mode 100755 diff --git a/server/test/requests.ts b/server/test/requests.ts old mode 100644 new mode 100755 diff --git a/server/tsconfig.json b/server/tsconfig.json old mode 100644 new mode 100755 diff --git a/server/tslint.json b/server/tslint.json old mode 100644 new mode 100755 diff --git a/server/types/AuthenticationSession.ts b/server/types/AuthenticationSession.ts old mode 100644 new mode 100755 diff --git a/server/types/Dependencies.ts b/server/types/Dependencies.ts old mode 100644 new mode 100755 diff --git a/server/types/Identity.ts b/server/types/Identity.ts old mode 100644 new mode 100755 diff --git a/server/types/TOTPSecret.ts b/server/types/TOTPSecret.ts old mode 100644 new mode 100755 diff --git a/server/types/U2FRegistration.ts b/server/types/U2FRegistration.ts old mode 100644 new mode 100755 diff --git a/server/types/dovehash.d.ts b/server/types/dovehash.d.ts old mode 100644 new mode 100755 diff --git a/server/types/speakeasy.d.ts b/server/types/speakeasy.d.ts old mode 100644 new mode 100755 diff --git a/shared/BelongToDomain.ts b/shared/BelongToDomain.ts old mode 100644 new mode 100755 diff --git a/shared/DomainExtractor.spec.ts b/shared/DomainExtractor.spec.ts old mode 100644 new mode 100755 diff --git a/shared/DomainExtractor.ts b/shared/DomainExtractor.ts old mode 100644 new mode 100755 diff --git a/shared/ErrorMessage.ts b/shared/ErrorMessage.ts old mode 100644 new mode 100755 diff --git a/shared/RedirectionMessage.ts b/shared/RedirectionMessage.ts old mode 100644 new mode 100755 diff --git a/shared/SignMessage.ts b/shared/SignMessage.ts old mode 100644 new mode 100755 diff --git a/shared/UserMessages.ts b/shared/UserMessages.ts old mode 100644 new mode 100755 diff --git a/shared/api.ts b/shared/api.ts old mode 100644 new mode 100755 diff --git a/shared/constants.ts b/shared/constants.ts old mode 100644 new mode 100755 diff --git a/shared/types/u2f.d.ts b/shared/types/u2f.d.ts old mode 100644 new mode 100755 diff --git a/test/complete-config/00-suite.ts b/test/complete-config/00-suite.ts old mode 100644 new mode 100755 diff --git a/test/complete-config/closed-redirection.ts b/test/complete-config/closed-redirection.ts old mode 100644 new mode 100755 diff --git a/test/complete-config/mongo-broken-connection.ts b/test/complete-config/mongo-broken-connection.ts old mode 100644 new mode 100755 diff --git a/test/configuration.ts b/test/configuration.ts old mode 100644 new mode 100755 diff --git a/test/environment.ts b/test/environment.ts old mode 100644 new mode 100755 diff --git a/test/features/access-control.feature b/test/features/access-control.feature old mode 100644 new mode 100755 diff --git a/test/features/auth-portal-redirection.feature b/test/features/auth-portal-redirection.feature old mode 100644 new mode 100755 diff --git a/test/features/authelia.feature b/test/features/authelia.feature old mode 100644 new mode 100755 diff --git a/test/features/authentication.feature b/test/features/authentication.feature old mode 100644 new mode 100755 diff --git a/test/features/forward-headers.feature b/test/features/forward-headers.feature old mode 100644 new mode 100755 diff --git a/test/features/redirection.feature b/test/features/redirection.feature old mode 100644 new mode 100755 diff --git a/test/features/registration.feature b/test/features/registration.feature old mode 100644 new mode 100755 diff --git a/test/features/regulation.feature b/test/features/regulation.feature old mode 100644 new mode 100755 diff --git a/test/features/reset-password.feature b/test/features/reset-password.feature old mode 100644 new mode 100755 diff --git a/test/features/resilience.feature b/test/features/resilience.feature old mode 100644 new mode 100755 diff --git a/test/features/restrictions.feature b/test/features/restrictions.feature old mode 100644 new mode 100755 diff --git a/test/features/session-timeout.feature b/test/features/session-timeout.feature old mode 100644 new mode 100755 diff --git a/test/features/single-factor-domain.feature b/test/features/single-factor-domain.feature old mode 100644 new mode 100755 diff --git a/test/features/step_definitions/access-control.ts b/test/features/step_definitions/access-control.ts old mode 100644 new mode 100755 diff --git a/test/features/step_definitions/authelia.ts b/test/features/step_definitions/authelia.ts old mode 100644 new mode 100755 diff --git a/test/features/step_definitions/authentication.ts b/test/features/step_definitions/authentication.ts old mode 100644 new mode 100755 diff --git a/test/features/step_definitions/forward-headers.ts b/test/features/step_definitions/forward-headers.ts old mode 100644 new mode 100755 diff --git a/test/features/step_definitions/hooks.ts b/test/features/step_definitions/hooks.ts old mode 100644 new mode 100755 diff --git a/test/features/step_definitions/notifications.ts b/test/features/step_definitions/notifications.ts old mode 100644 new mode 100755 diff --git a/test/features/step_definitions/redirection.ts b/test/features/step_definitions/redirection.ts old mode 100644 new mode 100755 diff --git a/test/features/step_definitions/registration.ts b/test/features/step_definitions/registration.ts old mode 100644 new mode 100755 diff --git a/test/features/step_definitions/regulation.ts b/test/features/step_definitions/regulation.ts old mode 100644 new mode 100755 diff --git a/test/features/step_definitions/reset-password.ts b/test/features/step_definitions/reset-password.ts old mode 100644 new mode 100755 diff --git a/test/features/step_definitions/resilience.ts b/test/features/step_definitions/resilience.ts old mode 100644 new mode 100755 diff --git a/test/features/step_definitions/restrictions.ts b/test/features/step_definitions/restrictions.ts old mode 100644 new mode 100755 diff --git a/test/features/step_definitions/session-timeout.ts b/test/features/step_definitions/session-timeout.ts old mode 100644 new mode 100755 diff --git a/test/features/step_definitions/single-factor.ts b/test/features/step_definitions/single-factor.ts old mode 100644 new mode 100755 diff --git a/test/features/support/world.ts b/test/features/support/world.ts old mode 100644 new mode 100755 diff --git a/test/helpers/access-secret.ts b/test/helpers/access-secret.ts old mode 100644 new mode 100755 diff --git a/test/helpers/click-on-button.ts b/test/helpers/click-on-button.ts old mode 100644 new mode 100755 diff --git a/test/helpers/click-on-link.ts b/test/helpers/click-on-link.ts old mode 100644 new mode 100755 diff --git a/test/helpers/fill-field.ts b/test/helpers/fill-field.ts old mode 100644 new mode 100755 diff --git a/test/helpers/fill-login-page-and-click.ts b/test/helpers/fill-login-page-and-click.ts old mode 100644 new mode 100755 diff --git a/test/helpers/full-login.ts b/test/helpers/full-login.ts old mode 100644 new mode 100755 diff --git a/test/helpers/get-identity-link.ts b/test/helpers/get-identity-link.ts old mode 100644 new mode 100755 diff --git a/test/helpers/login-and-register-totp.ts b/test/helpers/login-and-register-totp.ts old mode 100644 new mode 100755 diff --git a/test/helpers/login-as.ts b/test/helpers/login-as.ts old mode 100644 new mode 100755 diff --git a/test/helpers/register-totp.ts b/test/helpers/register-totp.ts old mode 100644 new mode 100755 diff --git a/test/helpers/see-notification.ts b/test/helpers/see-notification.ts old mode 100644 new mode 100755 diff --git a/test/helpers/validate-totp.ts b/test/helpers/validate-totp.ts old mode 100644 new mode 100755 diff --git a/test/helpers/visit-page.ts b/test/helpers/visit-page.ts old mode 100644 new mode 100755 diff --git a/test/helpers/wait-redirected.ts b/test/helpers/wait-redirected.ts old mode 100644 new mode 100755 diff --git a/test/helpers/with-driver.ts b/test/helpers/with-driver.ts old mode 100644 new mode 100755 diff --git a/test/inactivity/00-suite.ts b/test/inactivity/00-suite.ts old mode 100644 new mode 100755 diff --git a/test/inactivity/keep_me_logged_in.ts b/test/inactivity/keep_me_logged_in.ts old mode 100644 new mode 100755 diff --git a/test/minimal-config/00-suite.ts b/test/minimal-config/00-suite.ts old mode 100644 new mode 100755 diff --git a/test/minimal-config/bad_password.ts b/test/minimal-config/bad_password.ts old mode 100644 new mode 100755 diff --git a/test/minimal-config/fail_totp.ts b/test/minimal-config/fail_totp.ts old mode 100644 new mode 100755 diff --git a/test/minimal-config/register_totp.ts b/test/minimal-config/register_totp.ts old mode 100644 new mode 100755 diff --git a/test/minimal-config/reset_password.ts b/test/minimal-config/reset_password.ts old mode 100644 new mode 100755 diff --git a/test/minimal-config/validate_totp.ts b/test/minimal-config/validate_totp.ts old mode 100644 new mode 100755 diff --git a/themes/black/client/src/css/.directory b/themes/black/client/src/css/.directory old mode 100644 new mode 100755 diff --git a/themes/black/client/src/css/00-bootstrap.min.css b/themes/black/client/src/css/00-bootstrap.min.css old mode 100644 new mode 100755 diff --git a/themes/black/client/src/css/01-main.css b/themes/black/client/src/css/01-main.css old mode 100644 new mode 100755 index 318b90e25..e62ff8dd0 --- a/themes/black/client/src/css/01-main.css +++ b/themes/black/client/src/css/01-main.css @@ -2,7 +2,7 @@ body { /*background-image: url("//*img//*LargeTriangles.svg");*/ /*background-image: url("//*img//*RandomizedPattern.svg");*/ /*background-image: url("//*img//*background.svg");*/ - background-color:#000000;*/ + background-color:#000000; } canvas{ position:absolute; diff --git a/themes/black/client/src/css/02-login.css b/themes/black/client/src/css/02-login.css old mode 100644 new mode 100755 diff --git a/themes/black/client/src/css/03-errors.css b/themes/black/client/src/css/03-errors.css old mode 100644 new mode 100755 diff --git a/themes/black/client/src/css/03-password-reset-form.css b/themes/black/client/src/css/03-password-reset-form.css old mode 100644 new mode 100755 diff --git a/themes/black/client/src/css/03-password-reset-request.css b/themes/black/client/src/css/03-password-reset-request.css old mode 100644 new mode 100755 diff --git a/themes/black/client/src/css/03-totp-register.css b/themes/black/client/src/css/03-totp-register.css old mode 100644 new mode 100755 diff --git a/themes/black/client/src/css/03-u2f-register.css b/themes/black/client/src/css/03-u2f-register.css old mode 100644 new mode 100755 diff --git a/themes/black/client/src/img/RandomizedPattern.svg b/themes/black/client/src/img/RandomizedPattern.svg old mode 100644 new mode 100755 diff --git a/themes/black/client/src/img/background.jpg b/themes/black/client/src/img/background.jpg old mode 100644 new mode 100755 diff --git a/themes/black/client/src/img/icon.png b/themes/black/client/src/img/icon.png old mode 100644 new mode 100755 diff --git a/themes/black/client/src/img/mail.png b/themes/black/client/src/img/mail.png old mode 100644 new mode 100755 diff --git a/themes/black/client/src/img/notifications/.directory b/themes/black/client/src/img/notifications/.directory old mode 100644 new mode 100755 diff --git a/themes/black/client/src/img/notifications/error.png b/themes/black/client/src/img/notifications/error.png old mode 100644 new mode 100755 diff --git a/themes/black/client/src/img/notifications/info.png b/themes/black/client/src/img/notifications/info.png old mode 100644 new mode 100755 diff --git a/themes/black/client/src/img/notifications/success.png b/themes/black/client/src/img/notifications/success.png old mode 100644 new mode 100755 diff --git a/themes/black/client/src/img/notifications/warning.png b/themes/black/client/src/img/notifications/warning.png old mode 100644 new mode 100755 diff --git a/themes/black/client/src/img/padlock.png b/themes/black/client/src/img/padlock.png old mode 100644 new mode 100755 diff --git a/themes/black/client/src/img/password_white.png b/themes/black/client/src/img/password_white.png old mode 100644 new mode 100755 diff --git a/themes/black/client/src/img/pendrive.png b/themes/black/client/src/img/pendrive.png old mode 100644 new mode 100755 diff --git a/themes/black/client/src/img/sharingan.png b/themes/black/client/src/img/sharingan.png old mode 100644 new mode 100755 diff --git a/themes/black/client/src/img/stores/.directory b/themes/black/client/src/img/stores/.directory old mode 100644 new mode 100755 diff --git a/themes/black/client/src/img/stores/applestore-badge.svg b/themes/black/client/src/img/stores/applestore-badge.svg old mode 100644 new mode 100755 diff --git a/themes/black/client/src/img/stores/googleplay-badge.svg b/themes/black/client/src/img/stores/googleplay-badge.svg old mode 100644 new mode 100755 diff --git a/themes/black/client/src/img/success.png b/themes/black/client/src/img/success.png old mode 100644 new mode 100755 diff --git a/themes/black/client/src/img/user.png b/themes/black/client/src/img/user.png old mode 100644 new mode 100755 diff --git a/themes/black/client/src/img/warning.png b/themes/black/client/src/img/warning.png old mode 100644 new mode 100755 diff --git a/themes/black/client/src/index.ts b/themes/black/client/src/index.ts deleted file mode 100644 index 802004a8f..000000000 --- a/themes/black/client/src/index.ts +++ /dev/null @@ -1,34 +0,0 @@ - -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 () { - (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, (global as any).u2f); - 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, (global as any).u2f); - 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); -})(); diff --git a/themes/black/client/src/lib/GetPromised.ts b/themes/black/client/src/lib/GetPromised.ts deleted file mode 100644 index 779139654..000000000 --- a/themes/black/client/src/lib/GetPromised.ts +++ /dev/null @@ -1,14 +0,0 @@ -import BluebirdPromise = require("bluebird"); - -export default function ($: JQueryStatic, url: string, data: Object, fn: any, - dataType: string): BluebirdPromise { - return new BluebirdPromise((resolve, reject) => { - $.get(url, {}, undefined, dataType) - .done((data: any) => { - resolve(data); - }) - .fail((xhr: JQueryXHR, textStatus: string) => { - reject(textStatus); - }); - }); -} \ No newline at end of file diff --git a/themes/black/client/src/lib/INotifier.ts b/themes/black/client/src/lib/INotifier.ts deleted file mode 100644 index df947538f..000000000 --- a/themes/black/client/src/lib/INotifier.ts +++ /dev/null @@ -1,14 +0,0 @@ - -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; -} \ No newline at end of file diff --git a/themes/black/client/src/lib/Notifier.ts b/themes/black/client/src/lib/Notifier.ts deleted file mode 100644 index c0252b9b9..000000000 --- a/themes/black/client/src/lib/Notifier.ts +++ /dev/null @@ -1,83 +0,0 @@ - - -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('status %s\ - %s', 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); - } -} \ No newline at end of file diff --git a/themes/black/client/src/lib/QueryParametersRetriever.ts b/themes/black/client/src/lib/QueryParametersRetriever.ts deleted file mode 100644 index a529adb6e..000000000 --- a/themes/black/client/src/lib/QueryParametersRetriever.ts +++ /dev/null @@ -1,12 +0,0 @@ - -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, " ")); - } -} \ No newline at end of file diff --git a/themes/black/client/src/lib/SafeRedirect.ts b/themes/black/client/src/lib/SafeRedirect.ts deleted file mode 100644 index 7e7684b8a..000000000 --- a/themes/black/client/src/lib/SafeRedirect.ts +++ /dev/null @@ -1,10 +0,0 @@ -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(); -} \ No newline at end of file diff --git a/themes/black/client/src/lib/firstfactor/FirstFactorValidator.ts b/themes/black/client/src/lib/firstfactor/FirstFactorValidator.ts deleted file mode 100644 index eaa496fdd..000000000 --- a/themes/black/client/src/lib/firstfactor/FirstFactorValidator.ts +++ /dev/null @@ -1,46 +0,0 @@ - -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 { - return new BluebirdPromise(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)); - }); - }); -} diff --git a/themes/black/client/src/lib/firstfactor/UISelectors.ts b/themes/black/client/src/lib/firstfactor/UISelectors.ts deleted file mode 100644 index 0e971b3c3..000000000 --- a/themes/black/client/src/lib/firstfactor/UISelectors.ts +++ /dev/null @@ -1,5 +0,0 @@ - -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"; diff --git a/themes/black/client/src/lib/firstfactor/index.ts b/themes/black/client/src/lib/firstfactor/index.ts deleted file mode 100644 index 24affee2d..000000000 --- a/themes/black/client/src/lib/firstfactor/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -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); - }); -} - diff --git a/themes/black/client/src/lib/reset-password/constants.ts b/themes/black/client/src/lib/reset-password/constants.ts deleted file mode 100644 index d48d4e67d..000000000 --- a/themes/black/client/src/lib/reset-password/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export const FORM_SELECTOR = ".form-signin"; \ No newline at end of file diff --git a/themes/black/client/src/lib/reset-password/reset-password-form.ts b/themes/black/client/src/lib/reset-password/reset-password-form.ts deleted file mode 100644 index b94279cde..000000000 --- a/themes/black/client/src/lib/reset-password/reset-password-form.ts +++ /dev/null @@ -1,57 +0,0 @@ -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); - }); -} diff --git a/themes/black/client/src/lib/reset-password/reset-password-request.ts b/themes/black/client/src/lib/reset-password/reset-password-request.ts deleted file mode 100644 index 846226d75..000000000 --- a/themes/black/client/src/lib/reset-password/reset-password-request.ts +++ /dev/null @@ -1,56 +0,0 @@ - -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); - }); -} - diff --git a/themes/black/client/src/lib/secondfactor/TOTPValidator.ts b/themes/black/client/src/lib/secondfactor/TOTPValidator.ts deleted file mode 100644 index 5394139a6..000000000 --- a/themes/black/client/src/lib/secondfactor/TOTPValidator.ts +++ /dev/null @@ -1,28 +0,0 @@ - -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 { - return new BluebirdPromise(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)); - }); - }); -} \ No newline at end of file diff --git a/themes/black/client/src/lib/secondfactor/U2FValidator.ts b/themes/black/client/src/lib/secondfactor/U2FValidator.ts deleted file mode 100644 index 5812922f7..000000000 --- a/themes/black/client/src/lib/secondfactor/U2FValidator.ts +++ /dev/null @@ -1,42 +0,0 @@ -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 { - return new BluebirdPromise(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 { - 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, $); - }); -} diff --git a/themes/black/client/src/lib/secondfactor/constants.ts b/themes/black/client/src/lib/secondfactor/constants.ts deleted file mode 100644 index 50bba7571..000000000 --- a/themes/black/client/src/lib/secondfactor/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ - -export const TOTP_FORM_SELECTOR = ".form-signin.totp"; -export const TOTP_TOKEN_SELECTOR = ".form-signin #token"; diff --git a/themes/black/client/src/lib/secondfactor/index.ts b/themes/black/client/src/lib/secondfactor/index.ts deleted file mode 100644 index 279723dce..000000000 --- a/themes/black/client/src/lib/secondfactor/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -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); - }); -} \ No newline at end of file diff --git a/themes/black/client/src/lib/totp-register/totp-register.ts b/themes/black/client/src/lib/totp-register/totp-register.ts deleted file mode 100644 index 6a9aa7ee0..000000000 --- a/themes/black/client/src/lib/totp-register/totp-register.ts +++ /dev/null @@ -1,11 +0,0 @@ - -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); -} diff --git a/themes/black/client/src/lib/totp-register/ui-selector.ts b/themes/black/client/src/lib/totp-register/ui-selector.ts deleted file mode 100644 index 9d43fabea..000000000 --- a/themes/black/client/src/lib/totp-register/ui-selector.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export const QRCODE_ID_SELECTOR = "#qrcode"; \ No newline at end of file diff --git a/themes/black/client/src/lib/u2f-register/u2f-register.ts b/themes/black/client/src/lib/u2f-register/u2f-register.ts deleted file mode 100644 index abf40ee05..000000000 --- a/themes/black/client/src/lib/u2f-register/u2f-register.ts +++ /dev/null @@ -1,56 +0,0 @@ - -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 { - return new BluebirdPromise(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 { - 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); - }); - }); -} diff --git a/themes/black/client/src/thirdparties/qrcode.min.js b/themes/black/client/src/thirdparties/qrcode.min.js old mode 100644 new mode 100755 diff --git a/themes/black/client/src/thirdparties/u2f-api.js b/themes/black/client/src/thirdparties/u2f-api.js old mode 100644 new mode 100755 diff --git a/themes/black/client/test/Notifier.test.ts b/themes/black/client/test/Notifier.test.ts deleted file mode 100644 index 70bfea146..000000000 --- a/themes/black/client/test/Notifier.test.ts +++ /dev/null @@ -1,71 +0,0 @@ - -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"); - }); -}); \ No newline at end of file diff --git a/themes/black/client/test/firstfactor/FirstFactorValidator.test.ts b/themes/black/client/test/firstfactor/FirstFactorValidator.test.ts deleted file mode 100644 index ac8353278..000000000 --- a/themes/black/client/test/firstfactor/FirstFactorValidator.test.ts +++ /dev/null @@ -1,44 +0,0 @@ - -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", "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", "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."); - }); - }); -}); \ No newline at end of file diff --git a/themes/black/client/test/mocks/NotifierStub.ts b/themes/black/client/test/mocks/NotifierStub.ts deleted file mode 100644 index 9c268d66d..000000000 --- a/themes/black/client/test/mocks/NotifierStub.ts +++ /dev/null @@ -1,33 +0,0 @@ - -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(); - } -} \ No newline at end of file diff --git a/themes/black/client/test/mocks/jquery.ts b/themes/black/client/test/mocks/jquery.ts deleted file mode 100644 index 273f90861..000000000 --- a/themes/black/client/test/mocks/jquery.ts +++ /dev/null @@ -1,59 +0,0 @@ - -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() - }; -} diff --git a/themes/black/client/test/mocks/u2f-api.ts b/themes/black/client/test/mocks/u2f-api.ts deleted file mode 100644 index d123f6a95..000000000 --- a/themes/black/client/test/mocks/u2f-api.ts +++ /dev/null @@ -1,14 +0,0 @@ - -import sinon = require("sinon"); - -export interface U2FApiMock { - sign: sinon.SinonStub; - register: sinon.SinonStub; -} - -export function U2FApiMock(): U2FApiMock { - return { - sign: sinon.stub(), - register: sinon.stub() - }; -} \ No newline at end of file diff --git a/themes/black/client/test/secondfactor/TOTPValidator.test.ts b/themes/black/client/test/secondfactor/TOTPValidator.test.ts deleted file mode 100644 index 5dd6f15c3..000000000 --- a/themes/black/client/test/secondfactor/TOTPValidator.test.ts +++ /dev/null @@ -1,37 +0,0 @@ - -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(); - }); - }); -}); \ No newline at end of file diff --git a/themes/black/client/test/totp-register/totp-register.test.ts b/themes/black/client/test/totp-register/totp-register.test.ts deleted file mode 100644 index 86fc455a1..000000000 --- a/themes/black/client/test/totp-register/totp-register.test.ts +++ /dev/null @@ -1,31 +0,0 @@ - -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); - }); -}); \ No newline at end of file diff --git a/themes/black/client/tsconfig.json b/themes/black/client/tsconfig.json deleted file mode 100644 index 0bb4d62ff..000000000 --- a/themes/black/client/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "moduleResolution": "node", - "noImplicitAny": true, - "sourceMap": true, - "removeComments": true, - "outDir": "../dist", - "baseUrl": ".", - "paths": { - "*": [ - "./types/*", - "../shared/types/*" - ] - } - }, - "include": [ - "src/**/*" - ], - "exclude": [ - "test/**/*" - ] -} diff --git a/themes/black/client/tslint.json b/themes/black/client/tslint.json deleted file mode 100644 index c2c1b7501..000000000 --- a/themes/black/client/tslint.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "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 - } -} diff --git a/themes/black/server/.directory b/themes/black/server/.directory old mode 100644 new mode 100755 diff --git a/themes/black/server/src/index.ts b/themes/black/server/src/index.ts deleted file mode 100755 index fcbf4d022..000000000 --- a/themes/black/server/src/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -#! /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 "); - 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); diff --git a/themes/black/server/src/lib/.directory b/themes/black/server/src/lib/.directory deleted file mode 100644 index 006b379ad..000000000 --- a/themes/black/server/src/lib/.directory +++ /dev/null @@ -1,4 +0,0 @@ -[Dolphin] -Timestamp=2018,12,17,20,59,13 -Version=3 -ViewMode=1 diff --git a/themes/black/server/src/lib/AuthenticationSessionHandler.ts b/themes/black/server/src/lib/AuthenticationSessionHandler.ts deleted file mode 100644 index 57361bf8a..000000000 --- a/themes/black/server/src/lib/AuthenticationSessionHandler.ts +++ /dev/null @@ -1,45 +0,0 @@ - - -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; - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/ErrorReplies.ts b/themes/black/server/src/lib/ErrorReplies.ts deleted file mode 100644 index f1c5f4fd1..000000000 --- a/themes/black/server/src/lib/ErrorReplies.ts +++ /dev/null @@ -1,49 +0,0 @@ -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 }); -} \ No newline at end of file diff --git a/themes/black/server/src/lib/Exceptions.ts b/themes/black/server/src/lib/Exceptions.ts deleted file mode 100644 index 83fa4eb6b..000000000 --- a/themes/black/server/src/lib/Exceptions.ts +++ /dev/null @@ -1,88 +0,0 @@ - -export class LdapSearchError extends Error { - constructor(message?: string) { - super(message); - this.name = "LdapSearchError"; - (Object).setPrototypeOf(this, LdapSearchError.prototype); - } -} - -export class LdapBindError extends Error { - constructor(message?: string) { - super(message); - this.name = "LdapBindError"; - (Object).setPrototypeOf(this, LdapBindError.prototype); - } -} - -export class LdapError extends Error { - constructor(message?: string) { - super(message); - this.name = "LdapError"; - (Object).setPrototypeOf(this, LdapError.prototype); - } -} - -export class IdentityError extends Error { - constructor(message?: string) { - super(message); - this.name = "IdentityError"; - (Object).setPrototypeOf(this, IdentityError.prototype); - } -} - -export class AccessDeniedError extends Error { - constructor(message?: string) { - super(message); - this.name = "AccessDeniedError"; - (Object).setPrototypeOf(this, AccessDeniedError.prototype); - } -} - -export class AuthenticationRegulationError extends Error { - constructor(message?: string) { - super(message); - this.name = "AuthenticationRegulationError"; - (Object).setPrototypeOf(this, AuthenticationRegulationError.prototype); - } -} - -export class InvalidTOTPError extends Error { - constructor(message?: string) { - super(message); - this.name = "InvalidTOTPError"; - (Object).setPrototypeOf(this, InvalidTOTPError.prototype); - } -} - -export class NotAuthenticatedError extends Error { - constructor(message?: string) { - super(message); - this.name = "NotAuthenticatedError"; - (Object).setPrototypeOf(this, NotAuthenticatedError.prototype); - } -} - -export class NotAuthorizedError extends Error { - constructor(message?: string) { - super(message); - this.name = "NotAuthanticatedError"; - (Object).setPrototypeOf(this, NotAuthorizedError.prototype); - } -} - -export class FirstFactorValidationError extends Error { - constructor(message?: string) { - super(message); - this.name = "FirstFactorValidationError"; - (Object).setPrototypeOf(this, FirstFactorValidationError.prototype); - } -} - -export class SecondFactorValidationError extends Error { - constructor(message?: string) { - super(message); - this.name = "SecondFactorValidationError"; - (Object).setPrototypeOf(this, FirstFactorValidationError.prototype); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/FirstFactorValidator.ts b/themes/black/server/src/lib/FirstFactorValidator.ts deleted file mode 100644 index 231060002..000000000 --- a/themes/black/server/src/lib/FirstFactorValidator.ts +++ /dev/null @@ -1,20 +0,0 @@ - -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 { - 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(); - }); -} \ No newline at end of file diff --git a/themes/black/server/src/lib/IdentityCheckMiddleware.spec.ts b/themes/black/server/src/lib/IdentityCheckMiddleware.spec.ts deleted file mode 100644 index 842ed6bcb..000000000 --- a/themes/black/server/src/lib/IdentityCheckMiddleware.spec.ts +++ /dev/null @@ -1,176 +0,0 @@ - -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")); - }); - }); - }); -}); diff --git a/themes/black/server/src/lib/IdentityCheckMiddleware.ts b/themes/black/server/src/lib/IdentityCheckMiddleware.ts deleted file mode 100644 index e72ea4db1..000000000 --- a/themes/black/server/src/lib/IdentityCheckMiddleware.ts +++ /dev/null @@ -1,138 +0,0 @@ -import objectPath = require("object-path"); -import randomstring = require("randomstring"); -import BluebirdPromise = require("bluebird"); -import util = require("util"); -import Exceptions = require("./Exceptions"); -import fs = require("fs"); -import ejs = require("ejs"); -import { 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 { - - 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 { - 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 { - 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 { - - let authSession: AuthenticationSession; - const identityToken = objectPath.get( - 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 { - 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)); - }; -} diff --git a/themes/black/server/src/lib/IdentityCheckPreValidationTemplate.ts b/themes/black/server/src/lib/IdentityCheckPreValidationTemplate.ts deleted file mode 100644 index 0161ce406..000000000 --- a/themes/black/server/src/lib/IdentityCheckPreValidationTemplate.ts +++ /dev/null @@ -1,3 +0,0 @@ - - -export const PRE_VALIDATION_TEMPLATE = "need-identity-validation"; \ No newline at end of file diff --git a/themes/black/server/src/lib/IdentityValidable.ts b/themes/black/server/src/lib/IdentityValidable.ts deleted file mode 100644 index 075580c9e..000000000 --- a/themes/black/server/src/lib/IdentityValidable.ts +++ /dev/null @@ -1,19 +0,0 @@ -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; - postValidationInit(req: Express.Request): Bluebird; - - // 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; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/IdentityValidableStub.spec.ts b/themes/black/server/src/lib/IdentityValidableStub.spec.ts deleted file mode 100644 index 20a977140..000000000 --- a/themes/black/server/src/lib/IdentityValidableStub.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ - -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 { - return this.preValidationInitStub(req); - } - - postValidationInit(req: Express.Request): Bluebird { - 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(); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/Server.spec.ts b/themes/black/server/src/lib/Server.spec.ts deleted file mode 100644 index 365163254..000000000 --- a/themes/black/server/src/lib/Server.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ - -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(); - }); - }); -}); diff --git a/themes/black/server/src/lib/Server.ts b/themes/black/server/src/lib/Server.ts deleted file mode 100644 index 4090f6294..000000000 --- a/themes/black/server/src/lib/Server.ts +++ /dev/null @@ -1,93 +0,0 @@ -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 { - 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((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 { - 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(); - } -} - diff --git a/themes/black/server/src/lib/ServerVariables.ts b/themes/black/server/src/lib/ServerVariables.ts deleted file mode 100644 index cd3dd6dc8..000000000 --- a/themes/black/server/src/lib/ServerVariables.ts +++ /dev/null @@ -1,21 +0,0 @@ -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; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/ServerVariablesInitializer.ts b/themes/black/server/src/lib/ServerVariablesInitializer.ts deleted file mode 100644 index df79238cc..000000000 --- a/themes/black/server/src/lib/ServerVariablesInitializer.ts +++ /dev/null @@ -1,116 +0,0 @@ - -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 { - 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 { - - 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); - }); - } -} diff --git a/themes/black/server/src/lib/ServerVariablesMockBuilder.spec.ts b/themes/black/server/src/lib/ServerVariablesMockBuilder.spec.ts deleted file mode 100644 index 7874702a0..000000000 --- a/themes/black/server/src/lib/ServerVariablesMockBuilder.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -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 - }; - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/Level.ts b/themes/black/server/src/lib/authentication/Level.ts deleted file mode 100644 index 57b6a2346..000000000 --- a/themes/black/server/src/lib/authentication/Level.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum Level { - NOT_AUTHENTICATED = 0, - ONE_FACTOR = 1, - TWO_FACTOR = 2 -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/GroupsAndEmails.ts b/themes/black/server/src/lib/authentication/backends/GroupsAndEmails.ts deleted file mode 100644 index 3434ba66f..000000000 --- a/themes/black/server/src/lib/authentication/backends/GroupsAndEmails.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface GroupsAndEmails { - groups: string[]; - emails: string[]; -} diff --git a/themes/black/server/src/lib/authentication/backends/IUsersDatabase.ts b/themes/black/server/src/lib/authentication/backends/IUsersDatabase.ts deleted file mode 100644 index d7fa13b7a..000000000 --- a/themes/black/server/src/lib/authentication/backends/IUsersDatabase.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Bluebird = require("bluebird"); - -import { GroupsAndEmails } from "./GroupsAndEmails"; - -export interface IUsersDatabase { - checkUserPassword(username: string, password: string): Bluebird; - getEmails(username: string): Bluebird; - getGroups(username: string): Bluebird; - updatePassword(username: string, newPassword: string): Bluebird; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts b/themes/black/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts deleted file mode 100644 index 19341a5dd..000000000 --- a/themes/black/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -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 { - return this.checkUserPasswordStub(username, password); - } - - getEmails(username: string): Bluebird { - return this.getEmailsStub(username); - } - - getGroups(username: string): Bluebird { - return this.getGroupsStub(username); - } - - updatePassword(username: string, newPassword: string): Bluebird { - return this.updatePasswordStub(username, newPassword); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts b/themes/black/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts deleted file mode 100644 index a258a78f7..000000000 --- a/themes/black/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts +++ /dev/null @@ -1,224 +0,0 @@ -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()); - }); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/file/FileUsersDatabase.ts b/themes/black/server/src/lib/authentication/backends/file/FileUsersDatabase.ts deleted file mode 100644 index d34dde21e..000000000 --- a/themes/black/server/src/lib/authentication/backends/file/FileUsersDatabase.ts +++ /dev/null @@ -1,182 +0,0 @@ -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 { - return new Bluebird((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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - return this.readDatabase() - .then((database) => { - return this.checkUserExists(database, username) - .then(() => this.retrieveEmails(database, username)); - }); - } - - getGroups(username: string): Bluebird { - return this.readDatabase() - .then((database) => { - return this.checkUserExists(database, username) - .then(() => this.retrieveGroups(database, username)); - }); - } - - updatePassword(username: string, newPassword: string): Bluebird { - return this.readDatabase() - .then((database) => { - return this.checkUserExists(database, username) - .then(() => this.replacePassword(database, username, newPassword)); - }); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/file/ReadWriteQueue.ts b/themes/black/server/src/lib/authentication/backends/file/ReadWriteQueue.ts deleted file mode 100644 index 957ddaec5..000000000 --- a/themes/black/server/src/lib/authentication/backends/file/ReadWriteQueue.ts +++ /dev/null @@ -1,60 +0,0 @@ -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(); - } - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/ISession.ts b/themes/black/server/src/lib/authentication/backends/ldap/ISession.ts deleted file mode 100644 index da2c74433..000000000 --- a/themes/black/server/src/lib/authentication/backends/ldap/ISession.ts +++ /dev/null @@ -1,12 +0,0 @@ - -import BluebirdPromise = require("bluebird"); - -export interface ISession { - open(): BluebirdPromise; - close(): BluebirdPromise; - - searchUserDn(username: string): BluebirdPromise; - searchEmails(username: string): BluebirdPromise; - searchGroups(username: string): BluebirdPromise; - modifyPassword(username: string, newPassword: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/ISessionFactory.ts b/themes/black/server/src/lib/authentication/backends/ldap/ISessionFactory.ts deleted file mode 100644 index 014d1eea5..000000000 --- a/themes/black/server/src/lib/authentication/backends/ldap/ISessionFactory.ts +++ /dev/null @@ -1,6 +0,0 @@ - -import { ISession } from "./ISession"; - -export interface ISessionFactory { - create(userDN: string, password: string): ISession; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts b/themes/black/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts deleted file mode 100644 index f4a6e6306..000000000 --- a/themes/black/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts +++ /dev/null @@ -1,386 +0,0 @@ -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); - }) - }); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts b/themes/black/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts deleted file mode 100644 index edda62ec6..000000000 --- a/themes/black/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts +++ /dev/null @@ -1,107 +0,0 @@ -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 = (session: ISession) => Bluebird; - -export class LdapUsersDatabase implements IUsersDatabase { - private sessionFactory: ISessionFactory; - private configuration: LdapConfiguration; - - constructor( - sessionFactory: ISessionFactory, - configuration: LdapConfiguration) { - this.sessionFactory = sessionFactory; - this.configuration = configuration; - } - - private withSession( - username: string, - password: string, - cb: SessionCallback): Bluebird { - const session = this.sessionFactory.create(username, password); - return session.open() - .then(() => cb(session)) - .finally(() => session.close()); - } - - checkUserPassword(username: string, password: string): Bluebird { - const that = this; - function verifyUserPassword(userDN: string) { - return that.withSession( - 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 { - 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 { - 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 { - 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)); - }); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts b/themes/black/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts deleted file mode 100644 index 9dedfcb7c..000000000 --- a/themes/black/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -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"); - }); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/SafeSession.ts b/themes/black/server/src/lib/authentication/backends/ldap/SafeSession.ts deleted file mode 100644 index 572209066..000000000 --- a/themes/black/server/src/lib/authentication/backends/ldap/SafeSession.ts +++ /dev/null @@ -1,62 +0,0 @@ -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 { - return this.sesion.open(); - } - - close(): BluebirdPromise { - return this.sesion.close(); - } - - searchGroups(username: string): BluebirdPromise { - 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 { - 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 { - 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 { - try { - const sanitizedUsername = Sanitizer.sanitize(username); - return this.sesion.modifyPassword(sanitizedUsername, newPassword); - } - catch (e) { - return BluebirdPromise.reject(new Error(SPECIAL_CHAR_USED_MESSAGE)); - } - } -} diff --git a/themes/black/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts b/themes/black/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts deleted file mode 100644 index 9dd33fed4..000000000 --- a/themes/black/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -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 { 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); - }); -}); diff --git a/themes/black/server/src/lib/authentication/backends/ldap/Sanitizer.ts b/themes/black/server/src/lib/authentication/backends/ldap/Sanitizer.ts deleted file mode 100644 index be74132ad..000000000 --- a/themes/black/server/src/lib/authentication/backends/ldap/Sanitizer.ts +++ /dev/null @@ -1,25 +0,0 @@ - -// 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; - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/Session.spec.ts b/themes/black/server/src/lib/authentication/backends/ldap/Session.spec.ts deleted file mode 100644 index d55f6a805..000000000 --- a/themes/black/server/src/lib/authentication/backends/ldap/Session.spec.ts +++ /dev/null @@ -1,127 +0,0 @@ - -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"]); - }); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/Session.ts b/themes/black/server/src/lib/authentication/backends/ldap/Session.ts deleted file mode 100644 index e0284b3c4..000000000 --- a/themes/black/server/src/lib/authentication/backends/ldap/Session.ts +++ /dev/null @@ -1,156 +0,0 @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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(); - }); - } -} diff --git a/themes/black/server/src/lib/authentication/backends/ldap/SessionFactory.ts b/themes/black/server/src/lib/authentication/backends/ldap/SessionFactory.ts deleted file mode 100644 index 0b6c4bff3..000000000 --- a/themes/black/server/src/lib/authentication/backends/ldap/SessionFactory.ts +++ /dev/null @@ -1,37 +0,0 @@ -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 - ) - ); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts b/themes/black/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts deleted file mode 100644 index face3930d..000000000 --- a/themes/black/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts b/themes/black/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts deleted file mode 100644 index 5faf2ba18..000000000 --- a/themes/black/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -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 { - return this.openStub(); - } - - close(): Bluebird { - return this.closeStub(); - } - - searchUserDn(username: string): Bluebird { - return this.searchUserDnStub(username); - } - - searchEmails(username: string): Bluebird { - return this.searchEmailsStub(username); - } - - searchGroups(username: string): Bluebird { - return this.searchGroupsStub(username); - } - - modifyPassword(username: string, newPassword: string): Bluebird { - return this.modifyPasswordStub(username, newPassword); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/connector/Connector.ts b/themes/black/server/src/lib/authentication/backends/ldap/connector/Connector.ts deleted file mode 100644 index 2542ea7f9..000000000 --- a/themes/black/server/src/lib/authentication/backends/ldap/connector/Connector.ts +++ /dev/null @@ -1,69 +0,0 @@ -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; - unbindAsync(): Bluebird; - searchAsync(base: string, query: LdapJs.SearchOptions): Bluebird; - modifyAsync(userdn: string, change: LdapJs.Change): Bluebird; -} - -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 { - return this.client.bindAsync(username, password); - } - - unbindAsync(): Bluebird { - return this.client.unbindAsync(); - } - - searchAsync(base: string, query: any): Bluebird { - const that = this; - return this.client.searchAsync(base, query) - .then(function (res: EventEmitter) { - const doc: SearchEntry[] = []; - return new Bluebird((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 { - return this.client.modifyAsync(dn, changeRequest); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts b/themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts deleted file mode 100644 index 61fef07a4..000000000 --- a/themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts +++ /dev/null @@ -1,18 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts b/themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts deleted file mode 100644 index d11fa6386..000000000 --- a/themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -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(); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts b/themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts deleted file mode 100644 index 0b78225bf..000000000 --- a/themes/black/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -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 { - return this.bindAsyncStub(username, password); - } - - unbindAsync(): BluebirdPromise { - return this.unbindAsyncStub(); - } - - searchAsync(base: string, query: any): BluebirdPromise { - return this.searchAsyncStub(base, query); - } - - modifyAsync(dn: string, changeRequest: any): BluebirdPromise { - return this.modifyAsyncStub(dn, changeRequest); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/connector/IConnector.ts b/themes/black/server/src/lib/authentication/backends/ldap/connector/IConnector.ts deleted file mode 100644 index 1e63ab193..000000000 --- a/themes/black/server/src/lib/authentication/backends/ldap/connector/IConnector.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Bluebird = require("bluebird"); -import EventEmitter = require("events"); - -export interface IConnector { - bindAsync(username: string, password: string): Bluebird; - unbindAsync(): Bluebird; - searchAsync(base: string, query: any): Bluebird; - modifyAsync(dn: string, changeRequest: any): Bluebird; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts b/themes/black/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts deleted file mode 100644 index f9ed65eff..000000000 --- a/themes/black/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { IConnector } from "./IConnector"; - -export interface IConnectorFactory { - create(): IConnector; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/totp/ITotpHandler.ts b/themes/black/server/src/lib/authentication/totp/ITotpHandler.ts deleted file mode 100644 index d600d31e4..000000000 --- a/themes/black/server/src/lib/authentication/totp/ITotpHandler.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { TOTPSecret } from "../../../../types/TOTPSecret"; - -export interface ITotpHandler { - generate(label: string, issuer: string): TOTPSecret; - validate(token: string, secret: string): boolean; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/totp/TotpHandler.spec.ts b/themes/black/server/src/lib/authentication/totp/TotpHandler.spec.ts deleted file mode 100644 index 67cffa638..000000000 --- a/themes/black/server/src/lib/authentication/totp/TotpHandler.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ - -import { TotpHandler } from "./TotpHandler"; -import Sinon = require("sinon"); -import Speakeasy = require("speakeasy"); -import Assert = require("assert"); - -describe("authentication/totp/TotpHandler", function() { - let totpValidator: TotpHandler; - let validateStub: Sinon.SinonStub; - - beforeEach(() => { - validateStub = Sinon.stub(Speakeasy.totp, "verify"); - totpValidator = new TotpHandler(Speakeasy); - }); - - afterEach(function() { - validateStub.restore(); - }); - - it("should validate the TOTP token", function() { - const totp_secret = "NBD2ZV64R9UV1O7K"; - const token = "token"; - validateStub.withArgs({ - secret: totp_secret, - token: token, - encoding: "base32", - window: 1 - }).returns(true); - Assert(totpValidator.validate(token, totp_secret)); - }); - - it("should not validate a wrong TOTP token", function() { - const totp_secret = "NBD2ZV64R9UV1O7K"; - const token = "wrong token"; - validateStub.returns(false); - Assert(!totpValidator.validate(token, totp_secret)); - }); -}); - diff --git a/themes/black/server/src/lib/authentication/totp/TotpHandler.ts b/themes/black/server/src/lib/authentication/totp/TotpHandler.ts deleted file mode 100644 index dfab502a7..000000000 --- a/themes/black/server/src/lib/authentication/totp/TotpHandler.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ITotpHandler } from "./ITotpHandler"; -import { TOTPSecret } from "../../../../types/TOTPSecret"; -import Speakeasy = require("speakeasy"); - -const TOTP_ENCODING = "base32"; -const WINDOW: number = 1; - -export class TotpHandler implements ITotpHandler { - private speakeasy: typeof Speakeasy; - - constructor(speakeasy: typeof Speakeasy) { - this.speakeasy = speakeasy; - } - - generate(label: string, issuer: string): TOTPSecret { - const secret = this.speakeasy.generateSecret({ - otpauth_url: false - }) as TOTPSecret; - - secret.otpauth_url = this.speakeasy.otpauthURL({ - secret: secret.ascii, - label: label, - issuer: issuer - }); - return secret; - } - - validate(token: string, secret: string): boolean { - return this.speakeasy.totp.verify({ - secret: secret, - encoding: TOTP_ENCODING, - token: token, - window: WINDOW - }); - } -} diff --git a/themes/black/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts b/themes/black/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts deleted file mode 100644 index ea93330d9..000000000 --- a/themes/black/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import { ITotpHandler } from "./ITotpHandler"; -import { TOTPSecret } from "../../../../types/TOTPSecret"; - -export class TotpHandlerStub implements ITotpHandler { - generateStub: Sinon.SinonStub; - validateStub: Sinon.SinonStub; - - constructor() { - this.generateStub = Sinon.stub(); - this.validateStub = Sinon.stub(); - } - - generate(label: string, issuer: string): TOTPSecret { - return this.generateStub(label, issuer); - } - - validate(token: string, secret: string): boolean { - return this.validateStub(token, secret); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/u2f/IU2fHandler.ts b/themes/black/server/src/lib/authentication/u2f/IU2fHandler.ts deleted file mode 100644 index b9b7d6f26..000000000 --- a/themes/black/server/src/lib/authentication/u2f/IU2fHandler.ts +++ /dev/null @@ -1,9 +0,0 @@ -import U2f = require("u2f"); - -export interface IU2fHandler { - request(appId: string, keyHandle?: string): U2f.Request; - checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) - : U2f.RegistrationResult | U2f.Error; - checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) - : U2f.SignatureResult | U2f.Error; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authentication/u2f/U2fHandler.ts b/themes/black/server/src/lib/authentication/u2f/U2fHandler.ts deleted file mode 100644 index bf3891e5b..000000000 --- a/themes/black/server/src/lib/authentication/u2f/U2fHandler.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { IU2fHandler } from "./IU2fHandler"; -import U2f = require("u2f"); - -export class U2fHandler implements IU2fHandler { - private u2f: typeof U2f; - - constructor(u2f: typeof U2f) { - this.u2f = u2f; - } - - request(appId: string, keyHandle?: string): U2f.Request { - return this.u2f.request(appId, keyHandle); - } - - checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) - : U2f.RegistrationResult | U2f.Error { - return this.u2f.checkRegistration(registrationRequest, registrationResponse); - } - - checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) - : U2f.SignatureResult | U2f.Error { - return this.u2f.checkSignature(signatureRequest, signatureResponse, publicKey); - } -} diff --git a/themes/black/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts b/themes/black/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts deleted file mode 100644 index 135d7eb06..000000000 --- a/themes/black/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import U2f = require("u2f"); -import { IU2fHandler } from "./IU2fHandler"; - - -export class U2fHandlerStub implements IU2fHandler { - requestStub: Sinon.SinonStub; - checkRegistrationStub: Sinon.SinonStub; - checkSignatureStub: Sinon.SinonStub; - - constructor() { - this.requestStub = Sinon.stub(); - this.checkRegistrationStub = Sinon.stub(); - this.checkSignatureStub = Sinon.stub(); - } - - request(appId: string, keyHandle?: string): U2f.Request { - return this.requestStub(appId, keyHandle); - } - - checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) - : U2f.RegistrationResult | U2f.Error { - return this.checkRegistrationStub(registrationRequest, registrationResponse); - } - - checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) - : U2f.SignatureResult | U2f.Error { - return this.checkSignatureStub(signatureRequest, signatureResponse, publicKey); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authorization/Authorizer.spec.ts b/themes/black/server/src/lib/authorization/Authorizer.spec.ts deleted file mode 100644 index 586814042..000000000 --- a/themes/black/server/src/lib/authorization/Authorizer.spec.ts +++ /dev/null @@ -1,372 +0,0 @@ - -import Assert = require("assert"); -import winston = require("winston"); -import { Authorizer } from "./Authorizer"; -import { ACLConfiguration, ACLRule } from "../configuration/schema/AclConfiguration"; -import { Level } from "./Level"; - -describe("authorization/Authorizer", function () { - let authorizer: Authorizer; - let configuration: ACLConfiguration; - - describe("configuration is null", function() { - it("should allow access to anything, anywhere for anybody", function() { - configuration = undefined; - authorizer = new Authorizer(configuration, winston); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1", "group2"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "admin.example.com", resource: "/"}, {user: "user3", groups: ["group3"]}), Level.BYPASS); - }); - }); - - describe("configuration is not null", function () { - beforeEach(function () { - configuration = { - default_policy: "deny", - rules: [] - }; - authorizer = new Authorizer(configuration, winston); - }); - - describe("check access control with default policy to deny", function () { - beforeEach(function () { - configuration.default_policy = "deny"; - }); - - it("should deny access when no rule is provided", function () { - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should control access when multiple domain matcher is provided", function () { - configuration.rules = [{ - domain: "*.mail.example.com", - policy: "two_factor", - subject: "user:user1", - resources: [".*"] - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should allow access to all resources when resources is not provided", function () { - configuration.rules = [{ - domain: "*.mail.example.com", - policy: "two_factor", - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - describe("check user rules", function () { - it("should allow access when user has a matching allowing rule", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: [".*"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should deny to other users", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: [".*"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user2", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); - }); - - it("should allow user access only to specific resources", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: ["/private/.*", "^/begin", "/end$"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/middle/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/begin"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/not/begin"}, {user: "user1", groups: ["group1"]}), Level.DENY); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end/x"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should allow access to multiple domains", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: [".*"], - subject: "user:user1" - }, { - domain: "home1.example.com", - policy: "one_factor", - resources: [".*"], - subject: "user:user1" - }, { - domain: "home2.example.com", - policy: "deny", - resources: [".*"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home1.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); - Assert.equal(authorizer.authorization({domain: "home2.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home3.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should apply rules in order", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "one_factor", - resources: ["/my/private/resource"], - subject: "user:user1" - }, { - domain: "home.example.com", - policy: "deny", - resources: ["^/my/private/.*"], - subject: "user:user1" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/my/.*"], - subject: "user:user1" - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/poney"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/duck"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/resource"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); - }); - }); - - describe("check group rules", function () { - it("should allow access when user is in group having a matching allowing rule", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: ["^/$"], - subject: "group:group1" - }, { - domain: "home.example.com", - policy: "one_factor", - resources: ["^/test$"], - subject: "group:group2" - }, { - domain: "home.example.com", - policy: "deny", - resources: ["^/private$"], - subject: "group:group2" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.ONE_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); - }); - }); - }); - - describe("check any rules", function () { - it("should control access when any rules are defined", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "bypass", - resources: ["^/public$"] - }, { - domain: "home.example.com", - policy: "deny", - resources: ["^/private$"] - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, - {user: "user4", groups: ["group5"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, - {user: "user4", groups: ["group5"]}), Level.DENY); - }); - }); - - describe("check access control with default policy to allow", function () { - beforeEach(function () { - configuration.default_policy = "bypass"; - }); - - it("should allow access to anything when no rule is provided", function () { - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - }); - - it("should deny access to one resource when defined", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "deny", - resources: ["/test"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - }); - }); - - describe("check access control with complete use case", function () { - beforeEach(function () { - configuration.default_policy = "deny"; - }); - - it("should control access of multiple user (real use case)", function () { - // Let say we have three users: admin, john, harry. - // admin is in groups ["admins"] - // john is in groups ["dev", "admin-private"] - // harry is in groups ["dev"] - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: ["^/public$", "^/$"] - }, { - domain: "home.example.com", - policy: "two_factor", - resources: [".*"], - subject: "group:admins" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/private/?.*"], - subject: "group:admin-private" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/private/john$"], - subject: "user:john" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/private/harry"], - subject: "user:harry" - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); - }); - - it("should allow when allowed at group level and denied at user level", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "deny", - resources: ["^/dev/bob$"], - subject: "user:john" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"], - subject: "group:dev" - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); - }); - - it("should allow access when allowed at 'any' level and denied at user level", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "deny", - resources: ["^/dev/bob$"], - subject: "user:john" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"] - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); - }); - - it("should allow access when allowed at 'any' level and denied at group level", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "deny", - resources: ["^/dev/bob$"], - subject: "group:dev" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"] - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); - }); - - it("should respect rules precedence", function () { - // the priority from least to most is 'default_policy', 'all', 'group', 'user' - // and the first rules in each category as a lower priority than the latest. - // You can think of it that way: they override themselves inside each category. - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"], - subject: "user:john" - }, { - domain: "home.example.com", - policy: "deny", - resources: ["^/dev/bob$"], - subject: "group:dev" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"] - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - }); - }); - }); -}); diff --git a/themes/black/server/src/lib/authorization/Authorizer.ts b/themes/black/server/src/lib/authorization/Authorizer.ts deleted file mode 100644 index 889b7ec20..000000000 --- a/themes/black/server/src/lib/authorization/Authorizer.ts +++ /dev/null @@ -1,85 +0,0 @@ - -import { ACLConfiguration, ACLPolicy, ACLRule } from "../configuration/schema/AclConfiguration"; -import { IAuthorizer } from "./IAuthorizer"; -import { Winston } from "../../../types/Dependencies"; -import { MultipleDomainMatcher } from "./MultipleDomainMatcher"; -import { Level } from "./Level"; -import { Object } from "./Object"; -import { Subject } from "./Subject"; - -function MatchDomain(actualDomain: string) { - return function (rule: ACLRule): boolean { - return MultipleDomainMatcher.match(actualDomain, rule.domain); - }; -} - -function MatchResource(actualResource: string) { - return function (rule: ACLRule): boolean { - // If resources key is not provided, the rule applies to all resources. - if (!rule.resources) return true; - - for (let i = 0; i < rule.resources.length; ++i) { - const regexp = new RegExp(rule.resources[i]); - if (regexp.test(actualResource)) return true; - } - return false; - }; -} - -function MatchSubject(subject: Subject) { - return (rule: ACLRule) => { - // If no subject, matches anybody - if (!rule.subject) return true; - - if (rule.subject.startsWith("user:")) { - const ruleUser = rule.subject.split(":")[1]; - if (subject.user == ruleUser) return true; - } - - if (rule.subject.startsWith("group:")) { - const ruleGroup = rule.subject.split(":")[1]; - if (subject.groups.indexOf(ruleGroup) > -1) return true; - } - return false; - }; -} - -export class Authorizer implements IAuthorizer { - private logger: Winston; - private readonly configuration: ACLConfiguration; - - constructor(configuration: ACLConfiguration, logger_: Winston) { - this.logger = logger_; - this.configuration = configuration; - } - - private getMatchingRules(object: Object, subject: Subject): ACLRule[] { - const rules = this.configuration.rules; - if (!rules) return []; - return rules - .filter(MatchDomain(object.domain)) - .filter(MatchResource(object.resource)) - .filter(MatchSubject(subject)); - } - - private ruleToLevel(policy: string): Level { - if (policy == "bypass") { - return Level.BYPASS; - } else if (policy == "one_factor") { - return Level.ONE_FACTOR; - } else if (policy == "two_factor") { - return Level.TWO_FACTOR; - } - return Level.DENY; - } - - authorization(object: Object, subject: Subject): Level { - if (!this.configuration) return Level.BYPASS; - - const rules = this.getMatchingRules(object, subject); - - return (rules.length > 0) - ? this.ruleToLevel(rules[0].policy) // extract the policy of the first matching rule - : this.ruleToLevel(this.configuration.default_policy); // otherwise use the default policy - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authorization/AuthorizerStub.spec.ts b/themes/black/server/src/lib/authorization/AuthorizerStub.spec.ts deleted file mode 100644 index 9bd6f4a85..000000000 --- a/themes/black/server/src/lib/authorization/AuthorizerStub.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Sinon = require("sinon"); -import { IAuthorizer } from "./IAuthorizer"; -import { Level } from "./Level"; -import { Object } from "./Object"; -import { Subject } from "./Subject"; - -export class AuthorizerStub implements IAuthorizer { - authorizationMock: Sinon.SinonStub; - - constructor() { - this.authorizationMock = Sinon.stub(); - } - - authorization(object: Object, subject: Subject): Level { - return this.authorizationMock(object, subject); - } -} diff --git a/themes/black/server/src/lib/authorization/IAuthorizer.ts b/themes/black/server/src/lib/authorization/IAuthorizer.ts deleted file mode 100644 index fe7ba367a..000000000 --- a/themes/black/server/src/lib/authorization/IAuthorizer.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Level } from "./Level"; -import { Subject } from "./Subject"; -import { Object } from "./Object"; - -export interface IAuthorizer { - authorization(object: Object, subject: Subject): Level; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authorization/Level.ts b/themes/black/server/src/lib/authorization/Level.ts deleted file mode 100644 index d12802610..000000000 --- a/themes/black/server/src/lib/authorization/Level.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum Level { - BYPASS = 0, - ONE_FACTOR = 1, - TWO_FACTOR = 2, - DENY = 3 -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authorization/MultipleDomainMatcher.ts b/themes/black/server/src/lib/authorization/MultipleDomainMatcher.ts deleted file mode 100644 index 64c647a4a..000000000 --- a/themes/black/server/src/lib/authorization/MultipleDomainMatcher.ts +++ /dev/null @@ -1,12 +0,0 @@ - -export class MultipleDomainMatcher { - static match(domain: string, pattern: string): boolean { - if (pattern.startsWith("*") && - domain.endsWith(pattern.substr(1))) { - return true; - } - else if (domain == pattern) { - return true; - } - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authorization/Object.ts b/themes/black/server/src/lib/authorization/Object.ts deleted file mode 100644 index 5411b0d24..000000000 --- a/themes/black/server/src/lib/authorization/Object.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface Object { - domain: string; - resource: string; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/authorization/Subject.ts b/themes/black/server/src/lib/authorization/Subject.ts deleted file mode 100644 index 310d6b4c0..000000000 --- a/themes/black/server/src/lib/authorization/Subject.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface Subject { - user: string; - groups: string[]; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/ConfigurationParser.spec.ts b/themes/black/server/src/lib/configuration/ConfigurationParser.spec.ts deleted file mode 100644 index 60c0f6182..000000000 --- a/themes/black/server/src/lib/configuration/ConfigurationParser.spec.ts +++ /dev/null @@ -1,171 +0,0 @@ -import * as Assert from "assert"; -import { Configuration } from "./schema/Configuration"; -import { ACLConfiguration } from "./schema/AclConfiguration"; -import { ConfigurationParser } from "./ConfigurationParser"; - -describe("configuration/ConfigurationParser", function () { - function buildYamlConfig(): Configuration { - const yaml_config: Configuration = { - port: 8080, - authentication_backend: { - ldap: { - url: "http://ldap", - base_dn: "dc=example,dc=com", - additional_users_dn: "ou=users", - additional_groups_dn: "ou=groups", - user: "user", - password: "pass" - }, - }, - session: { - domain: "example.com", - secret: "secret", - expiration: 40000 - }, - storage: { - local: { - path: "/mydirectory" - } - }, - regulation: { - max_retries: 3, - find_time: 5 * 60, - ban_time: 5 * 60 - }, - logs_level: "debug", - notifier: { - email: { - username: "user", - password: "password", - sender: "admin@example.com", - service: "gmail" - } - } - }; - return yaml_config; - } - - describe("port", function () { - it("should read the port from the yaml file", function () { - const yaml_config = buildYamlConfig(); - yaml_config.port = 7070; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.port, 7070); - }); - - it("should default the port to 8080 if not provided", function () { - const yaml_config = buildYamlConfig(); - delete yaml_config.port; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.port, 8080); - }); - }); - - describe("test session configuration", function() { - it("should get the session attributes", function () { - const yaml_config = buildYamlConfig(); - yaml_config.session = { - domain: "example.com", - secret: "secret", - expiration: 3600, - inactivity: 4000 - }; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.session.domain, "example.com"); - Assert.equal(config.session.secret, "secret"); - Assert.equal(config.session.expiration, 3600); - Assert.equal(config.session.inactivity, 4000); - }); - - it("should be ok not specifying inactivity", function () { - const yaml_config = buildYamlConfig(); - yaml_config.session = { - domain: "example.com", - secret: "secret", - expiration: 3600 - }; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.session.domain, "example.com"); - Assert.equal(config.session.secret, "secret"); - Assert.equal(config.session.expiration, 3600); - Assert.equal(config.session.inactivity, undefined); - }); - }); - - it("should get the log level", function () { - const yaml_config = buildYamlConfig(); - yaml_config.logs_level = "debug"; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.logs_level, "debug"); - }); - - it("should get the notifier config", function () { - const userConfig = buildYamlConfig(); - userConfig.notifier = { - email: { - username: "user", - password: "pass", - sender: "admin@example.com", - service: "gmail" - } - }; - const config = ConfigurationParser.parse(userConfig); - Assert.deepEqual(config.notifier, { - email: { - username: "user", - password: "pass", - sender: "admin@example.com", - service: "gmail" - } - }); - }); - - describe("access_control", function() { - it("should adapt access_control when it is already ok", function () { - const userConfig = buildYamlConfig(); - userConfig.access_control = { - default_policy: "deny", - rules: [{ - domain: "www.example.com", - policy: "two_factor", - subject: "user:user" - }, { - domain: "public.example.com", - policy: "two_factor" - }] - }; - const config = ConfigurationParser.parse(userConfig); - Assert.deepEqual(config.access_control, { - default_policy: "deny", - rules: [{ - domain: "www.example.com", - policy: "two_factor", - subject: "user:user" - }, { - domain: "public.example.com", - policy: "two_factor" - }] - } as ACLConfiguration); - }); - - - it("should adapt access_control when it is empty", function () { - const userConfig = buildYamlConfig(); - userConfig.access_control = {} as any; - const config = ConfigurationParser.parse(userConfig); - Assert.deepEqual(config.access_control, { - default_policy: "bypass", - rules: [] - }); - }); - }); - - describe("default_redirection_url", function() { - it("should parse default_redirection_url", function() { - const userConfig = buildYamlConfig(); - userConfig.default_redirection_url = "dummy_url"; - const config = ConfigurationParser.parse(userConfig); - Assert.deepEqual(config.default_redirection_url, "dummy_url"); - }); - }); -}); diff --git a/themes/black/server/src/lib/configuration/ConfigurationParser.ts b/themes/black/server/src/lib/configuration/ConfigurationParser.ts deleted file mode 100644 index d92d163c4..000000000 --- a/themes/black/server/src/lib/configuration/ConfigurationParser.ts +++ /dev/null @@ -1,39 +0,0 @@ - -import * as ObjectPath from "object-path"; -import { Configuration, complete } from "./schema/Configuration"; -import Ajv = require("ajv"); -import Path = require("path"); -import Util = require("util"); - -export class ConfigurationParser { - private static parseTypes(configuration: Configuration): string[] { - const schema = require(Path.resolve(__dirname, "./Configuration.schema.json")); - const ajv = new Ajv({ - allErrors: true, - missingRefs: "fail" - }); - ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json")); - const valid = ajv.validate(schema, configuration); - if (!valid) - return ajv.errors.map( - (e: Ajv.ErrorObject) => { return ajv.errorsText([e]); }); - return []; - } - - static parse(configuration: Configuration): Configuration { - const validationErrors = this.parseTypes(configuration); - if (validationErrors.length > 0) { - validationErrors.forEach((e: string) => { console.log(e); }); - throw new Error("Malformed configuration (schema). Please double-check your configuration file."); - } - - const [newConfiguration, completionErrors] = complete(configuration); - - if (completionErrors.length > 0) { - completionErrors.forEach((e: string) => { console.log(e); }); - throw new Error("Malformed configuration (validator). Please double-check your configuration file."); - } - return newConfiguration; - } -} - diff --git a/themes/black/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts b/themes/black/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts deleted file mode 100644 index d4a3093ee..000000000 --- a/themes/black/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { SessionConfigurationBuilder } from "./SessionConfigurationBuilder"; -import { Configuration } from "./schema/Configuration"; -import { GlobalDependencies } from "../../../types/Dependencies"; - -import ExpressSession = require("express-session"); -import ConnectRedis = require("connect-redis"); -import Sinon = require("sinon"); -import Assert = require("assert"); - -describe("configuration/SessionConfigurationBuilder", function () { - const configuration: Configuration = { - access_control: { - default_policy: "deny", - rules: [] - }, - totp: { - issuer: "authelia.com" - }, - authentication_backend: { - ldap: { - url: "ldap://ldap", - user: "user", - base_dn: "dc=example,dc=com", - password: "password", - additional_groups_dn: "ou=groups", - additional_users_dn: "ou=users", - group_name_attribute: "", - groups_filter: "", - mail_attribute: "", - users_filter: "" - }, - }, - logs_level: "debug", - notifier: { - filesystem: { - filename: "/test" - } - }, - port: 8080, - session: { - name: "authelia_session", - domain: "example.com", - expiration: 3600, - secret: "secret" - }, - regulation: { - max_retries: 3, - ban_time: 5 * 60, - find_time: 5 * 60 - }, - storage: { - local: { - in_memory: true - } - } - }; - - const deps: GlobalDependencies = { - ConnectRedis: Sinon.spy() as any, - ldapjs: Sinon.spy() as any, - nedb: Sinon.spy() as any, - session: Sinon.spy() as any, - speakeasy: Sinon.spy() as any, - u2f: Sinon.spy() as any, - winston: Sinon.spy() as any, - Redis: Sinon.spy() as any - }; - - it("should return session options without redis options", function () { - const options = SessionConfigurationBuilder.build(configuration, deps); - const expectedOptions = { - name: "authelia_session", - secret: "secret", - resave: false, - saveUninitialized: true, - cookie: { - secure: true, - httpOnly: true, - maxAge: 3600, - domain: "example.com" - } - }; - - Assert.deepEqual(expectedOptions, options); - }); - - it("should return session options with redis options", function () { - configuration.session["redis"] = { - host: "redis.example.com", - port: 6379 - }; - const RedisStoreMock = Sinon.spy(); - const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); - - deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; - deps.Redis = { - createClient: Sinon.mock().returns(redisClient) - } as any; - - const options = SessionConfigurationBuilder.build(configuration, deps); - - const expectedOptions: ExpressSession.SessionOptions = { - secret: "secret", - resave: false, - saveUninitialized: true, - name: "authelia_session", - cookie: { - secure: true, - httpOnly: true, - maxAge: 3600, - domain: "example.com" - }, - store: Sinon.match.object as any - }; - - Assert((deps.ConnectRedis as Sinon.SinonStub).calledWith(deps.session)); - Assert.equal(options.secret, expectedOptions.secret); - Assert.equal(options.resave, expectedOptions.resave); - Assert.equal(options.saveUninitialized, expectedOptions.saveUninitialized); - Assert.deepEqual(options.cookie, expectedOptions.cookie); - Assert(options.store != undefined); - }); - - it("should return session options with redis password", function () { - configuration.session["redis"] = { - host: "redis.example.com", - port: 6379, - password: "authelia_pass" - }; - const RedisStoreMock = Sinon.spy(); - const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); - const createClientStub = Sinon.stub(); - - deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; - deps.Redis = { - createClient: createClientStub - } as any; - - createClientStub.returns(redisClient); - - const options = SessionConfigurationBuilder.build(configuration, deps); - - Assert(createClientStub.calledWith({ - host: "redis.example.com", - port: 6379, - password: "authelia_pass" - })); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/SessionConfigurationBuilder.ts b/themes/black/server/src/lib/configuration/SessionConfigurationBuilder.ts deleted file mode 100644 index 6ce643d9d..000000000 --- a/themes/black/server/src/lib/configuration/SessionConfigurationBuilder.ts +++ /dev/null @@ -1,52 +0,0 @@ -import ExpressSession = require("express-session"); -import Redis = require("redis"); - -import { Configuration } from "./schema/Configuration"; -import { GlobalDependencies } from "../../../types/Dependencies"; -import { RedisStoreOptions } from "connect-redis"; - -export class SessionConfigurationBuilder { - - static build(configuration: Configuration, deps: GlobalDependencies): ExpressSession.SessionOptions { - const sessionOptions: ExpressSession.SessionOptions = { - name: configuration.session.name, - secret: configuration.session.secret, - resave: false, - saveUninitialized: true, - cookie: { - secure: true, - httpOnly: true, - maxAge: configuration.session.expiration, - domain: configuration.session.domain - }, - }; - - if (configuration.session.redis) { - let redisOptions; - const options: Redis.ClientOpts = { - host: configuration.session.redis.host, - port: configuration.session.redis.port - }; - - if (configuration.session.redis.password) { - options["password"] = configuration.session.redis.password; - } - const client = deps.Redis.createClient(options); - - client.on("error", function (err: Error) { - console.error("Redis error:", err); - }); - - redisOptions = { - client: client, - logErrors: true - }; - - if (redisOptions) { - const RedisStore = deps.ConnectRedis(deps.session); - sessionOptions.store = new RedisStore(redisOptions); - } - } - return sessionOptions; - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/AclConfiguration.spec.ts b/themes/black/server/src/lib/configuration/schema/AclConfiguration.spec.ts deleted file mode 100644 index d1e2a03a2..000000000 --- a/themes/black/server/src/lib/configuration/schema/AclConfiguration.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ACLConfiguration, complete } from "./AclConfiguration"; -import Assert = require("assert"); - -describe("configuration/schema/AclConfiguration", function() { - it("should complete ACLConfiguration", function() { - const configuration: ACLConfiguration = {}; - const [newConfiguration, errors] = complete(configuration); - - Assert.deepEqual(newConfiguration.default_policy, "bypass"); - Assert.deepEqual(newConfiguration.rules, []); - }); - - it("should return errors when subject is not good", function() { - const configuration: ACLConfiguration = { - default_policy: "deny", - rules: [{ - domain: "dev.example.com", - subject: "user:abc", - policy: "bypass" - }, { - domain: "dev.example.com", - subject: "user:def", - policy: "bypass" - }, { - domain: "dev.example.com", - subject: "badkey:abc", - policy: "bypass" - }] - }; - const [newConfiguration, errors] = complete(configuration); - - Assert.deepEqual(errors, ["Rule 2 has wrong subject. It should be starting with user: or group:."]); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/AclConfiguration.ts b/themes/black/server/src/lib/configuration/schema/AclConfiguration.ts deleted file mode 100644 index 40401dd64..000000000 --- a/themes/black/server/src/lib/configuration/schema/AclConfiguration.ts +++ /dev/null @@ -1,41 +0,0 @@ - -export type ACLPolicy = "deny" | "bypass" | "one_factor" | "two_factor"; - -export type ACLRule = { - domain: string; - resources?: string[]; - subject?: string; - policy: ACLPolicy; -}; - -export interface ACLConfiguration { - default_policy?: ACLPolicy; - rules?: ACLRule[]; -} - -export function complete(configuration: ACLConfiguration): [ACLConfiguration, string[]] { - const newConfiguration: ACLConfiguration = (configuration) - ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.default_policy) { - newConfiguration.default_policy = "bypass"; - } - - if (!newConfiguration.rules) { - newConfiguration.rules = []; - } - - if (newConfiguration.rules.length > 0) { - const errors: string[] = []; - newConfiguration.rules.forEach((r, idx) => { - if (r.subject && !r.subject.match(/^(user|group):[a-zA-Z0-9]+$/)) { - errors.push(`Rule ${idx} has wrong subject. It should be starting with user: or group:.`); - } - }); - if (errors.length > 0) { - return [newConfiguration, errors]; - } - } - - return [newConfiguration, []]; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts b/themes/black/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts deleted file mode 100644 index 3ca86381c..000000000 --- a/themes/black/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { AuthenticationBackendConfiguration, complete } from "./AuthenticationBackendConfiguration"; -import Assert = require("assert"); - -describe("configuration/schema/AuthenticationBackendConfiguration", function() { - it("should ensure there is at least one key", function() { - const configuration: AuthenticationBackendConfiguration = {} as any; - const [newConfiguration, error] = complete(configuration); - - Assert.equal(error, "Authentication backend must have one of the following keys:`ldap` or `file`"); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts b/themes/black/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts deleted file mode 100644 index 7f77f894f..000000000 --- a/themes/black/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { LdapConfiguration } from "./LdapConfiguration"; -import { FileUsersDatabaseConfiguration } from "./FileUsersDatabaseConfiguration"; - -export interface AuthenticationBackendConfiguration { - ldap?: LdapConfiguration; - file?: FileUsersDatabaseConfiguration; -} - -export function complete( - configuration: AuthenticationBackendConfiguration) - : [AuthenticationBackendConfiguration, string] { - - const newConfiguration: AuthenticationBackendConfiguration = (configuration) - ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (Object.keys(newConfiguration).length != 1) { - return [ - newConfiguration, - "Authentication backend must have one of the following keys:" + - "`ldap` or `file`" - ]; - } - - return [newConfiguration, undefined]; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/Configuration.ts b/themes/black/server/src/lib/configuration/schema/Configuration.ts deleted file mode 100644 index 8d16a5fb2..000000000 --- a/themes/black/server/src/lib/configuration/schema/Configuration.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ACLConfiguration, complete as AclConfigurationComplete } from "./AclConfiguration"; -import { AuthenticationBackendConfiguration, complete as AuthenticationBackendComplete } from "./AuthenticationBackendConfiguration"; -import { NotifierConfiguration, complete as NotifierConfigurationComplete } from "./NotifierConfiguration"; -import { RegulationConfiguration, complete as RegulationConfigurationComplete } from "./RegulationConfiguration"; -import { SessionConfiguration, complete as SessionConfigurationComplete } from "./SessionConfiguration"; -import { StorageConfiguration, complete as StorageConfigurationComplete } from "./StorageConfiguration"; -import { TotpConfiguration, complete as TotpConfigurationComplete } from "./TotpConfiguration"; - -export interface Configuration { - access_control?: ACLConfiguration; - authentication_backend: AuthenticationBackendConfiguration; - default_redirection_url?: string; - logs_level?: string; - notifier?: NotifierConfiguration; - port?: number; - regulation?: RegulationConfiguration; - session?: SessionConfiguration; - storage?: StorageConfiguration; - totp?: TotpConfiguration; -} - -export function complete( - configuration: Configuration): - [Configuration, string[]] { - - const newConfiguration: Configuration = JSON.parse( - JSON.stringify(configuration)); - const errors: string[] = []; - - const [acls, aclsErrors] = AclConfigurationComplete( - newConfiguration.access_control); - - newConfiguration.access_control = acls; - if (aclsErrors.length > 0) { - errors.concat(aclsErrors); - } - - const [backend, error] = - AuthenticationBackendComplete( - newConfiguration.authentication_backend); - - if (error) errors.push(error); - newConfiguration.authentication_backend = backend; - - if (!newConfiguration.logs_level) { - newConfiguration.logs_level = "info"; - } - - const [notifier, notifierError] = NotifierConfigurationComplete( - newConfiguration.notifier); - newConfiguration.notifier = notifier; - if (notifierError) errors.push(notifierError); - - if (!newConfiguration.port) { - newConfiguration.port = 8080; - } - - newConfiguration.regulation = RegulationConfigurationComplete( - newConfiguration.regulation); - newConfiguration.session = SessionConfigurationComplete( - newConfiguration.session); - newConfiguration.storage = StorageConfigurationComplete( - newConfiguration.storage); - newConfiguration.totp = TotpConfigurationComplete( - newConfiguration.totp); - - return [newConfiguration, errors]; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts b/themes/black/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts deleted file mode 100644 index d19002ba9..000000000 --- a/themes/black/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts +++ /dev/null @@ -1,4 +0,0 @@ - -export interface FileUsersDatabaseConfiguration { - path: string; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/LdapConfiguration.spec.ts b/themes/black/server/src/lib/configuration/schema/LdapConfiguration.spec.ts deleted file mode 100644 index cc73d1085..000000000 --- a/themes/black/server/src/lib/configuration/schema/LdapConfiguration.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Assert = require("assert"); -import { LdapConfiguration, complete } from "./LdapConfiguration"; - -describe("configuration/schema/AuthenticationMethodsConfiguration", function() { - it("should ensure at least one key is provided", function() { - const configuration: LdapConfiguration = { - url: "ldap.example.com", - base_dn: "dc=example,dc=com", - user: "admin", - password: "password" - }; - const newConfiguration = complete(configuration); - - Assert.deepEqual(newConfiguration, { - url: "ldap.example.com", - base_dn: "dc=example,dc=com", - user: "admin", - password: "password", - users_filter: "cn={0}", - group_name_attribute: "cn", - groups_filter: "member={dn}", - mail_attribute: "mail" - }); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/LdapConfiguration.ts b/themes/black/server/src/lib/configuration/schema/LdapConfiguration.ts deleted file mode 100644 index 5dacb9390..000000000 --- a/themes/black/server/src/lib/configuration/schema/LdapConfiguration.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Util = require("util"); - -export interface LdapConfiguration { - url: string; - base_dn: string; - - additional_users_dn?: string; - users_filter?: string; - - additional_groups_dn?: string; - groups_filter?: string; - - group_name_attribute?: string; - mail_attribute?: string; - - user: string; // admin username - password: string; // admin password -} - -export function complete(configuration: LdapConfiguration): LdapConfiguration { - const newConfiguration: LdapConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.users_filter) { - newConfiguration.users_filter = "cn={0}"; - } - - if (!newConfiguration.groups_filter) { - newConfiguration.groups_filter = "member={dn}"; - } - - if (!newConfiguration.group_name_attribute) { - newConfiguration.group_name_attribute = "cn"; - } - - if (!newConfiguration.mail_attribute) { - newConfiguration.mail_attribute = "mail"; - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts b/themes/black/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts deleted file mode 100644 index 6c576e8e0..000000000 --- a/themes/black/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Assert = require("assert"); -import { NotifierConfiguration, complete } from "./NotifierConfiguration"; - -describe("configuration/schema/NotifierConfiguration", function() { - it("should use a default notifier when none is provided", function() { - const configuration: NotifierConfiguration = {}; - const [newConfiguration, error] = complete(configuration); - - Assert.deepEqual(newConfiguration.filesystem, {filename: "/tmp/authelia/notification.txt"}); - }); - - it("should ensure correct key is provided", function() { - const configuration = { - abc: "badvalue" - }; - const [newConfiguration, error] = complete(configuration as any); - - Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"); - }); - - it("should ensure there is no more than one key", function() { - const configuration: NotifierConfiguration = { - smtp: { - host: "smtp.example.com", - port: 25, - secure: false, - sender: "test@example.com" - }, - email: { - username: "test", - password: "test", - sender: "test@example.com", - service: "gmail" - } - }; - const [newConfiguration, error] = complete(configuration); - - Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/NotifierConfiguration.ts b/themes/black/server/src/lib/configuration/schema/NotifierConfiguration.ts deleted file mode 100644 index 7bcce15c2..000000000 --- a/themes/black/server/src/lib/configuration/schema/NotifierConfiguration.ts +++ /dev/null @@ -1,45 +0,0 @@ - -export interface EmailNotifierConfiguration { - username: string; - password: string; - sender: string; - service: string; -} - -export interface SmtpNotifierConfiguration { - username?: string; - password?: string; - host: string; - port: number; - secure: boolean; - sender: string; -} - -export interface FileSystemNotifierConfiguration { - filename: string; -} - -export interface NotifierConfiguration { - email?: EmailNotifierConfiguration; - smtp?: SmtpNotifierConfiguration; - filesystem?: FileSystemNotifierConfiguration; -} - -export function complete(configuration: NotifierConfiguration): [NotifierConfiguration, string] { - const newConfiguration: NotifierConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (Object.keys(newConfiguration).length == 0) - newConfiguration.filesystem = { filename: "/tmp/authelia/notification.txt" }; - - const ERROR = "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"; - - if (Object.keys(newConfiguration).length != 1) - return [newConfiguration, ERROR]; - - const key = Object.keys(newConfiguration)[0]; - - if (key != "filesystem" && key != "smtp" && key != "email") - return [newConfiguration, ERROR]; - - return [newConfiguration, undefined]; -} diff --git a/themes/black/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts b/themes/black/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts deleted file mode 100644 index dce2caf4e..000000000 --- a/themes/black/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Assert = require("assert"); -import { RegulationConfiguration, complete } from "./RegulationConfiguration"; - -describe("configuration/schema/RegulationConfiguration", function() { - it("should return default regulation configuration", function() { - const configuration: RegulationConfiguration = {}; - const newConfiguration = complete(configuration); - - Assert.equal(newConfiguration.ban_time, 300); - Assert.equal(newConfiguration.find_time, 120); - Assert.equal(newConfiguration.max_retries, 3); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/RegulationConfiguration.ts b/themes/black/server/src/lib/configuration/schema/RegulationConfiguration.ts deleted file mode 100644 index 117463f43..000000000 --- a/themes/black/server/src/lib/configuration/schema/RegulationConfiguration.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface RegulationConfiguration { - max_retries?: number; - find_time?: number; - ban_time?: number; -} - -export function complete(configuration: RegulationConfiguration): RegulationConfiguration { - const newConfiguration: RegulationConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.max_retries) { - newConfiguration.max_retries = 3; - } - - if (!newConfiguration.find_time) { - newConfiguration.find_time = 120; // seconds - } - - if (!newConfiguration.ban_time) { - newConfiguration.ban_time = 300; // seconds - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/SessionConfiguration.spec.ts b/themes/black/server/src/lib/configuration/schema/SessionConfiguration.spec.ts deleted file mode 100644 index e54010837..000000000 --- a/themes/black/server/src/lib/configuration/schema/SessionConfiguration.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Assert = require("assert"); -import { SessionConfiguration, complete } from "./SessionConfiguration"; - -describe("configuration/schema/SessionConfiguration", function() { - it("should return default regulation configuration", function() { - const configuration: SessionConfiguration = { - domain: "example.com", - secret: "unsecure_secret" - }; - const newConfiguration = complete(configuration); - - Assert.equal(newConfiguration.name, 'authelia_session'); - Assert.equal(newConfiguration.expiration, 3600000); - Assert.equal(newConfiguration.inactivity, undefined); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/SessionConfiguration.ts b/themes/black/server/src/lib/configuration/schema/SessionConfiguration.ts deleted file mode 100644 index 2c88bb215..000000000 --- a/themes/black/server/src/lib/configuration/schema/SessionConfiguration.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface SessionRedisOptions { - host: string; - port: number; - password?: string; -} - -export interface SessionConfiguration { - name?: string; - domain: string; - secret: string; - expiration?: number; - inactivity?: number; - redis?: SessionRedisOptions; -} - -export function complete(configuration: SessionConfiguration): SessionConfiguration { - const newConfiguration: SessionConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.name) { - newConfiguration.name = "authelia_session"; - } - - if (!newConfiguration.expiration) { - newConfiguration.expiration = 3600000; // 1 hour - } - - if (!newConfiguration.inactivity) { - newConfiguration.inactivity = undefined; // disabled - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/StorageConfiguration.spec.ts b/themes/black/server/src/lib/configuration/schema/StorageConfiguration.spec.ts deleted file mode 100644 index 9d02a11b6..000000000 --- a/themes/black/server/src/lib/configuration/schema/StorageConfiguration.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Assert = require("assert"); -import { StorageConfiguration, complete } from "./StorageConfiguration"; - -describe("configuration/schema/StorageConfiguration", function() { - it("should return default regulation configuration", function() { - const configuration: StorageConfiguration = {}; - const newConfiguration = complete(configuration); - - Assert.deepEqual(newConfiguration, { - local: { - in_memory: true - } - }); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/StorageConfiguration.ts b/themes/black/server/src/lib/configuration/schema/StorageConfiguration.ts deleted file mode 100644 index 47e356ef4..000000000 --- a/themes/black/server/src/lib/configuration/schema/StorageConfiguration.ts +++ /dev/null @@ -1,30 +0,0 @@ -export interface MongoStorageConfiguration { - url: string; - database: string; - auth?: { - username: string; - password: string; - }; -} - -export interface LocalStorageConfiguration { - path?: string; - in_memory?: boolean; -} - -export interface StorageConfiguration { - local?: LocalStorageConfiguration; - mongo?: MongoStorageConfiguration; -} - -export function complete(configuration: StorageConfiguration): StorageConfiguration { - const newConfiguration: StorageConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.local && !newConfiguration.mongo) { - newConfiguration.local = { - in_memory: true - }; - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/TotpConfiguration.ts b/themes/black/server/src/lib/configuration/schema/TotpConfiguration.ts deleted file mode 100644 index 683135639..000000000 --- a/themes/black/server/src/lib/configuration/schema/TotpConfiguration.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface TotpConfiguration { - issuer: string; -} - -export function complete(configuration: TotpConfiguration): TotpConfiguration { - const newConfiguration: TotpConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.issuer) { - newConfiguration.issuer = "authelia.com"; - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts b/themes/black/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts deleted file mode 100644 index 8008b4833..000000000 --- a/themes/black/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts +++ /dev/null @@ -1,9 +0,0 @@ - -export interface UserInfo { - username: string; - password_hash: string; - email: string; - groups?: string[]; -} - -export type UserDatabaseConfiguration = UserInfo[]; \ No newline at end of file diff --git a/themes/black/server/src/lib/connectors/mongo/IMongoClient.d.ts b/themes/black/server/src/lib/connectors/mongo/IMongoClient.d.ts deleted file mode 100644 index 36cb4b8bf..000000000 --- a/themes/black/server/src/lib/connectors/mongo/IMongoClient.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import MongoDB = require("mongodb"); -import Bluebird = require("bluebird"); - -export interface IMongoClient { - collection(name: string): Bluebird -} \ No newline at end of file diff --git a/themes/black/server/src/lib/connectors/mongo/MongoClient.spec.ts b/themes/black/server/src/lib/connectors/mongo/MongoClient.spec.ts deleted file mode 100644 index ca0c68593..000000000 --- a/themes/black/server/src/lib/connectors/mongo/MongoClient.spec.ts +++ /dev/null @@ -1,119 +0,0 @@ -import Assert = require("assert"); -import Bluebird = require("bluebird"); -import MongoDB = require("mongodb"); -import Sinon = require("sinon"); - -import { MongoClient } from "./MongoClient"; -import { GlobalLoggerStub } from "../../logging/GlobalLoggerStub.spec"; -import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration"; - -describe("connectors/mongo/MongoClient", function () { - let MongoClientStub: any; - let mongoClientStub: any; - let mongoDatabaseStub: any; - let logger: GlobalLoggerStub = new GlobalLoggerStub(); - - const configuration: MongoStorageConfiguration = { - url: "mongo://url", - database: "databasename" - }; - - describe("connection", () => { - before(() => { - mongoClientStub = { - db: Sinon.stub() - }; - mongoDatabaseStub = { - on: Sinon.stub(), - collection: Sinon.stub() - } - MongoClientStub = Sinon.stub( - MongoDB.MongoClient, "connect"); - MongoClientStub.yields( - undefined, mongoClientStub); - mongoClientStub.db.returns( - mongoDatabaseStub); - }); - - after(() => { - MongoClientStub.restore(); - }); - - it("should use credentials from configuration", () => { - configuration.auth = { - username: "authelia", - password: "authelia_pass" - }; - - const client = new MongoClient(configuration, logger); - return client.collection("test") - .then(() => { - Assert(MongoClientStub.calledWith("mongo://url", { - auth: { - user: "authelia", - password: "authelia_pass" - } - })) - }); - }); - }); - - describe("collection", () => { - before(function() { - mongoClientStub = { - db: Sinon.stub() - }; - mongoDatabaseStub = { - on: Sinon.stub(), - collection: Sinon.stub() - } - }); - - describe("Connection to mongo is ok", function() { - before(function () { - MongoClientStub = Sinon.stub( - MongoDB.MongoClient, "connect"); - MongoClientStub.yields( - undefined, mongoClientStub); - mongoClientStub.db.returns( - mongoDatabaseStub); - }); - - after(function () { - MongoClientStub.restore(); - }); - - it("should create a collection", function () { - const COLLECTION_NAME = "mycollection"; - const client = new MongoClient(configuration, logger); - - mongoDatabaseStub.collection.returns("COL"); - return client.collection(COLLECTION_NAME) - .then((collection) => mongoDatabaseStub.collection.calledWith(COLLECTION_NAME)); - }); - }); - - describe("Connection to mongo is broken", function() { - before(function () { - MongoClientStub = Sinon.stub( - MongoDB.MongoClient, "connect"); - MongoClientStub.yields( - new Error("Failed connection"), undefined); - }); - - after(function () { - MongoClientStub.restore(); - }); - - it("should fail creating the collection", function() { - const COLLECTION_NAME = "mycollection"; - const client = new MongoClient(configuration, logger); - - mongoDatabaseStub.collection.returns("COL"); - return client.collection(COLLECTION_NAME) - .then((collection) => Bluebird.reject(new Error("should not be here."))) - .catch((err) => Bluebird.resolve()); - }); - }) - }); -}); diff --git a/themes/black/server/src/lib/connectors/mongo/MongoClient.ts b/themes/black/server/src/lib/connectors/mongo/MongoClient.ts deleted file mode 100644 index d15731e97..000000000 --- a/themes/black/server/src/lib/connectors/mongo/MongoClient.ts +++ /dev/null @@ -1,76 +0,0 @@ - -import MongoDB = require("mongodb"); -import { IMongoClient } from "./IMongoClient"; -import Bluebird = require("bluebird"); -import { AUTHENTICATION_FAILED } from "../../../../../shared/UserMessages"; -import { IGlobalLogger } from "../../logging/IGlobalLogger"; -import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration"; - -export class MongoClient implements IMongoClient { - private configuration: MongoStorageConfiguration; - - private database: MongoDB.Db; - private client: MongoDB.MongoClient; - private logger: IGlobalLogger; - - constructor( - configuration: MongoStorageConfiguration, - logger: IGlobalLogger) { - - this.configuration = configuration; - this.logger = logger; - } - - connect(): Bluebird { - const that = this; - const options: MongoDB.MongoClientOptions = {}; - if (that.configuration.auth) { - options["auth"] = { - user: that.configuration.auth.username, - password: that.configuration.auth.password - }; - } - - return new Bluebird((resolve, reject) => { - MongoDB.MongoClient.connect( - this.configuration.url, - options, - function(err, client) { - if (err) { - reject(err); - return; - } - resolve(client); - }); - }) - .then(function (client: MongoDB.MongoClient) { - that.database = client.db(that.configuration.database); - that.database.on("close", () => { - that.logger.info("[MongoClient] Lost connection."); - }); - that.database.on("reconnect", () => { - that.logger.info("[MongoClient] Reconnected."); - }); - that.client = client; - }); - } - - close(): Bluebird { - if (this.client) { - this.client.close(); - this.database = undefined; - this.client = undefined; - } - return Bluebird.resolve(); - } - - collection(name: string): Bluebird { - if (!this.client) { - const that = this; - return this.connect() - .then(() => Bluebird.resolve(that.database.collection(name))); - } - - return Bluebird.resolve(this.database.collection(name)); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/connectors/mongo/MongoClientStub.spec.ts b/themes/black/server/src/lib/connectors/mongo/MongoClientStub.spec.ts deleted file mode 100644 index 1cfd48e32..000000000 --- a/themes/black/server/src/lib/connectors/mongo/MongoClientStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Sinon = require("sinon"); -import MongoDB = require("mongodb"); -import Bluebird = require("bluebird"); -import { IMongoClient } from "../../../../src/lib/connectors/mongo/IMongoClient"; - -export class MongoClientStub implements IMongoClient { - public collectionStub: Sinon.SinonStub; - - constructor() { - this.collectionStub = Sinon.stub(); - } - - collection(name: string): Bluebird { - return this.collectionStub(name); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/logging/GlobalLogger.ts b/themes/black/server/src/lib/logging/GlobalLogger.ts deleted file mode 100644 index 4da7acf49..000000000 --- a/themes/black/server/src/lib/logging/GlobalLogger.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { IGlobalLogger } from "./IGlobalLogger"; -import Util = require("util"); -import Express = require("express"); -import Winston = require("winston"); - -declare module "express" { - interface Request { - id: string; - } -} - -export class GlobalLogger implements IGlobalLogger { - private winston: typeof Winston; - constructor(winston: typeof Winston) { - this.winston = winston; - } - - private buildMessage(message: string, ...args: any[]): string { - return Util.format("date='%s' message='%s'", new Date(), - Util.format(message, ...args)); - } - - info(message: string, ...args: any[]): void { - this.winston.info(this.buildMessage(message, ...args)); - } - - debug(message: string, ...args: any[]): void { - this.winston.debug(this.buildMessage(message, ...args)); - } - - error(message: string, ...args: any[]): void { - this.winston.debug(this.buildMessage(message, ...args)); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/logging/GlobalLoggerStub.spec.ts b/themes/black/server/src/lib/logging/GlobalLoggerStub.spec.ts deleted file mode 100644 index d4bb13710..000000000 --- a/themes/black/server/src/lib/logging/GlobalLoggerStub.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Sinon = require("sinon"); -import { GlobalLogger } from "./GlobalLogger"; -import Winston = require("winston"); -import Express = require("express"); -import { IGlobalLogger } from "./IGlobalLogger"; - -export class GlobalLoggerStub implements IGlobalLogger { - infoStub: Sinon.SinonStub; - debugStub: Sinon.SinonStub; - errorStub: Sinon.SinonStub; - private globalLogger: IGlobalLogger; - - constructor(enableLogging?: boolean) { - this.infoStub = Sinon.stub(); - this.debugStub = Sinon.stub(); - this.errorStub = Sinon.stub(); - if (enableLogging) - this.globalLogger = new GlobalLogger(Winston); - } - - info(message: string, ...args: any[]): void { - if (this.globalLogger) - this.globalLogger.info(message, ...args); - this.infoStub(message, ...args); - } - - debug(message: string, ...args: any[]): void { - if (this.globalLogger) - this.globalLogger.info(message, ...args); - this.debugStub(message, ...args); - } - - error(message: string, ...args: any[]): void { - if (this.globalLogger) - this.globalLogger.info(message, ...args); - this.errorStub(message, ...args); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/logging/IGlobalLogger.ts b/themes/black/server/src/lib/logging/IGlobalLogger.ts deleted file mode 100644 index 548515ec7..000000000 --- a/themes/black/server/src/lib/logging/IGlobalLogger.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface IGlobalLogger { - info(message: string, ...args: any[]): void; - debug(message: string, ...args: any[]): void; - error(message: string, ...args: any[]): void; -} diff --git a/themes/black/server/src/lib/logging/IRequestLogger.ts b/themes/black/server/src/lib/logging/IRequestLogger.ts deleted file mode 100644 index 126a601fe..000000000 --- a/themes/black/server/src/lib/logging/IRequestLogger.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Express = require("express"); - -export interface IRequestLogger { - info(req: Express.Request, message: string, ...args: any[]): void; - debug(req: Express.Request, message: string, ...args: any[]): void; - error(req: Express.Request, message: string, ...args: any[]): void; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/logging/RequestLogger.ts b/themes/black/server/src/lib/logging/RequestLogger.ts deleted file mode 100644 index c45c66018..000000000 --- a/themes/black/server/src/lib/logging/RequestLogger.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { IRequestLogger } from "./IRequestLogger"; -import Util = require("util"); -import Express = require("express"); -import Winston = require("winston"); - -declare module "express" { - interface Request { - id: string; - } -} - -export class RequestLogger implements IRequestLogger { - private winston: typeof Winston; - - constructor(winston: typeof Winston) { - this.winston = winston; - } - - private formatHeader(req: Express.Request) { - const clientIP = req.ip; // The IP of the original client going through the proxy chain. - return Util.format("date='%s' method='%s', path='%s' requestId='%s' sessionId='%s' ip='%s'", - new Date(), req.method, req.path, req.id, req.sessionID, clientIP); - } - - private formatBody(message: string) { - return Util.format("message='%s'", message); - } - - private formatMessage(req: Express.Request, message: string) { - return Util.format("%s %s", this.formatHeader(req), - this.formatBody(message)); - } - - info(req: Express.Request, message: string, ...args: any[]): void { - this.winston.info(this.formatMessage(req, message), ...args); - } - - debug(req: Express.Request, message: string, ...args: any[]): void { - this.winston.debug(this.formatMessage(req, message), ...args); - } - - error(req: Express.Request, message: string, ...args: any[]): void { - this.winston.error(this.formatMessage(req, message), ...args); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/logging/RequestLoggerStub.spec.ts b/themes/black/server/src/lib/logging/RequestLoggerStub.spec.ts deleted file mode 100644 index b0e375210..000000000 --- a/themes/black/server/src/lib/logging/RequestLoggerStub.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { IRequestLogger } from "./IRequestLogger"; -import Sinon = require("sinon"); -import { RequestLogger } from "./RequestLogger"; -import Winston = require("winston"); -import Express = require("express"); - -export class RequestLoggerStub implements IRequestLogger { - infoStub: Sinon.SinonStub; - debugStub: Sinon.SinonStub; - errorStub: Sinon.SinonStub; - private requestLogger: RequestLogger; - - constructor(enableLogging?: boolean) { - this.infoStub = Sinon.stub(); - this.debugStub = Sinon.stub(); - this.errorStub = Sinon.stub(); - if (enableLogging) - this.requestLogger = new RequestLogger(Winston); - } - - info(req: Express.Request, message: string, ...args: any[]): void { - if (this.requestLogger) - this.requestLogger.info(req, message, ...args); - this.infoStub(req, message, ...args); - } - - debug(req: Express.Request, message: string, ...args: any[]): void { - if (this.requestLogger) - this.requestLogger.info(req, message, ...args); - this.debugStub(req, message, ...args); - } - - error(req: Express.Request, message: string, ...args: any[]): void { - if (this.requestLogger) - this.requestLogger.info(req, message, ...args); - this.errorStub(req, message, ...args); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/AbstractEmailNotifier.ts b/themes/black/server/src/lib/notifiers/AbstractEmailNotifier.ts deleted file mode 100644 index 198e4e5db..000000000 --- a/themes/black/server/src/lib/notifiers/AbstractEmailNotifier.ts +++ /dev/null @@ -1,23 +0,0 @@ - -import { INotifier } from "../notifiers/INotifier"; -import { Identity } from "../../../types/Identity"; - -import Fs = require("fs"); -import Path = require("path"); -import Ejs = require("ejs"); -import BluebirdPromise = require("bluebird"); - -const email_template = Fs.readFileSync(Path.join(__dirname, "../../resources/email-template.ejs"), "UTF-8"); - -export abstract class AbstractEmailNotifier implements INotifier { - notify(to: string, subject: string, link: string): BluebirdPromise { - const d = { - url: link, - button_title: "Continue", - title: subject - }; - return this.sendEmail(to, subject, Ejs.render(email_template, d)); - } - - abstract sendEmail(to: string, subject: string, content: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/EmailNotifier.spec.ts b/themes/black/server/src/lib/notifiers/EmailNotifier.spec.ts deleted file mode 100644 index 8211bbc02..000000000 --- a/themes/black/server/src/lib/notifiers/EmailNotifier.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as sinon from "sinon"; -import * as Assert from "assert"; -import BluebirdPromise = require("bluebird"); - -import { MailSenderStub } from "./MailSenderStub.spec"; -import EmailNotifier = require("./EmailNotifier"); - - -describe("notifiers/EmailNotifier", function () { - it("should send an email to given user", function () { - const mailSender = new MailSenderStub(); - const options = { - username: "user_gmail", - password: "pass_gmail", - sender: "admin@example.com", - service: "gmail" - }; - - mailSender.sendStub.returns(BluebirdPromise.resolve()); - const sender = new EmailNotifier.EmailNotifier(options, mailSender); - const subject = "subject"; - const url = "http://test.com"; - - return sender.notify("user@example.com", subject, url) - .then(function () { - Assert.equal(mailSender.sendStub.getCall(0).args[0].to, "user@example.com"); - Assert.equal(mailSender.sendStub.getCall(0).args[0].subject, "subject"); - return BluebirdPromise.resolve(); - }); - }); - - it("should fail while sending an email", function () { - const mailSender = new MailSenderStub(); - const options = { - username: "user_gmail", - password: "pass_gmail", - sender: "admin@example.com", - service: "gmail" - }; - - mailSender.sendStub.returns(BluebirdPromise.reject(new Error("Failed to send mail"))); - const sender = new EmailNotifier.EmailNotifier(options, mailSender); - const subject = "subject"; - const url = "http://test.com"; - - return sender.notify("user@example.com", subject, url) - .then(function () { - return BluebirdPromise.reject(new Error()); - }, function() { - Assert.equal(mailSender.sendStub.getCall(0).args[0].from, "admin@example.com"); - return BluebirdPromise.resolve(); - }); - }); -}); diff --git a/themes/black/server/src/lib/notifiers/EmailNotifier.ts b/themes/black/server/src/lib/notifiers/EmailNotifier.ts deleted file mode 100644 index 4df7c861e..000000000 --- a/themes/black/server/src/lib/notifiers/EmailNotifier.ts +++ /dev/null @@ -1,27 +0,0 @@ - -import * as BluebirdPromise from "bluebird"; - -import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; -import { EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; -import { IMailSender } from "./IMailSender"; - -export class EmailNotifier extends AbstractEmailNotifier { - private mailSender: IMailSender; - private sender: string; - - constructor(options: EmailNotifierConfiguration, mailSender: IMailSender) { - super(); - this.mailSender = mailSender; - this.sender = options.sender; - } - - sendEmail(to: string, subject: string, content: string) { - const mailOptions = { - from: this.sender, - to: to, - subject: subject, - html: content - }; - return this.mailSender.send(mailOptions); - } -} diff --git a/themes/black/server/src/lib/notifiers/FileSystemNotifier.ts b/themes/black/server/src/lib/notifiers/FileSystemNotifier.ts deleted file mode 100644 index 23f6242c4..000000000 --- a/themes/black/server/src/lib/notifiers/FileSystemNotifier.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as BluebirdPromise from "bluebird"; -import * as util from "util"; -import * as Fs from "fs"; -import { INotifier } from "./INotifier"; -import { Identity } from "../../../types/Identity"; - -import { FileSystemNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; - -export class FileSystemNotifier implements INotifier { - private filename: string; - - constructor(options: FileSystemNotifierConfiguration) { - this.filename = options.filename; - } - - notify(to: string, subject: string, link: string): BluebirdPromise { - const content = util.format("Date: %s\nEmail: %s\nSubject: %s\nLink: %s", - new Date().toString(), to, subject, link); - const writeFilePromised: any = BluebirdPromise.promisify(Fs.writeFile); - return writeFilePromised(this.filename, content); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/IMailSender.ts b/themes/black/server/src/lib/notifiers/IMailSender.ts deleted file mode 100644 index 34ac464a8..000000000 --- a/themes/black/server/src/lib/notifiers/IMailSender.ts +++ /dev/null @@ -1,6 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Nodemailer = require("nodemailer"); - -export interface IMailSender { - send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/IMailSenderBuilder.ts b/themes/black/server/src/lib/notifiers/IMailSenderBuilder.ts deleted file mode 100644 index 36d4dcdf7..000000000 --- a/themes/black/server/src/lib/notifiers/IMailSenderBuilder.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IMailSender } from "./IMailSender"; -import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; - -export interface IMailSenderBuilder { - buildEmail(options: EmailNotifierConfiguration): IMailSender; - buildSmtp(options: SmtpNotifierConfiguration): IMailSender; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/INotifier.ts b/themes/black/server/src/lib/notifiers/INotifier.ts deleted file mode 100644 index b9a6b138c..000000000 --- a/themes/black/server/src/lib/notifiers/INotifier.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as BluebirdPromise from "bluebird"; - -export interface INotifier { - notify(to: string, subject: string, link: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/MailSender.ts b/themes/black/server/src/lib/notifiers/MailSender.ts deleted file mode 100644 index 536a88e63..000000000 --- a/themes/black/server/src/lib/notifiers/MailSender.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { IMailSender } from "./IMailSender"; -import Nodemailer = require("nodemailer"); -import NodemailerDirectTransport = require("nodemailer-direct-transport"); -import NodemailerSmtpTransport = require("nodemailer-smtp-transport"); -import BluebirdPromise = require("bluebird"); - -export class MailSender implements IMailSender { - private transporter: Nodemailer.Transporter; - - constructor(options: NodemailerDirectTransport.DirectOptions | - NodemailerSmtpTransport.SmtpOptions, nodemailer: typeof Nodemailer) { - this.transporter = nodemailer.createTransport(options); - } - - verify(): BluebirdPromise { - const that = this; - return new BluebirdPromise(function (resolve, reject) { - that.transporter.verify(function (error: Error, success: any) { - if (error) { - reject(new Error("Unable to connect to SMTP server. \ - Please check the service is running and your credentials are correct.")); - return; - } - resolve(); - }); - }); - } - - send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise { - const that = this; - return new BluebirdPromise(function (resolve, reject) { - that.transporter.sendMail(mailOptions, (error: Error, - data: Nodemailer.SentMessageInfo) => { - if (error) { - reject(new Error("Error while sending email: " + error.message)); - return; - } - resolve(); - }); - }); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/MailSenderBuilder.spec.ts b/themes/black/server/src/lib/notifiers/MailSenderBuilder.spec.ts deleted file mode 100644 index 41e0db426..000000000 --- a/themes/black/server/src/lib/notifiers/MailSenderBuilder.spec.ts +++ /dev/null @@ -1,67 +0,0 @@ - -import { MailSenderBuilder } from ".//MailSenderBuilder"; -import Nodemailer = require("nodemailer"); -import Sinon = require("sinon"); -import Assert = require("assert"); - -describe("notifiers/MailSenderBuilder", function() { - let createTransportStub: Sinon.SinonStub; - beforeEach(function() { - createTransportStub = Sinon.stub(Nodemailer, "createTransport"); - }); - - afterEach(function() { - createTransportStub.restore(); - }); - - it("should create a email mail sender", function() { - const mailSenderBuilder = new MailSenderBuilder(Nodemailer); - mailSenderBuilder.buildEmail({ - username: "user_gmail", - password: "pass_gmail", - sender: "admin@example.com", - service: "gmail" - }); - Assert.equal(createTransportStub.getCall(0).args[0].auth.user, "user_gmail"); - Assert.equal(createTransportStub.getCall(0).args[0].auth.pass, "pass_gmail"); - Assert.equal(createTransportStub.getCall(0).args[0].service, "gmail"); - }); - - describe("build smtp mail sender", function() { - it("should create a smtp mail sender with authenticated user", function() { - const mailSenderBuilder = new MailSenderBuilder(Nodemailer); - mailSenderBuilder.buildSmtp({ - host: "mail.example.com", - password: "password", - port: 25, - secure: true, - username: "user", - sender: "admin@example.com" - }); - Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { - host: "mail.example.com", - auth: { - pass: "password", - user: "user" - }, - port: 25, - secure: true, - }); - }); - - it("should create a smtp mail sender with anonymous user", function() { - const mailSenderBuilder = new MailSenderBuilder(Nodemailer); - mailSenderBuilder.buildSmtp({ - host: "mail.example.com", - port: 25, - secure: true, - sender: "admin@example.com" - }); - Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { - host: "mail.example.com", - port: 25, - secure: true, - }); - }); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/MailSenderBuilder.ts b/themes/black/server/src/lib/notifiers/MailSenderBuilder.ts deleted file mode 100644 index 1d06be52f..000000000 --- a/themes/black/server/src/lib/notifiers/MailSenderBuilder.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { IMailSender } from "./IMailSender"; -import { IMailSenderBuilder } from "./IMailSenderBuilder"; -import { MailSender } from "./MailSender"; -import Nodemailer = require("nodemailer"); -import NodemailerSmtpTransport = require("nodemailer-smtp-transport"); -import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; - -export class MailSenderBuilder implements IMailSenderBuilder { - private nodemailer: typeof Nodemailer; - - constructor(nodemailer: typeof Nodemailer) { - this.nodemailer = nodemailer; - } - - buildEmail(options: EmailNotifierConfiguration): IMailSender { - const emailOptions = { - service: options.service, - auth: { - user: options.username, - pass: options.password - } - }; - return new MailSender(emailOptions, this.nodemailer); - } - - buildSmtp(options: SmtpNotifierConfiguration): IMailSender { - const smtpOptions: NodemailerSmtpTransport.SmtpOptions = { - host: options.host, - port: options.port, - secure: options.secure, // upgrade later with STARTTLS - }; - - if (options.username && options.password) { - smtpOptions.auth = { - user: options.username, - pass: options.password - }; - } - - return new MailSender(smtpOptions, this.nodemailer); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts b/themes/black/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts deleted file mode 100644 index 5b76f6e56..000000000 --- a/themes/black/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { IMailSenderBuilder } from "../../../src/lib/notifiers/IMailSenderBuilder"; -import BluebirdPromise = require("bluebird"); -import Nodemailer = require("nodemailer"); -import Sinon = require("sinon"); -import { IMailSender } from "../../../src/lib/notifiers/IMailSender"; -import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../../../src/lib/configuration/schema/NotifierConfiguration"; - -export class MailSenderBuilderStub implements IMailSenderBuilder { - buildEmailStub: Sinon.SinonStub; - buildSmtpStub: Sinon.SinonStub; - - constructor() { - this.buildEmailStub = Sinon.stub(); - this.buildSmtpStub = Sinon.stub(); - } - - buildEmail(options: EmailNotifierConfiguration): IMailSender { - return this.buildEmailStub(options); - } - - buildSmtp(options: SmtpNotifierConfiguration): IMailSender { - return this.buildSmtpStub(options); - } - -} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/MailSenderStub.spec.ts b/themes/black/server/src/lib/notifiers/MailSenderStub.spec.ts deleted file mode 100644 index d57c458fc..000000000 --- a/themes/black/server/src/lib/notifiers/MailSenderStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { IMailSender } from "../../../src/lib/notifiers/IMailSender"; -import BluebirdPromise = require("bluebird"); -import Nodemailer = require("nodemailer"); -import Sinon = require("sinon"); - -export class MailSenderStub implements IMailSender { - sendStub: Sinon.SinonStub; - - constructor() { - this.sendStub = Sinon.stub(); - } - - send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise { - return this.sendStub(mailOptions); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/NotifierFactory.spec.ts b/themes/black/server/src/lib/notifiers/NotifierFactory.spec.ts deleted file mode 100644 index f15e7667f..000000000 --- a/themes/black/server/src/lib/notifiers/NotifierFactory.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ - -import * as sinon from "sinon"; -import * as BluebirdPromise from "bluebird"; -import * as assert from "assert"; - -import { NotifierFactory } from "./NotifierFactory"; -import { EmailNotifier } from "./EmailNotifier"; -import { SmtpNotifier } from "./SmtpNotifier"; -import { MailSenderBuilderStub } from "./MailSenderBuilderStub.spec"; - - -describe("notifiers/NotifierFactory", function () { - let mailSenderBuilderStub: MailSenderBuilderStub; - it("should build a Email Notifier", function () { - const options = { - email: { - username: "abc", - password: "password", - sender: "admin@example.com", - service: "gmail" - } - }; - mailSenderBuilderStub = new MailSenderBuilderStub(); - assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof EmailNotifier); - }); - - it("should build a SMTP Notifier", function () { - const options = { - smtp: { - username: "user", - password: "pass", - secure: true, - host: "localhost", - port: 25, - sender: "admin@example.com" - } - }; - - mailSenderBuilderStub = new MailSenderBuilderStub(); - assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof SmtpNotifier); - }); -}); diff --git a/themes/black/server/src/lib/notifiers/NotifierFactory.ts b/themes/black/server/src/lib/notifiers/NotifierFactory.ts deleted file mode 100644 index a89155feb..000000000 --- a/themes/black/server/src/lib/notifiers/NotifierFactory.ts +++ /dev/null @@ -1,33 +0,0 @@ - -import { NotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; -import Nodemailer = require("nodemailer"); -import { INotifier } from "./INotifier"; - -import { FileSystemNotifier } from "./FileSystemNotifier"; -import { EmailNotifier } from "./EmailNotifier"; -import { SmtpNotifier } from "./SmtpNotifier"; -import { IMailSender } from "./IMailSender"; -import { IMailSenderBuilder } from "./IMailSenderBuilder"; - -export class NotifierFactory { - static build(options: NotifierConfiguration, mailSenderBuilder: IMailSenderBuilder): INotifier { - if ("email" in options) { - const mailSender = mailSenderBuilder.buildEmail(options.email); - return new EmailNotifier(options.email, mailSender); - } - else if ("smtp" in options) { - const mailSender = mailSenderBuilder.buildSmtp(options.smtp); - return new SmtpNotifier(options.smtp, mailSender); - } - else if ("filesystem" in options) { - return new FileSystemNotifier(options.filesystem); - } - else { - throw new Error("No available notifier option detected."); - } - } -} - - - - diff --git a/themes/black/server/src/lib/notifiers/NotifierStub.spec.ts b/themes/black/server/src/lib/notifiers/NotifierStub.spec.ts deleted file mode 100644 index f99231b50..000000000 --- a/themes/black/server/src/lib/notifiers/NotifierStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); - -import { INotifier } from "./INotifier"; - -export class NotifierStub implements INotifier { - notifyStub: Sinon.SinonStub; - - constructor() { - this.notifyStub = Sinon.stub(); - } - - notify(to: string, subject: string, link: string): BluebirdPromise { - return this.notifyStub(to, subject, link); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/notifiers/SmtpNotifier.ts b/themes/black/server/src/lib/notifiers/SmtpNotifier.ts deleted file mode 100644 index f93a6d4a0..000000000 --- a/themes/black/server/src/lib/notifiers/SmtpNotifier.ts +++ /dev/null @@ -1,30 +0,0 @@ - - -import * as BluebirdPromise from "bluebird"; - -import { IMailSender } from "./IMailSender"; -import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; -import { SmtpNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; - -export class SmtpNotifier extends AbstractEmailNotifier { - private mailSender: IMailSender; - private sender: string; - - constructor(options: SmtpNotifierConfiguration, - mailSender: IMailSender) { - super(); - this.mailSender = mailSender; - this.sender = options.sender; - } - - sendEmail(to: string, subject: string, content: string) { - const mailOptions = { - from: this.sender, - to: to, - subject: subject, - html: content - }; - const that = this; - return this.mailSender.send(mailOptions); - } -} diff --git a/themes/black/server/src/lib/regulation/IRegulator.ts b/themes/black/server/src/lib/regulation/IRegulator.ts deleted file mode 100644 index c49425b24..000000000 --- a/themes/black/server/src/lib/regulation/IRegulator.ts +++ /dev/null @@ -1,6 +0,0 @@ -import BluebirdPromise = require("bluebird"); - -export interface IRegulator { - mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; - regulate(userId: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/regulation/Regulator.spec.ts b/themes/black/server/src/lib/regulation/Regulator.spec.ts deleted file mode 100644 index f9c6e6086..000000000 --- a/themes/black/server/src/lib/regulation/Regulator.spec.ts +++ /dev/null @@ -1,186 +0,0 @@ - -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import Assert = require("assert"); - -import { Regulator } from "./Regulator"; -import MockDate = require("mockdate"); -import exceptions = require("../Exceptions"); -import { UserDataStoreStub } from "../storage/UserDataStoreStub.spec"; - -describe("regulation/Regulator", function () { - const USER1 = "USER1"; - const USER2 = "USER2"; - let userDataStoreStub: UserDataStoreStub; - - beforeEach(function () { - userDataStoreStub = new UserDataStoreStub(); - const dataStore: { [userId: string]: { userId: string, date: Date, isAuthenticationSuccessful: boolean }[] } = { - [USER1]: [], - [USER2]: [] - }; - - userDataStoreStub.saveAuthenticationTraceStub.callsFake(function (userId, isAuthenticationSuccessful) { - dataStore[userId].unshift({ - userId: userId, - date: new Date(), - isAuthenticationSuccessful: isAuthenticationSuccessful, - }); - return BluebirdPromise.resolve(); - }); - - userDataStoreStub.retrieveLatestAuthenticationTracesStub.callsFake(function (userId, count) { - const ret = (dataStore[userId].length <= count) ? dataStore[userId] : dataStore[userId].slice(0, 3); - return BluebirdPromise.resolve(ret); - }); - }); - - afterEach(function () { - MockDate.reset(); - }); - - function markAuthenticationAt(regulator: Regulator, user: string, time: string, success: boolean) { - MockDate.set(time); - return regulator.mark(user, success); - } - - it("should mark 2 authentication and regulate (accept)", function () { - const regulator = new Regulator(userDataStoreStub, 3, 10, 10); - - return regulator.mark(USER1, false) - .then(function () { - return regulator.mark(USER1, true); - }) - .then(function () { - return regulator.regulate(USER1); - }); - }); - - it("should mark 3 authentications and regulate (reject)", function () { - const regulator = new Regulator(userDataStoreStub, 3, 10, 10); - - return regulator.mark(USER1, false) - .then(function () { - return regulator.mark(USER1, false); - }) - .then(function () { - return regulator.mark(USER1, false); - }) - .then(function () { - return regulator.regulate(USER1); - }) - .then(function () { return BluebirdPromise.reject(new Error("should not be here!")); }) - .catch(exceptions.AuthenticationRegulationError, function () { - return BluebirdPromise.resolve(); - }); - }); - - it("should mark 1 failed, 1 successful and 1 failed authentications within minimum time and regulate (accept)", function () { - const regulator = new Regulator(userDataStoreStub, 3, 60, 30); - - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", true); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:20", false); - }) - .then(function () { - return regulator.regulate(USER1); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:30", false); - }) - .then(function () { - return regulator.regulate(USER1); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:39", false); - }) - .then(function () { - return regulator.regulate(USER1); - }) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here!")); - }, - function () { - return BluebirdPromise.resolve(); - }); - }); - - it("should regulate user if number of failures is greater than 3 in allowed time lapse", function () { - function markAuthentications(regulator: Regulator, user: string) { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:45", false); - }) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:01:05", false); - }) - .then(function () { - return regulator.regulate(user); - }); - } - - const regulator1 = new Regulator(userDataStoreStub, 3, 60, 60); - const regulator2 = new Regulator(userDataStoreStub, 3, 2 * 60, 60); - - const p1 = markAuthentications(regulator1, USER1); - const p2 = markAuthentications(regulator2, USER2); - - return BluebirdPromise.join(p1, p2) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here...")); - }, function () { - Assert(p1.isFulfilled()); - Assert(p2.isRejected()); - }); - }); - - it("should user wait after regulation to authenticate again", function () { - function markAuthentications(regulator: Regulator, user: string) { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:10", false); - }) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:15", false); - }) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:25", false); - }) - .then(function () { - MockDate.set("1/2/2000 00:00:54"); - return regulator.regulate(user); - }) - .then(function () { - return BluebirdPromise.reject(new Error("should fail at this time")); - }, function () { - MockDate.set("1/2/2000 00:00:56"); - return regulator.regulate(user); - }); - } - - const regulator = new Regulator(userDataStoreStub, 4, 30, 30); - return markAuthentications(regulator, USER1); - }); - - it("should disable regulation when max_retries is set to 0", function () { - const maxRetries = 0; - const regulator = new Regulator(userDataStoreStub, maxRetries, 60, 30); - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", false); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:15", false); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:25", false); - }) - .then(function () { - MockDate.set("1/2/2000 00:00:26"); - return regulator.regulate(USER1); - }); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/regulation/Regulator.ts b/themes/black/server/src/lib/regulation/Regulator.ts deleted file mode 100644 index 1037a6a17..000000000 --- a/themes/black/server/src/lib/regulation/Regulator.ts +++ /dev/null @@ -1,55 +0,0 @@ - -import * as BluebirdPromise from "bluebird"; -import exceptions = require("../Exceptions"); -import { IUserDataStore } from "../storage/IUserDataStore"; -import { AuthenticationTraceDocument } from "../storage/AuthenticationTraceDocument"; -import { IRegulator } from "./IRegulator"; - -export class Regulator implements IRegulator { - private userDataStore: IUserDataStore; - private banTime: number; - private findTime: number; - private maxRetries: number; - - constructor(userDataStore: any, maxRetries: number, findTime: number, banTime: number) { - this.userDataStore = userDataStore; - this.banTime = banTime; - this.findTime = findTime; - this.maxRetries = maxRetries; - } - - // Mark authentication - mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { - return this.userDataStore.saveAuthenticationTrace(userId, isAuthenticationSuccessful); - } - - regulate(userId: string): BluebirdPromise { - const that = this; - - if (that.maxRetries <= 0) return BluebirdPromise.resolve(); - - return this.userDataStore.retrieveLatestAuthenticationTraces(userId, that.maxRetries) - .then((docs: AuthenticationTraceDocument[]) => { - // less than the max authorized number of authentication in time range, thus authorizing access - if (docs.length < that.maxRetries) return BluebirdPromise.resolve(); - - const numberOfFailedAuth = docs - .map(function (d: AuthenticationTraceDocument) { return d.isAuthenticationSuccessful == false ? 1 : 0; }) - .reduce(function (acc, v) { return acc + v; }, 0); - - if (numberOfFailedAuth < this.maxRetries) return BluebirdPromise.resolve(); - - const newestDocument = docs[0]; - const oldestDocument = docs[that.maxRetries - 1]; - - const authenticationsTimeRangeInSeconds = (newestDocument.date.getTime() - oldestDocument.date.getTime()) / 1000; - const tooManyAuthInTimelapse = (authenticationsTimeRangeInSeconds < this.findTime); - const stillInBannedTimeRange = (new Date(new Date().getTime() - this.banTime * 1000) < newestDocument.date); - - if (tooManyAuthInTimelapse && stillInBannedTimeRange) - throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes."); - - return BluebirdPromise.resolve(); - }); - } -} diff --git a/themes/black/server/src/lib/regulation/RegulatorStub.spec.ts b/themes/black/server/src/lib/regulation/RegulatorStub.spec.ts deleted file mode 100644 index ca8a00fb1..000000000 --- a/themes/black/server/src/lib/regulation/RegulatorStub.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Bluebird = require("bluebird"); -import Sinon = require("sinon"); -import { IRegulator } from "./IRegulator"; - - -export class RegulatorStub implements IRegulator { - markStub: Sinon.SinonStub; - regulateStub: Sinon.SinonStub; - - constructor() { - this.markStub = Sinon.stub(); - this.regulateStub = Sinon.stub(); - } - - mark(userId: string, isAuthenticationSuccessful: boolean): Bluebird { - return this.markStub(userId, isAuthenticationSuccessful); - } - - regulate(userId: string): Bluebird { - return this.regulateStub(userId); - } -} diff --git a/themes/black/server/src/lib/routes/error/401/get.spec.ts b/themes/black/server/src/lib/routes/error/401/get.spec.ts deleted file mode 100644 index 9fdac9c3c..000000000 --- a/themes/black/server/src/lib/routes/error/401/get.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import Sinon = require("sinon"); -import Express = require("express"); -import Assert = require("assert"); -import Get401 from "./get"; -import { ServerVariables } from "../../../ServerVariables"; -import { ServerVariablesMockBuilder, ServerVariablesMock } - from "../../../ServerVariablesMockBuilder.spec"; - -describe("routes/error/401/get", function () { - let vars: ServerVariables; - let mocks: ServerVariablesMock; - let req: any; - let res: any; - let renderSpy: Sinon.SinonSpy; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - vars = s.variables; - mocks = s.mocks; - - renderSpy = Sinon.spy(); - req = { - headers: {} - }; - res = { - render: renderSpy - }; - }); - - it("should set redirection url to the default redirection url", function () { - vars.config.default_redirection_url = "http://default-redirection"; - return Get401(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/401", { - redirection_url: "http://default-redirection" - })); - }); - }); - - it("should set redirection url to the referer", function () { - req.headers["referer"] = "http://redirection"; - return Get401(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/401", { - redirection_url: "http://redirection" - })); - }); - }); - - it("should render without redirecting the user", function () { - return Get401(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/401", { - redirection_url: undefined - })); - }); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/error/401/get.ts b/themes/black/server/src/lib/routes/error/401/get.ts deleted file mode 100644 index ca4a3963d..000000000 --- a/themes/black/server/src/lib/routes/error/401/get.ts +++ /dev/null @@ -1,15 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import redirector from "../redirector"; -import { ServerVariables } from "../../../ServerVariables"; - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response): BluebirdPromise { - const redirectionUrl = redirector(req, vars); - res.render("errors/401", { - redirection_url: redirectionUrl - }); - return BluebirdPromise.resolve(); - }; -} diff --git a/themes/black/server/src/lib/routes/error/403/get.spec.ts b/themes/black/server/src/lib/routes/error/403/get.spec.ts deleted file mode 100644 index 22eb84853..000000000 --- a/themes/black/server/src/lib/routes/error/403/get.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import Sinon = require("sinon"); -import Express = require("express"); -import Assert = require("assert"); -import Get403 from "./get"; -import { ServerVariables } from "../../../ServerVariables"; -import { ServerVariablesMockBuilder, ServerVariablesMock } - from "../../../ServerVariablesMockBuilder.spec"; - -describe("routes/error/403/get", function () { - let vars: ServerVariables; - let mocks: ServerVariablesMock; - let req: any; - let res: any; - let renderSpy: Sinon.SinonSpy; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - vars = s.variables; - mocks = s.mocks; - - renderSpy = Sinon.spy(); - req = { - headers: {} - }; - res = { - render: renderSpy - }; - }); - - it("should set redirection url to the default redirection url", function () { - vars.config.default_redirection_url = "http://default-redirection"; - return Get403(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/403", { - redirection_url: "http://default-redirection" - })); - }); - }); - - it("should set redirection url to the referer", function () { - req.headers["referer"] = "http://redirection"; - return Get403(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/403", { - redirection_url: "http://redirection" - })); - }); - }); - - it("should render without redirecting the user", function () { - return Get403(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/403", { - redirection_url: undefined - })); - }); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/error/403/get.ts b/themes/black/server/src/lib/routes/error/403/get.ts deleted file mode 100644 index 3ab0319e5..000000000 --- a/themes/black/server/src/lib/routes/error/403/get.ts +++ /dev/null @@ -1,15 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import redirector from "../redirector"; -import { ServerVariables } from "../../../ServerVariables"; - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response): BluebirdPromise { - const redirectionUrl = redirector(req, vars); - res.render("errors/403", { - redirection_url: redirectionUrl - }); - return BluebirdPromise.resolve(); - }; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/error/404/get.spec.ts b/themes/black/server/src/lib/routes/error/404/get.spec.ts deleted file mode 100644 index 73e4e6cef..000000000 --- a/themes/black/server/src/lib/routes/error/404/get.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Sinon = require("sinon"); -import Express = require("express"); -import Assert = require("assert"); -import Get404 from "./get"; - -describe("routes/error/404/get", function () { - it("should render the page", function () { - const req = {} as Express.Request; - const res = { - render: Sinon.stub() - }; - - return Get404(req, res as any) - .then(function () { - Assert(res.render.calledOnce); - Assert(res.render.calledWith("errors/404")); - }); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/error/404/get.ts b/themes/black/server/src/lib/routes/error/404/get.ts deleted file mode 100644 index 6693b6fc8..000000000 --- a/themes/black/server/src/lib/routes/error/404/get.ts +++ /dev/null @@ -1,8 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import express = require("express"); - -export default function (req: express.Request, res: express.Response): BluebirdPromise { - res.render("errors/404"); - return BluebirdPromise.resolve(); -} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/error/redirector.ts b/themes/black/server/src/lib/routes/error/redirector.ts deleted file mode 100644 index b1a3ccc11..000000000 --- a/themes/black/server/src/lib/routes/error/redirector.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Express = require("express"); -import { ServerVariables } from "../../ServerVariables"; - -export default function (req: Express.Request, vars: ServerVariables): string { - let redirectionUrl: string; - - if (req.headers && req.headers["referer"]) - redirectionUrl = "" + req.headers["referer"]; - else if (vars.config.default_redirection_url) - redirectionUrl = vars.config.default_redirection_url; - - return redirectionUrl; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/firstfactor/get.ts b/themes/black/server/src/lib/routes/firstfactor/get.ts deleted file mode 100644 index d94f656c8..000000000 --- a/themes/black/server/src/lib/routes/firstfactor/get.ts +++ /dev/null @@ -1,72 +0,0 @@ - -import express = require("express"); -import Endpoints = require("../../../../../shared/api"); -import BluebirdPromise = require("bluebird"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import Constants = require("../../../../../shared/constants"); -import Util = require("util"); -import { ServerVariables } from "../../ServerVariables"; -import { SafeRedirector } from "../../utils/SafeRedirection"; -import { Level } from "../../authentication/Level"; - -function getRedirectParam( - req: express.Request) { - return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : undefined; -} - -function redirectToSecondFactorPage( - req: express.Request, - res: express.Response) { - - const redirectUrl = getRedirectParam(req); - if (!redirectUrl) - res.redirect(Endpoints.SECOND_FACTOR_GET); - else - res.redirect( - Util.format("%s?%s=%s", - Endpoints.SECOND_FACTOR_GET, - Constants.REDIRECT_QUERY_PARAM, - redirectUrl)); -} - -function redirectToService( - req: express.Request, - res: express.Response, - redirector: SafeRedirector) { - const redirectUrl = getRedirectParam(req); - if (!redirectUrl) { - res.redirect(Endpoints.LOGGED_IN); - } else { - redirector.redirectOrElse(res, redirectUrl, Endpoints.LOGGED_IN); - } -} - -function renderFirstFactor( - res: express.Response) { - - res.render("firstfactor", { - first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST, - reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET - }); -} - -export default function ( - vars: ServerVariables) { - - const redirector = new SafeRedirector(vars.config.session.domain); - return function (req: express.Request, res: express.Response): BluebirdPromise { - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, vars.logger); - if (authSession.authentication_level == Level.ONE_FACTOR) { - redirectToSecondFactorPage(req, res); - } else if (authSession.authentication_level == Level.TWO_FACTOR) { - redirectToService(req, res, redirector); - } else { - renderFirstFactor(res); - } - resolve(); - }); - }; -} diff --git a/themes/black/server/src/lib/routes/firstfactor/post.spec.ts b/themes/black/server/src/lib/routes/firstfactor/post.spec.ts deleted file mode 100644 index e1d078cdd..000000000 --- a/themes/black/server/src/lib/routes/firstfactor/post.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ - -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import Assert = require("assert"); -import FirstFactorPost = require("./post"); -import exceptions = require("../../Exceptions"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../types/AuthenticationSession"; -import Endpoints = require("../../../../../shared/api"); -import AuthenticationRegulatorMock = require("../../regulation/RegulatorStub.spec"); -import ExpressMock = require("../../stubs/express.spec"); -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../ServerVariables"; - -describe("routes/firstfactor/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let emails: string[]; - let groups: string[]; - let vars: ServerVariables; - let mocks: ServerVariablesMock; - let authSession: AuthenticationSession; - - beforeEach(function () { - emails = ["test_ok@example.com"]; - groups = ["group1", "group2" ]; - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - mocks.authorizer.authorizationMock.returns(true); - mocks.regulator.regulateStub.returns(BluebirdPromise.resolve()); - mocks.regulator.markStub.returns(BluebirdPromise.resolve()); - - req = { - originalUrl: "/api/firstfactor", - body: { - username: "username", - password: "password" - }, - query: { - redirect: "http://redirect.url" - }, - session: { - cookie: {} - }, - headers: { - host: "home.example.com" - } - }; - - res = ExpressMock.ResponseMock(); - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - }); - - it("should reply with 204 if success", function () { - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.resolve({ - emails: emails, - groups: groups - })); - return FirstFactorPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal("username", authSession.userid); - Assert(res.send.calledOnce); - }); - }); - - describe("keep me logged in", () => { - beforeEach(() => { - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.resolve({ - emails: emails, - groups: groups - })); - req.body.keepMeLoggedIn = "true"; - return FirstFactorPost.default(vars)(req as any, res as any); - }); - - it("should set keep_me_logged_in session variable to true", function () { - Assert.equal(authSession.keep_me_logged_in, true); - }); - - it("should set cookie maxAge to one year", function () { - Assert.equal(req.session.cookie.maxAge, 365 * 24 * 60 * 60 * 1000); - }); - }); - - it("should retrieve email from LDAP", function () { - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.resolve([{ mail: ["test@example.com"] }])); - return FirstFactorPost.default(vars)(req as any, res as any); - }); - - it("should set first email address as user session variable", function () { - const emails = ["test_ok@example.com"]; - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.resolve({ - emails: emails, - groups: groups - })); - - return FirstFactorPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal("test_ok@example.com", authSession.email); - }); - }); - - it("should return error message when LDAP authenticator throws", function () { - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.reject(new exceptions.LdapBindError("Bad credentials"))); - - return FirstFactorPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.equal(mocks.regulator.markStub.getCall(0).args[0], "username"); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - }); - }); - - it("should return error message when regulator rejects authentication", function () { - const err = new exceptions.AuthenticationRegulationError("Authentication regulation..."); - mocks.regulator.regulateStub.returns(BluebirdPromise.reject(err)); - return FirstFactorPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - }); - }); -}); - - diff --git a/themes/black/server/src/lib/routes/firstfactor/post.ts b/themes/black/server/src/lib/routes/firstfactor/post.ts deleted file mode 100644 index 565681d6a..000000000 --- a/themes/black/server/src/lib/routes/firstfactor/post.ts +++ /dev/null @@ -1,101 +0,0 @@ - -import Exceptions = require("../../Exceptions"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import Endpoint = require("../../../../../shared/api"); -import ErrorReplies = require("../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import Constants = require("../../../../../shared/constants"); -import UserMessages = require("../../../../../shared/UserMessages"); -import { ServerVariables } from "../../ServerVariables"; -import { AuthenticationSession } from "../../../../types/AuthenticationSession"; -import { GroupsAndEmails } from "../../authentication/backends/GroupsAndEmails"; -import { Level as AuthenticationLevel } from "../../authentication/Level"; -import { Level as AuthorizationLevel } from "../../authorization/Level"; -import { URLDecomposer } from "../../utils/URLDecomposer"; - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response) - : BluebirdPromise { - const username: string = req.body.username; - const password: string = req.body.password; - const keepMeLoggedIn: boolean = req.body.keepMeLoggedIn && - req.body.keepMeLoggedIn === "true"; - let authSession: AuthenticationSession; - - if (keepMeLoggedIn) { - // Stay connected for 1 year. - vars.logger.debug(req, "User requested to stay logged in for one year."); - req.session.cookie.maxAge = 365 * 24 * 60 * 60 * 1000; - } - - return BluebirdPromise.resolve() - .then(function () { - if (!username || !password) { - return BluebirdPromise.reject(new Error("No username or password.")); - } - vars.logger.info(req, "Starting authentication of user \"%s\"", username); - authSession = AuthenticationSessionHandler.get(req, vars.logger); - return vars.regulator.regulate(username); - }) - .then(function () { - vars.logger.info(req, "No regulation applied."); - return vars.usersDatabase.checkUserPassword(username, password); - }) - .then(function (groupsAndEmails: GroupsAndEmails) { - vars.logger.info(req, - "LDAP binding successful. Retrieved information about user are %s", - JSON.stringify(groupsAndEmails)); - authSession.userid = username; - authSession.keep_me_logged_in = keepMeLoggedIn; - authSession.authentication_level = AuthenticationLevel.ONE_FACTOR; - const redirectUrl: string = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined" - // Fuck, don't know why it is a string! - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : ""; - - const emails: string[] = groupsAndEmails.emails; - const groups: string[] = groupsAndEmails.groups; - const decomposition = URLDecomposer.fromUrl(redirectUrl); - const authorizationLevel = (decomposition) - ? vars.authorizer.authorization( - {domain: decomposition.domain, resource: decomposition.path}, - {user: username, groups: groups}) - : AuthorizationLevel.TWO_FACTOR; - - if (emails.length > 0) - authSession.email = emails[0]; - authSession.groups = groups; - - vars.logger.debug(req, "Mark successful authentication to regulator."); - vars.regulator.mark(username, true); - - if (authorizationLevel <= AuthorizationLevel.ONE_FACTOR) { - let newRedirectionUrl: string = redirectUrl; - if (!newRedirectionUrl) - newRedirectionUrl = Endpoint.LOGGED_IN; - res.send({ - redirect: newRedirectionUrl - }); - vars.logger.debug(req, "Redirect to '%s'", redirectUrl); - } - else { - let newRedirectUrl = Endpoint.SECOND_FACTOR_GET; - if (redirectUrl) { - newRedirectUrl += "?" + Constants.REDIRECT_QUERY_PARAM + "=" - + redirectUrl; - } - vars.logger.debug(req, "Redirect to '%s'", newRedirectUrl); - res.send({ - redirect: newRedirectUrl - }); - } - return BluebirdPromise.resolve(); - }) - .catch(Exceptions.LdapBindError, function (err: Error) { - vars.regulator.mark(username, false); - return ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)(err); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)); - }; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/loggedin/get.ts b/themes/black/server/src/lib/routes/loggedin/get.ts deleted file mode 100644 index 283a041b1..000000000 --- a/themes/black/server/src/lib/routes/loggedin/get.ts +++ /dev/null @@ -1,23 +0,0 @@ -import Express = require("express"); -import Endpoints = require("../../../../../shared/api"); -import BluebirdPromise = require("bluebird"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { ServerVariables } from "../../ServerVariables"; -import ErrorReplies = require("../../ErrorReplies"); - -export default function (vars: ServerVariables) { - function handler(req: Express.Request, res: Express.Response): BluebirdPromise { - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, vars.logger); - res.render("already-logged-in", { - logout_endpoint: Endpoints.LOGOUT_GET, - username: authSession.userid, - redirection_url: vars.config.default_redirection_url - }); - resolve(); - }) - .catch(ErrorReplies.replyWithError401(req, res, vars.logger)); - } - - return handler; -} diff --git a/themes/black/server/src/lib/routes/logout/get.ts b/themes/black/server/src/lib/routes/logout/get.ts deleted file mode 100644 index 4d5112146..000000000 --- a/themes/black/server/src/lib/routes/logout/get.ts +++ /dev/null @@ -1,20 +0,0 @@ - -import express = require("express"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import Constants = require("../../../../../shared/constants"); -import { ServerVariables } from "../../ServerVariables"; - -function getRedirectParam(req: express.Request) { - return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : undefined; -} - -export default function (vars: ServerVariables) { - return function(req: express.Request, res: express.Response) { - const redirect_param = getRedirectParam(req); - const redirect_url = redirect_param || "/"; - AuthenticationSessionHandler.reset(req); - res.redirect(redirect_url); - }; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/password-reset/constants.ts b/themes/black/server/src/lib/routes/password-reset/constants.ts deleted file mode 100644 index 5c639e92a..000000000 --- a/themes/black/server/src/lib/routes/password-reset/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export const CHALLENGE = "reset-password"; \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/password-reset/form/post.spec.ts b/themes/black/server/src/lib/routes/password-reset/form/post.spec.ts deleted file mode 100644 index ed029c906..000000000 --- a/themes/black/server/src/lib/routes/password-reset/form/post.spec.ts +++ /dev/null @@ -1,122 +0,0 @@ - -import PasswordResetFormPost = require("./post"); -import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; -import { UserDataStore } from "../../../storage/UserDataStore"; -import Sinon = require("sinon"); -import Assert = require("assert"); -import BluebirdPromise = require("bluebird"); -import ExpressMock = require("../../../stubs/express.spec"); -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../ServerVariables"; -import { Level } from "../../../authentication/Level"; - -describe("routes/password-reset/form/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let vars: ServerVariables; - let mocks: ServerVariablesMock; - let authSession: AuthenticationSession; - - beforeEach(function () { - req = { - originalUrl: "/api/password-reset", - body: { - userid: "user" - }, - session: {}, - headers: { - host: "localhost" - } - }; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - - mocks.config.authentication_backend.ldap = { - url: "ldap://ldapjs", - mail_attribute: "mail", - user: "user", - password: "password", - additional_users_dn: "ou=users", - additional_groups_dn: "ou=groups", - base_dn: "dc=example,dc=com", - users_filter: "user", - group_name_attribute: "cn", - groups_filter: "groups" - }; - - res = ExpressMock.ResponseMock(); - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - authSession.userid = "user"; - authSession.email = "user@example.com"; - authSession.authentication_level = Level.ONE_FACTOR; - }); - - describe("test reset password post", () => { - it("should update the password and reset auth_session for reauthentication", function () { - req.body = {}; - req.body.password = "new-password"; - - mocks.usersDatabase.updatePasswordStub.returns(BluebirdPromise.resolve()); - - authSession.identity_check = { - userid: "user", - challenge: "reset-password" - }; - return PasswordResetFormPost.default(vars)(req as any, res as any) - .then(function () { - return AuthenticationSessionHandler.get(req as any, vars.logger); - }).then(function (_authSession) { - Assert.equal(res.status.getCall(0).args[0], 204); - Assert.equal(_authSession.authentication_level, Level.NOT_AUTHENTICATED); - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if identity_challenge does not exist", function () { - authSession.identity_check = { - userid: "user", - challenge: undefined - }; - return PasswordResetFormPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "An error occurred during password reset. Your password has not been changed." - }); - }); - }); - - it("should fail when ldap fails", function () { - req.body = {}; - req.body.password = "new-password"; - - mocks.usersDatabase.updatePasswordStub - .returns(BluebirdPromise.reject("Internal error with LDAP")); - - authSession.identity_check = { - challenge: "reset-password", - userid: "user" - }; - return PasswordResetFormPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "An error occurred during password reset. Your password has not been changed." - }); - return BluebirdPromise.resolve(); - }); - }); - }); -}); diff --git a/themes/black/server/src/lib/routes/password-reset/form/post.ts b/themes/black/server/src/lib/routes/password-reset/form/post.ts deleted file mode 100644 index fccd7471b..000000000 --- a/themes/black/server/src/lib/routes/password-reset/form/post.ts +++ /dev/null @@ -1,50 +0,0 @@ - -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import objectPath = require("object-path"); -import exceptions = require("../../../Exceptions"); -import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; -import ErrorReplies = require("../../../ErrorReplies"); -import UserMessages = require("../../../../../../shared/UserMessages"); -import { ServerVariables } from "../../../ServerVariables"; - -import Constants = require("./../constants"); - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const newPassword = objectPath.get(req, "body.password"); - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - if (!authSession.identity_check) { - reject(new Error("No identity check initiated")); - return; - } - - vars.logger.info(req, "User %s wants to reset his/her password.", - authSession.identity_check.userid); - vars.logger.debug(req, "Challenge %s", authSession.identity_check.challenge); - - if (authSession.identity_check.challenge != Constants.CHALLENGE) { - reject(new Error("Bad challenge.")); - return; - } - resolve(); - }) - .then(function () { - return vars.usersDatabase.updatePassword(authSession.identity_check.userid, newPassword); - }) - .then(function () { - vars.logger.info(req, "Password reset for user '%s'", - authSession.identity_check.userid); - AuthenticationSessionHandler.reset(req); - res.status(204); - res.send(); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.RESET_PASSWORD_FAILED)); - }; -} diff --git a/themes/black/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts b/themes/black/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts deleted file mode 100644 index ac6a41754..000000000 --- a/themes/black/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts +++ /dev/null @@ -1,92 +0,0 @@ - -import PasswordResetHandler - from "./PasswordResetHandler"; -import { UserDataStore } from "../../../storage/UserDataStore"; -import Sinon = require("sinon"); -import winston = require("winston"); -import assert = require("assert"); -import BluebirdPromise = require("bluebird"); -import ExpressMock = require("../../../stubs/express.spec"); -import { ServerVariablesMock, ServerVariablesMockBuilder } - from "../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../ServerVariables"; - -describe("routes/password-reset/identity/PasswordResetHandler", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - req = { - originalUrl: "/non-api/xxx", - query: { - userid: "user" - }, - session: { - auth: { - userid: "user", - email: "user@example.com", - first_factor: true, - second_factor: false - } - }, - headers: { - host: "localhost" - } - }; - - const options = { - inMemoryOnly: true - }; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - mocks.userDataStore.saveU2FRegistrationStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.produceIdentityValidationTokenStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.consumeIdentityValidationTokenStub - .returns(BluebirdPromise.resolve({})); - res = ExpressMock.ResponseMock(); - }); - - describe("test reset password identity pre check", () => { - it("should fail when no userid is provided", function () { - req.query.userid = undefined; - const handler = new PasswordResetHandler(vars.logger, - vars.usersDatabase); - return handler.preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject("It should fail"); - }) - .catch(function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if ldap fail", function () { - mocks.usersDatabase.getEmailsStub - .returns(BluebirdPromise.reject("Internal error")); - new PasswordResetHandler(vars.logger, vars.usersDatabase) - .preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here")); - }, - function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should returns identity when ldap replies", function () { - mocks.usersDatabase.getEmailsStub - .returns(BluebirdPromise.resolve(["test@example.com"])); - return new PasswordResetHandler(vars.logger, vars.usersDatabase) - .preValidationInit(req as any); - }); - }); -}); diff --git a/themes/black/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts b/themes/black/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts deleted file mode 100644 index 42ae92cda..000000000 --- a/themes/black/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts +++ /dev/null @@ -1,69 +0,0 @@ -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import objectPath = require("object-path"); - -import exceptions = require("../../../Exceptions"); -import { Identity } from "../../../../../types/Identity"; -import { IdentityValidable } from "../../../IdentityValidable"; -import { PRE_VALIDATION_TEMPLATE } from "../../../IdentityCheckPreValidationTemplate"; -import Constants = require("../constants"); -import { IRequestLogger } from "../../../logging/IRequestLogger"; -import { IUsersDatabase } from "../../../authentication/backends/IUsersDatabase"; - -export const TEMPLATE_NAME = "password-reset-form"; - -export default class PasswordResetHandler implements IdentityValidable { - private logger: IRequestLogger; - private usersDatabase: IUsersDatabase; - - constructor(logger: IRequestLogger, usersDatabase: IUsersDatabase) { - this.logger = logger; - this.usersDatabase = usersDatabase; - } - - challenge(): string { - return Constants.CHALLENGE; - } - - preValidationInit(req: express.Request): BluebirdPromise { - const that = this; - const userid: string = - objectPath.get(req, "query.userid"); - return BluebirdPromise.resolve() - .then(function () { - that.logger.debug(req, "User '%s' requested a password reset", userid); - if (!userid) - return BluebirdPromise.reject( - new exceptions.AccessDeniedError("No user id provided")); - - return that.usersDatabase.getEmails(userid); - }) - .then(function (emails: string[]) { - if (!emails && emails.length <= 0) throw new Error("No email found"); - const identity = { - email: emails[0], - userid: userid - }; - return BluebirdPromise.resolve(identity); - }) - .catch(function (err: Error) { - return BluebirdPromise.reject(new exceptions.IdentityError(err.message)); - }); - } - - preValidationResponse(req: express.Request, res: express.Response) { - res.render(PRE_VALIDATION_TEMPLATE); - } - - postValidationInit(req: express.Request) { - return BluebirdPromise.resolve(); - } - - postValidationResponse(req: express.Request, res: express.Response) { - res.render(TEMPLATE_NAME); - } - - mailSubject(): string { - return "Reset your password"; - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/password-reset/request/get.ts b/themes/black/server/src/lib/routes/password-reset/request/get.ts deleted file mode 100644 index 8f3ae2b4b..000000000 --- a/themes/black/server/src/lib/routes/password-reset/request/get.ts +++ /dev/null @@ -1,13 +0,0 @@ - -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import objectPath = require("object-path"); -import exceptions = require("../../../Exceptions"); - -import Constants = require("./../constants"); - -const TEMPLATE_NAME = "password-reset-request"; - -export default function (req: express.Request, res: express.Response) { - res.render(TEMPLATE_NAME); -} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/secondfactor/get.spec.ts b/themes/black/server/src/lib/routes/secondfactor/get.spec.ts deleted file mode 100644 index 6c77e1f69..000000000 --- a/themes/black/server/src/lib/routes/secondfactor/get.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import SecondFactorGet from "./get"; -import { ServerVariablesMockBuilder, ServerVariablesMock } - from "../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../ServerVariables"; -import Sinon = require("sinon"); -import ExpressMock = require("../../stubs/express.spec"); -import Assert = require("assert"); -import Endpoints = require("../../../../../shared/api"); -import BluebirdPromise = require("bluebird"); - -describe("routes/secondfactor/get", function () { - let mocks: ServerVariablesMock; - let vars: ServerVariables; - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req = ExpressMock.RequestMock(); - res = ExpressMock.ResponseMock(); - - req.session = { - auth: { - userid: "user", - first_factor: true, - second_factor: false - } - }; - }); - - describe("test rendering", function () { - it("should render second factor page", function () { - req.session.auth.second_factor = false; - return SecondFactorGet(vars)(req as any, res as any) - .then(function () { - Assert(res.render.calledWith("secondfactor")); - return BluebirdPromise.resolve(); - }); - }); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/secondfactor/get.ts b/themes/black/server/src/lib/routes/secondfactor/get.ts deleted file mode 100644 index 9f6deb4c6..000000000 --- a/themes/black/server/src/lib/routes/secondfactor/get.ts +++ /dev/null @@ -1,28 +0,0 @@ - -import Express = require("express"); -import Endpoints = require("../../../../../shared/api"); -import BluebirdPromise = require("bluebird"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { ServerVariables } from "../../ServerVariables"; - -const TEMPLATE_NAME = "secondfactor"; - -export default function (vars: ServerVariables) { - function handler(req: Express.Request, res: Express.Response) - : BluebirdPromise { - - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, vars.logger); - - res.render(TEMPLATE_NAME, { - username: authSession.userid, - totp_identity_start_endpoint: - Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, - u2f_identity_start_endpoint: - Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET - }); - resolve(); - }); - } - return handler; -} diff --git a/themes/black/server/src/lib/routes/secondfactor/redirect.spec.ts b/themes/black/server/src/lib/routes/secondfactor/redirect.spec.ts deleted file mode 100644 index ea66e6dca..000000000 --- a/themes/black/server/src/lib/routes/secondfactor/redirect.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import Redirect from "./redirect"; -import ExpressMock = require("../../stubs/express.spec"); -import { ServerVariablesMockBuilder, ServerVariablesMock } -from "../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../ServerVariables"; -import Assert = require("assert"); - -describe("routes/secondfactor/redirect", function() { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req = ExpressMock.RequestMock(); - res = ExpressMock.ResponseMock(); - }); - - it("should redirect to default_redirection_url", function() { - vars.config.default_redirection_url = "http://default_redirection_url"; - Redirect(vars)(req as any, res as any) - .then(function() { - Assert(res.json.calledWith({ - redirect: "http://default_redirection_url" - })); - }); - }); - - it("should redirect to /", function() { - Redirect(vars)(req as any, res as any) - .then(function() { - Assert(res.json.calledWith({ - redirect: "/" - })); - }); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/secondfactor/redirect.ts b/themes/black/server/src/lib/routes/secondfactor/redirect.ts deleted file mode 100644 index 5d84d9ebf..000000000 --- a/themes/black/server/src/lib/routes/secondfactor/redirect.ts +++ /dev/null @@ -1,30 +0,0 @@ - -import express = require("express"); -import objectPath = require("object-path"); -import Endpoints = require("../../../../../shared/api"); -import { ServerVariables } from "../../ServerVariables"; -import BluebirdPromise = require("bluebird"); -import ErrorReplies = require("../../ErrorReplies"); -import UserMessages = require("../../../../../shared/UserMessages"); -import { RedirectionMessage } from "../../../../../shared/RedirectionMessage"; -import Constants = require("../../../../../shared/constants"); - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response) - : BluebirdPromise { - - return new BluebirdPromise(function (resolve, reject) { - let redirectUrl: string = "/"; - if (vars.config.default_redirection_url) { - redirectUrl = vars.config.default_redirection_url; - } - vars.logger.debug(req, "Request redirection to \"%s\".", redirectUrl); - res.json({ - redirect: redirectUrl - } as RedirectionMessage); - return resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - }; -} diff --git a/themes/black/server/src/lib/routes/secondfactor/totp/constants.ts b/themes/black/server/src/lib/routes/secondfactor/totp/constants.ts deleted file mode 100644 index 7b5a1dcfe..000000000 --- a/themes/black/server/src/lib/routes/secondfactor/totp/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ - -export const CHALLENGE = "totp-register"; -export const TEMPLATE_NAME = "totp-register"; - diff --git a/themes/black/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts b/themes/black/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts deleted file mode 100644 index 78b8ea3ea..000000000 --- a/themes/black/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts +++ /dev/null @@ -1,116 +0,0 @@ -import Sinon = require("sinon"); -import RegistrationHandler from "./RegistrationHandler"; -import { Identity } from "../../../../../../types/Identity"; -import { UserDataStore } from "../../../../storage/UserDataStore"; -import BluebirdPromise = require("bluebird"); -import ExpressMock = require("../../../../stubs/express.spec"); -import { ServerVariablesMock, ServerVariablesMockBuilder } - from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; -import Assert = require("assert"); - -describe("routes/secondfactor/totp/identity/RegistrationHandler", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req = ExpressMock.RequestMock(); - req.session = { - auth: { - userid: "user", - email: "user@example.com", - first_factor: true, - second_factor: false, - identity_check: { - userid: "user", - challenge: "totp-register" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.produceIdentityValidationTokenStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.consumeIdentityValidationTokenStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.saveTOTPSecretStub - .returns(BluebirdPromise.resolve({})); - - res = ExpressMock.ResponseMock(); - }); - - describe("test totp registration pre validation", function () { - it("should fail if first_factor has not been passed", function () { - req.session.auth.first_factor = false; - return new RegistrationHandler(vars.logger, vars.userDataStore, - vars.totpHandler, vars.config.totp) - .preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject(new Error("It should fail")); - }) - .catch(function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if userid is missing", function (done) { - req.session.auth.first_factor = false; - req.session.auth.userid = undefined; - - new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler, - vars.config.totp) - .preValidationInit(req as any) - .catch(function (err: Error) { - done(); - }); - }); - - it("should fail if email is missing", function (done) { - req.session.auth.first_factor = false; - req.session.auth.email = undefined; - - new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler, - vars.config.totp) - .preValidationInit(req as any) - .catch(function (err: Error) { - done(); - }); - }); - - it("should succeed if first factor passed, userid and email are provided", - function () { - return new RegistrationHandler(vars.logger, vars.userDataStore, - vars.totpHandler, vars.config.totp) - .preValidationInit(req as any); - }); - }); - - describe("test totp registration post validation", function () { - it("should generate a secret using userId as label and issuer defined in config", function () { - vars.config.totp = { - issuer: "issuer" - }; - return new RegistrationHandler(vars.logger, vars.userDataStore, - vars.totpHandler, vars.config.totp) - .postValidationResponse(req as any, res as any) - .then(function() { - Assert(mocks.totpHandler.generateStub.calledWithExactly("user", "issuer")); - }); - }); - }); -}); diff --git a/themes/black/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts b/themes/black/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts deleted file mode 100644 index b39b6d045..000000000 --- a/themes/black/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts +++ /dev/null @@ -1,112 +0,0 @@ - -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import objectPath = require("object-path"); - -import { Identity } from "../../../../../../types/Identity"; -import { IdentityValidable } from "../../../../IdentityValidable"; -import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; -import Constants = require("../constants"); -import Endpoints = require("../../../../../../../shared/api"); -import ErrorReplies = require("../../../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import FirstFactorValidator = require("../../../../FirstFactorValidator"); -import { IRequestLogger } from "../../../../logging/IRequestLogger"; -import { IUserDataStore } from "../../../../storage/IUserDataStore"; -import { ITotpHandler } from "../../../../authentication/totp/ITotpHandler"; -import { TOTPSecret } from "../../../../../../types/TOTPSecret"; -import { TotpConfiguration } from "../../../../configuration/schema/TotpConfiguration"; - - -export default class RegistrationHandler implements IdentityValidable { - private logger: IRequestLogger; - private userDataStore: IUserDataStore; - private totp: ITotpHandler; - private configuration: TotpConfiguration; - - constructor(logger: IRequestLogger, - userDataStore: IUserDataStore, - totp: ITotpHandler, configuration: TotpConfiguration) { - this.logger = logger; - this.userDataStore = userDataStore; - this.totp = totp; - this.configuration = configuration; - } - - challenge(): string { - return Constants.CHALLENGE; - } - - private retrieveIdentity(req: express.Request): BluebirdPromise { - const that = this; - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, that.logger); - const userid = authSession.userid; - const email = authSession.email; - - if (!(userid && email)) { - return reject(new Error("User ID or email is missing")); - } - - const identity = { - email: email, - userid: userid - }; - return resolve(identity); - }); - } - - preValidationInit(req: express.Request): BluebirdPromise { - const that = this; - return FirstFactorValidator.validate(req, this.logger) - .then(function () { - return that.retrieveIdentity(req); - }); - } - - preValidationResponse(req: express.Request, res: express.Response) { - res.render(PRE_VALIDATION_TEMPLATE); - } - - postValidationInit(req: express.Request) { - return FirstFactorValidator.validate(req, this.logger); - } - - postValidationResponse(req: express.Request, res: express.Response) - : BluebirdPromise { - const that = this; - let secret: TOTPSecret; - let userId: string; - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, that.logger); - userId = authSession.userid; - - if (authSession.identity_check.challenge != Constants.CHALLENGE - || !userId) - return reject(new Error("Bad challenge.")); - - resolve(); - }) - .then(function () { - secret = that.totp.generate(userId, - that.configuration.issuer); - that.logger.debug(req, "Save the TOTP secret in DB"); - return that.userDataStore.saveTOTPSecret(userId, secret); - }) - .then(function () { - AuthenticationSessionHandler.reset(req); - - res.render(Constants.TEMPLATE_NAME, { - base32_secret: secret.base32, - otpauth_url: secret.otpauth_url, - login_endpoint: Endpoints.FIRST_FACTOR_GET - }); - }) - .catch(ErrorReplies.replyWithError200(req, res, that.logger, UserMessages.OPERATION_FAILED)); - } - - mailSubject(): string { - return "Set up Authelia's one-time password"; - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts b/themes/black/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts deleted file mode 100644 index 70a20d39d..000000000 --- a/themes/black/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import Sinon = require("sinon"); -import Assert = require("assert"); -import Exceptions = require("../../../../Exceptions"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import SignPost = require("./post"); -import { ServerVariables } from "../../../../ServerVariables"; - -import ExpressMock = require("../../../../stubs/express.spec"); -import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; -import { Level } from "../../../../authentication/Level"; - -describe("routes/secondfactor/totp/sign/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let authSession: AuthenticationSession; - let vars: ServerVariables; - let mocks: ServerVariablesMock; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - vars = s.variables; - mocks = s.mocks; - const app_get = Sinon.stub(); - req = { - originalUrl: "/api/totp-register", - app: {}, - body: { - token: "abc" - }, - session: {}, - query: { - redirect: "http://redirect" - } - }; - res = ExpressMock.ResponseMock(); - - const doc = { - userid: "user", - secret: { - base32: "ABCDEF" - } - }; - mocks.userDataStore.retrieveTOTPSecretStub.returns(BluebirdPromise.resolve(doc)); - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - authSession.userid = "user"; - authSession.authentication_level = Level.ONE_FACTOR; - }); - - - it("should send status code 200 when totp is valid", function () { - mocks.totpHandler.validateStub.returns(true); - return SignPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(authSession.authentication_level, Level.TWO_FACTOR); - return BluebirdPromise.resolve(); - }); - }); - - it("should send error message when totp is not valid", function () { - mocks.totpHandler.validateStub.returns(false); - return SignPost.default(vars)(req as any, res as any) - .then(function () { - Assert.notEqual(authSession.authentication_level, Level.TWO_FACTOR); - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); -}); - diff --git a/themes/black/server/src/lib/routes/secondfactor/totp/sign/post.ts b/themes/black/server/src/lib/routes/secondfactor/totp/sign/post.ts deleted file mode 100644 index 34a276d12..000000000 --- a/themes/black/server/src/lib/routes/secondfactor/totp/sign/post.ts +++ /dev/null @@ -1,42 +0,0 @@ -import Bluebird = require("bluebird"); -import Express = require("express"); - -import { TOTPSecretDocument } from "../../../../storage/TOTPSecretDocument"; -import Endpoints = require("../../../../../../../shared/api"); -import Redirect from "../../redirect"; -import ErrorReplies = require("../../../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { ServerVariables } from "../../../../ServerVariables"; -import { Level } from "../../../../authentication/Level"; - -const UNAUTHORIZED_MESSAGE = "Unauthorized access"; - -export default function (vars: ServerVariables) { - function handler(req: Express.Request, res: Express.Response): Bluebird { - let authSession: AuthenticationSession; - const token = req.body.token; - - return new Bluebird(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - vars.logger.info(req, "Initiate TOTP validation for user \"%s\".", authSession.userid); - resolve(); - }) - .then(function () { - return vars.userDataStore.retrieveTOTPSecret(authSession.userid); - }) - .then(function (doc: TOTPSecretDocument) { - if (!vars.totpHandler.validate(token, doc.secret.base32)) - return Bluebird.reject(new Error("Invalid TOTP token.")); - - vars.logger.debug(req, "TOTP validation succeeded."); - authSession.authentication_level = Level.TWO_FACTOR; - Redirect(vars)(req, res); - return Bluebird.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - return handler; -} diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts deleted file mode 100644 index 7f16c0ee8..000000000 --- a/themes/black/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts +++ /dev/null @@ -1,11 +0,0 @@ - -import util = require("util"); -import express = require("express"); - -function extract_app_id(req: express.Request): string { - return util.format("https://%s", req.headers.host); -} - -export = { - extract_app_id: extract_app_id -}; \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts deleted file mode 100644 index a54bfbfe6..000000000 --- a/themes/black/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts +++ /dev/null @@ -1,96 +0,0 @@ -import Sinon = require("sinon"); -import Assert = require("assert"); -import BluebirdPromise = require("bluebird"); - -import { Identity } from "../../../../../../types/Identity"; -import RegistrationHandler from "./RegistrationHandler"; -import ExpressMock = require("../../../../stubs/express.spec"); -import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; - -describe("routes/secondfactor/u2f/identity/RegistrationHandler", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req = ExpressMock.RequestMock(); - req.app = {}; - req.session = { - auth: { - userid: "user", - email: "user@example.com", - first_factor: true, - second_factor: false - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - - res = ExpressMock.ResponseMock(); - res.send = Sinon.spy(); - res.json = Sinon.spy(); - res.status = Sinon.spy(); - }); - - describe("test u2f registration check", test_registration_check); - - function test_registration_check() { - it("should fail if first_factor has not been passed", function () { - req.session.auth.first_factor = false; - return new RegistrationHandler(vars.logger).preValidationInit(req as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if userid is missing", function () { - req.session.auth.first_factor = false; - req.session.auth.userid = undefined; - - return new RegistrationHandler(vars.logger).preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here")); - }, - function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if email is missing", function () { - req.session.auth.first_factor = false; - req.session.auth.email = undefined; - - return new RegistrationHandler(vars.logger).preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here")); - }, - function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should succeed if first factor passed, userid and email are provided", function () { - req.session.auth.first_factor = true; - req.session.auth.email = "admin@example.com"; - req.session.auth.userid = "user"; - return new RegistrationHandler(vars.logger).preValidationInit(req as any); - }); - } -}); diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts deleted file mode 100644 index bc4713c77..000000000 --- a/themes/black/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts +++ /dev/null @@ -1,73 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import objectPath = require("object-path"); - -import { IdentityValidable } from "../../../../IdentityValidable"; -import { Identity } from "../../../../../../types/Identity"; -import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; -import FirstFactorValidator = require("../../../../FirstFactorValidator"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { IRequestLogger } from "../../../../logging/IRequestLogger"; - -const CHALLENGE = "u2f-register"; -const MAIL_SUBJECT = "Register your security key with Authelia"; - -const POST_VALIDATION_TEMPLATE_NAME = "u2f-register"; - - -export default class RegistrationHandler implements IdentityValidable { - private logger: IRequestLogger; - - constructor(logger: IRequestLogger) { - this.logger = logger; - } - - challenge(): string { - return CHALLENGE; - } - - private retrieveIdentity(req: express.Request): BluebirdPromise { - const that = this; - return new BluebirdPromise(function(resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, that.logger); - const userid = authSession.userid; - const email = authSession.email; - - if (!(userid && email)) { - return reject(new Error("User ID or email is missing")); - } - - const identity = { - email: email, - userid: userid - }; - return resolve(identity); - }); - } - - preValidationInit(req: express.Request): BluebirdPromise { - const that = this; - return FirstFactorValidator.validate(req, this.logger) - .then(function () { - return that.retrieveIdentity(req); - }); - } - - preValidationResponse(req: express.Request, res: express.Response) { - res.render(PRE_VALIDATION_TEMPLATE); - } - - postValidationInit(req: express.Request) { - return FirstFactorValidator.validate(req, this.logger); - } - - postValidationResponse(req: express.Request, res: express.Response) { - res.render(POST_VALIDATION_TEMPLATE_NAME); - } - - mailSubject(): string { - return MAIL_SUBJECT; - } -} - diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts deleted file mode 100644 index de3347a21..000000000 --- a/themes/black/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts +++ /dev/null @@ -1,146 +0,0 @@ - -import sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import assert = require("assert"); -import U2FRegisterPost = require("./post"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import ExpressMock = require("../../../../stubs/express.spec"); -import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; - - -describe("routes/secondfactor/u2f/register/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - let authSession: AuthenticationSession; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.originalUrl = "/api/xxxx"; - req.app = {}; - req.session = { - auth: { - userid: "user", - first_factor: true, - second_factor: false, - identity_check: { - challenge: "u2f-register", - userid: "user" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - }); - - describe("test registration", test_registration); - - - function test_registration() { - it("should save u2f meta and return status code 200", function () { - const expectedStatus = { - keyHandle: "keyHandle", - publicKey: "pbk", - certificate: "cert" - }; - mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve(expectedStatus)); - - authSession.register_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { - assert.equal("user", mocks.userDataStore.saveU2FRegistrationStub.getCall(0).args[0]); - assert.equal(authSession.identity_check, undefined); - }); - }); - - it("should return error message on finishRegistration error", function () { - mocks.u2f.checkRegistrationStub.returns({ errorCode: 500 }); - - authSession.register_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); - - it("should return error message when register_request is not provided", function () { - mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); - authSession.register_request = undefined; - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); - - it("should return error message when no auth request has been initiated", function () { - mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); - authSession.register_request = undefined; - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); - - it("should return error message when identity has not been verified", function () { - authSession.identity_check = undefined; - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); - } -}); - diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/register/post.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/register/post.ts deleted file mode 100644 index 7296ccbe5..000000000 --- a/themes/black/server/src/lib/routes/secondfactor/u2f/register/post.ts +++ /dev/null @@ -1,64 +0,0 @@ - -import { UserDataStore } from "../../../../storage/UserDataStore"; -import objectPath = require("object-path"); -import u2f_common = require("../U2FCommon"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import U2f = require("u2f"); -import { U2FRegistration } from "../../../../../../types/U2FRegistration"; -import redirect from "../../redirect"; -import ErrorReplies = require("../../../../ErrorReplies"); -import { ServerVariables } from "../../../../ServerVariables"; -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; - - -export default function (vars: ServerVariables) { - function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const appid = u2f_common.extract_app_id(req); - const registrationResponse: U2f.RegistrationData = req.body; - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - const registrationRequest = authSession.register_request; - - if (!registrationRequest) { - return reject(new Error("No registration request")); - } - - if (!authSession.identity_check - || authSession.identity_check.challenge != "u2f-register") { - return reject(new Error("Bad challenge for registration request")); - } - - vars.logger.info(req, "Finishing registration"); - vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); - vars.logger.debug(req, "RegistrationResponse = %s", JSON.stringify(registrationResponse)); - - return resolve(vars.u2f.checkRegistration(registrationRequest, registrationResponse)); - }) - .then(function (u2fResult: U2f.RegistrationResult | U2f.Error): BluebirdPromise { - if (objectPath.has(u2fResult, "errorCode")) - return BluebirdPromise.reject(new Error("Error while registering.")); - - const registrationResult: U2f.RegistrationResult = u2fResult as U2f.RegistrationResult; - vars.logger.info(req, "Store registration and reply"); - vars.logger.debug(req, "RegistrationResult = %s", JSON.stringify(registrationResult)); - const registration: U2FRegistration = { - keyHandle: registrationResult.keyHandle, - publicKey: registrationResult.publicKey - }; - return vars.userDataStore.saveU2FRegistration(authSession.userid, appid, registration); - }) - .then(function () { - authSession.identity_check = undefined; - redirect(vars)(req, res); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - return handler; -} diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts deleted file mode 100644 index a207c9109..000000000 --- a/themes/black/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ - -import sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import Assert = require("assert"); -import U2FRegisterRequestGet = require("./get"); -import ExpressMock = require("../../../../stubs/express.spec"); -import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; - -describe("routes/secondfactor/u2f/register_request/get", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.originalUrl = "/api/xxxx"; - req.app = {}; - req.session = { - auth: { - userid: "user", - first_factor: true, - second_factor: false, - identity_check: { - challenge: "u2f-register", - userid: "user" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - }); - - describe("test registration request", () => { - it("should send back the registration request and save it in the session", function () { - const expectedRequest = { - test: "abc" - }; - mocks.u2f.requestStub.returns(BluebirdPromise.resolve(expectedRequest)); - return U2FRegisterRequestGet.default(vars)(req as any, res as any) - .then(function () { - Assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); - }); - }); - - it("should return internal error on registration request", function () { - res.send = sinon.spy(); - const user_key_container = {}; - mocks.u2f.requestStub.returns(BluebirdPromise.reject("Internal error")); - return U2FRegisterRequestGet.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - }); - }); - - it("should return forbidden if identity has not been verified", function () { - req.session.auth.identity_check = undefined; - return U2FRegisterRequestGet.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(403, res.status.getCall(0).args[0]); - }); - }); - }); -}); - diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/register_request/get.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/register_request/get.ts deleted file mode 100644 index f611af933..000000000 --- a/themes/black/server/src/lib/routes/secondfactor/u2f/register_request/get.ts +++ /dev/null @@ -1,43 +0,0 @@ - -import { UserDataStore } from "../../../../storage/UserDataStore"; - -import objectPath = require("object-path"); -import u2f_common = require("../U2FCommon"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import U2f = require("u2f"); -import ErrorReplies = require("../../../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { ServerVariables } from "../../../../ServerVariables"; - -export default function (vars: ServerVariables) { - function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const appid: string = u2f_common.extract_app_id(req); - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - if (!authSession.identity_check - || authSession.identity_check.challenge != "u2f-register") { - res.status(403); - res.send(); - return reject(new Error("Bad challenge.")); - } - - vars.logger.info(req, "Starting registration for appId '%s'", appid); - return resolve(vars.u2f.request(appid)); - }) - .then(function (registrationRequest: U2f.Request) { - vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); - authSession.register_request = registrationRequest; - res.json(registrationRequest); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - - return handler; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts deleted file mode 100644 index 9b137e66d..000000000 --- a/themes/black/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts +++ /dev/null @@ -1,101 +0,0 @@ - -import sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import Assert = require("assert"); -import U2FSignPost = require("./post"); -import { ServerVariables } from "../../../../ServerVariables"; -import winston = require("winston"); - -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; -import ExpressMock = require("../../../../stubs/express.spec"); -import U2FMock = require("../../../../stubs/u2f.spec"); -import U2f = require("u2f"); -import { Level } from "../../../../authentication/Level"; - -describe("routes/secondfactor/u2f/sign/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.app = {}; - req.originalUrl = "/api/xxxx"; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req.session = { - auth: { - userid: "user", - authentication_level: Level.ONE_FACTOR, - identity_check: { - challenge: "u2f-register", - userid: "user" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const options = { - inMemoryOnly: true - }; - - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - }); - - it("should return status code 204", function () { - const expectedStatus = { - keyHandle: "keyHandle", - publicKey: "pbk", - certificate: "cert" - }; - mocks.u2f.checkSignatureStub.returns(expectedStatus); - - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ - registration: { - publicKey: "PUBKEY" - } - })); - - req.session.auth.sign_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - return U2FSignPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(req.session.auth.authentication_level, Level.TWO_FACTOR); - }); - }); - - it("should return unauthorized error on registration request internal error", function () { - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ - registration: { - publicKey: "PUBKEY" - } - })); - mocks.u2f.checkSignatureStub.returns({ errorCode: 500 }); - - req.session.auth.sign_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - return U2FSignPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], - { error: "Operation failed." }); - }); - }); -}); - diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/sign/post.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/sign/post.ts deleted file mode 100644 index 7ee711c2c..000000000 --- a/themes/black/server/src/lib/routes/secondfactor/u2f/sign/post.ts +++ /dev/null @@ -1,57 +0,0 @@ - -import objectPath = require("object-path"); -import u2f_common = require("../U2FCommon"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import { UserDataStore } from "../../../../storage/UserDataStore"; -import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; -import { Winston } from "../../../../../../types/Dependencies"; -import U2f = require("u2f"); -import exceptions = require("../../../../Exceptions"); -import redirect from "../../redirect"; -import ErrorReplies = require("../../../../ErrorReplies"); -import { ServerVariables } from "../../../../ServerVariables"; -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import { Level } from "../../../../authentication/Level"; - -export default function (vars: ServerVariables) { - function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const appId = u2f_common.extract_app_id(req); - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - if (!authSession.sign_request) { - const err = new Error("No sign request"); - ErrorReplies.replyWithError401(req, res, vars.logger)(err); - return reject(err); - } - resolve(); - }) - .then(function () { - const userid = authSession.userid; - return vars.userDataStore.retrieveU2FRegistration(userid, appId); - }) - .then(function (doc: U2FRegistrationDocument): BluebirdPromise { - const signRequest = authSession.sign_request; - const signData: U2f.SignatureData = req.body; - vars.logger.info(req, "Finish authentication"); - return BluebirdPromise.resolve(vars.u2f.checkSignature(signRequest, signData, doc.registration.publicKey)); - }) - .then(function (result: U2f.SignatureResult | U2f.Error): BluebirdPromise { - if (objectPath.has(result, "errorCode")) - return BluebirdPromise.reject(new Error("Error while signing")); - vars.logger.info(req, "Successful authentication"); - authSession.authentication_level = Level.TWO_FACTOR; - redirect(vars)(req, res); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - - return handler; -} - diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts deleted file mode 100644 index dd52b27e0..000000000 --- a/themes/black/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ - -import sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import assert = require("assert"); -import U2FSignRequestGet = require("./get"); -import ExpressMock = require("../../../../stubs/express.spec"); -import { Request } from "u2f"; -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; - -import { SignMessage } from "../../../../../../../shared/SignMessage"; - -describe("routes/secondfactor/u2f/sign_request/get", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.originalUrl = "/api/xxxx"; - req.app = {}; - req.session = { - auth: { - userid: "user", - first_factor: true, - second_factor: false, - identity_check: { - challenge: "u2f-register", - userid: "user" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - }); - - it("should send back the sign request and save it in the session", function () { - const expectedRequest: Request = { - version: "U2F_V2", - appId: 'app', - challenge: 'challenge!' - }; - mocks.u2f.requestStub.returns(expectedRequest); - mocks.userDataStore.retrieveU2FRegistrationStub - .returns(BluebirdPromise.resolve({ - registration: { - keyHandle: "KeyHandle" - } - })); - - return U2FSignRequestGet.default(vars)(req as any, res as any) - .then(() => { - assert.deepEqual(expectedRequest, req.session.auth.sign_request); - assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); - }); - }); -}); - diff --git a/themes/black/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts b/themes/black/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts deleted file mode 100644 index 9e93dde06..000000000 --- a/themes/black/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts +++ /dev/null @@ -1,42 +0,0 @@ - -import u2f_common = require("../../../secondfactor/u2f/U2FCommon"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; -import exceptions = require("../../../../Exceptions"); -import ErrorReplies = require("../../../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { ServerVariables } from "../../../../ServerVariables"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; - -export default function (vars: ServerVariables) { - function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const appId = u2f_common.extract_app_id(req); - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - resolve(); - }) - .then(function () { - return vars.userDataStore.retrieveU2FRegistration(authSession.userid, appId); - }) - .then(function (doc: U2FRegistrationDocument): BluebirdPromise { - if (!doc) - return BluebirdPromise.reject(new exceptions.AccessDeniedError("No U2F registration document found.")); - - const appId: string = u2f_common.extract_app_id(req); - vars.logger.info(req, "Start authentication of app '%s'", appId); - vars.logger.debug(req, "AppId = %s, keyHandle = %s", appId, JSON.stringify(doc.registration.keyHandle)); - - const request = vars.u2f.request(appId, doc.registration.keyHandle); - authSession.sign_request = request; - res.json(request); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - return handler; -} diff --git a/themes/black/server/src/lib/routes/verify/access_control.ts b/themes/black/server/src/lib/routes/verify/access_control.ts deleted file mode 100644 index 136239aeb..000000000 --- a/themes/black/server/src/lib/routes/verify/access_control.ts +++ /dev/null @@ -1,51 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import Util = require("util"); - -import Exceptions = require("../../Exceptions"); - -import { Level as AuthorizationLevel } from "../../authorization/Level"; -import { Level as AuthenticationLevel } from "../../authentication/Level"; -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { ServerVariables } from "../../ServerVariables"; - -function isAuthorized( - authorization: AuthorizationLevel, - authentication: AuthenticationLevel): boolean { - - if (authorization == AuthorizationLevel.BYPASS) { - return true; - } else if (authorization == AuthorizationLevel.ONE_FACTOR && - authentication >= AuthenticationLevel.ONE_FACTOR) { - return true; - } else if (authorization == AuthorizationLevel.TWO_FACTOR && - authentication >= AuthenticationLevel.TWO_FACTOR) { - return true; - } - return false; -} - -export default function ( - req: Express.Request, - vars: ServerVariables, - domain: string, resource: string, - user: string, groups: string[], - authenticationLevel: AuthenticationLevel) { - - return new BluebirdPromise(function (resolve, reject) { - const authorizationLevel = vars.authorizer - .authorization({domain, resource}, {user, groups}); - - if (!isAuthorized(authorizationLevel, authenticationLevel)) { - if (authorizationLevel == AuthorizationLevel.DENY) { - reject(new Exceptions.NotAuthorizedError( - Util.format("User %s is not authorized to access %s%s", user, domain, resource))); - return; - } - reject(new Exceptions.NotAuthenticatedError(Util.format( - "User '%s' is not sufficiently authorized to access %s%s.", user, domain, resource))); - return; - } - resolve(); - }); -} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/verify/get.spec.ts b/themes/black/server/src/lib/routes/verify/get.spec.ts deleted file mode 100644 index 67cf19fb6..000000000 --- a/themes/black/server/src/lib/routes/verify/get.spec.ts +++ /dev/null @@ -1,320 +0,0 @@ - -import Assert = require("assert"); -import BluebirdPromise = require("bluebird"); -import Express = require("express"); -import Sinon = require("sinon"); -import winston = require("winston"); - -import VerifyGet = require("./get"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../types/AuthenticationSession"; -import ExpressMock = require("../../stubs/express.spec"); -import { ServerVariables } from "../../ServerVariables"; -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../ServerVariablesMockBuilder.spec"; -import { Level } from "../../authentication/Level"; -import { Level as AuthorizationLevel } from "../../authorization/Level"; - -describe("routes/verify/get", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - let authSession: AuthenticationSession; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - res = ExpressMock.ResponseMock(); - req.originalUrl = "/api/xxxx"; - req.query = { - redirect: "undefined" - }; - AuthenticationSessionHandler.reset(req as any); - req.headers["x-original-url"] = "https://secret.example.com/"; - const s = ServerVariablesMockBuilder.build(false); - mocks = s.mocks; - vars = s.variables; - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - }); - - describe("with session cookie", function () { - it("should be already authenticated", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser"); - Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); - Assert.equal(204, res.status.getCall(0).args[0]); - }); - }); - - function test_session(_authSession: AuthenticationSession, status_code: number) { - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert.equal(status_code, res.status.getCall(0).args[0]); - }); - } - - function test_non_authenticated_401(authSession: AuthenticationSession) { - return test_session(authSession, 401); - } - - function test_unauthorized_403(authSession: AuthenticationSession) { - return test_session(authSession, 403); - } - - function test_authorized(authSession: AuthenticationSession) { - return test_session(authSession, 204); - } - - describe("given user tries to access a 2-factor endpoint", function () { - before(function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - }); - - describe("given different cases of session", function () { - it("should not be authenticated when second factor is missing", function () { - return test_non_authenticated_401({ - keep_me_logged_in: false, - userid: "user", - authentication_level: Level.ONE_FACTOR, - email: undefined, - groups: [], - last_activity_datetime: new Date().getTime() - }); - }); - - it("should not be authenticated when userid is missing", function () { - return test_non_authenticated_401({ - keep_me_logged_in: false, - userid: undefined, - authentication_level: Level.TWO_FACTOR, - email: undefined, - groups: [], - last_activity_datetime: new Date().getTime() - }); - }); - - it("should not be authenticated when level is insufficient", function () { - return test_non_authenticated_401({ - keep_me_logged_in: false, - userid: "user", - authentication_level: Level.NOT_AUTHENTICATED, - email: undefined, - groups: [], - last_activity_datetime: new Date().getTime() - }); - }); - - it("should not be authenticated when session has not be initiated", function () { - return test_non_authenticated_401(undefined); - }); - - it("should not be authenticated when domain is not allowed for user", function () { - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - req.headers["x-original-url"] = "https://test.example.com/"; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.DENY); - - return test_unauthorized_403({ - keep_me_logged_in: false, - authentication_level: Level.TWO_FACTOR, - userid: "user", - groups: ["group1", "group2"], - email: undefined, - last_activity_datetime: new Date().getTime() - }); - }); - }); - }); - - describe("given user tries to access a single factor endpoint", function () { - beforeEach(function () { - req.headers["x-original-url"] = "https://redirect.url/"; - }); - - it("should be authenticated when first factor is validated", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); - authSession.authentication_level = Level.ONE_FACTOR; - authSession.userid = "user1"; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWith(204)); - Assert(res.send.calledOnce); - }); - }); - - it("should be rejected with 401 when not authenticated", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); - authSession.authentication_level = Level.NOT_AUTHENTICATED; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWith(401)); - }); - }); - }); - - describe("inactivity period", function () { - it("should update last inactivity period on requests on /api/verify", function () { - mocks.config.session.inactivity = 200000; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - const currentTime = new Date().getTime() - 1000; - AuthenticationSessionHandler.reset(req as any); - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - authSession.last_activity_datetime = currentTime; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - return AuthenticationSessionHandler.get(req as any, vars.logger); - }) - .then(function (authSession) { - Assert(authSession.last_activity_datetime > currentTime); - }); - }); - - it("should reset session when max inactivity period has been reached", function () { - mocks.config.session.inactivity = 1; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - const currentTime = new Date().getTime() - 1000; - AuthenticationSessionHandler.reset(req as any); - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - authSession.last_activity_datetime = currentTime; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - return AuthenticationSessionHandler.get(req as any, vars.logger); - }) - .then(function (authSession) { - Assert.equal(authSession.authentication_level, Level.NOT_AUTHENTICATED); - Assert.equal(authSession.userid, undefined); - }); - }); - }); - }); - - describe("response type 401 | 302", function() { - it("should return error code 401", function() { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( - "Invalid credentials")); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should redirect to provided redirection url", function() { - const REDIRECT_URL = "http://redirection_url.com"; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( - "Invalid credentials")); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - req.query["rd"] = REDIRECT_URL; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.redirect.calledWithExactly(REDIRECT_URL)); - }); - }); - }); - - describe("with basic auth", function () { - it("should authenticate correctly", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.returns({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "john"); - Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); - Assert.equal(204, res.status.getCall(0).args[0]); - }); - }); - - it("should fail when endpoint is protected by two factors", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.config.access_control.rules = [{ - domain: "secret.example.com", - policy: "two_factor" - }]; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when base64 token is not valid", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic i_m*not_a_base64*token"; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when base64 token has not format user:psswd", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzOmJhZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when bad user password is provided", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( - "Invalid credentials")); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when resource is restricted", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - }); -}); - diff --git a/themes/black/server/src/lib/routes/verify/get.ts b/themes/black/server/src/lib/routes/verify/get.ts deleted file mode 100644 index f73861696..000000000 --- a/themes/black/server/src/lib/routes/verify/get.ts +++ /dev/null @@ -1,91 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Express = require("express"); -import Exceptions = require("../../Exceptions"); -import ErrorReplies = require("../../ErrorReplies"); -import { ServerVariables } from "../../ServerVariables"; -import GetWithSessionCookieMethod from "./get_session_cookie"; -import GetWithBasicAuthMethod from "./get_basic_auth"; -import Constants = require("../../../../../shared/constants"); -import ObjectPath = require("object-path"); - -import { AuthenticationSessionHandler } - from "../../AuthenticationSessionHandler"; -import { AuthenticationSession } - from "../../../../types/AuthenticationSession"; - -const REMOTE_USER = "Remote-User"; -const REMOTE_GROUPS = "Remote-Groups"; - - -function verifyWithSelectedMethod(req: Express.Request, res: Express.Response, - vars: ServerVariables, authSession: AuthenticationSession) - : () => BluebirdPromise<{ username: string, groups: string[] }> { - return function () { - const authorization: string = "" + req.headers["proxy-authorization"]; - if (authorization && authorization.startsWith("Basic ")) - return GetWithBasicAuthMethod(req, res, vars, authorization); - - return GetWithSessionCookieMethod(req, res, vars, authSession); - }; -} - -function setRedirectHeader(req: Express.Request, res: Express.Response) { - return function () { - const originalUrl = ObjectPath.get( - req, "headers.x-original-url"); - res.set("Redirect", originalUrl); - return BluebirdPromise.resolve(); - }; -} - -function setUserAndGroupsHeaders(res: Express.Response) { - return function (u: { username: string, groups: string[] }) { - res.setHeader(REMOTE_USER, u.username); - res.setHeader(REMOTE_GROUPS, u.groups.join(",")); - return BluebirdPromise.resolve(); - }; -} - -function replyWith200(res: Express.Response) { - return function () { - res.status(204); - res.send(); - }; -} - -function getRedirectParam(req: Express.Request) { - return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : undefined; -} - -export default function (vars: ServerVariables) { - return function (req: Express.Request, res: Express.Response) - : BluebirdPromise { - let authSession: AuthenticationSession; - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - resolve(); - }) - .then(setRedirectHeader(req, res)) - .then(verifyWithSelectedMethod(req, res, vars, authSession)) - .then(setUserAndGroupsHeaders(res)) - .then(replyWith200(res)) - // The user is authenticated but has restricted access -> 403 - .catch(Exceptions.NotAuthorizedError, - ErrorReplies.replyWithError403(req, res, vars.logger)) - .catch(Exceptions.NotAuthenticatedError, - ErrorReplies.replyWithError401(req, res, vars.logger)) - // The user is not yet authenticated -> 401 - .catch((err) => { - const redirectUrl = getRedirectParam(req); - if (redirectUrl) { - ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err); - } - else { - ErrorReplies.replyWithError401(req, res, vars.logger)(err); - } - }); - }; -} - diff --git a/themes/black/server/src/lib/routes/verify/get_basic_auth.ts b/themes/black/server/src/lib/routes/verify/get_basic_auth.ts deleted file mode 100644 index af23c76c9..000000000 --- a/themes/black/server/src/lib/routes/verify/get_basic_auth.ts +++ /dev/null @@ -1,55 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import ObjectPath = require("object-path"); -import { ServerVariables } from "../../ServerVariables"; -import { AuthenticationSession } - from "../../../../types/AuthenticationSession"; -import AccessControl from "./access_control"; -import { URLDecomposer } from "../../utils/URLDecomposer"; -import { Level } from "../../authentication/Level"; - -export default function (req: Express.Request, res: Express.Response, - vars: ServerVariables, authorizationHeader: string) - : BluebirdPromise<{ username: string, groups: string[] }> { - let username: string; - const uri = ObjectPath.get(req, "headers.x-original-url"); - const urlDecomposition = URLDecomposer.fromUrl(uri); - - return BluebirdPromise.resolve() - .then(() => { - const base64Re = new RegExp("^Basic ((?:[A-Za-z0-9+/]{4})*" + - "(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)$"); - const isTokenValidBase64 = base64Re.test(authorizationHeader); - - if (!isTokenValidBase64) { - return BluebirdPromise.reject(new Error("No valid base64 token found in the header")); - } - - const tokenMatches = authorizationHeader.match(base64Re); - const base64Token = tokenMatches[1]; - const decodedToken = Buffer.from(base64Token, "base64").toString(); - const splittedToken = decodedToken.split(":"); - - if (splittedToken.length != 2) { - return BluebirdPromise.reject(new Error( - "The authorization token is invalid. Expecting 'userid:password'")); - } - - username = splittedToken[0]; - const password = splittedToken[1]; - return vars.usersDatabase.checkUserPassword(username, password); - }) - .then(function (groupsAndEmails) { - return AccessControl(req, vars, urlDecomposition.domain, urlDecomposition.path, - username, groupsAndEmails.groups, Level.ONE_FACTOR) - .then(() => BluebirdPromise.resolve({ - username: username, - groups: groupsAndEmails.groups - })); - }) - .catch(function (err: Error) { - return BluebirdPromise.reject( - new Error("Unable to authenticate the user with basic auth. Cause: " - + err.message)); - }); -} \ No newline at end of file diff --git a/themes/black/server/src/lib/routes/verify/get_session_cookie.ts b/themes/black/server/src/lib/routes/verify/get_session_cookie.ts deleted file mode 100644 index 070344812..000000000 --- a/themes/black/server/src/lib/routes/verify/get_session_cookie.ts +++ /dev/null @@ -1,78 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import Util = require("util"); -import ObjectPath = require("object-path"); - -import Exceptions = require("../../Exceptions"); -import { Configuration } from "../../configuration/schema/Configuration"; -import { ServerVariables } from "../../ServerVariables"; -import { IRequestLogger } from "../../logging/IRequestLogger"; -import { AuthenticationSession } - from "../../../../types/AuthenticationSession"; -import { AuthenticationSessionHandler } - from "../../AuthenticationSessionHandler"; -import AccessControl from "./access_control"; -import { URLDecomposer } from "../../utils/URLDecomposer"; - -function verify_inactivity(req: Express.Request, - authSession: AuthenticationSession, - configuration: Configuration, logger: IRequestLogger) - : BluebirdPromise { - - // If inactivity is not specified, then inactivity timeout does not apply - if (!configuration.session.inactivity || authSession.keep_me_logged_in) { - return BluebirdPromise.resolve(); - } - - const lastActivityTime = authSession.last_activity_datetime; - const currentTime = new Date().getTime(); - authSession.last_activity_datetime = currentTime; - - const inactivityPeriodMs = currentTime - lastActivityTime; - logger.debug(req, "Inactivity period was %s s and max period was %s.", - inactivityPeriodMs / 1000, configuration.session.inactivity / 1000); - if (inactivityPeriodMs < configuration.session.inactivity) { - return BluebirdPromise.resolve(); - } - - logger.debug(req, "Session has been reset after too long inactivity period."); - AuthenticationSessionHandler.reset(req); - return BluebirdPromise.reject(new Error("Inactivity period exceeded.")); -} - -export default function (req: Express.Request, res: Express.Response, - vars: ServerVariables, authSession: AuthenticationSession) - : BluebirdPromise<{ username: string, groups: string[] }> { - - return BluebirdPromise.resolve() - .then(() => { - const username = authSession.userid; - const groups = authSession.groups; - - if (!authSession.userid) { - return BluebirdPromise.reject(new Exceptions.AccessDeniedError( - "userid is missing")); - } - - const originalUrl = ObjectPath.get( - req, "headers.x-original-url"); - const originalUri = - ObjectPath.get(req, "headers.x-original-uri"); - - const d = URLDecomposer.fromUrl(originalUrl); - vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", d.domain, - d.path, username, groups.join(",")); - return AccessControl(req, vars, d.domain, d.path, username, groups, - authSession.authentication_level); - }) - .then(() => { - return verify_inactivity(req, authSession, - vars.config, vars.logger); - }) - .then(() => { - return BluebirdPromise.resolve({ - username: authSession.userid, - groups: authSession.groups - }); - }); -} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/AuthenticationTraceDocument.d.ts b/themes/black/server/src/lib/storage/AuthenticationTraceDocument.d.ts deleted file mode 100644 index 69818c055..000000000 --- a/themes/black/server/src/lib/storage/AuthenticationTraceDocument.d.ts +++ /dev/null @@ -1,6 +0,0 @@ - -export interface AuthenticationTraceDocument { - userId: string; - date: Date; - isAuthenticationSuccessful: boolean; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/CollectionFactoryFactory.ts b/themes/black/server/src/lib/storage/CollectionFactoryFactory.ts deleted file mode 100644 index 92b29abfa..000000000 --- a/themes/black/server/src/lib/storage/CollectionFactoryFactory.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ICollectionFactory } from "./ICollectionFactory"; -import { NedbCollectionFactory } from "./nedb/NedbCollectionFactory"; -import { MongoCollectionFactory } from "./mongo/MongoCollectionFactory"; -import { IMongoClient } from "../connectors/mongo/IMongoClient"; - - -export class CollectionFactoryFactory { - static createNedb(options: Nedb.DataStoreOptions): ICollectionFactory { - return new NedbCollectionFactory(options); - } - - static createMongo(client: IMongoClient): ICollectionFactory { - return new MongoCollectionFactory(client); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/CollectionFactoryStub.spec.ts b/themes/black/server/src/lib/storage/CollectionFactoryStub.spec.ts deleted file mode 100644 index 17f8bb021..000000000 --- a/themes/black/server/src/lib/storage/CollectionFactoryStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Sinon = require("sinon"); -import { ICollection } from "./ICollection"; -import { ICollectionFactory } from "./ICollectionFactory"; - -export class CollectionFactoryStub implements ICollectionFactory { - buildStub: Sinon.SinonStub; - - constructor() { - this.buildStub = Sinon.stub(); - } - - build(collectionName: string): ICollection { - return this.buildStub(collectionName); - } -} diff --git a/themes/black/server/src/lib/storage/CollectionStub.spec.ts b/themes/black/server/src/lib/storage/CollectionStub.spec.ts deleted file mode 100644 index 42895d672..000000000 --- a/themes/black/server/src/lib/storage/CollectionStub.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Sinon = require("sinon"); -import { ICollection } from "./ICollection"; - -export class CollectionStub implements ICollection { - findStub: Sinon.SinonStub; - findOneStub: Sinon.SinonStub; - updateStub: Sinon.SinonStub; - removeStub: Sinon.SinonStub; - insertStub: Sinon.SinonStub; - - constructor() { - this.findStub = Sinon.stub(); - this.findOneStub = Sinon.stub(); - this.updateStub = Sinon.stub(); - this.removeStub = Sinon.stub(); - this.insertStub = Sinon.stub(); - } - - find(filter: any, sortKeys: any, count: number): BluebirdPromise { - return this.findStub(filter, sortKeys, count); - } - - findOne(filter: any): BluebirdPromise { - return this.findOneStub(filter); - } - - update(filter: any, document: any, options: any): BluebirdPromise { - return this.updateStub(filter, document, options); - } - - remove(filter: any): BluebirdPromise { - return this.removeStub(filter); - } - - insert(document: any): BluebirdPromise { - return this.insertStub(document); - } -} diff --git a/themes/black/server/src/lib/storage/ICollection.d.ts b/themes/black/server/src/lib/storage/ICollection.d.ts deleted file mode 100644 index caa6c2a87..000000000 --- a/themes/black/server/src/lib/storage/ICollection.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* istanbul ignore next */ -import BluebirdPromise = require("bluebird"); - -/* istanbul ignore next */ -export interface ICollection { - find(query: any, sortKeys: any, count: number): BluebirdPromise; - findOne(query: any): BluebirdPromise; - update(query: any, updateQuery: any, options?: any): BluebirdPromise; - remove(query: any): BluebirdPromise; - insert(document: any): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/ICollectionFactory.d.ts b/themes/black/server/src/lib/storage/ICollectionFactory.d.ts deleted file mode 100644 index 39eb42c77..000000000 --- a/themes/black/server/src/lib/storage/ICollectionFactory.d.ts +++ /dev/null @@ -1,6 +0,0 @@ - -import { ICollection } from "./ICollection"; - -export interface ICollectionFactory { - build(collectionName: string): ICollection; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/IUserDataStore.d.ts b/themes/black/server/src/lib/storage/IUserDataStore.d.ts deleted file mode 100644 index 81df482aa..000000000 --- a/themes/black/server/src/lib/storage/IUserDataStore.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import { TOTPSecretDocument } from "./TOTPSecretDocument"; -import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; -import { U2FRegistration } from "../../../types/U2FRegistration"; -import { TOTPSecret } from "../../../types/TOTPSecret"; -import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; -import { IdentityValidationDocument } from "./IdentityValidationDocument"; - -export interface IUserDataStore { - saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise; - retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise; - - saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; - retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise; - - produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise; - consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise; - - saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise; - retrieveTOTPSecret(userId: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/IdentityValidationDocument.d.ts b/themes/black/server/src/lib/storage/IdentityValidationDocument.d.ts deleted file mode 100644 index e7fd7b3f4..000000000 --- a/themes/black/server/src/lib/storage/IdentityValidationDocument.d.ts +++ /dev/null @@ -1,7 +0,0 @@ - -export interface IdentityValidationDocument { - userId: string; - token: string; - challenge: string; - maxDate: Date; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/TOTPSecretDocument.d.ts b/themes/black/server/src/lib/storage/TOTPSecretDocument.d.ts deleted file mode 100644 index a6c0bf9e6..000000000 --- a/themes/black/server/src/lib/storage/TOTPSecretDocument.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { TOTPSecret } from "../../../types/TOTPSecret"; - -export interface TOTPSecretDocument { - userid: string; - secret: TOTPSecret; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/U2FRegistrationDocument.d.ts b/themes/black/server/src/lib/storage/U2FRegistrationDocument.d.ts deleted file mode 100644 index efec6cb1d..000000000 --- a/themes/black/server/src/lib/storage/U2FRegistrationDocument.d.ts +++ /dev/null @@ -1,8 +0,0 @@ - -import { U2FRegistration } from "../../../types/U2FRegistration"; - -export interface U2FRegistrationDocument { - userId: string; - appId: string; - registration: U2FRegistration; -} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/UserDataStore.spec.ts b/themes/black/server/src/lib/storage/UserDataStore.spec.ts deleted file mode 100644 index 66fb85461..000000000 --- a/themes/black/server/src/lib/storage/UserDataStore.spec.ts +++ /dev/null @@ -1,264 +0,0 @@ - -import * as Assert from "assert"; -import * as Sinon from "sinon"; -import * as MockDate from "mockdate"; -import BluebirdPromise = require("bluebird"); - -import { UserDataStore } from "./UserDataStore"; -import { TOTPSecret } from "../../../types/TOTPSecret"; -import { U2FRegistration } from "../../../types/U2FRegistration"; -import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; -import { CollectionStub } from "./CollectionStub.spec"; -import { CollectionFactoryStub } from "./CollectionFactoryStub.spec"; - -describe("storage/UserDataStore", function () { - let factory: CollectionFactoryStub; - let collection: CollectionStub; - let userId: string; - let appId: string; - let totpSecret: TOTPSecret; - let u2fRegistration: U2FRegistration; - - beforeEach(function () { - factory = new CollectionFactoryStub(); - collection = new CollectionStub(); - - userId = "user"; - appId = "https://myappId"; - - totpSecret = { - ascii: "abc", - base32: "ABCDKZLEFZGREJK", - otpauth_url: "totp://test", - google_auth_qr: "dummy", - hex: "dummy", - qr_code_ascii: "dummy", - qr_code_base32: "dummy", - qr_code_hex: "dummy" - }; - - u2fRegistration = { - keyHandle: "KEY_HANDLE", - publicKey: "publickey" - }; - }); - - it("should correctly creates collections", function () { - new UserDataStore(factory); - - Assert.equal(4, factory.buildStub.callCount); - Assert(factory.buildStub.calledWith("authentication_traces")); - Assert(factory.buildStub.calledWith("identity_validation_tokens")); - Assert(factory.buildStub.calledWith("u2f_registrations")); - Assert(factory.buildStub.calledWith("totp_secrets")); - }); - - describe("TOTP secrets collection", function () { - it("should save a totp secret", function () { - factory.buildStub.returns(collection); - collection.updateStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.saveTOTPSecret(userId, totpSecret) - .then(function (doc) { - Assert(collection.updateStub.calledOnce); - Assert(collection.updateStub.calledWith({ userId: userId }, { - userId: userId, - secret: totpSecret - }, { upsert: true })); - return BluebirdPromise.resolve(); - }); - }); - - it("should retrieve a totp secret", function () { - factory.buildStub.returns(collection); - collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.retrieveTOTPSecret(userId) - .then(function (doc) { - Assert(collection.findOneStub.calledOnce); - Assert(collection.findOneStub.calledWith({ userId: userId })); - return BluebirdPromise.resolve(); - }); - }); - }); - - describe("U2F secrets collection", function () { - it("should save a U2F secret", function () { - factory.buildStub.returns(collection); - collection.updateStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.saveU2FRegistration(userId, appId, u2fRegistration) - .then(function (doc) { - Assert(collection.updateStub.calledOnce); - Assert(collection.updateStub.calledWith({ - userId: userId, - appId: appId - }, { - userId: userId, - appId: appId, - registration: u2fRegistration - }, { upsert: true })); - return BluebirdPromise.resolve(); - }); - }); - - it("should retrieve a U2F secret", function () { - factory.buildStub.returns(collection); - collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.retrieveU2FRegistration(userId, appId) - .then(function (doc) { - Assert(collection.findOneStub.calledOnce); - Assert(collection.findOneStub.calledWith({ - userId: userId, - appId: appId - })); - return BluebirdPromise.resolve(); - }); - }); - }); - - - describe("Regulator traces collection", function () { - it("should save a trace", function () { - factory.buildStub.returns(collection); - collection.insertStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.saveAuthenticationTrace(userId, true) - .then(function (doc) { - Assert(collection.insertStub.calledOnce); - Assert(collection.insertStub.calledWith({ - userId: userId, - date: Sinon.match.date, - isAuthenticationSuccessful: true - })); - return BluebirdPromise.resolve(); - }); - }); - - function should_retrieve_latest_authentication_traces(count: number) { - factory.buildStub.returns(collection); - collection.findStub.withArgs().returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.retrieveLatestAuthenticationTraces(userId, count) - .then(function (doc: AuthenticationTraceDocument[]) { - Assert(collection.findStub.calledOnce); - Assert(collection.findStub.calledWith({ - userId: userId, - }, { date: -1 }, count)); - return BluebirdPromise.resolve(); - }); - } - - it("should retrieve 3 latest failed authentication traces", function () { - should_retrieve_latest_authentication_traces(3); - }); - }); - - - describe("Identity validation collection", function () { - it("should save a identity validation token", function () { - factory.buildStub.returns(collection); - collection.insertStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - const maxAge = 400; - const token = "TOKEN"; - const challenge = "CHALLENGE"; - - return dataStore.produceIdentityValidationToken(userId, token, challenge, maxAge) - .then(function (doc) { - Assert(collection.insertStub.calledOnce); - Assert(collection.insertStub.calledWith({ - userId: userId, - token: token, - challenge: challenge, - maxDate: Sinon.match.date - })); - return BluebirdPromise.resolve(); - }); - }); - - it("should consume an identity token successfully", function () { - factory.buildStub.returns(collection); - - MockDate.set(100); - - const token = "TOKEN"; - const challenge = "CHALLENGE"; - - collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ - userId: "USER", - token: token, - challenge: challenge, - maxDate: new Date() - })); - collection.removeStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - MockDate.set(80); - - return dataStore.consumeIdentityValidationToken(token, challenge) - .then(function (doc) { - MockDate.reset(); - Assert(collection.findOneStub.calledOnce); - Assert(collection.findOneStub.calledWith({ - token: token, - challenge: challenge - })); - - Assert(collection.removeStub.calledOnce); - Assert(collection.removeStub.calledWith({ - token: token, - challenge: challenge - })); - return BluebirdPromise.resolve(); - }); - }); - - it("should consume an expired identity token", function () { - factory.buildStub.returns(collection); - - MockDate.set(0); - - const token = "TOKEN"; - const challenge = "CHALLENGE"; - - collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ - userId: "USER", - token: token, - challenge: challenge, - maxDate: new Date() - })); - - const dataStore = new UserDataStore(factory); - - MockDate.set(80000); - - return dataStore.consumeIdentityValidationToken(token, challenge) - .then(function () { return BluebirdPromise.reject(new Error("should not be here")); }) - .catch(function () { - MockDate.reset(); - Assert(collection.findOneStub.calledOnce); - Assert(collection.findOneStub.calledWith({ - token: token, - challenge: challenge - })); - return BluebirdPromise.resolve(); - }); - }); - }); -}); diff --git a/themes/black/server/src/lib/storage/UserDataStore.ts b/themes/black/server/src/lib/storage/UserDataStore.ts deleted file mode 100644 index 27b0cddbd..000000000 --- a/themes/black/server/src/lib/storage/UserDataStore.ts +++ /dev/null @@ -1,143 +0,0 @@ -import * as BluebirdPromise from "bluebird"; -import * as path from "path"; -import { IUserDataStore } from "./IUserDataStore"; -import { ICollection } from "./ICollection"; -import { ICollectionFactory } from "./ICollectionFactory"; -import { TOTPSecretDocument } from "./TOTPSecretDocument"; -import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; -import { U2FRegistration } from "../../../types/U2FRegistration"; -import { TOTPSecret } from "../../../types/TOTPSecret"; -import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; -import { IdentityValidationDocument } from "./IdentityValidationDocument"; - -// Constants - -const IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME = "identity_validation_tokens"; -const AUTHENTICATION_TRACES_COLLECTION_NAME = "authentication_traces"; - -const U2F_REGISTRATIONS_COLLECTION_NAME = "u2f_registrations"; -const TOTP_SECRETS_COLLECTION_NAME = "totp_secrets"; - - -export interface U2FRegistrationKey { - userId: string; - appId: string; -} - -// Source - -export class UserDataStore implements IUserDataStore { - private u2fSecretCollection: ICollection; - private identityCheckTokensCollection: ICollection; - private authenticationTracesCollection: ICollection; - private totpSecretCollection: ICollection; - - private collectionFactory: ICollectionFactory; - - constructor(collectionFactory: ICollectionFactory) { - this.collectionFactory = collectionFactory; - - this.u2fSecretCollection = this.collectionFactory.build(U2F_REGISTRATIONS_COLLECTION_NAME); - this.identityCheckTokensCollection = this.collectionFactory.build(IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME); - this.authenticationTracesCollection = this.collectionFactory.build(AUTHENTICATION_TRACES_COLLECTION_NAME); - this.totpSecretCollection = this.collectionFactory.build(TOTP_SECRETS_COLLECTION_NAME); - } - - saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { - const newDocument: U2FRegistrationDocument = { - userId: userId, - appId: appId, - registration: registration - }; - - const filter: U2FRegistrationKey = { - userId: userId, - appId: appId - }; - - return this.u2fSecretCollection.update(filter, newDocument, { upsert: true }); - } - - retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { - const filter: U2FRegistrationKey = { - userId: userId, - appId: appId - }; - return this.u2fSecretCollection.findOne(filter); - } - - saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { - const newDocument: AuthenticationTraceDocument = { - userId: userId, - date: new Date(), - isAuthenticationSuccessful: isAuthenticationSuccessful, - }; - - return this.authenticationTracesCollection.insert(newDocument); - } - - retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { - const q = { - userId: userId - }; - - return this.authenticationTracesCollection.find(q, { date: -1 }, count); - } - - produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { - const newDocument: IdentityValidationDocument = { - userId: userId, - token: token, - challenge: challenge, - maxDate: new Date(new Date().getTime() + maxAge) - }; - - return this.identityCheckTokensCollection.insert(newDocument); - } - - consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { - const that = this; - const filter = { - token: token, - challenge: challenge - }; - - let identityValidationDocument: IdentityValidationDocument; - - return this.identityCheckTokensCollection.findOne(filter) - .then(function (doc: IdentityValidationDocument) { - if (!doc) { - return BluebirdPromise.reject(new Error("Registration token does not exist")); - } - - identityValidationDocument = doc; - const current_date = new Date(); - if (current_date > doc.maxDate) - return BluebirdPromise.reject(new Error("Registration token is not valid anymore")); - - return that.identityCheckTokensCollection.remove(filter); - }) - .then(() => { - return BluebirdPromise.resolve(identityValidationDocument); - }); - } - - saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { - const doc = { - userId: userId, - secret: secret - }; - - const filter = { - userId: userId - }; - return this.totpSecretCollection.update(filter, doc, { upsert: true }); - } - - retrieveTOTPSecret(userId: string): BluebirdPromise { - const filter = { - userId: userId - }; - return this.totpSecretCollection.findOne(filter); - } -} diff --git a/themes/black/server/src/lib/storage/UserDataStoreStub.spec.ts b/themes/black/server/src/lib/storage/UserDataStoreStub.spec.ts deleted file mode 100644 index 5ea27a2de..000000000 --- a/themes/black/server/src/lib/storage/UserDataStoreStub.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); - -import { TOTPSecretDocument } from "./TOTPSecretDocument"; -import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; -import { U2FRegistration } from "../../../types/U2FRegistration"; -import { TOTPSecret } from "../../../types/TOTPSecret"; -import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; -import { IdentityValidationDocument } from "./IdentityValidationDocument"; -import { IUserDataStore } from "./IUserDataStore"; - -export class UserDataStoreStub implements IUserDataStore { - saveU2FRegistrationStub: Sinon.SinonStub; - retrieveU2FRegistrationStub: Sinon.SinonStub; - saveAuthenticationTraceStub: Sinon.SinonStub; - retrieveLatestAuthenticationTracesStub: Sinon.SinonStub; - produceIdentityValidationTokenStub: Sinon.SinonStub; - consumeIdentityValidationTokenStub: Sinon.SinonStub; - saveTOTPSecretStub: Sinon.SinonStub; - retrieveTOTPSecretStub: Sinon.SinonStub; - - constructor() { - this.saveU2FRegistrationStub = Sinon.stub(); - this.retrieveU2FRegistrationStub = Sinon.stub(); - this.saveAuthenticationTraceStub = Sinon.stub(); - this.retrieveLatestAuthenticationTracesStub = Sinon.stub(); - this.produceIdentityValidationTokenStub = Sinon.stub(); - this.consumeIdentityValidationTokenStub = Sinon.stub(); - this.saveTOTPSecretStub = Sinon.stub(); - this.retrieveTOTPSecretStub = Sinon.stub(); - } - - saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { - return this.saveU2FRegistrationStub(userId, appId, registration); - } - - retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { - return this.retrieveU2FRegistrationStub(userId, appId); - } - - saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { - return this.saveAuthenticationTraceStub(userId, isAuthenticationSuccessful); - } - - retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { - return this.retrieveLatestAuthenticationTracesStub(userId, count); - } - - produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { - return this.produceIdentityValidationTokenStub(userId, token, challenge, maxAge); - } - - consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { - return this.consumeIdentityValidationTokenStub(token, challenge); - } - - saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { - return this.saveTOTPSecretStub(userId, secret); - } - - retrieveTOTPSecret(userId: string): BluebirdPromise { - return this.retrieveTOTPSecretStub(userId); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/mongo/MongoCollection.spec.ts b/themes/black/server/src/lib/storage/mongo/MongoCollection.spec.ts deleted file mode 100644 index 74a773a1e..000000000 --- a/themes/black/server/src/lib/storage/mongo/MongoCollection.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -import Assert = require("assert"); -import Sinon = require("sinon"); -import MongoDB = require("mongodb"); -import BluebirdPromise = require("bluebird"); -import { MongoClientStub } from "../../connectors/mongo/MongoClientStub.spec"; -import { MongoCollection } from "./MongoCollection"; - -describe("storage/mongo/MongoCollection", function () { - let mongoCollectionStub: any; - let mongoClientStub: MongoClientStub; - let findStub: Sinon.SinonStub; - let findOneStub: Sinon.SinonStub; - let insertOneStub: Sinon.SinonStub; - let updateStub: Sinon.SinonStub; - let removeStub: Sinon.SinonStub; - let countStub: Sinon.SinonStub; - const COLLECTION_NAME = "collection"; - - before(function () { - mongoClientStub = new MongoClientStub(); - mongoCollectionStub = Sinon.createStubInstance(require("mongodb").Collection as any); - findStub = mongoCollectionStub.find as Sinon.SinonStub; - findOneStub = mongoCollectionStub.findOne as Sinon.SinonStub; - insertOneStub = mongoCollectionStub.insertOne as Sinon.SinonStub; - updateStub = mongoCollectionStub.update as Sinon.SinonStub; - removeStub = mongoCollectionStub.remove as Sinon.SinonStub; - countStub = mongoCollectionStub.count as Sinon.SinonStub; - mongoClientStub.collectionStub.returns( - BluebirdPromise.resolve(mongoCollectionStub) - ); - }); - - describe("find", function () { - it("should find a document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - findStub.returns({ - sort: Sinon.stub().returns({ - limit: Sinon.stub().returns({ - toArray: Sinon.stub().returns(BluebirdPromise.resolve([])) - }) - }) - }); - - return collection.find({ key: "KEY" }) - .then(function () { - Assert(findStub.calledWith({ key: "KEY" })); - }); - }); - }); - - describe("findOne", function () { - it("should find one document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - findOneStub.returns(BluebirdPromise.resolve({})); - - return collection.findOne({ key: "KEY" }) - .then(function () { - Assert(findOneStub.calledWith({ key: "KEY" })); - }); - }); - }); - - describe("insert", function () { - it("should insert a document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - insertOneStub.returns(BluebirdPromise.resolve({})); - - return collection.insert({ key: "KEY" }) - .then(function () { - Assert(insertOneStub.calledWith({ key: "KEY" })); - }); - }); - }); - - describe("update", function () { - it("should update a document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - updateStub.returns(BluebirdPromise.resolve({})); - - return collection.update({ key: "KEY" }, { key: "KEY", value: 1 }) - .then(function () { - Assert(updateStub.calledWith({ key: "KEY" }, { key: "KEY", value: 1 })); - }); - }); - }); - - describe("remove", function () { - it("should remove a document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - removeStub.returns(BluebirdPromise.resolve({})); - - return collection.remove({ key: "KEY" }) - .then(function () { - Assert(removeStub.calledWith({ key: "KEY" })); - }); - }); - }); - - describe("count", function () { - it("should count documents in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - countStub.returns(BluebirdPromise.resolve({})); - - return collection.count({ key: "KEY" }) - .then(function () { - Assert(countStub.calledWith({ key: "KEY" })); - }); - }); - }); -}); diff --git a/themes/black/server/src/lib/storage/mongo/MongoCollection.ts b/themes/black/server/src/lib/storage/mongo/MongoCollection.ts deleted file mode 100644 index 9771389f1..000000000 --- a/themes/black/server/src/lib/storage/mongo/MongoCollection.ts +++ /dev/null @@ -1,50 +0,0 @@ -import Bluebird = require("bluebird"); -import { ICollection } from "../ICollection"; -import MongoDB = require("mongodb"); -import { IMongoClient } from "../../connectors/mongo/IMongoClient"; - - -export class MongoCollection implements ICollection { - private mongoClient: IMongoClient; - private collectionName: string; - - constructor(collectionName: string, mongoClient: IMongoClient) { - this.collectionName = collectionName; - this.mongoClient = mongoClient; - } - - private collection(): Bluebird { - return this.mongoClient.collection(this.collectionName); - } - - find(query: any, sortKeys?: any, count?: number): Bluebird { - return this.collection() - .then((collection) => collection.find(query).sort(sortKeys).limit(count)) - .then((query) => query.toArray()); - } - - findOne(query: any): Bluebird { - return this.collection() - .then((collection) => collection.findOne(query)); - } - - update(query: any, updateQuery: any, options?: any): Bluebird { - return this.collection() - .then((collection) => collection.update(query, updateQuery, options)); - } - - remove(query: any): Bluebird { - return this.collection() - .then((collection) => collection.remove(query)); - } - - insert(document: any): Bluebird { - return this.collection() - .then((collection) => collection.insertOne(document)); - } - - count(query: any): Bluebird { - return this.collection() - .then((collection) => collection.count(query)); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts b/themes/black/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts deleted file mode 100644 index bd959cacb..000000000 --- a/themes/black/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Assert = require("assert"); -import Sinon = require("sinon"); -import { MongoClientStub } from "../../connectors/mongo/MongoClientStub.spec"; -import { MongoCollectionFactory } from "./MongoCollectionFactory"; - -describe("storage/mongo/MongoCollectionFactory", function () { - let mongoClient: MongoClientStub; - - before(function() { - mongoClient = new MongoClientStub(); - }); - - describe("create", function () { - it("should create a collection", function () { - const COLLECTION_NAME = "COLLECTION_NAME"; - - const factory = new MongoCollectionFactory(mongoClient); - Assert(factory.build(COLLECTION_NAME)); - }); - }); -}); diff --git a/themes/black/server/src/lib/storage/mongo/MongoCollectionFactory.ts b/themes/black/server/src/lib/storage/mongo/MongoCollectionFactory.ts deleted file mode 100644 index 14a8262c9..000000000 --- a/themes/black/server/src/lib/storage/mongo/MongoCollectionFactory.ts +++ /dev/null @@ -1,19 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import { ICollection } from "../ICollection"; -import { ICollectionFactory } from "../ICollectionFactory"; -import { MongoCollection } from "./MongoCollection"; -import path = require("path"); -import MongoDB = require("mongodb"); -import { IMongoClient } from "../../connectors/mongo/IMongoClient"; - -export class MongoCollectionFactory implements ICollectionFactory { - private mongoClient: IMongoClient; - - constructor(mongoClient: IMongoClient) { - this.mongoClient = mongoClient; - } - - build(collectionName: string): ICollection { - return new MongoCollection(collectionName, this.mongoClient); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/nedb/NedbCollection.spec.ts b/themes/black/server/src/lib/storage/nedb/NedbCollection.spec.ts deleted file mode 100644 index a69962b67..000000000 --- a/themes/black/server/src/lib/storage/nedb/NedbCollection.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -import Sinon = require("sinon"); -import Assert = require("assert"); - -import { NedbCollection } from "./NedbCollection"; - -describe("storage/nedb/NedbCollection", function () { - describe("insert", function () { - it("should insert one entry", function () { - const nedbOptions = { - inMemoryOnly: true - }; - const collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou" }); - - return collection.count({}).then(function (count: number) { - Assert.equal(1, count); - }); - }); - - it("should insert three entries", function () { - const nedbOptions = { - inMemoryOnly: true - }; - const collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou" }); - collection.insert({ key: "hello" }); - collection.insert({ key: "hey" }); - - return collection.count({}).then(function (count: number) { - Assert.equal(3, count); - }); - }); - }); - - describe("find", function () { - let collection: NedbCollection; - before(function () { - const nedbOptions = { - inMemoryOnly: true - }; - collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou", value: 1 }); - collection.insert({ key: "hello" }); - collection.insert({ key: "hey" }); - collection.insert({ key: "coucou", value: 2 }); - }); - - it("should find one hello", function () { - return collection.find({ key: "hello" }, { key: 1 }) - .then(function (docs: { key: string }[]) { - Assert.equal(1, docs.length); - Assert(docs[0].key == "hello"); - }); - }); - - it("should find two coucou", function () { - return collection.find({ key: "coucou" }, { value: 1 }) - .then(function (docs: { value: number }[]) { - Assert.equal(2, docs.length); - }); - }); - }); - - describe("findOne", function () { - let collection: NedbCollection; - before(function () { - const nedbOptions = { - inMemoryOnly: true - }; - collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou", value: 1 }); - collection.insert({ key: "coucou", value: 1 }); - collection.insert({ key: "coucou", value: 1 }); - collection.insert({ key: "coucou", value: 1 }); - }); - - it("should find two coucou", function () { - const doc = { key: "coucou", value: 1 }; - return collection.count(doc) - .then(function (count: number) { - Assert.equal(4, count); - return collection.findOne(doc); - }); - }); - }); - - describe("update", function () { - let collection: NedbCollection; - before(function () { - const nedbOptions = { - inMemoryOnly: true - }; - collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou", value: 1 }); - }); - - it("should update the value", function () { - return collection.update({ key: "coucou" }, { key: "coucou", value: 2 }, { multi: true }) - .then(function () { - return collection.find({ key: "coucou" }); - }) - .then(function (docs: { key: string, value: number }[]) { - Assert.equal(1, docs.length); - Assert.equal(2, docs[0].value); - }); - }); - }); - - describe("update", function () { - let collection: NedbCollection; - before(function () { - const nedbOptions = { - inMemoryOnly: true - }; - collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou" }); - collection.insert({ key: "hello" }); - }); - - it("should update the value", function () { - return collection.remove({ key: "coucou" }) - .then(function () { - return collection.count({}); - }) - .then(function (count: number) { - Assert.equal(1, count); - }); - }); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/nedb/NedbCollection.ts b/themes/black/server/src/lib/storage/nedb/NedbCollection.ts deleted file mode 100644 index 88a93ad05..000000000 --- a/themes/black/server/src/lib/storage/nedb/NedbCollection.ts +++ /dev/null @@ -1,47 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import { ICollection } from "../ICollection"; -import Nedb = require("nedb"); - -declare module "nedb" { - export class NedbAsync extends Nedb { - constructor(pathOrOptions?: string | Nedb.DataStoreOptions); - updateAsync(query: any, updateQuery: any, options?: Nedb.UpdateOptions): BluebirdPromise; - findOneAsync(query: any): BluebirdPromise; - insertAsync(newDoc: T): BluebirdPromise; - removeAsync(query: any): BluebirdPromise; - countAsync(query: any): BluebirdPromise; - } -} - -export class NedbCollection implements ICollection { - private collection: Nedb.NedbAsync; - - constructor(options: Nedb.DataStoreOptions) { - this.collection = BluebirdPromise.promisifyAll(new Nedb(options)) as Nedb.NedbAsync; - } - - find(query: any, sortKeys?: any, count?: number): BluebirdPromise { - const q = this.collection.find(query).sort(sortKeys).limit(count); - return BluebirdPromise.promisify(q.exec, { context: q })(); - } - - findOne(query: any): BluebirdPromise { - return this.collection.findOneAsync(query); - } - - update(query: any, updateQuery: any, options?: any): BluebirdPromise { - return this.collection.updateAsync(query, updateQuery, options); - } - - remove(query: any): BluebirdPromise { - return this.collection.removeAsync(query); - } - - insert(document: any): BluebirdPromise { - return this.collection.insertAsync(document); - } - - count(query: any): BluebirdPromise { - return this.collection.countAsync(query); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts b/themes/black/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts deleted file mode 100644 index da90c661f..000000000 --- a/themes/black/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Sinon = require("sinon"); -import Assert = require("assert"); - -import { NedbCollectionFactory } from "./NedbCollectionFactory"; - -describe("storage/nedb/NedbCollectionFactory", function() { - it("should create a nedb collection", function() { - const nedbOptions = { - inMemoryOnly: true - }; - const factory = new NedbCollectionFactory(nedbOptions); - - const collection = factory.build("mycollection"); - Assert(collection); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/storage/nedb/NedbCollectionFactory.ts b/themes/black/server/src/lib/storage/nedb/NedbCollectionFactory.ts deleted file mode 100644 index 49c4dc853..000000000 --- a/themes/black/server/src/lib/storage/nedb/NedbCollectionFactory.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ICollection } from "../ICollection"; -import { ICollectionFactory } from "../ICollectionFactory"; -import { NedbCollection } from "./NedbCollection"; -import path = require("path"); -import Nedb = require("nedb"); - -export interface NedbOptions { - inMemoryOnly?: boolean; - directory?: string; -} - -export class NedbCollectionFactory implements ICollectionFactory { - private options: Nedb.DataStoreOptions; - - constructor(options: Nedb.DataStoreOptions) { - this.options = options; - } - - build(collectionName: string): ICollection { - const datastoreOptions: Nedb.DataStoreOptions = { - inMemoryOnly: this.options.inMemoryOnly || false, - autoload: true, - filename: (this.options.filename) ? path.resolve(this.options.filename, collectionName) : undefined - }; - - return new NedbCollection(datastoreOptions); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/stubs/express.spec.ts b/themes/black/server/src/lib/stubs/express.spec.ts deleted file mode 100644 index 48f15d7e1..000000000 --- a/themes/black/server/src/lib/stubs/express.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ - -import sinon = require("sinon"); -import express = require("express"); - -export interface RequestMock { - app?: any; - body?: any; - session?: any; - headers?: any; - get?: any; - query?: any; - originalUrl: string; -} - -export interface ResponseMock { - send: sinon.SinonStub | sinon.SinonSpy; - sendStatus: sinon.SinonStub; - sendFile: sinon.SinonStub; - sendfile: sinon.SinonStub; - status: sinon.SinonStub | sinon.SinonSpy; - json: sinon.SinonStub | sinon.SinonSpy; - links: sinon.SinonStub; - jsonp: sinon.SinonStub; - download: sinon.SinonStub; - contentType: sinon.SinonStub; - type: sinon.SinonStub; - format: sinon.SinonStub; - attachment: sinon.SinonStub; - set: sinon.SinonStub; - header: sinon.SinonStub; - headersSent: boolean; - get: sinon.SinonStub; - clearCookie: sinon.SinonStub; - cookie: sinon.SinonStub; - location: sinon.SinonStub; - redirect: sinon.SinonStub | sinon.SinonSpy; - render: sinon.SinonStub | sinon.SinonSpy; - locals: sinon.SinonStub; - charset: string; - vary: sinon.SinonStub; - app: any; - write: sinon.SinonStub; - writeContinue: sinon.SinonStub; - writeHead: sinon.SinonStub; - statusCode: number; - statusMessage: string; - setHeader: sinon.SinonStub; - setTimeout: sinon.SinonStub; - sendDate: boolean; - getHeader: sinon.SinonStub; -} - -export function RequestMock(): RequestMock { - return { - originalUrl: "/non-api/xxx", - app: { - get: sinon.stub() - }, - headers: { - "x-forwarded-for": "127.0.0.1" - }, - session: {} - }; -} -export function ResponseMock(): ResponseMock { - return { - send: sinon.stub(), - status: sinon.stub(), - json: sinon.stub(), - sendStatus: sinon.stub(), - links: sinon.stub(), - jsonp: sinon.stub(), - sendFile: sinon.stub(), - sendfile: sinon.stub(), - download: sinon.stub(), - contentType: sinon.stub(), - type: sinon.stub(), - format: sinon.stub(), - attachment: sinon.stub(), - set: sinon.stub(), - header: sinon.stub(), - headersSent: true, - get: sinon.stub(), - clearCookie: sinon.stub(), - cookie: sinon.stub(), - location: sinon.stub(), - redirect: sinon.stub(), - render: sinon.stub(), - locals: sinon.stub(), - charset: "utf-8", - vary: sinon.stub(), - app: sinon.stub(), - write: sinon.stub(), - writeContinue: sinon.stub(), - writeHead: sinon.stub(), - statusCode: 200, - statusMessage: "message", - setHeader: sinon.stub(), - setTimeout: sinon.stub(), - sendDate: true, - getHeader: sinon.stub() - }; -} diff --git a/themes/black/server/src/lib/stubs/ldapjs.spec.ts b/themes/black/server/src/lib/stubs/ldapjs.spec.ts deleted file mode 100644 index 045c0e11b..000000000 --- a/themes/black/server/src/lib/stubs/ldapjs.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ - -import Sinon = require("sinon"); - -export class LdapjsMock { - createClientStub: sinon.SinonStub; - - constructor() { - this.createClientStub = Sinon.stub(); - } - - createClient(params: any) { - return this.createClientStub(params); - } -} - -export class LdapjsClientMock { - bindStub: sinon.SinonStub; - unbindStub: sinon.SinonStub; - searchStub: sinon.SinonStub; - modifyStub: sinon.SinonStub; - onStub: sinon.SinonStub; - - constructor() { - this.bindStub = Sinon.stub(); - this.unbindStub = Sinon.stub(); - this.searchStub = Sinon.stub(); - this.modifyStub = Sinon.stub(); - this.onStub = Sinon.stub(); - } - - bind() { - return this.bindStub(); - } - - unbind() { - return this.unbindStub(); - } - - search() { - return this.searchStub(); - } - - modify() { - return this.modifyStub(); - } - - on() { - return this.onStub(); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/stubs/speakeasy.spec.ts b/themes/black/server/src/lib/stubs/speakeasy.spec.ts deleted file mode 100644 index 023614dcd..000000000 --- a/themes/black/server/src/lib/stubs/speakeasy.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ - -import sinon = require("sinon"); - -export = { - totp: sinon.stub(), - generateSecret: sinon.stub() -}; diff --git a/themes/black/server/src/lib/stubs/u2f.spec.ts b/themes/black/server/src/lib/stubs/u2f.spec.ts deleted file mode 100644 index 234b28c11..000000000 --- a/themes/black/server/src/lib/stubs/u2f.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ - -import sinon = require("sinon"); - -export interface U2FMock { - request: sinon.SinonStub; - checkSignature: sinon.SinonStub; - checkRegistration: sinon.SinonStub; -} - -export function U2FMock(): U2FMock { - return { - request: sinon.stub(), - checkSignature: sinon.stub(), - checkRegistration: sinon.stub() - }; -} diff --git a/themes/black/server/src/lib/utils/HashGenerator.spec.ts b/themes/black/server/src/lib/utils/HashGenerator.spec.ts deleted file mode 100644 index f19619a65..000000000 --- a/themes/black/server/src/lib/utils/HashGenerator.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Assert = require("assert"); -import { HashGenerator } from "./HashGenerator"; - -describe("utils/HashGenerator", function () { - it("should compute correct ssha512 (password)", function () { - return HashGenerator.ssha512("password", 500000, "jgiCMRyGXzoqpxS3") - .then(function (hash: string) { - Assert.equal(hash, "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"); - }); - }); - - it("should compute correct ssha512 (test)", function () { - return HashGenerator.ssha512("test", 500000, "abcdefghijklmnop") - .then(function (hash: string) { - Assert.equal(hash, "{CRYPT}$6$rounds=500000$abcdefghijklmnop$sTlNGf0VO/HTQIOXemmaBbV28HUch/qhWOA1/4dsDj6CDQYhUgXbYSPL6gccAsWMr2zD5fFWwhKmPdG.yxphs."); - }); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/utils/HashGenerator.ts b/themes/black/server/src/lib/utils/HashGenerator.ts deleted file mode 100644 index e67de32b7..000000000 --- a/themes/black/server/src/lib/utils/HashGenerator.ts +++ /dev/null @@ -1,23 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import RandomString = require("randomstring"); -import Util = require("util"); -const crypt = require("crypt3"); - -export class HashGenerator { - static ssha512( - password: string, - rounds: number = 500000, - salt?: string): BluebirdPromise { - const saltSize = 16; - // $6 means SHA512 - const _salt = Util.format("$6$rounds=%d$%s", rounds, - (salt) ? salt : RandomString.generate(16)); - - const cryptAsync = BluebirdPromise.promisify(crypt); - - return cryptAsync(password, _salt) - .then(function (hash: string) { - return BluebirdPromise.resolve(Util.format("{CRYPT}%s", hash)); - }); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/utils/ObjectCloner.ts b/themes/black/server/src/lib/utils/ObjectCloner.ts deleted file mode 100644 index 3e125d749..000000000 --- a/themes/black/server/src/lib/utils/ObjectCloner.ts +++ /dev/null @@ -1,6 +0,0 @@ - -export class ObjectCloner { - static clone(obj: any): any { - return JSON.parse(JSON.stringify(obj)); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/utils/SafeRedirection.spec.ts b/themes/black/server/src/lib/utils/SafeRedirection.spec.ts deleted file mode 100644 index 4126949fd..000000000 --- a/themes/black/server/src/lib/utils/SafeRedirection.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import Assert = require("assert"); -import Sinon = require("sinon"); -import { SafeRedirector } from "./SafeRedirection"; - -describe("web_server/middlewares/SafeRedirection", () => { - describe("Url is in protected domain", () => { - before(() => { - this.redirector = new SafeRedirector("example.com"); - this.res = {redirect: Sinon.stub()}; - }); - - it("should redirect to provided url", () => { - this.redirector.redirectOrElse(this.res, - "https://mysubdomain.example.com:8080/abc", - "https://authelia.example.com"); - Assert(this.res.redirect.calledWith("https://mysubdomain.example.com:8080/abc")); - }); - - it("should redirect to default url when wrong domain", () => { - this.redirector.redirectOrElse(this.res, - "https://mysubdomain.domain.rtf:8080/abc", - "https://authelia.example.com"); - Assert(this.res.redirect.calledWith("https://authelia.example.com")); - }); - - it("should redirect to default url when not terminating by domain", () => { - this.redirector.redirectOrElse(this.res, - "https://mysubdomain.example.com.rtf:8080/abc", - "https://authelia.example.com"); - Assert(this.res.redirect.calledWith("https://authelia.example.com")); - }); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/utils/SafeRedirection.ts b/themes/black/server/src/lib/utils/SafeRedirection.ts deleted file mode 100644 index 9e6a32e0c..000000000 --- a/themes/black/server/src/lib/utils/SafeRedirection.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Express = require("express"); -import { DomainExtractor } from "../../../../shared/DomainExtractor"; -import { BelongToDomain } from "../../../../shared/BelongToDomain"; - - -export class SafeRedirector { - private domain: string; - - constructor(domain: string) { - this.domain = domain; - } - - redirectOrElse( - res: Express.Response, - url: string, - defaultUrl: string): void { - if (BelongToDomain(url, this.domain)) { - res.redirect(url); - } - res.redirect(defaultUrl); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/utils/URLDecomposer.spec.ts b/themes/black/server/src/lib/utils/URLDecomposer.spec.ts deleted file mode 100644 index cbb038738..000000000 --- a/themes/black/server/src/lib/utils/URLDecomposer.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { URLDecomposer } from "./URLDecomposer"; -import Assert = require("assert"); - -describe("utils/URLDecomposer", function () { - describe("test fromUrl", function () { - it("should return domain from https url", function () { - const d = URLDecomposer.fromUrl("https://www.example.com/test/abc"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/test/abc"); - }); - - it("should return domain from http url", function () { - const d = URLDecomposer.fromUrl("http://www.example.com/test/abc"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/test/abc"); - }); - - it("should return domain when url contains port", function () { - const d = URLDecomposer.fromUrl("https://www.example.com:8080/test/abc"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/test/abc"); - }); - - it("should return default path when no path provided", function () { - const d = URLDecomposer.fromUrl("https://www.example.com:8080"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/"); - }); - - it("should return default path when provided", function () { - const d = URLDecomposer.fromUrl("https://www.example.com:8080/"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/"); - }); - - it("should return undefined when does not match", function () { - const d = URLDecomposer.fromUrl("https:///abc/test"); - Assert.equal(d, undefined); - }); - - it("should return undefined when does not match", function () { - const d = URLDecomposer.fromUrl("https:///abc/test"); - Assert.equal(d, undefined); - }); - }); -}); \ No newline at end of file diff --git a/themes/black/server/src/lib/utils/URLDecomposer.ts b/themes/black/server/src/lib/utils/URLDecomposer.ts deleted file mode 100644 index 9bdf2e9d2..000000000 --- a/themes/black/server/src/lib/utils/URLDecomposer.ts +++ /dev/null @@ -1,15 +0,0 @@ -export class URLDecomposer { - static fromUrl(url: string): {domain: string, path: string} { - if (!url) return; - const match = url.match(/https?:\/\/([a-z0-9_.-]+)(:[0-9]+)?(.*)/); - - if (!match) return; - - if (match[1] && !match[3]) { - return {domain: match[1], path: "/"}; - } else if (match[1] && match[3]) { - return {domain: match[1], path: match[3]}; - } - return; - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/web_server/Configurator.ts b/themes/black/server/src/lib/web_server/Configurator.ts deleted file mode 100644 index 6e404874a..000000000 --- a/themes/black/server/src/lib/web_server/Configurator.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Configuration } from "../configuration/schema/Configuration"; -import { GlobalDependencies } from "../../../types/Dependencies"; -import { SessionConfigurationBuilder } from - "../configuration/SessionConfigurationBuilder"; -import Path = require("path"); -import Express = require("express"); -import * as BodyParser from "body-parser"; -import { RestApi } from "./RestApi"; -import { WithHeadersLogged } from "./middlewares/WithHeadersLogged"; -import { ServerVariables } from "../ServerVariables"; -import Helmet = require("helmet"); - -const addRequestId = require("express-request-id")(); - -// Constants -const TRUST_PROXY = "trust proxy"; -const X_POWERED_BY = "x-powered-by"; -const VIEWS = "views"; -const VIEW_ENGINE = "view engine"; -const PUG = "pug"; - -export class Configurator { - static configure(config: Configuration, - app: Express.Application, - vars: ServerVariables, - deps: GlobalDependencies): void { - const viewsDirectory = Path.resolve(__dirname, "../../views"); - const publicHtmlDirectory = Path.resolve(__dirname, "../../public_html"); - - const expressSessionOptions = SessionConfigurationBuilder.build(config, deps); - - app.use(Express.static(publicHtmlDirectory)); - app.use(BodyParser.urlencoded({ extended: false })); - app.use(BodyParser.json()); - app.use(deps.session(expressSessionOptions)); - app.use(addRequestId); - app.use(WithHeadersLogged.middleware(vars.logger)); - app.disable(X_POWERED_BY); - app.enable(TRUST_PROXY); - app.use(Helmet()); - - app.set(VIEWS, viewsDirectory); - app.set(VIEW_ENGINE, PUG); - - RestApi.setup(app, vars); - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/web_server/RestApi.ts b/themes/black/server/src/lib/web_server/RestApi.ts deleted file mode 100644 index 9144a15b9..000000000 --- a/themes/black/server/src/lib/web_server/RestApi.ts +++ /dev/null @@ -1,125 +0,0 @@ -import Express = require("express"); - -import FirstFactorGet = require("../routes/firstfactor/get"); -import SecondFactorGet = require("../routes/secondfactor/get"); - -import FirstFactorPost = require("../routes/firstfactor/post"); -import LogoutGet = require("../routes/logout/get"); -import VerifyGet = require("../routes/verify/get"); -import TOTPSignGet = require("../routes/secondfactor/totp/sign/post"); - -import IdentityCheckMiddleware = require("../IdentityCheckMiddleware"); - -import TOTPRegistrationIdentityHandler from "../routes/secondfactor/totp/identity/RegistrationHandler"; -import U2FRegistrationIdentityHandler from "../routes/secondfactor/u2f/identity/RegistrationHandler"; -import ResetPasswordIdentityHandler from "../routes/password-reset/identity/PasswordResetHandler"; - -import U2FSignPost = require("../routes/secondfactor/u2f/sign/post"); -import U2FSignRequestGet = require("../routes/secondfactor/u2f/sign_request/get"); - -import U2FRegisterPost = require("../routes/secondfactor/u2f/register/post"); -import U2FRegisterRequestGet = require("../routes/secondfactor/u2f/register_request/get"); - -import ResetPasswordFormPost = require("../routes/password-reset/form/post"); -import ResetPasswordRequestPost = require("../routes/password-reset/request/get"); - -import Error401Get = require("../routes/error/401/get"); -import Error403Get = require("../routes/error/403/get"); -import Error404Get = require("../routes/error/404/get"); - -import LoggedIn = require("../routes/loggedin/get"); - -import { ServerVariables } from "../ServerVariables"; -import Endpoints = require("../../../../shared/api"); -import { RequireValidatedFirstFactor } from "./middlewares/RequireValidatedFirstFactor"; - -function setupTotp(app: Express.Application, vars: ServerVariables) { - app.post(Endpoints.SECOND_FACTOR_TOTP_POST, - RequireValidatedFirstFactor.middleware(vars.logger), - TOTPSignGet.default(vars)); - - app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, - RequireValidatedFirstFactor.middleware(vars.logger)); - - app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, - RequireValidatedFirstFactor.middleware(vars.logger)); - - IdentityCheckMiddleware.register(app, - Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, - Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, - new TOTPRegistrationIdentityHandler(vars.logger, - vars.userDataStore, vars.totpHandler, vars.config.totp), - vars); -} - -function setupU2f(app: Express.Application, vars: ServerVariables) { - app.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, - RequireValidatedFirstFactor.middleware(vars.logger), - U2FSignRequestGet.default(vars)); - - app.post(Endpoints.SECOND_FACTOR_U2F_SIGN_POST, - RequireValidatedFirstFactor.middleware(vars.logger), - U2FSignPost.default(vars)); - - app.get(Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, - RequireValidatedFirstFactor.middleware(vars.logger), - U2FRegisterRequestGet.default(vars)); - - app.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, - RequireValidatedFirstFactor.middleware(vars.logger), - U2FRegisterPost.default(vars)); - - app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, - RequireValidatedFirstFactor.middleware(vars.logger)); - - app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, - RequireValidatedFirstFactor.middleware(vars.logger)); - - IdentityCheckMiddleware.register(app, - Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, - Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, - new U2FRegistrationIdentityHandler(vars.logger), vars); -} - -function setupResetPassword(app: Express.Application, vars: ServerVariables) { - IdentityCheckMiddleware.register(app, - Endpoints.RESET_PASSWORD_IDENTITY_START_GET, - Endpoints.RESET_PASSWORD_IDENTITY_FINISH_GET, - new ResetPasswordIdentityHandler(vars.logger, vars.usersDatabase), - vars); - - app.get(Endpoints.RESET_PASSWORD_REQUEST_GET, - ResetPasswordRequestPost.default); - app.post(Endpoints.RESET_PASSWORD_FORM_POST, - ResetPasswordFormPost.default(vars)); -} - -function setupErrors(app: Express.Application, vars: ServerVariables) { - app.get(Endpoints.ERROR_401_GET, Error401Get.default(vars)); - app.get(Endpoints.ERROR_403_GET, Error403Get.default(vars)); - app.get(Endpoints.ERROR_404_GET, Error404Get.default); -} - -export class RestApi { - static setup(app: Express.Application, vars: ServerVariables): void { - app.get(Endpoints.FIRST_FACTOR_GET, FirstFactorGet.default(vars)); - - app.get(Endpoints.SECOND_FACTOR_GET, - RequireValidatedFirstFactor.middleware(vars.logger), - SecondFactorGet.default(vars)); - - app.get(Endpoints.LOGOUT_GET, LogoutGet.default(vars)); - - app.get(Endpoints.VERIFY_GET, VerifyGet.default(vars)); - app.post(Endpoints.FIRST_FACTOR_POST, FirstFactorPost.default(vars)); - - setupTotp(app, vars); - setupU2f(app, vars); - setupResetPassword(app, vars); - setupErrors(app, vars); - - app.get(Endpoints.LOGGED_IN, - RequireValidatedFirstFactor.middleware(vars.logger), - LoggedIn.default(vars)); - } -} diff --git a/themes/black/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts b/themes/black/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts deleted file mode 100644 index ecfd75765..000000000 --- a/themes/black/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts +++ /dev/null @@ -1,27 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import ErrorReplies = require("../../ErrorReplies"); -import { IRequestLogger } from "../../logging/IRequestLogger"; -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import Exceptions = require("../../Exceptions"); -import { Level } from "../../authentication/Level"; - -export class RequireValidatedFirstFactor { - static middleware(logger: IRequestLogger) { - return function (req: Express.Request, res: Express.Response, - next: Express.NextFunction): BluebirdPromise { - - 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.")); - - next(); - resolve(); - }) - .catch(ErrorReplies.replyWithError401(req, res, logger)); - }; - } -} \ No newline at end of file diff --git a/themes/black/server/src/lib/web_server/middlewares/WithHeadersLogged.ts b/themes/black/server/src/lib/web_server/middlewares/WithHeadersLogged.ts deleted file mode 100644 index 139db1141..000000000 --- a/themes/black/server/src/lib/web_server/middlewares/WithHeadersLogged.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Express = require("express"); -import { IRequestLogger } from "../../logging/IRequestLogger"; - -export class WithHeadersLogged { - static middleware(logger: IRequestLogger) { - return function (req: Express.Request, res: Express.Response, - next: Express.NextFunction): void { - logger.debug(req, "Headers = %s", JSON.stringify(req.headers)); - next(); - }; - } -} \ No newline at end of file diff --git a/themes/black/server/src/resources/email-template.ejs b/themes/black/server/src/resources/email-template.ejs old mode 100644 new mode 100755 diff --git a/themes/black/server/src/views/already-logged-in.pug b/themes/black/server/src/views/already-logged-in.pug old mode 100644 new mode 100755 diff --git a/themes/black/server/src/views/errors/.directory b/themes/black/server/src/views/errors/.directory old mode 100644 new mode 100755 diff --git a/themes/black/server/src/views/errors/401.pug b/themes/black/server/src/views/errors/401.pug old mode 100644 new mode 100755 diff --git a/themes/black/server/src/views/errors/403.pug b/themes/black/server/src/views/errors/403.pug old mode 100644 new mode 100755 diff --git a/themes/black/server/src/views/errors/404.pug b/themes/black/server/src/views/errors/404.pug old mode 100644 new mode 100755 diff --git a/themes/black/server/src/views/firstfactor.pug b/themes/black/server/src/views/firstfactor.pug old mode 100644 new mode 100755 diff --git a/themes/black/server/src/views/layout/layout.pug b/themes/black/server/src/views/layout/layout.pug old mode 100644 new mode 100755 diff --git a/themes/black/server/src/views/need-identity-validation.pug b/themes/black/server/src/views/need-identity-validation.pug old mode 100644 new mode 100755 diff --git a/themes/black/server/src/views/password-reset-form.pug b/themes/black/server/src/views/password-reset-form.pug old mode 100644 new mode 100755 diff --git a/themes/black/server/src/views/password-reset-request.pug b/themes/black/server/src/views/password-reset-request.pug old mode 100644 new mode 100755 diff --git a/themes/black/server/src/views/secondfactor.pug b/themes/black/server/src/views/secondfactor.pug old mode 100644 new mode 100755 diff --git a/themes/black/server/src/views/totp-register.pug b/themes/black/server/src/views/totp-register.pug old mode 100644 new mode 100755 diff --git a/themes/black/server/src/views/u2f-register.pug b/themes/black/server/src/views/u2f-register.pug old mode 100644 new mode 100755 diff --git a/themes/black/server/test/requests.ts b/themes/black/server/test/requests.ts deleted file mode 100644 index 93fa0de47..000000000 --- a/themes/black/server/test/requests.ts +++ /dev/null @@ -1,94 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import request = require("request"); -import assert = require("assert"); -import express = require("express"); -import nodemailer = require("nodemailer"); -import Endpoints = require("../../shared/api"); - -declare module "request" { - export interface RequestAPI { - getAsync(uri: string, options?: RequiredUriUrl): BluebirdPromise; - getAsync(uri: string): BluebirdPromise; - getAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; - - postAsync(uri: string, options?: CoreOptions): BluebirdPromise; - postAsync(uri: string): BluebirdPromise; - postAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; - } -} - -const requestAsync: typeof request = BluebirdPromise.promisifyAll(request) as typeof request; - -export = function (port: number) { - const PORT = port; - const BASE_URL = "http://localhost:" + PORT; - - function execute_totp(jar: request.CookieJar, token: string) { - return requestAsync.postAsync({ - url: BASE_URL + Endpoints.SECOND_FACTOR_TOTP_POST, - jar: jar, - form: { - token: token - } - }); - } - - function execute_u2f_authentication(jar: request.CookieJar) { - return requestAsync.getAsync({ - url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, - jar: jar - }) - .then(function (res: request.RequestResponse) { - assert.equal(res.statusCode, 200); - return requestAsync.postAsync({ - url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_POST, - jar: jar, - form: { - } - }); - }); - } - - function execute_verification(jar: request.CookieJar) { - return requestAsync.getAsync({ url: BASE_URL + Endpoints.VERIFY_GET, jar: jar }); - } - - function execute_login(jar: request.CookieJar) { - return requestAsync.getAsync({ url: BASE_URL + Endpoints.FIRST_FACTOR_GET, jar: jar }); - } - - function execute_first_factor(jar: request.CookieJar) { - return requestAsync.postAsync({ - url: BASE_URL + Endpoints.FIRST_FACTOR_POST, - jar: jar, - form: { - username: "test_ok", - password: "password" - } - }); - } - - function execute_failing_first_factor(jar: request.CookieJar) { - return requestAsync.postAsync({ - url: BASE_URL + Endpoints.FIRST_FACTOR_POST, - jar: jar, - form: { - username: "test_nok", - password: "password" - } - }); - } - - return { - login: execute_login, - verify: execute_verification, - u2f_authentication: execute_u2f_authentication, - first_factor: execute_first_factor, - failing_first_factor: execute_failing_first_factor, - totp: execute_totp, - }; -}; - diff --git a/themes/black/server/tsconfig.json b/themes/black/server/tsconfig.json deleted file mode 100644 index ebe98c5ed..000000000 --- a/themes/black/server/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "moduleResolution": "node", - "noImplicitAny": true, - "sourceMap": true, - "removeComments": true, - "outDir": "../dist", - "baseUrl": ".", - "paths": { - "*": [ - "./types/*", - "../shared/types/*" - ] - } - }, - "exclude": [ - "src/**/*.spec.ts" - ] -} diff --git a/themes/black/server/tslint.json b/themes/black/server/tslint.json deleted file mode 100644 index c2c1b7501..000000000 --- a/themes/black/server/tslint.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "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 - } -} diff --git a/themes/black/server/types/.directory b/themes/black/server/types/.directory deleted file mode 100644 index 1e65000ea..000000000 --- a/themes/black/server/types/.directory +++ /dev/null @@ -1,4 +0,0 @@ -[Dolphin] -Timestamp=2018,12,17,20,58,27 -Version=3 -ViewMode=1 diff --git a/themes/black/server/types/AuthenticationSession.ts b/themes/black/server/types/AuthenticationSession.ts deleted file mode 100644 index bbed0e715..000000000 --- a/themes/black/server/types/AuthenticationSession.ts +++ /dev/null @@ -1,18 +0,0 @@ -import U2f = require("u2f"); -import { Level } from "../src/lib/authentication/Level"; - -export interface AuthenticationSession { - userid: string; - authentication_level: Level; - keep_me_logged_in: boolean; - last_activity_datetime: number; - identity_check?: { - challenge: string; - userid: string; - }; - register_request?: U2f.Request; - sign_request?: U2f.Request; - email: string; - groups: string[]; - redirect?: string; -} \ No newline at end of file diff --git a/themes/black/server/types/Dependencies.ts b/themes/black/server/types/Dependencies.ts deleted file mode 100644 index f20404dbc..000000000 --- a/themes/black/server/types/Dependencies.ts +++ /dev/null @@ -1,29 +0,0 @@ -import winston = require("winston"); -import speakeasy = require("speakeasy"); -import nodemailer = require("nodemailer"); -import session = require("express-session"); -import nedb = require("nedb"); -import ldapjs = require("ldapjs"); -import u2f = require("u2f"); -import RedisSession = require("connect-redis"); -import Redis = require("redis"); - -export type Speakeasy = typeof speakeasy; -export type Winston = typeof winston; -export type Session = typeof session; -export type Nedb = typeof nedb; -export type Ldapjs = typeof ldapjs; -export type U2f = typeof u2f; -export type ConnectRedis = typeof RedisSession; -export type Redis = typeof Redis; - -export interface GlobalDependencies { - u2f: U2f; - ldapjs: Ldapjs; - session: Session; - Redis: Redis; - ConnectRedis: ConnectRedis; - winston: Winston; - speakeasy: Speakeasy; - nedb: Nedb; -} \ No newline at end of file diff --git a/themes/black/server/types/Identity.ts b/themes/black/server/types/Identity.ts deleted file mode 100644 index e985984e2..000000000 --- a/themes/black/server/types/Identity.ts +++ /dev/null @@ -1,6 +0,0 @@ - - -export interface Identity { - userid: string; - email: string; -} \ No newline at end of file diff --git a/themes/black/server/types/TOTPSecret.ts b/themes/black/server/types/TOTPSecret.ts deleted file mode 100644 index d6775f2f0..000000000 --- a/themes/black/server/types/TOTPSecret.ts +++ /dev/null @@ -1,11 +0,0 @@ - -export interface TOTPSecret { - ascii: string; - hex: string; - base32: string; - qr_code_ascii: string; - qr_code_hex: string; - qr_code_base32: string; - google_auth_qr: string; - otpauth_url: string; - } \ No newline at end of file diff --git a/themes/black/server/types/U2FRegistration.ts b/themes/black/server/types/U2FRegistration.ts deleted file mode 100644 index b6080af07..000000000 --- a/themes/black/server/types/U2FRegistration.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface U2FRegistration { - keyHandle: string; - publicKey: string; -} \ No newline at end of file diff --git a/themes/black/server/types/dovehash.d.ts b/themes/black/server/types/dovehash.d.ts deleted file mode 100644 index c354609c0..000000000 --- a/themes/black/server/types/dovehash.d.ts +++ /dev/null @@ -1,4 +0,0 @@ - -declare module "dovehash" { - function encode(algo: string, text: string): string; -} \ No newline at end of file diff --git a/themes/black/server/types/speakeasy.d.ts b/themes/black/server/types/speakeasy.d.ts deleted file mode 100644 index 6ea06948b..000000000 --- a/themes/black/server/types/speakeasy.d.ts +++ /dev/null @@ -1,96 +0,0 @@ -declare module "speakeasy" { - export = speakeasy - - interface SharedOptions { - encoding?: string - algorithm?: string - } - - interface DigestOptions extends SharedOptions { - secret: string - counter: number - } - - interface HOTPOptions extends SharedOptions { - secret: string - counter: number - digest?: Buffer - digits?: number - } - - interface HOTPVerifyOptions extends SharedOptions { - secret: string - token: string - counter: number - digits?: number - window?: number - } - - interface TOTPOptions extends SharedOptions { - secret: string - time?: number - step?: number - epoch?: number - counter?: number - digits?: number - } - - interface TOTPVerifyOptions extends SharedOptions { - secret: string - token: string - time?: number - step?: number - epoch?: number - counter?: number - digits?: number - window?: number - } - - interface GenerateSecretOptions { - length?: number - symbols?: boolean - otpauth_url?: boolean - name?: string - issuer?: string - } - - interface GeneratedSecret { - ascii: string - hex: string - base32: string - qr_code_ascii: string - qr_code_hex: string - qr_code_base32: string - google_auth_qr: string - otpauth_url: string - } - - interface OTPAuthURLOptions extends SharedOptions { - secret: string - label: string - type?: string - counter?: number - issuer?: string - digits?: number - period?: number - } - - interface Speakeasy { - digest: (options: DigestOptions) => Buffer - hotp: { - (options: HOTPOptions): string, - verifyDelta: (options: HOTPVerifyOptions) => boolean, - verify: (options: HOTPVerifyOptions) => boolean, - } - totp: { - (options: TOTPOptions): string - verifyDelta: (options: TOTPVerifyOptions) => boolean, - verify: (options: TOTPVerifyOptions) => boolean, - } - generateSecret: (options?: GenerateSecretOptions) => GeneratedSecret - generateSecretASCII: (length?: number, symbols?: boolean) => string - otpauthURL: (options: OTPAuthURLOptions) => string - } - - const speakeasy: Speakeasy -} \ No newline at end of file diff --git a/themes/main/client/src/css/.directory b/themes/default/client/src/css/.directory old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/css/.directory rename to themes/default/client/src/css/.directory diff --git a/themes/main/client/src/css/00-bootstrap.min.css b/themes/default/client/src/css/00-bootstrap.min.css old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/css/00-bootstrap.min.css rename to themes/default/client/src/css/00-bootstrap.min.css diff --git a/themes/main/client/src/css/01-main.css b/themes/default/client/src/css/01-main.css old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/css/01-main.css rename to themes/default/client/src/css/01-main.css diff --git a/themes/main/client/src/css/02-login.css b/themes/default/client/src/css/02-login.css old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/css/02-login.css rename to themes/default/client/src/css/02-login.css diff --git a/themes/main/client/src/css/03-errors.css b/themes/default/client/src/css/03-errors.css old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/css/03-errors.css rename to themes/default/client/src/css/03-errors.css diff --git a/themes/main/client/src/css/03-password-reset-form.css b/themes/default/client/src/css/03-password-reset-form.css old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/css/03-password-reset-form.css rename to themes/default/client/src/css/03-password-reset-form.css diff --git a/themes/main/client/src/css/03-password-reset-request.css b/themes/default/client/src/css/03-password-reset-request.css old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/css/03-password-reset-request.css rename to themes/default/client/src/css/03-password-reset-request.css diff --git a/themes/main/client/src/css/03-totp-register.css b/themes/default/client/src/css/03-totp-register.css old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/css/03-totp-register.css rename to themes/default/client/src/css/03-totp-register.css diff --git a/themes/main/client/src/css/03-u2f-register.css b/themes/default/client/src/css/03-u2f-register.css old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/css/03-u2f-register.css rename to themes/default/client/src/css/03-u2f-register.css diff --git a/themes/main/client/src/img/background.svg b/themes/default/client/src/img/background.svg old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/img/background.svg rename to themes/default/client/src/img/background.svg diff --git a/themes/main/client/src/img/icon.png b/themes/default/client/src/img/icon.png old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/img/icon.png rename to themes/default/client/src/img/icon.png diff --git a/themes/main/client/src/img/mail.png b/themes/default/client/src/img/mail.png old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/img/mail.png rename to themes/default/client/src/img/mail.png diff --git a/themes/main/client/src/img/notifications/.directory b/themes/default/client/src/img/notifications/.directory old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/img/notifications/.directory rename to themes/default/client/src/img/notifications/.directory diff --git a/themes/main/client/src/img/notifications/error.png b/themes/default/client/src/img/notifications/error.png old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/img/notifications/error.png rename to themes/default/client/src/img/notifications/error.png diff --git a/themes/main/client/src/img/notifications/info.png b/themes/default/client/src/img/notifications/info.png old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/img/notifications/info.png rename to themes/default/client/src/img/notifications/info.png diff --git a/themes/main/client/src/img/notifications/success.png b/themes/default/client/src/img/notifications/success.png old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/img/notifications/success.png rename to themes/default/client/src/img/notifications/success.png diff --git a/themes/main/client/src/img/notifications/warning.png b/themes/default/client/src/img/notifications/warning.png old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/img/notifications/warning.png rename to themes/default/client/src/img/notifications/warning.png diff --git a/themes/main/client/src/img/padlock.png b/themes/default/client/src/img/padlock.png old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/img/padlock.png rename to themes/default/client/src/img/padlock.png diff --git a/themes/main/client/src/img/password.png b/themes/default/client/src/img/password.png old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/img/password.png rename to themes/default/client/src/img/password.png diff --git a/themes/main/client/src/img/pendrive.png b/themes/default/client/src/img/pendrive.png old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/img/pendrive.png rename to themes/default/client/src/img/pendrive.png diff --git a/themes/main/client/src/img/stores/.directory b/themes/default/client/src/img/stores/.directory old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/img/stores/.directory rename to themes/default/client/src/img/stores/.directory diff --git a/themes/main/client/src/img/stores/applestore-badge.svg b/themes/default/client/src/img/stores/applestore-badge.svg old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/img/stores/applestore-badge.svg rename to themes/default/client/src/img/stores/applestore-badge.svg diff --git a/themes/main/client/src/img/stores/googleplay-badge.svg b/themes/default/client/src/img/stores/googleplay-badge.svg old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/img/stores/googleplay-badge.svg rename to themes/default/client/src/img/stores/googleplay-badge.svg diff --git a/themes/main/client/src/img/success.png b/themes/default/client/src/img/success.png old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/img/success.png rename to themes/default/client/src/img/success.png diff --git a/themes/main/client/src/img/user.png b/themes/default/client/src/img/user.png old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/img/user.png rename to themes/default/client/src/img/user.png diff --git a/themes/main/client/src/img/warning.png b/themes/default/client/src/img/warning.png old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/img/warning.png rename to themes/default/client/src/img/warning.png diff --git a/themes/main/client/src/thirdparties/qrcode.min.js b/themes/default/client/src/thirdparties/qrcode.min.js old mode 100644 new mode 100755 similarity index 100% rename from themes/main/client/src/thirdparties/qrcode.min.js rename to themes/default/client/src/thirdparties/qrcode.min.js diff --git a/themes/main/server/.directory b/themes/default/server/.directory old mode 100644 new mode 100755 similarity index 100% rename from themes/main/server/.directory rename to themes/default/server/.directory diff --git a/themes/main/server/src/resources/email-template.ejs b/themes/default/server/src/resources/email-template.ejs old mode 100644 new mode 100755 similarity index 100% rename from themes/main/server/src/resources/email-template.ejs rename to themes/default/server/src/resources/email-template.ejs diff --git a/themes/main/server/src/views/already-logged-in.pug b/themes/default/server/src/views/already-logged-in.pug old mode 100644 new mode 100755 similarity index 100% rename from themes/main/server/src/views/already-logged-in.pug rename to themes/default/server/src/views/already-logged-in.pug diff --git a/themes/main/server/src/views/errors/.directory b/themes/default/server/src/views/errors/.directory old mode 100644 new mode 100755 similarity index 100% rename from themes/main/server/src/views/errors/.directory rename to themes/default/server/src/views/errors/.directory diff --git a/themes/main/server/src/views/errors/401.pug b/themes/default/server/src/views/errors/401.pug old mode 100644 new mode 100755 similarity index 100% rename from themes/main/server/src/views/errors/401.pug rename to themes/default/server/src/views/errors/401.pug diff --git a/themes/main/server/src/views/errors/403.pug b/themes/default/server/src/views/errors/403.pug old mode 100644 new mode 100755 similarity index 100% rename from themes/main/server/src/views/errors/403.pug rename to themes/default/server/src/views/errors/403.pug diff --git a/themes/main/server/src/views/errors/404.pug b/themes/default/server/src/views/errors/404.pug old mode 100644 new mode 100755 similarity index 100% rename from themes/main/server/src/views/errors/404.pug rename to themes/default/server/src/views/errors/404.pug diff --git a/themes/main/server/src/views/firstfactor.pug b/themes/default/server/src/views/firstfactor.pug old mode 100644 new mode 100755 similarity index 100% rename from themes/main/server/src/views/firstfactor.pug rename to themes/default/server/src/views/firstfactor.pug diff --git a/themes/main/server/src/views/layout/layout.pug b/themes/default/server/src/views/layout/layout.pug old mode 100644 new mode 100755 similarity index 100% rename from themes/main/server/src/views/layout/layout.pug rename to themes/default/server/src/views/layout/layout.pug diff --git a/themes/main/server/src/views/need-identity-validation.pug b/themes/default/server/src/views/need-identity-validation.pug old mode 100644 new mode 100755 similarity index 100% rename from themes/main/server/src/views/need-identity-validation.pug rename to themes/default/server/src/views/need-identity-validation.pug diff --git a/themes/main/server/src/views/password-reset-form.pug b/themes/default/server/src/views/password-reset-form.pug old mode 100644 new mode 100755 similarity index 100% rename from themes/main/server/src/views/password-reset-form.pug rename to themes/default/server/src/views/password-reset-form.pug diff --git a/themes/main/server/src/views/password-reset-request.pug b/themes/default/server/src/views/password-reset-request.pug old mode 100644 new mode 100755 similarity index 100% rename from themes/main/server/src/views/password-reset-request.pug rename to themes/default/server/src/views/password-reset-request.pug diff --git a/themes/main/server/src/views/secondfactor.pug b/themes/default/server/src/views/secondfactor.pug old mode 100644 new mode 100755 similarity index 100% rename from themes/main/server/src/views/secondfactor.pug rename to themes/default/server/src/views/secondfactor.pug diff --git a/themes/main/server/src/views/totp-register.pug b/themes/default/server/src/views/totp-register.pug old mode 100644 new mode 100755 similarity index 100% rename from themes/main/server/src/views/totp-register.pug rename to themes/default/server/src/views/totp-register.pug diff --git a/themes/main/server/src/views/u2f-register.pug b/themes/default/server/src/views/u2f-register.pug old mode 100644 new mode 100755 similarity index 100% rename from themes/main/server/src/views/u2f-register.pug rename to themes/default/server/src/views/u2f-register.pug diff --git a/themes/main/client/src/index.ts b/themes/main/client/src/index.ts deleted file mode 100644 index 6c22d17c7..000000000 --- a/themes/main/client/src/index.ts +++ /dev/null @@ -1,34 +0,0 @@ - -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 () { - (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); -})(); diff --git a/themes/main/client/src/lib/GetPromised.ts b/themes/main/client/src/lib/GetPromised.ts deleted file mode 100644 index 779139654..000000000 --- a/themes/main/client/src/lib/GetPromised.ts +++ /dev/null @@ -1,14 +0,0 @@ -import BluebirdPromise = require("bluebird"); - -export default function ($: JQueryStatic, url: string, data: Object, fn: any, - dataType: string): BluebirdPromise { - return new BluebirdPromise((resolve, reject) => { - $.get(url, {}, undefined, dataType) - .done((data: any) => { - resolve(data); - }) - .fail((xhr: JQueryXHR, textStatus: string) => { - reject(textStatus); - }); - }); -} \ No newline at end of file diff --git a/themes/main/client/src/lib/INotifier.ts b/themes/main/client/src/lib/INotifier.ts deleted file mode 100644 index df947538f..000000000 --- a/themes/main/client/src/lib/INotifier.ts +++ /dev/null @@ -1,14 +0,0 @@ - -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; -} \ No newline at end of file diff --git a/themes/main/client/src/lib/Notifier.ts b/themes/main/client/src/lib/Notifier.ts deleted file mode 100644 index c0252b9b9..000000000 --- a/themes/main/client/src/lib/Notifier.ts +++ /dev/null @@ -1,83 +0,0 @@ - - -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('status %s\ - %s', 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); - } -} \ No newline at end of file diff --git a/themes/main/client/src/lib/QueryParametersRetriever.ts b/themes/main/client/src/lib/QueryParametersRetriever.ts deleted file mode 100644 index a529adb6e..000000000 --- a/themes/main/client/src/lib/QueryParametersRetriever.ts +++ /dev/null @@ -1,12 +0,0 @@ - -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, " ")); - } -} \ No newline at end of file diff --git a/themes/main/client/src/lib/SafeRedirect.ts b/themes/main/client/src/lib/SafeRedirect.ts deleted file mode 100644 index 7e7684b8a..000000000 --- a/themes/main/client/src/lib/SafeRedirect.ts +++ /dev/null @@ -1,10 +0,0 @@ -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(); -} \ No newline at end of file diff --git a/themes/main/client/src/lib/firstfactor/FirstFactorValidator.ts b/themes/main/client/src/lib/firstfactor/FirstFactorValidator.ts deleted file mode 100644 index eaa496fdd..000000000 --- a/themes/main/client/src/lib/firstfactor/FirstFactorValidator.ts +++ /dev/null @@ -1,46 +0,0 @@ - -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 { - return new BluebirdPromise(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)); - }); - }); -} diff --git a/themes/main/client/src/lib/firstfactor/UISelectors.ts b/themes/main/client/src/lib/firstfactor/UISelectors.ts deleted file mode 100644 index 0e971b3c3..000000000 --- a/themes/main/client/src/lib/firstfactor/UISelectors.ts +++ /dev/null @@ -1,5 +0,0 @@ - -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"; diff --git a/themes/main/client/src/lib/firstfactor/index.ts b/themes/main/client/src/lib/firstfactor/index.ts deleted file mode 100644 index 24affee2d..000000000 --- a/themes/main/client/src/lib/firstfactor/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -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); - }); -} - diff --git a/themes/main/client/src/lib/reset-password/constants.ts b/themes/main/client/src/lib/reset-password/constants.ts deleted file mode 100644 index d48d4e67d..000000000 --- a/themes/main/client/src/lib/reset-password/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export const FORM_SELECTOR = ".form-signin"; \ No newline at end of file diff --git a/themes/main/client/src/lib/reset-password/reset-password-form.ts b/themes/main/client/src/lib/reset-password/reset-password-form.ts deleted file mode 100644 index b94279cde..000000000 --- a/themes/main/client/src/lib/reset-password/reset-password-form.ts +++ /dev/null @@ -1,57 +0,0 @@ -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); - }); -} diff --git a/themes/main/client/src/lib/reset-password/reset-password-request.ts b/themes/main/client/src/lib/reset-password/reset-password-request.ts deleted file mode 100644 index 846226d75..000000000 --- a/themes/main/client/src/lib/reset-password/reset-password-request.ts +++ /dev/null @@ -1,56 +0,0 @@ - -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); - }); -} - diff --git a/themes/main/client/src/lib/secondfactor/TOTPValidator.ts b/themes/main/client/src/lib/secondfactor/TOTPValidator.ts deleted file mode 100644 index 5394139a6..000000000 --- a/themes/main/client/src/lib/secondfactor/TOTPValidator.ts +++ /dev/null @@ -1,28 +0,0 @@ - -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 { - return new BluebirdPromise(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)); - }); - }); -} \ No newline at end of file diff --git a/themes/main/client/src/lib/secondfactor/U2FValidator.ts b/themes/main/client/src/lib/secondfactor/U2FValidator.ts deleted file mode 100644 index 5812922f7..000000000 --- a/themes/main/client/src/lib/secondfactor/U2FValidator.ts +++ /dev/null @@ -1,42 +0,0 @@ -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 { - return new BluebirdPromise(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 { - 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, $); - }); -} diff --git a/themes/main/client/src/lib/secondfactor/constants.ts b/themes/main/client/src/lib/secondfactor/constants.ts deleted file mode 100644 index 50bba7571..000000000 --- a/themes/main/client/src/lib/secondfactor/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ - -export const TOTP_FORM_SELECTOR = ".form-signin.totp"; -export const TOTP_TOKEN_SELECTOR = ".form-signin #token"; diff --git a/themes/main/client/src/lib/secondfactor/index.ts b/themes/main/client/src/lib/secondfactor/index.ts deleted file mode 100644 index 279723dce..000000000 --- a/themes/main/client/src/lib/secondfactor/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -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); - }); -} \ No newline at end of file diff --git a/themes/main/client/src/lib/totp-register/totp-register.ts b/themes/main/client/src/lib/totp-register/totp-register.ts deleted file mode 100644 index 6a9aa7ee0..000000000 --- a/themes/main/client/src/lib/totp-register/totp-register.ts +++ /dev/null @@ -1,11 +0,0 @@ - -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); -} diff --git a/themes/main/client/src/lib/totp-register/ui-selector.ts b/themes/main/client/src/lib/totp-register/ui-selector.ts deleted file mode 100644 index 9d43fabea..000000000 --- a/themes/main/client/src/lib/totp-register/ui-selector.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export const QRCODE_ID_SELECTOR = "#qrcode"; \ No newline at end of file diff --git a/themes/main/client/src/lib/u2f-register/u2f-register.ts b/themes/main/client/src/lib/u2f-register/u2f-register.ts deleted file mode 100644 index abf40ee05..000000000 --- a/themes/main/client/src/lib/u2f-register/u2f-register.ts +++ /dev/null @@ -1,56 +0,0 @@ - -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 { - return new BluebirdPromise(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 { - 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); - }); - }); -} diff --git a/themes/main/client/test/Notifier.test.ts b/themes/main/client/test/Notifier.test.ts deleted file mode 100644 index 70bfea146..000000000 --- a/themes/main/client/test/Notifier.test.ts +++ /dev/null @@ -1,71 +0,0 @@ - -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"); - }); -}); \ No newline at end of file diff --git a/themes/main/client/test/firstfactor/FirstFactorValidator.test.ts b/themes/main/client/test/firstfactor/FirstFactorValidator.test.ts deleted file mode 100644 index 027bc71d8..000000000 --- a/themes/main/client/test/firstfactor/FirstFactorValidator.test.ts +++ /dev/null @@ -1,46 +0,0 @@ - -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."); - }); - }); -}); \ No newline at end of file diff --git a/themes/main/client/test/mocks/NotifierStub.ts b/themes/main/client/test/mocks/NotifierStub.ts deleted file mode 100644 index 9c268d66d..000000000 --- a/themes/main/client/test/mocks/NotifierStub.ts +++ /dev/null @@ -1,33 +0,0 @@ - -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(); - } -} \ No newline at end of file diff --git a/themes/main/client/test/mocks/jquery.ts b/themes/main/client/test/mocks/jquery.ts deleted file mode 100644 index 273f90861..000000000 --- a/themes/main/client/test/mocks/jquery.ts +++ /dev/null @@ -1,59 +0,0 @@ - -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() - }; -} diff --git a/themes/main/client/test/mocks/u2f-api.ts b/themes/main/client/test/mocks/u2f-api.ts deleted file mode 100644 index d123f6a95..000000000 --- a/themes/main/client/test/mocks/u2f-api.ts +++ /dev/null @@ -1,14 +0,0 @@ - -import sinon = require("sinon"); - -export interface U2FApiMock { - sign: sinon.SinonStub; - register: sinon.SinonStub; -} - -export function U2FApiMock(): U2FApiMock { - return { - sign: sinon.stub(), - register: sinon.stub() - }; -} \ No newline at end of file diff --git a/themes/main/client/test/secondfactor/TOTPValidator.test.ts b/themes/main/client/test/secondfactor/TOTPValidator.test.ts deleted file mode 100644 index 5dd6f15c3..000000000 --- a/themes/main/client/test/secondfactor/TOTPValidator.test.ts +++ /dev/null @@ -1,37 +0,0 @@ - -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(); - }); - }); -}); \ No newline at end of file diff --git a/themes/main/client/test/totp-register/totp-register.test.ts b/themes/main/client/test/totp-register/totp-register.test.ts deleted file mode 100644 index 86fc455a1..000000000 --- a/themes/main/client/test/totp-register/totp-register.test.ts +++ /dev/null @@ -1,31 +0,0 @@ - -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); - }); -}); \ No newline at end of file diff --git a/themes/main/client/tsconfig.json b/themes/main/client/tsconfig.json deleted file mode 100644 index 0bb4d62ff..000000000 --- a/themes/main/client/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "moduleResolution": "node", - "noImplicitAny": true, - "sourceMap": true, - "removeComments": true, - "outDir": "../dist", - "baseUrl": ".", - "paths": { - "*": [ - "./types/*", - "../shared/types/*" - ] - } - }, - "include": [ - "src/**/*" - ], - "exclude": [ - "test/**/*" - ] -} diff --git a/themes/main/client/tslint.json b/themes/main/client/tslint.json deleted file mode 100644 index c2c1b7501..000000000 --- a/themes/main/client/tslint.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "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 - } -} diff --git a/themes/main/server/src/index.ts b/themes/main/server/src/index.ts deleted file mode 100755 index fcbf4d022..000000000 --- a/themes/main/server/src/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -#! /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 "); - 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); diff --git a/themes/main/server/src/lib/.directory b/themes/main/server/src/lib/.directory deleted file mode 100644 index 006b379ad..000000000 --- a/themes/main/server/src/lib/.directory +++ /dev/null @@ -1,4 +0,0 @@ -[Dolphin] -Timestamp=2018,12,17,20,59,13 -Version=3 -ViewMode=1 diff --git a/themes/main/server/src/lib/AuthenticationSessionHandler.ts b/themes/main/server/src/lib/AuthenticationSessionHandler.ts deleted file mode 100644 index 57361bf8a..000000000 --- a/themes/main/server/src/lib/AuthenticationSessionHandler.ts +++ /dev/null @@ -1,45 +0,0 @@ - - -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; - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/ErrorReplies.ts b/themes/main/server/src/lib/ErrorReplies.ts deleted file mode 100644 index f1c5f4fd1..000000000 --- a/themes/main/server/src/lib/ErrorReplies.ts +++ /dev/null @@ -1,49 +0,0 @@ -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 }); -} \ No newline at end of file diff --git a/themes/main/server/src/lib/Exceptions.ts b/themes/main/server/src/lib/Exceptions.ts deleted file mode 100644 index 83fa4eb6b..000000000 --- a/themes/main/server/src/lib/Exceptions.ts +++ /dev/null @@ -1,88 +0,0 @@ - -export class LdapSearchError extends Error { - constructor(message?: string) { - super(message); - this.name = "LdapSearchError"; - (Object).setPrototypeOf(this, LdapSearchError.prototype); - } -} - -export class LdapBindError extends Error { - constructor(message?: string) { - super(message); - this.name = "LdapBindError"; - (Object).setPrototypeOf(this, LdapBindError.prototype); - } -} - -export class LdapError extends Error { - constructor(message?: string) { - super(message); - this.name = "LdapError"; - (Object).setPrototypeOf(this, LdapError.prototype); - } -} - -export class IdentityError extends Error { - constructor(message?: string) { - super(message); - this.name = "IdentityError"; - (Object).setPrototypeOf(this, IdentityError.prototype); - } -} - -export class AccessDeniedError extends Error { - constructor(message?: string) { - super(message); - this.name = "AccessDeniedError"; - (Object).setPrototypeOf(this, AccessDeniedError.prototype); - } -} - -export class AuthenticationRegulationError extends Error { - constructor(message?: string) { - super(message); - this.name = "AuthenticationRegulationError"; - (Object).setPrototypeOf(this, AuthenticationRegulationError.prototype); - } -} - -export class InvalidTOTPError extends Error { - constructor(message?: string) { - super(message); - this.name = "InvalidTOTPError"; - (Object).setPrototypeOf(this, InvalidTOTPError.prototype); - } -} - -export class NotAuthenticatedError extends Error { - constructor(message?: string) { - super(message); - this.name = "NotAuthenticatedError"; - (Object).setPrototypeOf(this, NotAuthenticatedError.prototype); - } -} - -export class NotAuthorizedError extends Error { - constructor(message?: string) { - super(message); - this.name = "NotAuthanticatedError"; - (Object).setPrototypeOf(this, NotAuthorizedError.prototype); - } -} - -export class FirstFactorValidationError extends Error { - constructor(message?: string) { - super(message); - this.name = "FirstFactorValidationError"; - (Object).setPrototypeOf(this, FirstFactorValidationError.prototype); - } -} - -export class SecondFactorValidationError extends Error { - constructor(message?: string) { - super(message); - this.name = "SecondFactorValidationError"; - (Object).setPrototypeOf(this, FirstFactorValidationError.prototype); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/FirstFactorValidator.ts b/themes/main/server/src/lib/FirstFactorValidator.ts deleted file mode 100644 index 231060002..000000000 --- a/themes/main/server/src/lib/FirstFactorValidator.ts +++ /dev/null @@ -1,20 +0,0 @@ - -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 { - 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(); - }); -} \ No newline at end of file diff --git a/themes/main/server/src/lib/IdentityCheckMiddleware.spec.ts b/themes/main/server/src/lib/IdentityCheckMiddleware.spec.ts deleted file mode 100644 index 842ed6bcb..000000000 --- a/themes/main/server/src/lib/IdentityCheckMiddleware.spec.ts +++ /dev/null @@ -1,176 +0,0 @@ - -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")); - }); - }); - }); -}); diff --git a/themes/main/server/src/lib/IdentityCheckMiddleware.ts b/themes/main/server/src/lib/IdentityCheckMiddleware.ts deleted file mode 100644 index e72ea4db1..000000000 --- a/themes/main/server/src/lib/IdentityCheckMiddleware.ts +++ /dev/null @@ -1,138 +0,0 @@ -import objectPath = require("object-path"); -import randomstring = require("randomstring"); -import BluebirdPromise = require("bluebird"); -import util = require("util"); -import Exceptions = require("./Exceptions"); -import fs = require("fs"); -import ejs = require("ejs"); -import { 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 { - - 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 { - 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 { - 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 { - - let authSession: AuthenticationSession; - const identityToken = objectPath.get( - 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 { - 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)); - }; -} diff --git a/themes/main/server/src/lib/IdentityCheckPreValidationTemplate.ts b/themes/main/server/src/lib/IdentityCheckPreValidationTemplate.ts deleted file mode 100644 index 0161ce406..000000000 --- a/themes/main/server/src/lib/IdentityCheckPreValidationTemplate.ts +++ /dev/null @@ -1,3 +0,0 @@ - - -export const PRE_VALIDATION_TEMPLATE = "need-identity-validation"; \ No newline at end of file diff --git a/themes/main/server/src/lib/IdentityValidable.ts b/themes/main/server/src/lib/IdentityValidable.ts deleted file mode 100644 index 075580c9e..000000000 --- a/themes/main/server/src/lib/IdentityValidable.ts +++ /dev/null @@ -1,19 +0,0 @@ -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; - postValidationInit(req: Express.Request): Bluebird; - - // 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; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/IdentityValidableStub.spec.ts b/themes/main/server/src/lib/IdentityValidableStub.spec.ts deleted file mode 100644 index 20a977140..000000000 --- a/themes/main/server/src/lib/IdentityValidableStub.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ - -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 { - return this.preValidationInitStub(req); - } - - postValidationInit(req: Express.Request): Bluebird { - 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(); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/Server.spec.ts b/themes/main/server/src/lib/Server.spec.ts deleted file mode 100644 index 365163254..000000000 --- a/themes/main/server/src/lib/Server.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ - -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(); - }); - }); -}); diff --git a/themes/main/server/src/lib/Server.ts b/themes/main/server/src/lib/Server.ts deleted file mode 100644 index 4090f6294..000000000 --- a/themes/main/server/src/lib/Server.ts +++ /dev/null @@ -1,93 +0,0 @@ -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 { - 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((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 { - 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(); - } -} - diff --git a/themes/main/server/src/lib/ServerVariables.ts b/themes/main/server/src/lib/ServerVariables.ts deleted file mode 100644 index cd3dd6dc8..000000000 --- a/themes/main/server/src/lib/ServerVariables.ts +++ /dev/null @@ -1,21 +0,0 @@ -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; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/ServerVariablesInitializer.ts b/themes/main/server/src/lib/ServerVariablesInitializer.ts deleted file mode 100644 index df79238cc..000000000 --- a/themes/main/server/src/lib/ServerVariablesInitializer.ts +++ /dev/null @@ -1,116 +0,0 @@ - -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 { - 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 { - - 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); - }); - } -} diff --git a/themes/main/server/src/lib/ServerVariablesMockBuilder.spec.ts b/themes/main/server/src/lib/ServerVariablesMockBuilder.spec.ts deleted file mode 100644 index 7874702a0..000000000 --- a/themes/main/server/src/lib/ServerVariablesMockBuilder.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -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 - }; - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/Level.ts b/themes/main/server/src/lib/authentication/Level.ts deleted file mode 100644 index 57b6a2346..000000000 --- a/themes/main/server/src/lib/authentication/Level.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum Level { - NOT_AUTHENTICATED = 0, - ONE_FACTOR = 1, - TWO_FACTOR = 2 -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/GroupsAndEmails.ts b/themes/main/server/src/lib/authentication/backends/GroupsAndEmails.ts deleted file mode 100644 index 3434ba66f..000000000 --- a/themes/main/server/src/lib/authentication/backends/GroupsAndEmails.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface GroupsAndEmails { - groups: string[]; - emails: string[]; -} diff --git a/themes/main/server/src/lib/authentication/backends/IUsersDatabase.ts b/themes/main/server/src/lib/authentication/backends/IUsersDatabase.ts deleted file mode 100644 index d7fa13b7a..000000000 --- a/themes/main/server/src/lib/authentication/backends/IUsersDatabase.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Bluebird = require("bluebird"); - -import { GroupsAndEmails } from "./GroupsAndEmails"; - -export interface IUsersDatabase { - checkUserPassword(username: string, password: string): Bluebird; - getEmails(username: string): Bluebird; - getGroups(username: string): Bluebird; - updatePassword(username: string, newPassword: string): Bluebird; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts b/themes/main/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts deleted file mode 100644 index 19341a5dd..000000000 --- a/themes/main/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -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 { - return this.checkUserPasswordStub(username, password); - } - - getEmails(username: string): Bluebird { - return this.getEmailsStub(username); - } - - getGroups(username: string): Bluebird { - return this.getGroupsStub(username); - } - - updatePassword(username: string, newPassword: string): Bluebird { - return this.updatePasswordStub(username, newPassword); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts b/themes/main/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts deleted file mode 100644 index a258a78f7..000000000 --- a/themes/main/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts +++ /dev/null @@ -1,224 +0,0 @@ -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()); - }); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/file/FileUsersDatabase.ts b/themes/main/server/src/lib/authentication/backends/file/FileUsersDatabase.ts deleted file mode 100644 index d34dde21e..000000000 --- a/themes/main/server/src/lib/authentication/backends/file/FileUsersDatabase.ts +++ /dev/null @@ -1,182 +0,0 @@ -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 { - return new Bluebird((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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - return this.readDatabase() - .then((database) => { - return this.checkUserExists(database, username) - .then(() => this.retrieveEmails(database, username)); - }); - } - - getGroups(username: string): Bluebird { - return this.readDatabase() - .then((database) => { - return this.checkUserExists(database, username) - .then(() => this.retrieveGroups(database, username)); - }); - } - - updatePassword(username: string, newPassword: string): Bluebird { - return this.readDatabase() - .then((database) => { - return this.checkUserExists(database, username) - .then(() => this.replacePassword(database, username, newPassword)); - }); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/file/ReadWriteQueue.ts b/themes/main/server/src/lib/authentication/backends/file/ReadWriteQueue.ts deleted file mode 100644 index 957ddaec5..000000000 --- a/themes/main/server/src/lib/authentication/backends/file/ReadWriteQueue.ts +++ /dev/null @@ -1,60 +0,0 @@ -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(); - } - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/ISession.ts b/themes/main/server/src/lib/authentication/backends/ldap/ISession.ts deleted file mode 100644 index da2c74433..000000000 --- a/themes/main/server/src/lib/authentication/backends/ldap/ISession.ts +++ /dev/null @@ -1,12 +0,0 @@ - -import BluebirdPromise = require("bluebird"); - -export interface ISession { - open(): BluebirdPromise; - close(): BluebirdPromise; - - searchUserDn(username: string): BluebirdPromise; - searchEmails(username: string): BluebirdPromise; - searchGroups(username: string): BluebirdPromise; - modifyPassword(username: string, newPassword: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/ISessionFactory.ts b/themes/main/server/src/lib/authentication/backends/ldap/ISessionFactory.ts deleted file mode 100644 index 014d1eea5..000000000 --- a/themes/main/server/src/lib/authentication/backends/ldap/ISessionFactory.ts +++ /dev/null @@ -1,6 +0,0 @@ - -import { ISession } from "./ISession"; - -export interface ISessionFactory { - create(userDN: string, password: string): ISession; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts b/themes/main/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts deleted file mode 100644 index f4a6e6306..000000000 --- a/themes/main/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts +++ /dev/null @@ -1,386 +0,0 @@ -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); - }) - }); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts b/themes/main/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts deleted file mode 100644 index edda62ec6..000000000 --- a/themes/main/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts +++ /dev/null @@ -1,107 +0,0 @@ -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 = (session: ISession) => Bluebird; - -export class LdapUsersDatabase implements IUsersDatabase { - private sessionFactory: ISessionFactory; - private configuration: LdapConfiguration; - - constructor( - sessionFactory: ISessionFactory, - configuration: LdapConfiguration) { - this.sessionFactory = sessionFactory; - this.configuration = configuration; - } - - private withSession( - username: string, - password: string, - cb: SessionCallback): Bluebird { - const session = this.sessionFactory.create(username, password); - return session.open() - .then(() => cb(session)) - .finally(() => session.close()); - } - - checkUserPassword(username: string, password: string): Bluebird { - const that = this; - function verifyUserPassword(userDN: string) { - return that.withSession( - 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 { - 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 { - 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 { - 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)); - }); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts b/themes/main/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts deleted file mode 100644 index 9dedfcb7c..000000000 --- a/themes/main/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -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"); - }); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/SafeSession.ts b/themes/main/server/src/lib/authentication/backends/ldap/SafeSession.ts deleted file mode 100644 index 572209066..000000000 --- a/themes/main/server/src/lib/authentication/backends/ldap/SafeSession.ts +++ /dev/null @@ -1,62 +0,0 @@ -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 { - return this.sesion.open(); - } - - close(): BluebirdPromise { - return this.sesion.close(); - } - - searchGroups(username: string): BluebirdPromise { - 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 { - 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 { - 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 { - try { - const sanitizedUsername = Sanitizer.sanitize(username); - return this.sesion.modifyPassword(sanitizedUsername, newPassword); - } - catch (e) { - return BluebirdPromise.reject(new Error(SPECIAL_CHAR_USED_MESSAGE)); - } - } -} diff --git a/themes/main/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts b/themes/main/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts deleted file mode 100644 index 9dd33fed4..000000000 --- a/themes/main/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -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 { 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); - }); -}); diff --git a/themes/main/server/src/lib/authentication/backends/ldap/Sanitizer.ts b/themes/main/server/src/lib/authentication/backends/ldap/Sanitizer.ts deleted file mode 100644 index be74132ad..000000000 --- a/themes/main/server/src/lib/authentication/backends/ldap/Sanitizer.ts +++ /dev/null @@ -1,25 +0,0 @@ - -// 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; - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/Session.spec.ts b/themes/main/server/src/lib/authentication/backends/ldap/Session.spec.ts deleted file mode 100644 index d55f6a805..000000000 --- a/themes/main/server/src/lib/authentication/backends/ldap/Session.spec.ts +++ /dev/null @@ -1,127 +0,0 @@ - -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"]); - }); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/Session.ts b/themes/main/server/src/lib/authentication/backends/ldap/Session.ts deleted file mode 100644 index e0284b3c4..000000000 --- a/themes/main/server/src/lib/authentication/backends/ldap/Session.ts +++ /dev/null @@ -1,156 +0,0 @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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(); - }); - } -} diff --git a/themes/main/server/src/lib/authentication/backends/ldap/SessionFactory.ts b/themes/main/server/src/lib/authentication/backends/ldap/SessionFactory.ts deleted file mode 100644 index 0b6c4bff3..000000000 --- a/themes/main/server/src/lib/authentication/backends/ldap/SessionFactory.ts +++ /dev/null @@ -1,37 +0,0 @@ -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 - ) - ); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts b/themes/main/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts deleted file mode 100644 index face3930d..000000000 --- a/themes/main/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts b/themes/main/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts deleted file mode 100644 index 5faf2ba18..000000000 --- a/themes/main/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -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 { - return this.openStub(); - } - - close(): Bluebird { - return this.closeStub(); - } - - searchUserDn(username: string): Bluebird { - return this.searchUserDnStub(username); - } - - searchEmails(username: string): Bluebird { - return this.searchEmailsStub(username); - } - - searchGroups(username: string): Bluebird { - return this.searchGroupsStub(username); - } - - modifyPassword(username: string, newPassword: string): Bluebird { - return this.modifyPasswordStub(username, newPassword); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/connector/Connector.ts b/themes/main/server/src/lib/authentication/backends/ldap/connector/Connector.ts deleted file mode 100644 index 2542ea7f9..000000000 --- a/themes/main/server/src/lib/authentication/backends/ldap/connector/Connector.ts +++ /dev/null @@ -1,69 +0,0 @@ -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; - unbindAsync(): Bluebird; - searchAsync(base: string, query: LdapJs.SearchOptions): Bluebird; - modifyAsync(userdn: string, change: LdapJs.Change): Bluebird; -} - -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 { - return this.client.bindAsync(username, password); - } - - unbindAsync(): Bluebird { - return this.client.unbindAsync(); - } - - searchAsync(base: string, query: any): Bluebird { - const that = this; - return this.client.searchAsync(base, query) - .then(function (res: EventEmitter) { - const doc: SearchEntry[] = []; - return new Bluebird((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 { - return this.client.modifyAsync(dn, changeRequest); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts b/themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts deleted file mode 100644 index 61fef07a4..000000000 --- a/themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts +++ /dev/null @@ -1,18 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts b/themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts deleted file mode 100644 index d11fa6386..000000000 --- a/themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -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(); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts b/themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts deleted file mode 100644 index 0b78225bf..000000000 --- a/themes/main/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -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 { - return this.bindAsyncStub(username, password); - } - - unbindAsync(): BluebirdPromise { - return this.unbindAsyncStub(); - } - - searchAsync(base: string, query: any): BluebirdPromise { - return this.searchAsyncStub(base, query); - } - - modifyAsync(dn: string, changeRequest: any): BluebirdPromise { - return this.modifyAsyncStub(dn, changeRequest); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/connector/IConnector.ts b/themes/main/server/src/lib/authentication/backends/ldap/connector/IConnector.ts deleted file mode 100644 index 1e63ab193..000000000 --- a/themes/main/server/src/lib/authentication/backends/ldap/connector/IConnector.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Bluebird = require("bluebird"); -import EventEmitter = require("events"); - -export interface IConnector { - bindAsync(username: string, password: string): Bluebird; - unbindAsync(): Bluebird; - searchAsync(base: string, query: any): Bluebird; - modifyAsync(dn: string, changeRequest: any): Bluebird; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts b/themes/main/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts deleted file mode 100644 index f9ed65eff..000000000 --- a/themes/main/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { IConnector } from "./IConnector"; - -export interface IConnectorFactory { - create(): IConnector; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/totp/ITotpHandler.ts b/themes/main/server/src/lib/authentication/totp/ITotpHandler.ts deleted file mode 100644 index d600d31e4..000000000 --- a/themes/main/server/src/lib/authentication/totp/ITotpHandler.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { TOTPSecret } from "../../../../types/TOTPSecret"; - -export interface ITotpHandler { - generate(label: string, issuer: string): TOTPSecret; - validate(token: string, secret: string): boolean; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/totp/TotpHandler.spec.ts b/themes/main/server/src/lib/authentication/totp/TotpHandler.spec.ts deleted file mode 100644 index 67cffa638..000000000 --- a/themes/main/server/src/lib/authentication/totp/TotpHandler.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ - -import { TotpHandler } from "./TotpHandler"; -import Sinon = require("sinon"); -import Speakeasy = require("speakeasy"); -import Assert = require("assert"); - -describe("authentication/totp/TotpHandler", function() { - let totpValidator: TotpHandler; - let validateStub: Sinon.SinonStub; - - beforeEach(() => { - validateStub = Sinon.stub(Speakeasy.totp, "verify"); - totpValidator = new TotpHandler(Speakeasy); - }); - - afterEach(function() { - validateStub.restore(); - }); - - it("should validate the TOTP token", function() { - const totp_secret = "NBD2ZV64R9UV1O7K"; - const token = "token"; - validateStub.withArgs({ - secret: totp_secret, - token: token, - encoding: "base32", - window: 1 - }).returns(true); - Assert(totpValidator.validate(token, totp_secret)); - }); - - it("should not validate a wrong TOTP token", function() { - const totp_secret = "NBD2ZV64R9UV1O7K"; - const token = "wrong token"; - validateStub.returns(false); - Assert(!totpValidator.validate(token, totp_secret)); - }); -}); - diff --git a/themes/main/server/src/lib/authentication/totp/TotpHandler.ts b/themes/main/server/src/lib/authentication/totp/TotpHandler.ts deleted file mode 100644 index dfab502a7..000000000 --- a/themes/main/server/src/lib/authentication/totp/TotpHandler.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ITotpHandler } from "./ITotpHandler"; -import { TOTPSecret } from "../../../../types/TOTPSecret"; -import Speakeasy = require("speakeasy"); - -const TOTP_ENCODING = "base32"; -const WINDOW: number = 1; - -export class TotpHandler implements ITotpHandler { - private speakeasy: typeof Speakeasy; - - constructor(speakeasy: typeof Speakeasy) { - this.speakeasy = speakeasy; - } - - generate(label: string, issuer: string): TOTPSecret { - const secret = this.speakeasy.generateSecret({ - otpauth_url: false - }) as TOTPSecret; - - secret.otpauth_url = this.speakeasy.otpauthURL({ - secret: secret.ascii, - label: label, - issuer: issuer - }); - return secret; - } - - validate(token: string, secret: string): boolean { - return this.speakeasy.totp.verify({ - secret: secret, - encoding: TOTP_ENCODING, - token: token, - window: WINDOW - }); - } -} diff --git a/themes/main/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts b/themes/main/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts deleted file mode 100644 index ea93330d9..000000000 --- a/themes/main/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import { ITotpHandler } from "./ITotpHandler"; -import { TOTPSecret } from "../../../../types/TOTPSecret"; - -export class TotpHandlerStub implements ITotpHandler { - generateStub: Sinon.SinonStub; - validateStub: Sinon.SinonStub; - - constructor() { - this.generateStub = Sinon.stub(); - this.validateStub = Sinon.stub(); - } - - generate(label: string, issuer: string): TOTPSecret { - return this.generateStub(label, issuer); - } - - validate(token: string, secret: string): boolean { - return this.validateStub(token, secret); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/u2f/IU2fHandler.ts b/themes/main/server/src/lib/authentication/u2f/IU2fHandler.ts deleted file mode 100644 index b9b7d6f26..000000000 --- a/themes/main/server/src/lib/authentication/u2f/IU2fHandler.ts +++ /dev/null @@ -1,9 +0,0 @@ -import U2f = require("u2f"); - -export interface IU2fHandler { - request(appId: string, keyHandle?: string): U2f.Request; - checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) - : U2f.RegistrationResult | U2f.Error; - checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) - : U2f.SignatureResult | U2f.Error; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authentication/u2f/U2fHandler.ts b/themes/main/server/src/lib/authentication/u2f/U2fHandler.ts deleted file mode 100644 index bf3891e5b..000000000 --- a/themes/main/server/src/lib/authentication/u2f/U2fHandler.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { IU2fHandler } from "./IU2fHandler"; -import U2f = require("u2f"); - -export class U2fHandler implements IU2fHandler { - private u2f: typeof U2f; - - constructor(u2f: typeof U2f) { - this.u2f = u2f; - } - - request(appId: string, keyHandle?: string): U2f.Request { - return this.u2f.request(appId, keyHandle); - } - - checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) - : U2f.RegistrationResult | U2f.Error { - return this.u2f.checkRegistration(registrationRequest, registrationResponse); - } - - checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) - : U2f.SignatureResult | U2f.Error { - return this.u2f.checkSignature(signatureRequest, signatureResponse, publicKey); - } -} diff --git a/themes/main/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts b/themes/main/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts deleted file mode 100644 index 135d7eb06..000000000 --- a/themes/main/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import U2f = require("u2f"); -import { IU2fHandler } from "./IU2fHandler"; - - -export class U2fHandlerStub implements IU2fHandler { - requestStub: Sinon.SinonStub; - checkRegistrationStub: Sinon.SinonStub; - checkSignatureStub: Sinon.SinonStub; - - constructor() { - this.requestStub = Sinon.stub(); - this.checkRegistrationStub = Sinon.stub(); - this.checkSignatureStub = Sinon.stub(); - } - - request(appId: string, keyHandle?: string): U2f.Request { - return this.requestStub(appId, keyHandle); - } - - checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) - : U2f.RegistrationResult | U2f.Error { - return this.checkRegistrationStub(registrationRequest, registrationResponse); - } - - checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) - : U2f.SignatureResult | U2f.Error { - return this.checkSignatureStub(signatureRequest, signatureResponse, publicKey); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authorization/Authorizer.spec.ts b/themes/main/server/src/lib/authorization/Authorizer.spec.ts deleted file mode 100644 index 586814042..000000000 --- a/themes/main/server/src/lib/authorization/Authorizer.spec.ts +++ /dev/null @@ -1,372 +0,0 @@ - -import Assert = require("assert"); -import winston = require("winston"); -import { Authorizer } from "./Authorizer"; -import { ACLConfiguration, ACLRule } from "../configuration/schema/AclConfiguration"; -import { Level } from "./Level"; - -describe("authorization/Authorizer", function () { - let authorizer: Authorizer; - let configuration: ACLConfiguration; - - describe("configuration is null", function() { - it("should allow access to anything, anywhere for anybody", function() { - configuration = undefined; - authorizer = new Authorizer(configuration, winston); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1", "group2"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "admin.example.com", resource: "/"}, {user: "user3", groups: ["group3"]}), Level.BYPASS); - }); - }); - - describe("configuration is not null", function () { - beforeEach(function () { - configuration = { - default_policy: "deny", - rules: [] - }; - authorizer = new Authorizer(configuration, winston); - }); - - describe("check access control with default policy to deny", function () { - beforeEach(function () { - configuration.default_policy = "deny"; - }); - - it("should deny access when no rule is provided", function () { - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should control access when multiple domain matcher is provided", function () { - configuration.rules = [{ - domain: "*.mail.example.com", - policy: "two_factor", - subject: "user:user1", - resources: [".*"] - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should allow access to all resources when resources is not provided", function () { - configuration.rules = [{ - domain: "*.mail.example.com", - policy: "two_factor", - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - describe("check user rules", function () { - it("should allow access when user has a matching allowing rule", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: [".*"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should deny to other users", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: [".*"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user2", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); - }); - - it("should allow user access only to specific resources", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: ["/private/.*", "^/begin", "/end$"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/middle/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/begin"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/not/begin"}, {user: "user1", groups: ["group1"]}), Level.DENY); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end/x"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should allow access to multiple domains", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: [".*"], - subject: "user:user1" - }, { - domain: "home1.example.com", - policy: "one_factor", - resources: [".*"], - subject: "user:user1" - }, { - domain: "home2.example.com", - policy: "deny", - resources: [".*"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home1.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); - Assert.equal(authorizer.authorization({domain: "home2.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home3.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should apply rules in order", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "one_factor", - resources: ["/my/private/resource"], - subject: "user:user1" - }, { - domain: "home.example.com", - policy: "deny", - resources: ["^/my/private/.*"], - subject: "user:user1" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/my/.*"], - subject: "user:user1" - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/poney"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/duck"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/resource"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); - }); - }); - - describe("check group rules", function () { - it("should allow access when user is in group having a matching allowing rule", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: ["^/$"], - subject: "group:group1" - }, { - domain: "home.example.com", - policy: "one_factor", - resources: ["^/test$"], - subject: "group:group2" - }, { - domain: "home.example.com", - policy: "deny", - resources: ["^/private$"], - subject: "group:group2" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.ONE_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); - }); - }); - }); - - describe("check any rules", function () { - it("should control access when any rules are defined", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "bypass", - resources: ["^/public$"] - }, { - domain: "home.example.com", - policy: "deny", - resources: ["^/private$"] - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, - {user: "user4", groups: ["group5"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, - {user: "user4", groups: ["group5"]}), Level.DENY); - }); - }); - - describe("check access control with default policy to allow", function () { - beforeEach(function () { - configuration.default_policy = "bypass"; - }); - - it("should allow access to anything when no rule is provided", function () { - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - }); - - it("should deny access to one resource when defined", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "deny", - resources: ["/test"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - }); - }); - - describe("check access control with complete use case", function () { - beforeEach(function () { - configuration.default_policy = "deny"; - }); - - it("should control access of multiple user (real use case)", function () { - // Let say we have three users: admin, john, harry. - // admin is in groups ["admins"] - // john is in groups ["dev", "admin-private"] - // harry is in groups ["dev"] - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: ["^/public$", "^/$"] - }, { - domain: "home.example.com", - policy: "two_factor", - resources: [".*"], - subject: "group:admins" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/private/?.*"], - subject: "group:admin-private" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/private/john$"], - subject: "user:john" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/private/harry"], - subject: "user:harry" - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); - }); - - it("should allow when allowed at group level and denied at user level", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "deny", - resources: ["^/dev/bob$"], - subject: "user:john" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"], - subject: "group:dev" - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); - }); - - it("should allow access when allowed at 'any' level and denied at user level", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "deny", - resources: ["^/dev/bob$"], - subject: "user:john" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"] - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); - }); - - it("should allow access when allowed at 'any' level and denied at group level", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "deny", - resources: ["^/dev/bob$"], - subject: "group:dev" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"] - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); - }); - - it("should respect rules precedence", function () { - // the priority from least to most is 'default_policy', 'all', 'group', 'user' - // and the first rules in each category as a lower priority than the latest. - // You can think of it that way: they override themselves inside each category. - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"], - subject: "user:john" - }, { - domain: "home.example.com", - policy: "deny", - resources: ["^/dev/bob$"], - subject: "group:dev" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"] - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - }); - }); - }); -}); diff --git a/themes/main/server/src/lib/authorization/Authorizer.ts b/themes/main/server/src/lib/authorization/Authorizer.ts deleted file mode 100644 index 889b7ec20..000000000 --- a/themes/main/server/src/lib/authorization/Authorizer.ts +++ /dev/null @@ -1,85 +0,0 @@ - -import { ACLConfiguration, ACLPolicy, ACLRule } from "../configuration/schema/AclConfiguration"; -import { IAuthorizer } from "./IAuthorizer"; -import { Winston } from "../../../types/Dependencies"; -import { MultipleDomainMatcher } from "./MultipleDomainMatcher"; -import { Level } from "./Level"; -import { Object } from "./Object"; -import { Subject } from "./Subject"; - -function MatchDomain(actualDomain: string) { - return function (rule: ACLRule): boolean { - return MultipleDomainMatcher.match(actualDomain, rule.domain); - }; -} - -function MatchResource(actualResource: string) { - return function (rule: ACLRule): boolean { - // If resources key is not provided, the rule applies to all resources. - if (!rule.resources) return true; - - for (let i = 0; i < rule.resources.length; ++i) { - const regexp = new RegExp(rule.resources[i]); - if (regexp.test(actualResource)) return true; - } - return false; - }; -} - -function MatchSubject(subject: Subject) { - return (rule: ACLRule) => { - // If no subject, matches anybody - if (!rule.subject) return true; - - if (rule.subject.startsWith("user:")) { - const ruleUser = rule.subject.split(":")[1]; - if (subject.user == ruleUser) return true; - } - - if (rule.subject.startsWith("group:")) { - const ruleGroup = rule.subject.split(":")[1]; - if (subject.groups.indexOf(ruleGroup) > -1) return true; - } - return false; - }; -} - -export class Authorizer implements IAuthorizer { - private logger: Winston; - private readonly configuration: ACLConfiguration; - - constructor(configuration: ACLConfiguration, logger_: Winston) { - this.logger = logger_; - this.configuration = configuration; - } - - private getMatchingRules(object: Object, subject: Subject): ACLRule[] { - const rules = this.configuration.rules; - if (!rules) return []; - return rules - .filter(MatchDomain(object.domain)) - .filter(MatchResource(object.resource)) - .filter(MatchSubject(subject)); - } - - private ruleToLevel(policy: string): Level { - if (policy == "bypass") { - return Level.BYPASS; - } else if (policy == "one_factor") { - return Level.ONE_FACTOR; - } else if (policy == "two_factor") { - return Level.TWO_FACTOR; - } - return Level.DENY; - } - - authorization(object: Object, subject: Subject): Level { - if (!this.configuration) return Level.BYPASS; - - const rules = this.getMatchingRules(object, subject); - - return (rules.length > 0) - ? this.ruleToLevel(rules[0].policy) // extract the policy of the first matching rule - : this.ruleToLevel(this.configuration.default_policy); // otherwise use the default policy - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authorization/AuthorizerStub.spec.ts b/themes/main/server/src/lib/authorization/AuthorizerStub.spec.ts deleted file mode 100644 index 9bd6f4a85..000000000 --- a/themes/main/server/src/lib/authorization/AuthorizerStub.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Sinon = require("sinon"); -import { IAuthorizer } from "./IAuthorizer"; -import { Level } from "./Level"; -import { Object } from "./Object"; -import { Subject } from "./Subject"; - -export class AuthorizerStub implements IAuthorizer { - authorizationMock: Sinon.SinonStub; - - constructor() { - this.authorizationMock = Sinon.stub(); - } - - authorization(object: Object, subject: Subject): Level { - return this.authorizationMock(object, subject); - } -} diff --git a/themes/main/server/src/lib/authorization/IAuthorizer.ts b/themes/main/server/src/lib/authorization/IAuthorizer.ts deleted file mode 100644 index fe7ba367a..000000000 --- a/themes/main/server/src/lib/authorization/IAuthorizer.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Level } from "./Level"; -import { Subject } from "./Subject"; -import { Object } from "./Object"; - -export interface IAuthorizer { - authorization(object: Object, subject: Subject): Level; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authorization/Level.ts b/themes/main/server/src/lib/authorization/Level.ts deleted file mode 100644 index d12802610..000000000 --- a/themes/main/server/src/lib/authorization/Level.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum Level { - BYPASS = 0, - ONE_FACTOR = 1, - TWO_FACTOR = 2, - DENY = 3 -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authorization/MultipleDomainMatcher.ts b/themes/main/server/src/lib/authorization/MultipleDomainMatcher.ts deleted file mode 100644 index 64c647a4a..000000000 --- a/themes/main/server/src/lib/authorization/MultipleDomainMatcher.ts +++ /dev/null @@ -1,12 +0,0 @@ - -export class MultipleDomainMatcher { - static match(domain: string, pattern: string): boolean { - if (pattern.startsWith("*") && - domain.endsWith(pattern.substr(1))) { - return true; - } - else if (domain == pattern) { - return true; - } - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authorization/Object.ts b/themes/main/server/src/lib/authorization/Object.ts deleted file mode 100644 index 5411b0d24..000000000 --- a/themes/main/server/src/lib/authorization/Object.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface Object { - domain: string; - resource: string; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/authorization/Subject.ts b/themes/main/server/src/lib/authorization/Subject.ts deleted file mode 100644 index 310d6b4c0..000000000 --- a/themes/main/server/src/lib/authorization/Subject.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface Subject { - user: string; - groups: string[]; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/ConfigurationParser.spec.ts b/themes/main/server/src/lib/configuration/ConfigurationParser.spec.ts deleted file mode 100644 index 60c0f6182..000000000 --- a/themes/main/server/src/lib/configuration/ConfigurationParser.spec.ts +++ /dev/null @@ -1,171 +0,0 @@ -import * as Assert from "assert"; -import { Configuration } from "./schema/Configuration"; -import { ACLConfiguration } from "./schema/AclConfiguration"; -import { ConfigurationParser } from "./ConfigurationParser"; - -describe("configuration/ConfigurationParser", function () { - function buildYamlConfig(): Configuration { - const yaml_config: Configuration = { - port: 8080, - authentication_backend: { - ldap: { - url: "http://ldap", - base_dn: "dc=example,dc=com", - additional_users_dn: "ou=users", - additional_groups_dn: "ou=groups", - user: "user", - password: "pass" - }, - }, - session: { - domain: "example.com", - secret: "secret", - expiration: 40000 - }, - storage: { - local: { - path: "/mydirectory" - } - }, - regulation: { - max_retries: 3, - find_time: 5 * 60, - ban_time: 5 * 60 - }, - logs_level: "debug", - notifier: { - email: { - username: "user", - password: "password", - sender: "admin@example.com", - service: "gmail" - } - } - }; - return yaml_config; - } - - describe("port", function () { - it("should read the port from the yaml file", function () { - const yaml_config = buildYamlConfig(); - yaml_config.port = 7070; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.port, 7070); - }); - - it("should default the port to 8080 if not provided", function () { - const yaml_config = buildYamlConfig(); - delete yaml_config.port; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.port, 8080); - }); - }); - - describe("test session configuration", function() { - it("should get the session attributes", function () { - const yaml_config = buildYamlConfig(); - yaml_config.session = { - domain: "example.com", - secret: "secret", - expiration: 3600, - inactivity: 4000 - }; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.session.domain, "example.com"); - Assert.equal(config.session.secret, "secret"); - Assert.equal(config.session.expiration, 3600); - Assert.equal(config.session.inactivity, 4000); - }); - - it("should be ok not specifying inactivity", function () { - const yaml_config = buildYamlConfig(); - yaml_config.session = { - domain: "example.com", - secret: "secret", - expiration: 3600 - }; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.session.domain, "example.com"); - Assert.equal(config.session.secret, "secret"); - Assert.equal(config.session.expiration, 3600); - Assert.equal(config.session.inactivity, undefined); - }); - }); - - it("should get the log level", function () { - const yaml_config = buildYamlConfig(); - yaml_config.logs_level = "debug"; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.logs_level, "debug"); - }); - - it("should get the notifier config", function () { - const userConfig = buildYamlConfig(); - userConfig.notifier = { - email: { - username: "user", - password: "pass", - sender: "admin@example.com", - service: "gmail" - } - }; - const config = ConfigurationParser.parse(userConfig); - Assert.deepEqual(config.notifier, { - email: { - username: "user", - password: "pass", - sender: "admin@example.com", - service: "gmail" - } - }); - }); - - describe("access_control", function() { - it("should adapt access_control when it is already ok", function () { - const userConfig = buildYamlConfig(); - userConfig.access_control = { - default_policy: "deny", - rules: [{ - domain: "www.example.com", - policy: "two_factor", - subject: "user:user" - }, { - domain: "public.example.com", - policy: "two_factor" - }] - }; - const config = ConfigurationParser.parse(userConfig); - Assert.deepEqual(config.access_control, { - default_policy: "deny", - rules: [{ - domain: "www.example.com", - policy: "two_factor", - subject: "user:user" - }, { - domain: "public.example.com", - policy: "two_factor" - }] - } as ACLConfiguration); - }); - - - it("should adapt access_control when it is empty", function () { - const userConfig = buildYamlConfig(); - userConfig.access_control = {} as any; - const config = ConfigurationParser.parse(userConfig); - Assert.deepEqual(config.access_control, { - default_policy: "bypass", - rules: [] - }); - }); - }); - - describe("default_redirection_url", function() { - it("should parse default_redirection_url", function() { - const userConfig = buildYamlConfig(); - userConfig.default_redirection_url = "dummy_url"; - const config = ConfigurationParser.parse(userConfig); - Assert.deepEqual(config.default_redirection_url, "dummy_url"); - }); - }); -}); diff --git a/themes/main/server/src/lib/configuration/ConfigurationParser.ts b/themes/main/server/src/lib/configuration/ConfigurationParser.ts deleted file mode 100644 index d92d163c4..000000000 --- a/themes/main/server/src/lib/configuration/ConfigurationParser.ts +++ /dev/null @@ -1,39 +0,0 @@ - -import * as ObjectPath from "object-path"; -import { Configuration, complete } from "./schema/Configuration"; -import Ajv = require("ajv"); -import Path = require("path"); -import Util = require("util"); - -export class ConfigurationParser { - private static parseTypes(configuration: Configuration): string[] { - const schema = require(Path.resolve(__dirname, "./Configuration.schema.json")); - const ajv = new Ajv({ - allErrors: true, - missingRefs: "fail" - }); - ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json")); - const valid = ajv.validate(schema, configuration); - if (!valid) - return ajv.errors.map( - (e: Ajv.ErrorObject) => { return ajv.errorsText([e]); }); - return []; - } - - static parse(configuration: Configuration): Configuration { - const validationErrors = this.parseTypes(configuration); - if (validationErrors.length > 0) { - validationErrors.forEach((e: string) => { console.log(e); }); - throw new Error("Malformed configuration (schema). Please double-check your configuration file."); - } - - const [newConfiguration, completionErrors] = complete(configuration); - - if (completionErrors.length > 0) { - completionErrors.forEach((e: string) => { console.log(e); }); - throw new Error("Malformed configuration (validator). Please double-check your configuration file."); - } - return newConfiguration; - } -} - diff --git a/themes/main/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts b/themes/main/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts deleted file mode 100644 index d4a3093ee..000000000 --- a/themes/main/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { SessionConfigurationBuilder } from "./SessionConfigurationBuilder"; -import { Configuration } from "./schema/Configuration"; -import { GlobalDependencies } from "../../../types/Dependencies"; - -import ExpressSession = require("express-session"); -import ConnectRedis = require("connect-redis"); -import Sinon = require("sinon"); -import Assert = require("assert"); - -describe("configuration/SessionConfigurationBuilder", function () { - const configuration: Configuration = { - access_control: { - default_policy: "deny", - rules: [] - }, - totp: { - issuer: "authelia.com" - }, - authentication_backend: { - ldap: { - url: "ldap://ldap", - user: "user", - base_dn: "dc=example,dc=com", - password: "password", - additional_groups_dn: "ou=groups", - additional_users_dn: "ou=users", - group_name_attribute: "", - groups_filter: "", - mail_attribute: "", - users_filter: "" - }, - }, - logs_level: "debug", - notifier: { - filesystem: { - filename: "/test" - } - }, - port: 8080, - session: { - name: "authelia_session", - domain: "example.com", - expiration: 3600, - secret: "secret" - }, - regulation: { - max_retries: 3, - ban_time: 5 * 60, - find_time: 5 * 60 - }, - storage: { - local: { - in_memory: true - } - } - }; - - const deps: GlobalDependencies = { - ConnectRedis: Sinon.spy() as any, - ldapjs: Sinon.spy() as any, - nedb: Sinon.spy() as any, - session: Sinon.spy() as any, - speakeasy: Sinon.spy() as any, - u2f: Sinon.spy() as any, - winston: Sinon.spy() as any, - Redis: Sinon.spy() as any - }; - - it("should return session options without redis options", function () { - const options = SessionConfigurationBuilder.build(configuration, deps); - const expectedOptions = { - name: "authelia_session", - secret: "secret", - resave: false, - saveUninitialized: true, - cookie: { - secure: true, - httpOnly: true, - maxAge: 3600, - domain: "example.com" - } - }; - - Assert.deepEqual(expectedOptions, options); - }); - - it("should return session options with redis options", function () { - configuration.session["redis"] = { - host: "redis.example.com", - port: 6379 - }; - const RedisStoreMock = Sinon.spy(); - const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); - - deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; - deps.Redis = { - createClient: Sinon.mock().returns(redisClient) - } as any; - - const options = SessionConfigurationBuilder.build(configuration, deps); - - const expectedOptions: ExpressSession.SessionOptions = { - secret: "secret", - resave: false, - saveUninitialized: true, - name: "authelia_session", - cookie: { - secure: true, - httpOnly: true, - maxAge: 3600, - domain: "example.com" - }, - store: Sinon.match.object as any - }; - - Assert((deps.ConnectRedis as Sinon.SinonStub).calledWith(deps.session)); - Assert.equal(options.secret, expectedOptions.secret); - Assert.equal(options.resave, expectedOptions.resave); - Assert.equal(options.saveUninitialized, expectedOptions.saveUninitialized); - Assert.deepEqual(options.cookie, expectedOptions.cookie); - Assert(options.store != undefined); - }); - - it("should return session options with redis password", function () { - configuration.session["redis"] = { - host: "redis.example.com", - port: 6379, - password: "authelia_pass" - }; - const RedisStoreMock = Sinon.spy(); - const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); - const createClientStub = Sinon.stub(); - - deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; - deps.Redis = { - createClient: createClientStub - } as any; - - createClientStub.returns(redisClient); - - const options = SessionConfigurationBuilder.build(configuration, deps); - - Assert(createClientStub.calledWith({ - host: "redis.example.com", - port: 6379, - password: "authelia_pass" - })); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/SessionConfigurationBuilder.ts b/themes/main/server/src/lib/configuration/SessionConfigurationBuilder.ts deleted file mode 100644 index 6ce643d9d..000000000 --- a/themes/main/server/src/lib/configuration/SessionConfigurationBuilder.ts +++ /dev/null @@ -1,52 +0,0 @@ -import ExpressSession = require("express-session"); -import Redis = require("redis"); - -import { Configuration } from "./schema/Configuration"; -import { GlobalDependencies } from "../../../types/Dependencies"; -import { RedisStoreOptions } from "connect-redis"; - -export class SessionConfigurationBuilder { - - static build(configuration: Configuration, deps: GlobalDependencies): ExpressSession.SessionOptions { - const sessionOptions: ExpressSession.SessionOptions = { - name: configuration.session.name, - secret: configuration.session.secret, - resave: false, - saveUninitialized: true, - cookie: { - secure: true, - httpOnly: true, - maxAge: configuration.session.expiration, - domain: configuration.session.domain - }, - }; - - if (configuration.session.redis) { - let redisOptions; - const options: Redis.ClientOpts = { - host: configuration.session.redis.host, - port: configuration.session.redis.port - }; - - if (configuration.session.redis.password) { - options["password"] = configuration.session.redis.password; - } - const client = deps.Redis.createClient(options); - - client.on("error", function (err: Error) { - console.error("Redis error:", err); - }); - - redisOptions = { - client: client, - logErrors: true - }; - - if (redisOptions) { - const RedisStore = deps.ConnectRedis(deps.session); - sessionOptions.store = new RedisStore(redisOptions); - } - } - return sessionOptions; - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/AclConfiguration.spec.ts b/themes/main/server/src/lib/configuration/schema/AclConfiguration.spec.ts deleted file mode 100644 index d1e2a03a2..000000000 --- a/themes/main/server/src/lib/configuration/schema/AclConfiguration.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ACLConfiguration, complete } from "./AclConfiguration"; -import Assert = require("assert"); - -describe("configuration/schema/AclConfiguration", function() { - it("should complete ACLConfiguration", function() { - const configuration: ACLConfiguration = {}; - const [newConfiguration, errors] = complete(configuration); - - Assert.deepEqual(newConfiguration.default_policy, "bypass"); - Assert.deepEqual(newConfiguration.rules, []); - }); - - it("should return errors when subject is not good", function() { - const configuration: ACLConfiguration = { - default_policy: "deny", - rules: [{ - domain: "dev.example.com", - subject: "user:abc", - policy: "bypass" - }, { - domain: "dev.example.com", - subject: "user:def", - policy: "bypass" - }, { - domain: "dev.example.com", - subject: "badkey:abc", - policy: "bypass" - }] - }; - const [newConfiguration, errors] = complete(configuration); - - Assert.deepEqual(errors, ["Rule 2 has wrong subject. It should be starting with user: or group:."]); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/AclConfiguration.ts b/themes/main/server/src/lib/configuration/schema/AclConfiguration.ts deleted file mode 100644 index 40401dd64..000000000 --- a/themes/main/server/src/lib/configuration/schema/AclConfiguration.ts +++ /dev/null @@ -1,41 +0,0 @@ - -export type ACLPolicy = "deny" | "bypass" | "one_factor" | "two_factor"; - -export type ACLRule = { - domain: string; - resources?: string[]; - subject?: string; - policy: ACLPolicy; -}; - -export interface ACLConfiguration { - default_policy?: ACLPolicy; - rules?: ACLRule[]; -} - -export function complete(configuration: ACLConfiguration): [ACLConfiguration, string[]] { - const newConfiguration: ACLConfiguration = (configuration) - ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.default_policy) { - newConfiguration.default_policy = "bypass"; - } - - if (!newConfiguration.rules) { - newConfiguration.rules = []; - } - - if (newConfiguration.rules.length > 0) { - const errors: string[] = []; - newConfiguration.rules.forEach((r, idx) => { - if (r.subject && !r.subject.match(/^(user|group):[a-zA-Z0-9]+$/)) { - errors.push(`Rule ${idx} has wrong subject. It should be starting with user: or group:.`); - } - }); - if (errors.length > 0) { - return [newConfiguration, errors]; - } - } - - return [newConfiguration, []]; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts b/themes/main/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts deleted file mode 100644 index 3ca86381c..000000000 --- a/themes/main/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { AuthenticationBackendConfiguration, complete } from "./AuthenticationBackendConfiguration"; -import Assert = require("assert"); - -describe("configuration/schema/AuthenticationBackendConfiguration", function() { - it("should ensure there is at least one key", function() { - const configuration: AuthenticationBackendConfiguration = {} as any; - const [newConfiguration, error] = complete(configuration); - - Assert.equal(error, "Authentication backend must have one of the following keys:`ldap` or `file`"); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts b/themes/main/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts deleted file mode 100644 index 7f77f894f..000000000 --- a/themes/main/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { LdapConfiguration } from "./LdapConfiguration"; -import { FileUsersDatabaseConfiguration } from "./FileUsersDatabaseConfiguration"; - -export interface AuthenticationBackendConfiguration { - ldap?: LdapConfiguration; - file?: FileUsersDatabaseConfiguration; -} - -export function complete( - configuration: AuthenticationBackendConfiguration) - : [AuthenticationBackendConfiguration, string] { - - const newConfiguration: AuthenticationBackendConfiguration = (configuration) - ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (Object.keys(newConfiguration).length != 1) { - return [ - newConfiguration, - "Authentication backend must have one of the following keys:" + - "`ldap` or `file`" - ]; - } - - return [newConfiguration, undefined]; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/Configuration.ts b/themes/main/server/src/lib/configuration/schema/Configuration.ts deleted file mode 100644 index 8d16a5fb2..000000000 --- a/themes/main/server/src/lib/configuration/schema/Configuration.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ACLConfiguration, complete as AclConfigurationComplete } from "./AclConfiguration"; -import { AuthenticationBackendConfiguration, complete as AuthenticationBackendComplete } from "./AuthenticationBackendConfiguration"; -import { NotifierConfiguration, complete as NotifierConfigurationComplete } from "./NotifierConfiguration"; -import { RegulationConfiguration, complete as RegulationConfigurationComplete } from "./RegulationConfiguration"; -import { SessionConfiguration, complete as SessionConfigurationComplete } from "./SessionConfiguration"; -import { StorageConfiguration, complete as StorageConfigurationComplete } from "./StorageConfiguration"; -import { TotpConfiguration, complete as TotpConfigurationComplete } from "./TotpConfiguration"; - -export interface Configuration { - access_control?: ACLConfiguration; - authentication_backend: AuthenticationBackendConfiguration; - default_redirection_url?: string; - logs_level?: string; - notifier?: NotifierConfiguration; - port?: number; - regulation?: RegulationConfiguration; - session?: SessionConfiguration; - storage?: StorageConfiguration; - totp?: TotpConfiguration; -} - -export function complete( - configuration: Configuration): - [Configuration, string[]] { - - const newConfiguration: Configuration = JSON.parse( - JSON.stringify(configuration)); - const errors: string[] = []; - - const [acls, aclsErrors] = AclConfigurationComplete( - newConfiguration.access_control); - - newConfiguration.access_control = acls; - if (aclsErrors.length > 0) { - errors.concat(aclsErrors); - } - - const [backend, error] = - AuthenticationBackendComplete( - newConfiguration.authentication_backend); - - if (error) errors.push(error); - newConfiguration.authentication_backend = backend; - - if (!newConfiguration.logs_level) { - newConfiguration.logs_level = "info"; - } - - const [notifier, notifierError] = NotifierConfigurationComplete( - newConfiguration.notifier); - newConfiguration.notifier = notifier; - if (notifierError) errors.push(notifierError); - - if (!newConfiguration.port) { - newConfiguration.port = 8080; - } - - newConfiguration.regulation = RegulationConfigurationComplete( - newConfiguration.regulation); - newConfiguration.session = SessionConfigurationComplete( - newConfiguration.session); - newConfiguration.storage = StorageConfigurationComplete( - newConfiguration.storage); - newConfiguration.totp = TotpConfigurationComplete( - newConfiguration.totp); - - return [newConfiguration, errors]; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts b/themes/main/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts deleted file mode 100644 index d19002ba9..000000000 --- a/themes/main/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts +++ /dev/null @@ -1,4 +0,0 @@ - -export interface FileUsersDatabaseConfiguration { - path: string; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/LdapConfiguration.spec.ts b/themes/main/server/src/lib/configuration/schema/LdapConfiguration.spec.ts deleted file mode 100644 index cc73d1085..000000000 --- a/themes/main/server/src/lib/configuration/schema/LdapConfiguration.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Assert = require("assert"); -import { LdapConfiguration, complete } from "./LdapConfiguration"; - -describe("configuration/schema/AuthenticationMethodsConfiguration", function() { - it("should ensure at least one key is provided", function() { - const configuration: LdapConfiguration = { - url: "ldap.example.com", - base_dn: "dc=example,dc=com", - user: "admin", - password: "password" - }; - const newConfiguration = complete(configuration); - - Assert.deepEqual(newConfiguration, { - url: "ldap.example.com", - base_dn: "dc=example,dc=com", - user: "admin", - password: "password", - users_filter: "cn={0}", - group_name_attribute: "cn", - groups_filter: "member={dn}", - mail_attribute: "mail" - }); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/LdapConfiguration.ts b/themes/main/server/src/lib/configuration/schema/LdapConfiguration.ts deleted file mode 100644 index 5dacb9390..000000000 --- a/themes/main/server/src/lib/configuration/schema/LdapConfiguration.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Util = require("util"); - -export interface LdapConfiguration { - url: string; - base_dn: string; - - additional_users_dn?: string; - users_filter?: string; - - additional_groups_dn?: string; - groups_filter?: string; - - group_name_attribute?: string; - mail_attribute?: string; - - user: string; // admin username - password: string; // admin password -} - -export function complete(configuration: LdapConfiguration): LdapConfiguration { - const newConfiguration: LdapConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.users_filter) { - newConfiguration.users_filter = "cn={0}"; - } - - if (!newConfiguration.groups_filter) { - newConfiguration.groups_filter = "member={dn}"; - } - - if (!newConfiguration.group_name_attribute) { - newConfiguration.group_name_attribute = "cn"; - } - - if (!newConfiguration.mail_attribute) { - newConfiguration.mail_attribute = "mail"; - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts b/themes/main/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts deleted file mode 100644 index 6c576e8e0..000000000 --- a/themes/main/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Assert = require("assert"); -import { NotifierConfiguration, complete } from "./NotifierConfiguration"; - -describe("configuration/schema/NotifierConfiguration", function() { - it("should use a default notifier when none is provided", function() { - const configuration: NotifierConfiguration = {}; - const [newConfiguration, error] = complete(configuration); - - Assert.deepEqual(newConfiguration.filesystem, {filename: "/tmp/authelia/notification.txt"}); - }); - - it("should ensure correct key is provided", function() { - const configuration = { - abc: "badvalue" - }; - const [newConfiguration, error] = complete(configuration as any); - - Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"); - }); - - it("should ensure there is no more than one key", function() { - const configuration: NotifierConfiguration = { - smtp: { - host: "smtp.example.com", - port: 25, - secure: false, - sender: "test@example.com" - }, - email: { - username: "test", - password: "test", - sender: "test@example.com", - service: "gmail" - } - }; - const [newConfiguration, error] = complete(configuration); - - Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/NotifierConfiguration.ts b/themes/main/server/src/lib/configuration/schema/NotifierConfiguration.ts deleted file mode 100644 index 7bcce15c2..000000000 --- a/themes/main/server/src/lib/configuration/schema/NotifierConfiguration.ts +++ /dev/null @@ -1,45 +0,0 @@ - -export interface EmailNotifierConfiguration { - username: string; - password: string; - sender: string; - service: string; -} - -export interface SmtpNotifierConfiguration { - username?: string; - password?: string; - host: string; - port: number; - secure: boolean; - sender: string; -} - -export interface FileSystemNotifierConfiguration { - filename: string; -} - -export interface NotifierConfiguration { - email?: EmailNotifierConfiguration; - smtp?: SmtpNotifierConfiguration; - filesystem?: FileSystemNotifierConfiguration; -} - -export function complete(configuration: NotifierConfiguration): [NotifierConfiguration, string] { - const newConfiguration: NotifierConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (Object.keys(newConfiguration).length == 0) - newConfiguration.filesystem = { filename: "/tmp/authelia/notification.txt" }; - - const ERROR = "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"; - - if (Object.keys(newConfiguration).length != 1) - return [newConfiguration, ERROR]; - - const key = Object.keys(newConfiguration)[0]; - - if (key != "filesystem" && key != "smtp" && key != "email") - return [newConfiguration, ERROR]; - - return [newConfiguration, undefined]; -} diff --git a/themes/main/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts b/themes/main/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts deleted file mode 100644 index dce2caf4e..000000000 --- a/themes/main/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Assert = require("assert"); -import { RegulationConfiguration, complete } from "./RegulationConfiguration"; - -describe("configuration/schema/RegulationConfiguration", function() { - it("should return default regulation configuration", function() { - const configuration: RegulationConfiguration = {}; - const newConfiguration = complete(configuration); - - Assert.equal(newConfiguration.ban_time, 300); - Assert.equal(newConfiguration.find_time, 120); - Assert.equal(newConfiguration.max_retries, 3); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/RegulationConfiguration.ts b/themes/main/server/src/lib/configuration/schema/RegulationConfiguration.ts deleted file mode 100644 index 117463f43..000000000 --- a/themes/main/server/src/lib/configuration/schema/RegulationConfiguration.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface RegulationConfiguration { - max_retries?: number; - find_time?: number; - ban_time?: number; -} - -export function complete(configuration: RegulationConfiguration): RegulationConfiguration { - const newConfiguration: RegulationConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.max_retries) { - newConfiguration.max_retries = 3; - } - - if (!newConfiguration.find_time) { - newConfiguration.find_time = 120; // seconds - } - - if (!newConfiguration.ban_time) { - newConfiguration.ban_time = 300; // seconds - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/SessionConfiguration.spec.ts b/themes/main/server/src/lib/configuration/schema/SessionConfiguration.spec.ts deleted file mode 100644 index e54010837..000000000 --- a/themes/main/server/src/lib/configuration/schema/SessionConfiguration.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Assert = require("assert"); -import { SessionConfiguration, complete } from "./SessionConfiguration"; - -describe("configuration/schema/SessionConfiguration", function() { - it("should return default regulation configuration", function() { - const configuration: SessionConfiguration = { - domain: "example.com", - secret: "unsecure_secret" - }; - const newConfiguration = complete(configuration); - - Assert.equal(newConfiguration.name, 'authelia_session'); - Assert.equal(newConfiguration.expiration, 3600000); - Assert.equal(newConfiguration.inactivity, undefined); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/SessionConfiguration.ts b/themes/main/server/src/lib/configuration/schema/SessionConfiguration.ts deleted file mode 100644 index 2c88bb215..000000000 --- a/themes/main/server/src/lib/configuration/schema/SessionConfiguration.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface SessionRedisOptions { - host: string; - port: number; - password?: string; -} - -export interface SessionConfiguration { - name?: string; - domain: string; - secret: string; - expiration?: number; - inactivity?: number; - redis?: SessionRedisOptions; -} - -export function complete(configuration: SessionConfiguration): SessionConfiguration { - const newConfiguration: SessionConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.name) { - newConfiguration.name = "authelia_session"; - } - - if (!newConfiguration.expiration) { - newConfiguration.expiration = 3600000; // 1 hour - } - - if (!newConfiguration.inactivity) { - newConfiguration.inactivity = undefined; // disabled - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/StorageConfiguration.spec.ts b/themes/main/server/src/lib/configuration/schema/StorageConfiguration.spec.ts deleted file mode 100644 index 9d02a11b6..000000000 --- a/themes/main/server/src/lib/configuration/schema/StorageConfiguration.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Assert = require("assert"); -import { StorageConfiguration, complete } from "./StorageConfiguration"; - -describe("configuration/schema/StorageConfiguration", function() { - it("should return default regulation configuration", function() { - const configuration: StorageConfiguration = {}; - const newConfiguration = complete(configuration); - - Assert.deepEqual(newConfiguration, { - local: { - in_memory: true - } - }); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/StorageConfiguration.ts b/themes/main/server/src/lib/configuration/schema/StorageConfiguration.ts deleted file mode 100644 index 47e356ef4..000000000 --- a/themes/main/server/src/lib/configuration/schema/StorageConfiguration.ts +++ /dev/null @@ -1,30 +0,0 @@ -export interface MongoStorageConfiguration { - url: string; - database: string; - auth?: { - username: string; - password: string; - }; -} - -export interface LocalStorageConfiguration { - path?: string; - in_memory?: boolean; -} - -export interface StorageConfiguration { - local?: LocalStorageConfiguration; - mongo?: MongoStorageConfiguration; -} - -export function complete(configuration: StorageConfiguration): StorageConfiguration { - const newConfiguration: StorageConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.local && !newConfiguration.mongo) { - newConfiguration.local = { - in_memory: true - }; - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/TotpConfiguration.ts b/themes/main/server/src/lib/configuration/schema/TotpConfiguration.ts deleted file mode 100644 index 683135639..000000000 --- a/themes/main/server/src/lib/configuration/schema/TotpConfiguration.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface TotpConfiguration { - issuer: string; -} - -export function complete(configuration: TotpConfiguration): TotpConfiguration { - const newConfiguration: TotpConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.issuer) { - newConfiguration.issuer = "authelia.com"; - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts b/themes/main/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts deleted file mode 100644 index 8008b4833..000000000 --- a/themes/main/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts +++ /dev/null @@ -1,9 +0,0 @@ - -export interface UserInfo { - username: string; - password_hash: string; - email: string; - groups?: string[]; -} - -export type UserDatabaseConfiguration = UserInfo[]; \ No newline at end of file diff --git a/themes/main/server/src/lib/connectors/mongo/IMongoClient.d.ts b/themes/main/server/src/lib/connectors/mongo/IMongoClient.d.ts deleted file mode 100644 index 36cb4b8bf..000000000 --- a/themes/main/server/src/lib/connectors/mongo/IMongoClient.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import MongoDB = require("mongodb"); -import Bluebird = require("bluebird"); - -export interface IMongoClient { - collection(name: string): Bluebird -} \ No newline at end of file diff --git a/themes/main/server/src/lib/connectors/mongo/MongoClient.spec.ts b/themes/main/server/src/lib/connectors/mongo/MongoClient.spec.ts deleted file mode 100644 index ca0c68593..000000000 --- a/themes/main/server/src/lib/connectors/mongo/MongoClient.spec.ts +++ /dev/null @@ -1,119 +0,0 @@ -import Assert = require("assert"); -import Bluebird = require("bluebird"); -import MongoDB = require("mongodb"); -import Sinon = require("sinon"); - -import { MongoClient } from "./MongoClient"; -import { GlobalLoggerStub } from "../../logging/GlobalLoggerStub.spec"; -import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration"; - -describe("connectors/mongo/MongoClient", function () { - let MongoClientStub: any; - let mongoClientStub: any; - let mongoDatabaseStub: any; - let logger: GlobalLoggerStub = new GlobalLoggerStub(); - - const configuration: MongoStorageConfiguration = { - url: "mongo://url", - database: "databasename" - }; - - describe("connection", () => { - before(() => { - mongoClientStub = { - db: Sinon.stub() - }; - mongoDatabaseStub = { - on: Sinon.stub(), - collection: Sinon.stub() - } - MongoClientStub = Sinon.stub( - MongoDB.MongoClient, "connect"); - MongoClientStub.yields( - undefined, mongoClientStub); - mongoClientStub.db.returns( - mongoDatabaseStub); - }); - - after(() => { - MongoClientStub.restore(); - }); - - it("should use credentials from configuration", () => { - configuration.auth = { - username: "authelia", - password: "authelia_pass" - }; - - const client = new MongoClient(configuration, logger); - return client.collection("test") - .then(() => { - Assert(MongoClientStub.calledWith("mongo://url", { - auth: { - user: "authelia", - password: "authelia_pass" - } - })) - }); - }); - }); - - describe("collection", () => { - before(function() { - mongoClientStub = { - db: Sinon.stub() - }; - mongoDatabaseStub = { - on: Sinon.stub(), - collection: Sinon.stub() - } - }); - - describe("Connection to mongo is ok", function() { - before(function () { - MongoClientStub = Sinon.stub( - MongoDB.MongoClient, "connect"); - MongoClientStub.yields( - undefined, mongoClientStub); - mongoClientStub.db.returns( - mongoDatabaseStub); - }); - - after(function () { - MongoClientStub.restore(); - }); - - it("should create a collection", function () { - const COLLECTION_NAME = "mycollection"; - const client = new MongoClient(configuration, logger); - - mongoDatabaseStub.collection.returns("COL"); - return client.collection(COLLECTION_NAME) - .then((collection) => mongoDatabaseStub.collection.calledWith(COLLECTION_NAME)); - }); - }); - - describe("Connection to mongo is broken", function() { - before(function () { - MongoClientStub = Sinon.stub( - MongoDB.MongoClient, "connect"); - MongoClientStub.yields( - new Error("Failed connection"), undefined); - }); - - after(function () { - MongoClientStub.restore(); - }); - - it("should fail creating the collection", function() { - const COLLECTION_NAME = "mycollection"; - const client = new MongoClient(configuration, logger); - - mongoDatabaseStub.collection.returns("COL"); - return client.collection(COLLECTION_NAME) - .then((collection) => Bluebird.reject(new Error("should not be here."))) - .catch((err) => Bluebird.resolve()); - }); - }) - }); -}); diff --git a/themes/main/server/src/lib/connectors/mongo/MongoClient.ts b/themes/main/server/src/lib/connectors/mongo/MongoClient.ts deleted file mode 100644 index d15731e97..000000000 --- a/themes/main/server/src/lib/connectors/mongo/MongoClient.ts +++ /dev/null @@ -1,76 +0,0 @@ - -import MongoDB = require("mongodb"); -import { IMongoClient } from "./IMongoClient"; -import Bluebird = require("bluebird"); -import { AUTHENTICATION_FAILED } from "../../../../../shared/UserMessages"; -import { IGlobalLogger } from "../../logging/IGlobalLogger"; -import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration"; - -export class MongoClient implements IMongoClient { - private configuration: MongoStorageConfiguration; - - private database: MongoDB.Db; - private client: MongoDB.MongoClient; - private logger: IGlobalLogger; - - constructor( - configuration: MongoStorageConfiguration, - logger: IGlobalLogger) { - - this.configuration = configuration; - this.logger = logger; - } - - connect(): Bluebird { - const that = this; - const options: MongoDB.MongoClientOptions = {}; - if (that.configuration.auth) { - options["auth"] = { - user: that.configuration.auth.username, - password: that.configuration.auth.password - }; - } - - return new Bluebird((resolve, reject) => { - MongoDB.MongoClient.connect( - this.configuration.url, - options, - function(err, client) { - if (err) { - reject(err); - return; - } - resolve(client); - }); - }) - .then(function (client: MongoDB.MongoClient) { - that.database = client.db(that.configuration.database); - that.database.on("close", () => { - that.logger.info("[MongoClient] Lost connection."); - }); - that.database.on("reconnect", () => { - that.logger.info("[MongoClient] Reconnected."); - }); - that.client = client; - }); - } - - close(): Bluebird { - if (this.client) { - this.client.close(); - this.database = undefined; - this.client = undefined; - } - return Bluebird.resolve(); - } - - collection(name: string): Bluebird { - if (!this.client) { - const that = this; - return this.connect() - .then(() => Bluebird.resolve(that.database.collection(name))); - } - - return Bluebird.resolve(this.database.collection(name)); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/connectors/mongo/MongoClientStub.spec.ts b/themes/main/server/src/lib/connectors/mongo/MongoClientStub.spec.ts deleted file mode 100644 index 1cfd48e32..000000000 --- a/themes/main/server/src/lib/connectors/mongo/MongoClientStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Sinon = require("sinon"); -import MongoDB = require("mongodb"); -import Bluebird = require("bluebird"); -import { IMongoClient } from "../../../../src/lib/connectors/mongo/IMongoClient"; - -export class MongoClientStub implements IMongoClient { - public collectionStub: Sinon.SinonStub; - - constructor() { - this.collectionStub = Sinon.stub(); - } - - collection(name: string): Bluebird { - return this.collectionStub(name); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/logging/GlobalLogger.ts b/themes/main/server/src/lib/logging/GlobalLogger.ts deleted file mode 100644 index 4da7acf49..000000000 --- a/themes/main/server/src/lib/logging/GlobalLogger.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { IGlobalLogger } from "./IGlobalLogger"; -import Util = require("util"); -import Express = require("express"); -import Winston = require("winston"); - -declare module "express" { - interface Request { - id: string; - } -} - -export class GlobalLogger implements IGlobalLogger { - private winston: typeof Winston; - constructor(winston: typeof Winston) { - this.winston = winston; - } - - private buildMessage(message: string, ...args: any[]): string { - return Util.format("date='%s' message='%s'", new Date(), - Util.format(message, ...args)); - } - - info(message: string, ...args: any[]): void { - this.winston.info(this.buildMessage(message, ...args)); - } - - debug(message: string, ...args: any[]): void { - this.winston.debug(this.buildMessage(message, ...args)); - } - - error(message: string, ...args: any[]): void { - this.winston.debug(this.buildMessage(message, ...args)); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/logging/GlobalLoggerStub.spec.ts b/themes/main/server/src/lib/logging/GlobalLoggerStub.spec.ts deleted file mode 100644 index d4bb13710..000000000 --- a/themes/main/server/src/lib/logging/GlobalLoggerStub.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Sinon = require("sinon"); -import { GlobalLogger } from "./GlobalLogger"; -import Winston = require("winston"); -import Express = require("express"); -import { IGlobalLogger } from "./IGlobalLogger"; - -export class GlobalLoggerStub implements IGlobalLogger { - infoStub: Sinon.SinonStub; - debugStub: Sinon.SinonStub; - errorStub: Sinon.SinonStub; - private globalLogger: IGlobalLogger; - - constructor(enableLogging?: boolean) { - this.infoStub = Sinon.stub(); - this.debugStub = Sinon.stub(); - this.errorStub = Sinon.stub(); - if (enableLogging) - this.globalLogger = new GlobalLogger(Winston); - } - - info(message: string, ...args: any[]): void { - if (this.globalLogger) - this.globalLogger.info(message, ...args); - this.infoStub(message, ...args); - } - - debug(message: string, ...args: any[]): void { - if (this.globalLogger) - this.globalLogger.info(message, ...args); - this.debugStub(message, ...args); - } - - error(message: string, ...args: any[]): void { - if (this.globalLogger) - this.globalLogger.info(message, ...args); - this.errorStub(message, ...args); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/logging/IGlobalLogger.ts b/themes/main/server/src/lib/logging/IGlobalLogger.ts deleted file mode 100644 index 548515ec7..000000000 --- a/themes/main/server/src/lib/logging/IGlobalLogger.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface IGlobalLogger { - info(message: string, ...args: any[]): void; - debug(message: string, ...args: any[]): void; - error(message: string, ...args: any[]): void; -} diff --git a/themes/main/server/src/lib/logging/IRequestLogger.ts b/themes/main/server/src/lib/logging/IRequestLogger.ts deleted file mode 100644 index 126a601fe..000000000 --- a/themes/main/server/src/lib/logging/IRequestLogger.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Express = require("express"); - -export interface IRequestLogger { - info(req: Express.Request, message: string, ...args: any[]): void; - debug(req: Express.Request, message: string, ...args: any[]): void; - error(req: Express.Request, message: string, ...args: any[]): void; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/logging/RequestLogger.ts b/themes/main/server/src/lib/logging/RequestLogger.ts deleted file mode 100644 index c45c66018..000000000 --- a/themes/main/server/src/lib/logging/RequestLogger.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { IRequestLogger } from "./IRequestLogger"; -import Util = require("util"); -import Express = require("express"); -import Winston = require("winston"); - -declare module "express" { - interface Request { - id: string; - } -} - -export class RequestLogger implements IRequestLogger { - private winston: typeof Winston; - - constructor(winston: typeof Winston) { - this.winston = winston; - } - - private formatHeader(req: Express.Request) { - const clientIP = req.ip; // The IP of the original client going through the proxy chain. - return Util.format("date='%s' method='%s', path='%s' requestId='%s' sessionId='%s' ip='%s'", - new Date(), req.method, req.path, req.id, req.sessionID, clientIP); - } - - private formatBody(message: string) { - return Util.format("message='%s'", message); - } - - private formatMessage(req: Express.Request, message: string) { - return Util.format("%s %s", this.formatHeader(req), - this.formatBody(message)); - } - - info(req: Express.Request, message: string, ...args: any[]): void { - this.winston.info(this.formatMessage(req, message), ...args); - } - - debug(req: Express.Request, message: string, ...args: any[]): void { - this.winston.debug(this.formatMessage(req, message), ...args); - } - - error(req: Express.Request, message: string, ...args: any[]): void { - this.winston.error(this.formatMessage(req, message), ...args); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/logging/RequestLoggerStub.spec.ts b/themes/main/server/src/lib/logging/RequestLoggerStub.spec.ts deleted file mode 100644 index b0e375210..000000000 --- a/themes/main/server/src/lib/logging/RequestLoggerStub.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { IRequestLogger } from "./IRequestLogger"; -import Sinon = require("sinon"); -import { RequestLogger } from "./RequestLogger"; -import Winston = require("winston"); -import Express = require("express"); - -export class RequestLoggerStub implements IRequestLogger { - infoStub: Sinon.SinonStub; - debugStub: Sinon.SinonStub; - errorStub: Sinon.SinonStub; - private requestLogger: RequestLogger; - - constructor(enableLogging?: boolean) { - this.infoStub = Sinon.stub(); - this.debugStub = Sinon.stub(); - this.errorStub = Sinon.stub(); - if (enableLogging) - this.requestLogger = new RequestLogger(Winston); - } - - info(req: Express.Request, message: string, ...args: any[]): void { - if (this.requestLogger) - this.requestLogger.info(req, message, ...args); - this.infoStub(req, message, ...args); - } - - debug(req: Express.Request, message: string, ...args: any[]): void { - if (this.requestLogger) - this.requestLogger.info(req, message, ...args); - this.debugStub(req, message, ...args); - } - - error(req: Express.Request, message: string, ...args: any[]): void { - if (this.requestLogger) - this.requestLogger.info(req, message, ...args); - this.errorStub(req, message, ...args); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/AbstractEmailNotifier.ts b/themes/main/server/src/lib/notifiers/AbstractEmailNotifier.ts deleted file mode 100644 index 198e4e5db..000000000 --- a/themes/main/server/src/lib/notifiers/AbstractEmailNotifier.ts +++ /dev/null @@ -1,23 +0,0 @@ - -import { INotifier } from "../notifiers/INotifier"; -import { Identity } from "../../../types/Identity"; - -import Fs = require("fs"); -import Path = require("path"); -import Ejs = require("ejs"); -import BluebirdPromise = require("bluebird"); - -const email_template = Fs.readFileSync(Path.join(__dirname, "../../resources/email-template.ejs"), "UTF-8"); - -export abstract class AbstractEmailNotifier implements INotifier { - notify(to: string, subject: string, link: string): BluebirdPromise { - const d = { - url: link, - button_title: "Continue", - title: subject - }; - return this.sendEmail(to, subject, Ejs.render(email_template, d)); - } - - abstract sendEmail(to: string, subject: string, content: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/EmailNotifier.spec.ts b/themes/main/server/src/lib/notifiers/EmailNotifier.spec.ts deleted file mode 100644 index 8211bbc02..000000000 --- a/themes/main/server/src/lib/notifiers/EmailNotifier.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as sinon from "sinon"; -import * as Assert from "assert"; -import BluebirdPromise = require("bluebird"); - -import { MailSenderStub } from "./MailSenderStub.spec"; -import EmailNotifier = require("./EmailNotifier"); - - -describe("notifiers/EmailNotifier", function () { - it("should send an email to given user", function () { - const mailSender = new MailSenderStub(); - const options = { - username: "user_gmail", - password: "pass_gmail", - sender: "admin@example.com", - service: "gmail" - }; - - mailSender.sendStub.returns(BluebirdPromise.resolve()); - const sender = new EmailNotifier.EmailNotifier(options, mailSender); - const subject = "subject"; - const url = "http://test.com"; - - return sender.notify("user@example.com", subject, url) - .then(function () { - Assert.equal(mailSender.sendStub.getCall(0).args[0].to, "user@example.com"); - Assert.equal(mailSender.sendStub.getCall(0).args[0].subject, "subject"); - return BluebirdPromise.resolve(); - }); - }); - - it("should fail while sending an email", function () { - const mailSender = new MailSenderStub(); - const options = { - username: "user_gmail", - password: "pass_gmail", - sender: "admin@example.com", - service: "gmail" - }; - - mailSender.sendStub.returns(BluebirdPromise.reject(new Error("Failed to send mail"))); - const sender = new EmailNotifier.EmailNotifier(options, mailSender); - const subject = "subject"; - const url = "http://test.com"; - - return sender.notify("user@example.com", subject, url) - .then(function () { - return BluebirdPromise.reject(new Error()); - }, function() { - Assert.equal(mailSender.sendStub.getCall(0).args[0].from, "admin@example.com"); - return BluebirdPromise.resolve(); - }); - }); -}); diff --git a/themes/main/server/src/lib/notifiers/EmailNotifier.ts b/themes/main/server/src/lib/notifiers/EmailNotifier.ts deleted file mode 100644 index 4df7c861e..000000000 --- a/themes/main/server/src/lib/notifiers/EmailNotifier.ts +++ /dev/null @@ -1,27 +0,0 @@ - -import * as BluebirdPromise from "bluebird"; - -import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; -import { EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; -import { IMailSender } from "./IMailSender"; - -export class EmailNotifier extends AbstractEmailNotifier { - private mailSender: IMailSender; - private sender: string; - - constructor(options: EmailNotifierConfiguration, mailSender: IMailSender) { - super(); - this.mailSender = mailSender; - this.sender = options.sender; - } - - sendEmail(to: string, subject: string, content: string) { - const mailOptions = { - from: this.sender, - to: to, - subject: subject, - html: content - }; - return this.mailSender.send(mailOptions); - } -} diff --git a/themes/main/server/src/lib/notifiers/FileSystemNotifier.ts b/themes/main/server/src/lib/notifiers/FileSystemNotifier.ts deleted file mode 100644 index 23f6242c4..000000000 --- a/themes/main/server/src/lib/notifiers/FileSystemNotifier.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as BluebirdPromise from "bluebird"; -import * as util from "util"; -import * as Fs from "fs"; -import { INotifier } from "./INotifier"; -import { Identity } from "../../../types/Identity"; - -import { FileSystemNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; - -export class FileSystemNotifier implements INotifier { - private filename: string; - - constructor(options: FileSystemNotifierConfiguration) { - this.filename = options.filename; - } - - notify(to: string, subject: string, link: string): BluebirdPromise { - const content = util.format("Date: %s\nEmail: %s\nSubject: %s\nLink: %s", - new Date().toString(), to, subject, link); - const writeFilePromised: any = BluebirdPromise.promisify(Fs.writeFile); - return writeFilePromised(this.filename, content); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/IMailSender.ts b/themes/main/server/src/lib/notifiers/IMailSender.ts deleted file mode 100644 index 34ac464a8..000000000 --- a/themes/main/server/src/lib/notifiers/IMailSender.ts +++ /dev/null @@ -1,6 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Nodemailer = require("nodemailer"); - -export interface IMailSender { - send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/IMailSenderBuilder.ts b/themes/main/server/src/lib/notifiers/IMailSenderBuilder.ts deleted file mode 100644 index 36d4dcdf7..000000000 --- a/themes/main/server/src/lib/notifiers/IMailSenderBuilder.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IMailSender } from "./IMailSender"; -import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; - -export interface IMailSenderBuilder { - buildEmail(options: EmailNotifierConfiguration): IMailSender; - buildSmtp(options: SmtpNotifierConfiguration): IMailSender; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/INotifier.ts b/themes/main/server/src/lib/notifiers/INotifier.ts deleted file mode 100644 index b9a6b138c..000000000 --- a/themes/main/server/src/lib/notifiers/INotifier.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as BluebirdPromise from "bluebird"; - -export interface INotifier { - notify(to: string, subject: string, link: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/MailSender.ts b/themes/main/server/src/lib/notifiers/MailSender.ts deleted file mode 100644 index 536a88e63..000000000 --- a/themes/main/server/src/lib/notifiers/MailSender.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { IMailSender } from "./IMailSender"; -import Nodemailer = require("nodemailer"); -import NodemailerDirectTransport = require("nodemailer-direct-transport"); -import NodemailerSmtpTransport = require("nodemailer-smtp-transport"); -import BluebirdPromise = require("bluebird"); - -export class MailSender implements IMailSender { - private transporter: Nodemailer.Transporter; - - constructor(options: NodemailerDirectTransport.DirectOptions | - NodemailerSmtpTransport.SmtpOptions, nodemailer: typeof Nodemailer) { - this.transporter = nodemailer.createTransport(options); - } - - verify(): BluebirdPromise { - const that = this; - return new BluebirdPromise(function (resolve, reject) { - that.transporter.verify(function (error: Error, success: any) { - if (error) { - reject(new Error("Unable to connect to SMTP server. \ - Please check the service is running and your credentials are correct.")); - return; - } - resolve(); - }); - }); - } - - send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise { - const that = this; - return new BluebirdPromise(function (resolve, reject) { - that.transporter.sendMail(mailOptions, (error: Error, - data: Nodemailer.SentMessageInfo) => { - if (error) { - reject(new Error("Error while sending email: " + error.message)); - return; - } - resolve(); - }); - }); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/MailSenderBuilder.spec.ts b/themes/main/server/src/lib/notifiers/MailSenderBuilder.spec.ts deleted file mode 100644 index 41e0db426..000000000 --- a/themes/main/server/src/lib/notifiers/MailSenderBuilder.spec.ts +++ /dev/null @@ -1,67 +0,0 @@ - -import { MailSenderBuilder } from ".//MailSenderBuilder"; -import Nodemailer = require("nodemailer"); -import Sinon = require("sinon"); -import Assert = require("assert"); - -describe("notifiers/MailSenderBuilder", function() { - let createTransportStub: Sinon.SinonStub; - beforeEach(function() { - createTransportStub = Sinon.stub(Nodemailer, "createTransport"); - }); - - afterEach(function() { - createTransportStub.restore(); - }); - - it("should create a email mail sender", function() { - const mailSenderBuilder = new MailSenderBuilder(Nodemailer); - mailSenderBuilder.buildEmail({ - username: "user_gmail", - password: "pass_gmail", - sender: "admin@example.com", - service: "gmail" - }); - Assert.equal(createTransportStub.getCall(0).args[0].auth.user, "user_gmail"); - Assert.equal(createTransportStub.getCall(0).args[0].auth.pass, "pass_gmail"); - Assert.equal(createTransportStub.getCall(0).args[0].service, "gmail"); - }); - - describe("build smtp mail sender", function() { - it("should create a smtp mail sender with authenticated user", function() { - const mailSenderBuilder = new MailSenderBuilder(Nodemailer); - mailSenderBuilder.buildSmtp({ - host: "mail.example.com", - password: "password", - port: 25, - secure: true, - username: "user", - sender: "admin@example.com" - }); - Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { - host: "mail.example.com", - auth: { - pass: "password", - user: "user" - }, - port: 25, - secure: true, - }); - }); - - it("should create a smtp mail sender with anonymous user", function() { - const mailSenderBuilder = new MailSenderBuilder(Nodemailer); - mailSenderBuilder.buildSmtp({ - host: "mail.example.com", - port: 25, - secure: true, - sender: "admin@example.com" - }); - Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { - host: "mail.example.com", - port: 25, - secure: true, - }); - }); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/MailSenderBuilder.ts b/themes/main/server/src/lib/notifiers/MailSenderBuilder.ts deleted file mode 100644 index 1d06be52f..000000000 --- a/themes/main/server/src/lib/notifiers/MailSenderBuilder.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { IMailSender } from "./IMailSender"; -import { IMailSenderBuilder } from "./IMailSenderBuilder"; -import { MailSender } from "./MailSender"; -import Nodemailer = require("nodemailer"); -import NodemailerSmtpTransport = require("nodemailer-smtp-transport"); -import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; - -export class MailSenderBuilder implements IMailSenderBuilder { - private nodemailer: typeof Nodemailer; - - constructor(nodemailer: typeof Nodemailer) { - this.nodemailer = nodemailer; - } - - buildEmail(options: EmailNotifierConfiguration): IMailSender { - const emailOptions = { - service: options.service, - auth: { - user: options.username, - pass: options.password - } - }; - return new MailSender(emailOptions, this.nodemailer); - } - - buildSmtp(options: SmtpNotifierConfiguration): IMailSender { - const smtpOptions: NodemailerSmtpTransport.SmtpOptions = { - host: options.host, - port: options.port, - secure: options.secure, // upgrade later with STARTTLS - }; - - if (options.username && options.password) { - smtpOptions.auth = { - user: options.username, - pass: options.password - }; - } - - return new MailSender(smtpOptions, this.nodemailer); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts b/themes/main/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts deleted file mode 100644 index 5b76f6e56..000000000 --- a/themes/main/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { IMailSenderBuilder } from "../../../src/lib/notifiers/IMailSenderBuilder"; -import BluebirdPromise = require("bluebird"); -import Nodemailer = require("nodemailer"); -import Sinon = require("sinon"); -import { IMailSender } from "../../../src/lib/notifiers/IMailSender"; -import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../../../src/lib/configuration/schema/NotifierConfiguration"; - -export class MailSenderBuilderStub implements IMailSenderBuilder { - buildEmailStub: Sinon.SinonStub; - buildSmtpStub: Sinon.SinonStub; - - constructor() { - this.buildEmailStub = Sinon.stub(); - this.buildSmtpStub = Sinon.stub(); - } - - buildEmail(options: EmailNotifierConfiguration): IMailSender { - return this.buildEmailStub(options); - } - - buildSmtp(options: SmtpNotifierConfiguration): IMailSender { - return this.buildSmtpStub(options); - } - -} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/MailSenderStub.spec.ts b/themes/main/server/src/lib/notifiers/MailSenderStub.spec.ts deleted file mode 100644 index d57c458fc..000000000 --- a/themes/main/server/src/lib/notifiers/MailSenderStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { IMailSender } from "../../../src/lib/notifiers/IMailSender"; -import BluebirdPromise = require("bluebird"); -import Nodemailer = require("nodemailer"); -import Sinon = require("sinon"); - -export class MailSenderStub implements IMailSender { - sendStub: Sinon.SinonStub; - - constructor() { - this.sendStub = Sinon.stub(); - } - - send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise { - return this.sendStub(mailOptions); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/NotifierFactory.spec.ts b/themes/main/server/src/lib/notifiers/NotifierFactory.spec.ts deleted file mode 100644 index f15e7667f..000000000 --- a/themes/main/server/src/lib/notifiers/NotifierFactory.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ - -import * as sinon from "sinon"; -import * as BluebirdPromise from "bluebird"; -import * as assert from "assert"; - -import { NotifierFactory } from "./NotifierFactory"; -import { EmailNotifier } from "./EmailNotifier"; -import { SmtpNotifier } from "./SmtpNotifier"; -import { MailSenderBuilderStub } from "./MailSenderBuilderStub.spec"; - - -describe("notifiers/NotifierFactory", function () { - let mailSenderBuilderStub: MailSenderBuilderStub; - it("should build a Email Notifier", function () { - const options = { - email: { - username: "abc", - password: "password", - sender: "admin@example.com", - service: "gmail" - } - }; - mailSenderBuilderStub = new MailSenderBuilderStub(); - assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof EmailNotifier); - }); - - it("should build a SMTP Notifier", function () { - const options = { - smtp: { - username: "user", - password: "pass", - secure: true, - host: "localhost", - port: 25, - sender: "admin@example.com" - } - }; - - mailSenderBuilderStub = new MailSenderBuilderStub(); - assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof SmtpNotifier); - }); -}); diff --git a/themes/main/server/src/lib/notifiers/NotifierFactory.ts b/themes/main/server/src/lib/notifiers/NotifierFactory.ts deleted file mode 100644 index a89155feb..000000000 --- a/themes/main/server/src/lib/notifiers/NotifierFactory.ts +++ /dev/null @@ -1,33 +0,0 @@ - -import { NotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; -import Nodemailer = require("nodemailer"); -import { INotifier } from "./INotifier"; - -import { FileSystemNotifier } from "./FileSystemNotifier"; -import { EmailNotifier } from "./EmailNotifier"; -import { SmtpNotifier } from "./SmtpNotifier"; -import { IMailSender } from "./IMailSender"; -import { IMailSenderBuilder } from "./IMailSenderBuilder"; - -export class NotifierFactory { - static build(options: NotifierConfiguration, mailSenderBuilder: IMailSenderBuilder): INotifier { - if ("email" in options) { - const mailSender = mailSenderBuilder.buildEmail(options.email); - return new EmailNotifier(options.email, mailSender); - } - else if ("smtp" in options) { - const mailSender = mailSenderBuilder.buildSmtp(options.smtp); - return new SmtpNotifier(options.smtp, mailSender); - } - else if ("filesystem" in options) { - return new FileSystemNotifier(options.filesystem); - } - else { - throw new Error("No available notifier option detected."); - } - } -} - - - - diff --git a/themes/main/server/src/lib/notifiers/NotifierStub.spec.ts b/themes/main/server/src/lib/notifiers/NotifierStub.spec.ts deleted file mode 100644 index f99231b50..000000000 --- a/themes/main/server/src/lib/notifiers/NotifierStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); - -import { INotifier } from "./INotifier"; - -export class NotifierStub implements INotifier { - notifyStub: Sinon.SinonStub; - - constructor() { - this.notifyStub = Sinon.stub(); - } - - notify(to: string, subject: string, link: string): BluebirdPromise { - return this.notifyStub(to, subject, link); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/notifiers/SmtpNotifier.ts b/themes/main/server/src/lib/notifiers/SmtpNotifier.ts deleted file mode 100644 index f93a6d4a0..000000000 --- a/themes/main/server/src/lib/notifiers/SmtpNotifier.ts +++ /dev/null @@ -1,30 +0,0 @@ - - -import * as BluebirdPromise from "bluebird"; - -import { IMailSender } from "./IMailSender"; -import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; -import { SmtpNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; - -export class SmtpNotifier extends AbstractEmailNotifier { - private mailSender: IMailSender; - private sender: string; - - constructor(options: SmtpNotifierConfiguration, - mailSender: IMailSender) { - super(); - this.mailSender = mailSender; - this.sender = options.sender; - } - - sendEmail(to: string, subject: string, content: string) { - const mailOptions = { - from: this.sender, - to: to, - subject: subject, - html: content - }; - const that = this; - return this.mailSender.send(mailOptions); - } -} diff --git a/themes/main/server/src/lib/regulation/IRegulator.ts b/themes/main/server/src/lib/regulation/IRegulator.ts deleted file mode 100644 index c49425b24..000000000 --- a/themes/main/server/src/lib/regulation/IRegulator.ts +++ /dev/null @@ -1,6 +0,0 @@ -import BluebirdPromise = require("bluebird"); - -export interface IRegulator { - mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; - regulate(userId: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/regulation/Regulator.spec.ts b/themes/main/server/src/lib/regulation/Regulator.spec.ts deleted file mode 100644 index f9c6e6086..000000000 --- a/themes/main/server/src/lib/regulation/Regulator.spec.ts +++ /dev/null @@ -1,186 +0,0 @@ - -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import Assert = require("assert"); - -import { Regulator } from "./Regulator"; -import MockDate = require("mockdate"); -import exceptions = require("../Exceptions"); -import { UserDataStoreStub } from "../storage/UserDataStoreStub.spec"; - -describe("regulation/Regulator", function () { - const USER1 = "USER1"; - const USER2 = "USER2"; - let userDataStoreStub: UserDataStoreStub; - - beforeEach(function () { - userDataStoreStub = new UserDataStoreStub(); - const dataStore: { [userId: string]: { userId: string, date: Date, isAuthenticationSuccessful: boolean }[] } = { - [USER1]: [], - [USER2]: [] - }; - - userDataStoreStub.saveAuthenticationTraceStub.callsFake(function (userId, isAuthenticationSuccessful) { - dataStore[userId].unshift({ - userId: userId, - date: new Date(), - isAuthenticationSuccessful: isAuthenticationSuccessful, - }); - return BluebirdPromise.resolve(); - }); - - userDataStoreStub.retrieveLatestAuthenticationTracesStub.callsFake(function (userId, count) { - const ret = (dataStore[userId].length <= count) ? dataStore[userId] : dataStore[userId].slice(0, 3); - return BluebirdPromise.resolve(ret); - }); - }); - - afterEach(function () { - MockDate.reset(); - }); - - function markAuthenticationAt(regulator: Regulator, user: string, time: string, success: boolean) { - MockDate.set(time); - return regulator.mark(user, success); - } - - it("should mark 2 authentication and regulate (accept)", function () { - const regulator = new Regulator(userDataStoreStub, 3, 10, 10); - - return regulator.mark(USER1, false) - .then(function () { - return regulator.mark(USER1, true); - }) - .then(function () { - return regulator.regulate(USER1); - }); - }); - - it("should mark 3 authentications and regulate (reject)", function () { - const regulator = new Regulator(userDataStoreStub, 3, 10, 10); - - return regulator.mark(USER1, false) - .then(function () { - return regulator.mark(USER1, false); - }) - .then(function () { - return regulator.mark(USER1, false); - }) - .then(function () { - return regulator.regulate(USER1); - }) - .then(function () { return BluebirdPromise.reject(new Error("should not be here!")); }) - .catch(exceptions.AuthenticationRegulationError, function () { - return BluebirdPromise.resolve(); - }); - }); - - it("should mark 1 failed, 1 successful and 1 failed authentications within minimum time and regulate (accept)", function () { - const regulator = new Regulator(userDataStoreStub, 3, 60, 30); - - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", true); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:20", false); - }) - .then(function () { - return regulator.regulate(USER1); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:30", false); - }) - .then(function () { - return regulator.regulate(USER1); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:39", false); - }) - .then(function () { - return regulator.regulate(USER1); - }) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here!")); - }, - function () { - return BluebirdPromise.resolve(); - }); - }); - - it("should regulate user if number of failures is greater than 3 in allowed time lapse", function () { - function markAuthentications(regulator: Regulator, user: string) { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:45", false); - }) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:01:05", false); - }) - .then(function () { - return regulator.regulate(user); - }); - } - - const regulator1 = new Regulator(userDataStoreStub, 3, 60, 60); - const regulator2 = new Regulator(userDataStoreStub, 3, 2 * 60, 60); - - const p1 = markAuthentications(regulator1, USER1); - const p2 = markAuthentications(regulator2, USER2); - - return BluebirdPromise.join(p1, p2) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here...")); - }, function () { - Assert(p1.isFulfilled()); - Assert(p2.isRejected()); - }); - }); - - it("should user wait after regulation to authenticate again", function () { - function markAuthentications(regulator: Regulator, user: string) { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:10", false); - }) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:15", false); - }) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:25", false); - }) - .then(function () { - MockDate.set("1/2/2000 00:00:54"); - return regulator.regulate(user); - }) - .then(function () { - return BluebirdPromise.reject(new Error("should fail at this time")); - }, function () { - MockDate.set("1/2/2000 00:00:56"); - return regulator.regulate(user); - }); - } - - const regulator = new Regulator(userDataStoreStub, 4, 30, 30); - return markAuthentications(regulator, USER1); - }); - - it("should disable regulation when max_retries is set to 0", function () { - const maxRetries = 0; - const regulator = new Regulator(userDataStoreStub, maxRetries, 60, 30); - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", false); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:15", false); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:25", false); - }) - .then(function () { - MockDate.set("1/2/2000 00:00:26"); - return regulator.regulate(USER1); - }); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/regulation/Regulator.ts b/themes/main/server/src/lib/regulation/Regulator.ts deleted file mode 100644 index 1037a6a17..000000000 --- a/themes/main/server/src/lib/regulation/Regulator.ts +++ /dev/null @@ -1,55 +0,0 @@ - -import * as BluebirdPromise from "bluebird"; -import exceptions = require("../Exceptions"); -import { IUserDataStore } from "../storage/IUserDataStore"; -import { AuthenticationTraceDocument } from "../storage/AuthenticationTraceDocument"; -import { IRegulator } from "./IRegulator"; - -export class Regulator implements IRegulator { - private userDataStore: IUserDataStore; - private banTime: number; - private findTime: number; - private maxRetries: number; - - constructor(userDataStore: any, maxRetries: number, findTime: number, banTime: number) { - this.userDataStore = userDataStore; - this.banTime = banTime; - this.findTime = findTime; - this.maxRetries = maxRetries; - } - - // Mark authentication - mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { - return this.userDataStore.saveAuthenticationTrace(userId, isAuthenticationSuccessful); - } - - regulate(userId: string): BluebirdPromise { - const that = this; - - if (that.maxRetries <= 0) return BluebirdPromise.resolve(); - - return this.userDataStore.retrieveLatestAuthenticationTraces(userId, that.maxRetries) - .then((docs: AuthenticationTraceDocument[]) => { - // less than the max authorized number of authentication in time range, thus authorizing access - if (docs.length < that.maxRetries) return BluebirdPromise.resolve(); - - const numberOfFailedAuth = docs - .map(function (d: AuthenticationTraceDocument) { return d.isAuthenticationSuccessful == false ? 1 : 0; }) - .reduce(function (acc, v) { return acc + v; }, 0); - - if (numberOfFailedAuth < this.maxRetries) return BluebirdPromise.resolve(); - - const newestDocument = docs[0]; - const oldestDocument = docs[that.maxRetries - 1]; - - const authenticationsTimeRangeInSeconds = (newestDocument.date.getTime() - oldestDocument.date.getTime()) / 1000; - const tooManyAuthInTimelapse = (authenticationsTimeRangeInSeconds < this.findTime); - const stillInBannedTimeRange = (new Date(new Date().getTime() - this.banTime * 1000) < newestDocument.date); - - if (tooManyAuthInTimelapse && stillInBannedTimeRange) - throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes."); - - return BluebirdPromise.resolve(); - }); - } -} diff --git a/themes/main/server/src/lib/regulation/RegulatorStub.spec.ts b/themes/main/server/src/lib/regulation/RegulatorStub.spec.ts deleted file mode 100644 index ca8a00fb1..000000000 --- a/themes/main/server/src/lib/regulation/RegulatorStub.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Bluebird = require("bluebird"); -import Sinon = require("sinon"); -import { IRegulator } from "./IRegulator"; - - -export class RegulatorStub implements IRegulator { - markStub: Sinon.SinonStub; - regulateStub: Sinon.SinonStub; - - constructor() { - this.markStub = Sinon.stub(); - this.regulateStub = Sinon.stub(); - } - - mark(userId: string, isAuthenticationSuccessful: boolean): Bluebird { - return this.markStub(userId, isAuthenticationSuccessful); - } - - regulate(userId: string): Bluebird { - return this.regulateStub(userId); - } -} diff --git a/themes/main/server/src/lib/routes/error/401/get.spec.ts b/themes/main/server/src/lib/routes/error/401/get.spec.ts deleted file mode 100644 index 9fdac9c3c..000000000 --- a/themes/main/server/src/lib/routes/error/401/get.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import Sinon = require("sinon"); -import Express = require("express"); -import Assert = require("assert"); -import Get401 from "./get"; -import { ServerVariables } from "../../../ServerVariables"; -import { ServerVariablesMockBuilder, ServerVariablesMock } - from "../../../ServerVariablesMockBuilder.spec"; - -describe("routes/error/401/get", function () { - let vars: ServerVariables; - let mocks: ServerVariablesMock; - let req: any; - let res: any; - let renderSpy: Sinon.SinonSpy; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - vars = s.variables; - mocks = s.mocks; - - renderSpy = Sinon.spy(); - req = { - headers: {} - }; - res = { - render: renderSpy - }; - }); - - it("should set redirection url to the default redirection url", function () { - vars.config.default_redirection_url = "http://default-redirection"; - return Get401(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/401", { - redirection_url: "http://default-redirection" - })); - }); - }); - - it("should set redirection url to the referer", function () { - req.headers["referer"] = "http://redirection"; - return Get401(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/401", { - redirection_url: "http://redirection" - })); - }); - }); - - it("should render without redirecting the user", function () { - return Get401(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/401", { - redirection_url: undefined - })); - }); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/error/401/get.ts b/themes/main/server/src/lib/routes/error/401/get.ts deleted file mode 100644 index ca4a3963d..000000000 --- a/themes/main/server/src/lib/routes/error/401/get.ts +++ /dev/null @@ -1,15 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import redirector from "../redirector"; -import { ServerVariables } from "../../../ServerVariables"; - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response): BluebirdPromise { - const redirectionUrl = redirector(req, vars); - res.render("errors/401", { - redirection_url: redirectionUrl - }); - return BluebirdPromise.resolve(); - }; -} diff --git a/themes/main/server/src/lib/routes/error/403/get.spec.ts b/themes/main/server/src/lib/routes/error/403/get.spec.ts deleted file mode 100644 index 22eb84853..000000000 --- a/themes/main/server/src/lib/routes/error/403/get.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import Sinon = require("sinon"); -import Express = require("express"); -import Assert = require("assert"); -import Get403 from "./get"; -import { ServerVariables } from "../../../ServerVariables"; -import { ServerVariablesMockBuilder, ServerVariablesMock } - from "../../../ServerVariablesMockBuilder.spec"; - -describe("routes/error/403/get", function () { - let vars: ServerVariables; - let mocks: ServerVariablesMock; - let req: any; - let res: any; - let renderSpy: Sinon.SinonSpy; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - vars = s.variables; - mocks = s.mocks; - - renderSpy = Sinon.spy(); - req = { - headers: {} - }; - res = { - render: renderSpy - }; - }); - - it("should set redirection url to the default redirection url", function () { - vars.config.default_redirection_url = "http://default-redirection"; - return Get403(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/403", { - redirection_url: "http://default-redirection" - })); - }); - }); - - it("should set redirection url to the referer", function () { - req.headers["referer"] = "http://redirection"; - return Get403(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/403", { - redirection_url: "http://redirection" - })); - }); - }); - - it("should render without redirecting the user", function () { - return Get403(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/403", { - redirection_url: undefined - })); - }); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/error/403/get.ts b/themes/main/server/src/lib/routes/error/403/get.ts deleted file mode 100644 index 3ab0319e5..000000000 --- a/themes/main/server/src/lib/routes/error/403/get.ts +++ /dev/null @@ -1,15 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import redirector from "../redirector"; -import { ServerVariables } from "../../../ServerVariables"; - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response): BluebirdPromise { - const redirectionUrl = redirector(req, vars); - res.render("errors/403", { - redirection_url: redirectionUrl - }); - return BluebirdPromise.resolve(); - }; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/error/404/get.spec.ts b/themes/main/server/src/lib/routes/error/404/get.spec.ts deleted file mode 100644 index 73e4e6cef..000000000 --- a/themes/main/server/src/lib/routes/error/404/get.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Sinon = require("sinon"); -import Express = require("express"); -import Assert = require("assert"); -import Get404 from "./get"; - -describe("routes/error/404/get", function () { - it("should render the page", function () { - const req = {} as Express.Request; - const res = { - render: Sinon.stub() - }; - - return Get404(req, res as any) - .then(function () { - Assert(res.render.calledOnce); - Assert(res.render.calledWith("errors/404")); - }); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/error/404/get.ts b/themes/main/server/src/lib/routes/error/404/get.ts deleted file mode 100644 index 6693b6fc8..000000000 --- a/themes/main/server/src/lib/routes/error/404/get.ts +++ /dev/null @@ -1,8 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import express = require("express"); - -export default function (req: express.Request, res: express.Response): BluebirdPromise { - res.render("errors/404"); - return BluebirdPromise.resolve(); -} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/error/redirector.ts b/themes/main/server/src/lib/routes/error/redirector.ts deleted file mode 100644 index b1a3ccc11..000000000 --- a/themes/main/server/src/lib/routes/error/redirector.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Express = require("express"); -import { ServerVariables } from "../../ServerVariables"; - -export default function (req: Express.Request, vars: ServerVariables): string { - let redirectionUrl: string; - - if (req.headers && req.headers["referer"]) - redirectionUrl = "" + req.headers["referer"]; - else if (vars.config.default_redirection_url) - redirectionUrl = vars.config.default_redirection_url; - - return redirectionUrl; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/firstfactor/get.ts b/themes/main/server/src/lib/routes/firstfactor/get.ts deleted file mode 100644 index d94f656c8..000000000 --- a/themes/main/server/src/lib/routes/firstfactor/get.ts +++ /dev/null @@ -1,72 +0,0 @@ - -import express = require("express"); -import Endpoints = require("../../../../../shared/api"); -import BluebirdPromise = require("bluebird"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import Constants = require("../../../../../shared/constants"); -import Util = require("util"); -import { ServerVariables } from "../../ServerVariables"; -import { SafeRedirector } from "../../utils/SafeRedirection"; -import { Level } from "../../authentication/Level"; - -function getRedirectParam( - req: express.Request) { - return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : undefined; -} - -function redirectToSecondFactorPage( - req: express.Request, - res: express.Response) { - - const redirectUrl = getRedirectParam(req); - if (!redirectUrl) - res.redirect(Endpoints.SECOND_FACTOR_GET); - else - res.redirect( - Util.format("%s?%s=%s", - Endpoints.SECOND_FACTOR_GET, - Constants.REDIRECT_QUERY_PARAM, - redirectUrl)); -} - -function redirectToService( - req: express.Request, - res: express.Response, - redirector: SafeRedirector) { - const redirectUrl = getRedirectParam(req); - if (!redirectUrl) { - res.redirect(Endpoints.LOGGED_IN); - } else { - redirector.redirectOrElse(res, redirectUrl, Endpoints.LOGGED_IN); - } -} - -function renderFirstFactor( - res: express.Response) { - - res.render("firstfactor", { - first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST, - reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET - }); -} - -export default function ( - vars: ServerVariables) { - - const redirector = new SafeRedirector(vars.config.session.domain); - return function (req: express.Request, res: express.Response): BluebirdPromise { - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, vars.logger); - if (authSession.authentication_level == Level.ONE_FACTOR) { - redirectToSecondFactorPage(req, res); - } else if (authSession.authentication_level == Level.TWO_FACTOR) { - redirectToService(req, res, redirector); - } else { - renderFirstFactor(res); - } - resolve(); - }); - }; -} diff --git a/themes/main/server/src/lib/routes/firstfactor/post.spec.ts b/themes/main/server/src/lib/routes/firstfactor/post.spec.ts deleted file mode 100644 index e1d078cdd..000000000 --- a/themes/main/server/src/lib/routes/firstfactor/post.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ - -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import Assert = require("assert"); -import FirstFactorPost = require("./post"); -import exceptions = require("../../Exceptions"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../types/AuthenticationSession"; -import Endpoints = require("../../../../../shared/api"); -import AuthenticationRegulatorMock = require("../../regulation/RegulatorStub.spec"); -import ExpressMock = require("../../stubs/express.spec"); -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../ServerVariables"; - -describe("routes/firstfactor/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let emails: string[]; - let groups: string[]; - let vars: ServerVariables; - let mocks: ServerVariablesMock; - let authSession: AuthenticationSession; - - beforeEach(function () { - emails = ["test_ok@example.com"]; - groups = ["group1", "group2" ]; - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - mocks.authorizer.authorizationMock.returns(true); - mocks.regulator.regulateStub.returns(BluebirdPromise.resolve()); - mocks.regulator.markStub.returns(BluebirdPromise.resolve()); - - req = { - originalUrl: "/api/firstfactor", - body: { - username: "username", - password: "password" - }, - query: { - redirect: "http://redirect.url" - }, - session: { - cookie: {} - }, - headers: { - host: "home.example.com" - } - }; - - res = ExpressMock.ResponseMock(); - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - }); - - it("should reply with 204 if success", function () { - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.resolve({ - emails: emails, - groups: groups - })); - return FirstFactorPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal("username", authSession.userid); - Assert(res.send.calledOnce); - }); - }); - - describe("keep me logged in", () => { - beforeEach(() => { - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.resolve({ - emails: emails, - groups: groups - })); - req.body.keepMeLoggedIn = "true"; - return FirstFactorPost.default(vars)(req as any, res as any); - }); - - it("should set keep_me_logged_in session variable to true", function () { - Assert.equal(authSession.keep_me_logged_in, true); - }); - - it("should set cookie maxAge to one year", function () { - Assert.equal(req.session.cookie.maxAge, 365 * 24 * 60 * 60 * 1000); - }); - }); - - it("should retrieve email from LDAP", function () { - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.resolve([{ mail: ["test@example.com"] }])); - return FirstFactorPost.default(vars)(req as any, res as any); - }); - - it("should set first email address as user session variable", function () { - const emails = ["test_ok@example.com"]; - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.resolve({ - emails: emails, - groups: groups - })); - - return FirstFactorPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal("test_ok@example.com", authSession.email); - }); - }); - - it("should return error message when LDAP authenticator throws", function () { - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.reject(new exceptions.LdapBindError("Bad credentials"))); - - return FirstFactorPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.equal(mocks.regulator.markStub.getCall(0).args[0], "username"); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - }); - }); - - it("should return error message when regulator rejects authentication", function () { - const err = new exceptions.AuthenticationRegulationError("Authentication regulation..."); - mocks.regulator.regulateStub.returns(BluebirdPromise.reject(err)); - return FirstFactorPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - }); - }); -}); - - diff --git a/themes/main/server/src/lib/routes/firstfactor/post.ts b/themes/main/server/src/lib/routes/firstfactor/post.ts deleted file mode 100644 index 565681d6a..000000000 --- a/themes/main/server/src/lib/routes/firstfactor/post.ts +++ /dev/null @@ -1,101 +0,0 @@ - -import Exceptions = require("../../Exceptions"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import Endpoint = require("../../../../../shared/api"); -import ErrorReplies = require("../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import Constants = require("../../../../../shared/constants"); -import UserMessages = require("../../../../../shared/UserMessages"); -import { ServerVariables } from "../../ServerVariables"; -import { AuthenticationSession } from "../../../../types/AuthenticationSession"; -import { GroupsAndEmails } from "../../authentication/backends/GroupsAndEmails"; -import { Level as AuthenticationLevel } from "../../authentication/Level"; -import { Level as AuthorizationLevel } from "../../authorization/Level"; -import { URLDecomposer } from "../../utils/URLDecomposer"; - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response) - : BluebirdPromise { - const username: string = req.body.username; - const password: string = req.body.password; - const keepMeLoggedIn: boolean = req.body.keepMeLoggedIn && - req.body.keepMeLoggedIn === "true"; - let authSession: AuthenticationSession; - - if (keepMeLoggedIn) { - // Stay connected for 1 year. - vars.logger.debug(req, "User requested to stay logged in for one year."); - req.session.cookie.maxAge = 365 * 24 * 60 * 60 * 1000; - } - - return BluebirdPromise.resolve() - .then(function () { - if (!username || !password) { - return BluebirdPromise.reject(new Error("No username or password.")); - } - vars.logger.info(req, "Starting authentication of user \"%s\"", username); - authSession = AuthenticationSessionHandler.get(req, vars.logger); - return vars.regulator.regulate(username); - }) - .then(function () { - vars.logger.info(req, "No regulation applied."); - return vars.usersDatabase.checkUserPassword(username, password); - }) - .then(function (groupsAndEmails: GroupsAndEmails) { - vars.logger.info(req, - "LDAP binding successful. Retrieved information about user are %s", - JSON.stringify(groupsAndEmails)); - authSession.userid = username; - authSession.keep_me_logged_in = keepMeLoggedIn; - authSession.authentication_level = AuthenticationLevel.ONE_FACTOR; - const redirectUrl: string = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined" - // Fuck, don't know why it is a string! - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : ""; - - const emails: string[] = groupsAndEmails.emails; - const groups: string[] = groupsAndEmails.groups; - const decomposition = URLDecomposer.fromUrl(redirectUrl); - const authorizationLevel = (decomposition) - ? vars.authorizer.authorization( - {domain: decomposition.domain, resource: decomposition.path}, - {user: username, groups: groups}) - : AuthorizationLevel.TWO_FACTOR; - - if (emails.length > 0) - authSession.email = emails[0]; - authSession.groups = groups; - - vars.logger.debug(req, "Mark successful authentication to regulator."); - vars.regulator.mark(username, true); - - if (authorizationLevel <= AuthorizationLevel.ONE_FACTOR) { - let newRedirectionUrl: string = redirectUrl; - if (!newRedirectionUrl) - newRedirectionUrl = Endpoint.LOGGED_IN; - res.send({ - redirect: newRedirectionUrl - }); - vars.logger.debug(req, "Redirect to '%s'", redirectUrl); - } - else { - let newRedirectUrl = Endpoint.SECOND_FACTOR_GET; - if (redirectUrl) { - newRedirectUrl += "?" + Constants.REDIRECT_QUERY_PARAM + "=" - + redirectUrl; - } - vars.logger.debug(req, "Redirect to '%s'", newRedirectUrl); - res.send({ - redirect: newRedirectUrl - }); - } - return BluebirdPromise.resolve(); - }) - .catch(Exceptions.LdapBindError, function (err: Error) { - vars.regulator.mark(username, false); - return ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)(err); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)); - }; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/loggedin/get.ts b/themes/main/server/src/lib/routes/loggedin/get.ts deleted file mode 100644 index 283a041b1..000000000 --- a/themes/main/server/src/lib/routes/loggedin/get.ts +++ /dev/null @@ -1,23 +0,0 @@ -import Express = require("express"); -import Endpoints = require("../../../../../shared/api"); -import BluebirdPromise = require("bluebird"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { ServerVariables } from "../../ServerVariables"; -import ErrorReplies = require("../../ErrorReplies"); - -export default function (vars: ServerVariables) { - function handler(req: Express.Request, res: Express.Response): BluebirdPromise { - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, vars.logger); - res.render("already-logged-in", { - logout_endpoint: Endpoints.LOGOUT_GET, - username: authSession.userid, - redirection_url: vars.config.default_redirection_url - }); - resolve(); - }) - .catch(ErrorReplies.replyWithError401(req, res, vars.logger)); - } - - return handler; -} diff --git a/themes/main/server/src/lib/routes/logout/get.ts b/themes/main/server/src/lib/routes/logout/get.ts deleted file mode 100644 index 4d5112146..000000000 --- a/themes/main/server/src/lib/routes/logout/get.ts +++ /dev/null @@ -1,20 +0,0 @@ - -import express = require("express"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import Constants = require("../../../../../shared/constants"); -import { ServerVariables } from "../../ServerVariables"; - -function getRedirectParam(req: express.Request) { - return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : undefined; -} - -export default function (vars: ServerVariables) { - return function(req: express.Request, res: express.Response) { - const redirect_param = getRedirectParam(req); - const redirect_url = redirect_param || "/"; - AuthenticationSessionHandler.reset(req); - res.redirect(redirect_url); - }; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/password-reset/constants.ts b/themes/main/server/src/lib/routes/password-reset/constants.ts deleted file mode 100644 index 5c639e92a..000000000 --- a/themes/main/server/src/lib/routes/password-reset/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export const CHALLENGE = "reset-password"; \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/password-reset/form/post.spec.ts b/themes/main/server/src/lib/routes/password-reset/form/post.spec.ts deleted file mode 100644 index ed029c906..000000000 --- a/themes/main/server/src/lib/routes/password-reset/form/post.spec.ts +++ /dev/null @@ -1,122 +0,0 @@ - -import PasswordResetFormPost = require("./post"); -import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; -import { UserDataStore } from "../../../storage/UserDataStore"; -import Sinon = require("sinon"); -import Assert = require("assert"); -import BluebirdPromise = require("bluebird"); -import ExpressMock = require("../../../stubs/express.spec"); -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../ServerVariables"; -import { Level } from "../../../authentication/Level"; - -describe("routes/password-reset/form/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let vars: ServerVariables; - let mocks: ServerVariablesMock; - let authSession: AuthenticationSession; - - beforeEach(function () { - req = { - originalUrl: "/api/password-reset", - body: { - userid: "user" - }, - session: {}, - headers: { - host: "localhost" - } - }; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - - mocks.config.authentication_backend.ldap = { - url: "ldap://ldapjs", - mail_attribute: "mail", - user: "user", - password: "password", - additional_users_dn: "ou=users", - additional_groups_dn: "ou=groups", - base_dn: "dc=example,dc=com", - users_filter: "user", - group_name_attribute: "cn", - groups_filter: "groups" - }; - - res = ExpressMock.ResponseMock(); - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - authSession.userid = "user"; - authSession.email = "user@example.com"; - authSession.authentication_level = Level.ONE_FACTOR; - }); - - describe("test reset password post", () => { - it("should update the password and reset auth_session for reauthentication", function () { - req.body = {}; - req.body.password = "new-password"; - - mocks.usersDatabase.updatePasswordStub.returns(BluebirdPromise.resolve()); - - authSession.identity_check = { - userid: "user", - challenge: "reset-password" - }; - return PasswordResetFormPost.default(vars)(req as any, res as any) - .then(function () { - return AuthenticationSessionHandler.get(req as any, vars.logger); - }).then(function (_authSession) { - Assert.equal(res.status.getCall(0).args[0], 204); - Assert.equal(_authSession.authentication_level, Level.NOT_AUTHENTICATED); - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if identity_challenge does not exist", function () { - authSession.identity_check = { - userid: "user", - challenge: undefined - }; - return PasswordResetFormPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "An error occurred during password reset. Your password has not been changed." - }); - }); - }); - - it("should fail when ldap fails", function () { - req.body = {}; - req.body.password = "new-password"; - - mocks.usersDatabase.updatePasswordStub - .returns(BluebirdPromise.reject("Internal error with LDAP")); - - authSession.identity_check = { - challenge: "reset-password", - userid: "user" - }; - return PasswordResetFormPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "An error occurred during password reset. Your password has not been changed." - }); - return BluebirdPromise.resolve(); - }); - }); - }); -}); diff --git a/themes/main/server/src/lib/routes/password-reset/form/post.ts b/themes/main/server/src/lib/routes/password-reset/form/post.ts deleted file mode 100644 index fccd7471b..000000000 --- a/themes/main/server/src/lib/routes/password-reset/form/post.ts +++ /dev/null @@ -1,50 +0,0 @@ - -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import objectPath = require("object-path"); -import exceptions = require("../../../Exceptions"); -import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; -import ErrorReplies = require("../../../ErrorReplies"); -import UserMessages = require("../../../../../../shared/UserMessages"); -import { ServerVariables } from "../../../ServerVariables"; - -import Constants = require("./../constants"); - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const newPassword = objectPath.get(req, "body.password"); - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - if (!authSession.identity_check) { - reject(new Error("No identity check initiated")); - return; - } - - vars.logger.info(req, "User %s wants to reset his/her password.", - authSession.identity_check.userid); - vars.logger.debug(req, "Challenge %s", authSession.identity_check.challenge); - - if (authSession.identity_check.challenge != Constants.CHALLENGE) { - reject(new Error("Bad challenge.")); - return; - } - resolve(); - }) - .then(function () { - return vars.usersDatabase.updatePassword(authSession.identity_check.userid, newPassword); - }) - .then(function () { - vars.logger.info(req, "Password reset for user '%s'", - authSession.identity_check.userid); - AuthenticationSessionHandler.reset(req); - res.status(204); - res.send(); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.RESET_PASSWORD_FAILED)); - }; -} diff --git a/themes/main/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts b/themes/main/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts deleted file mode 100644 index ac6a41754..000000000 --- a/themes/main/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts +++ /dev/null @@ -1,92 +0,0 @@ - -import PasswordResetHandler - from "./PasswordResetHandler"; -import { UserDataStore } from "../../../storage/UserDataStore"; -import Sinon = require("sinon"); -import winston = require("winston"); -import assert = require("assert"); -import BluebirdPromise = require("bluebird"); -import ExpressMock = require("../../../stubs/express.spec"); -import { ServerVariablesMock, ServerVariablesMockBuilder } - from "../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../ServerVariables"; - -describe("routes/password-reset/identity/PasswordResetHandler", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - req = { - originalUrl: "/non-api/xxx", - query: { - userid: "user" - }, - session: { - auth: { - userid: "user", - email: "user@example.com", - first_factor: true, - second_factor: false - } - }, - headers: { - host: "localhost" - } - }; - - const options = { - inMemoryOnly: true - }; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - mocks.userDataStore.saveU2FRegistrationStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.produceIdentityValidationTokenStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.consumeIdentityValidationTokenStub - .returns(BluebirdPromise.resolve({})); - res = ExpressMock.ResponseMock(); - }); - - describe("test reset password identity pre check", () => { - it("should fail when no userid is provided", function () { - req.query.userid = undefined; - const handler = new PasswordResetHandler(vars.logger, - vars.usersDatabase); - return handler.preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject("It should fail"); - }) - .catch(function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if ldap fail", function () { - mocks.usersDatabase.getEmailsStub - .returns(BluebirdPromise.reject("Internal error")); - new PasswordResetHandler(vars.logger, vars.usersDatabase) - .preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here")); - }, - function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should returns identity when ldap replies", function () { - mocks.usersDatabase.getEmailsStub - .returns(BluebirdPromise.resolve(["test@example.com"])); - return new PasswordResetHandler(vars.logger, vars.usersDatabase) - .preValidationInit(req as any); - }); - }); -}); diff --git a/themes/main/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts b/themes/main/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts deleted file mode 100644 index 42ae92cda..000000000 --- a/themes/main/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts +++ /dev/null @@ -1,69 +0,0 @@ -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import objectPath = require("object-path"); - -import exceptions = require("../../../Exceptions"); -import { Identity } from "../../../../../types/Identity"; -import { IdentityValidable } from "../../../IdentityValidable"; -import { PRE_VALIDATION_TEMPLATE } from "../../../IdentityCheckPreValidationTemplate"; -import Constants = require("../constants"); -import { IRequestLogger } from "../../../logging/IRequestLogger"; -import { IUsersDatabase } from "../../../authentication/backends/IUsersDatabase"; - -export const TEMPLATE_NAME = "password-reset-form"; - -export default class PasswordResetHandler implements IdentityValidable { - private logger: IRequestLogger; - private usersDatabase: IUsersDatabase; - - constructor(logger: IRequestLogger, usersDatabase: IUsersDatabase) { - this.logger = logger; - this.usersDatabase = usersDatabase; - } - - challenge(): string { - return Constants.CHALLENGE; - } - - preValidationInit(req: express.Request): BluebirdPromise { - const that = this; - const userid: string = - objectPath.get(req, "query.userid"); - return BluebirdPromise.resolve() - .then(function () { - that.logger.debug(req, "User '%s' requested a password reset", userid); - if (!userid) - return BluebirdPromise.reject( - new exceptions.AccessDeniedError("No user id provided")); - - return that.usersDatabase.getEmails(userid); - }) - .then(function (emails: string[]) { - if (!emails && emails.length <= 0) throw new Error("No email found"); - const identity = { - email: emails[0], - userid: userid - }; - return BluebirdPromise.resolve(identity); - }) - .catch(function (err: Error) { - return BluebirdPromise.reject(new exceptions.IdentityError(err.message)); - }); - } - - preValidationResponse(req: express.Request, res: express.Response) { - res.render(PRE_VALIDATION_TEMPLATE); - } - - postValidationInit(req: express.Request) { - return BluebirdPromise.resolve(); - } - - postValidationResponse(req: express.Request, res: express.Response) { - res.render(TEMPLATE_NAME); - } - - mailSubject(): string { - return "Reset your password"; - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/password-reset/request/get.ts b/themes/main/server/src/lib/routes/password-reset/request/get.ts deleted file mode 100644 index 8f3ae2b4b..000000000 --- a/themes/main/server/src/lib/routes/password-reset/request/get.ts +++ /dev/null @@ -1,13 +0,0 @@ - -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import objectPath = require("object-path"); -import exceptions = require("../../../Exceptions"); - -import Constants = require("./../constants"); - -const TEMPLATE_NAME = "password-reset-request"; - -export default function (req: express.Request, res: express.Response) { - res.render(TEMPLATE_NAME); -} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/secondfactor/get.spec.ts b/themes/main/server/src/lib/routes/secondfactor/get.spec.ts deleted file mode 100644 index 6c77e1f69..000000000 --- a/themes/main/server/src/lib/routes/secondfactor/get.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import SecondFactorGet from "./get"; -import { ServerVariablesMockBuilder, ServerVariablesMock } - from "../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../ServerVariables"; -import Sinon = require("sinon"); -import ExpressMock = require("../../stubs/express.spec"); -import Assert = require("assert"); -import Endpoints = require("../../../../../shared/api"); -import BluebirdPromise = require("bluebird"); - -describe("routes/secondfactor/get", function () { - let mocks: ServerVariablesMock; - let vars: ServerVariables; - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req = ExpressMock.RequestMock(); - res = ExpressMock.ResponseMock(); - - req.session = { - auth: { - userid: "user", - first_factor: true, - second_factor: false - } - }; - }); - - describe("test rendering", function () { - it("should render second factor page", function () { - req.session.auth.second_factor = false; - return SecondFactorGet(vars)(req as any, res as any) - .then(function () { - Assert(res.render.calledWith("secondfactor")); - return BluebirdPromise.resolve(); - }); - }); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/secondfactor/get.ts b/themes/main/server/src/lib/routes/secondfactor/get.ts deleted file mode 100644 index 9f6deb4c6..000000000 --- a/themes/main/server/src/lib/routes/secondfactor/get.ts +++ /dev/null @@ -1,28 +0,0 @@ - -import Express = require("express"); -import Endpoints = require("../../../../../shared/api"); -import BluebirdPromise = require("bluebird"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { ServerVariables } from "../../ServerVariables"; - -const TEMPLATE_NAME = "secondfactor"; - -export default function (vars: ServerVariables) { - function handler(req: Express.Request, res: Express.Response) - : BluebirdPromise { - - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, vars.logger); - - res.render(TEMPLATE_NAME, { - username: authSession.userid, - totp_identity_start_endpoint: - Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, - u2f_identity_start_endpoint: - Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET - }); - resolve(); - }); - } - return handler; -} diff --git a/themes/main/server/src/lib/routes/secondfactor/redirect.spec.ts b/themes/main/server/src/lib/routes/secondfactor/redirect.spec.ts deleted file mode 100644 index ea66e6dca..000000000 --- a/themes/main/server/src/lib/routes/secondfactor/redirect.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import Redirect from "./redirect"; -import ExpressMock = require("../../stubs/express.spec"); -import { ServerVariablesMockBuilder, ServerVariablesMock } -from "../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../ServerVariables"; -import Assert = require("assert"); - -describe("routes/secondfactor/redirect", function() { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req = ExpressMock.RequestMock(); - res = ExpressMock.ResponseMock(); - }); - - it("should redirect to default_redirection_url", function() { - vars.config.default_redirection_url = "http://default_redirection_url"; - Redirect(vars)(req as any, res as any) - .then(function() { - Assert(res.json.calledWith({ - redirect: "http://default_redirection_url" - })); - }); - }); - - it("should redirect to /", function() { - Redirect(vars)(req as any, res as any) - .then(function() { - Assert(res.json.calledWith({ - redirect: "/" - })); - }); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/secondfactor/redirect.ts b/themes/main/server/src/lib/routes/secondfactor/redirect.ts deleted file mode 100644 index 5d84d9ebf..000000000 --- a/themes/main/server/src/lib/routes/secondfactor/redirect.ts +++ /dev/null @@ -1,30 +0,0 @@ - -import express = require("express"); -import objectPath = require("object-path"); -import Endpoints = require("../../../../../shared/api"); -import { ServerVariables } from "../../ServerVariables"; -import BluebirdPromise = require("bluebird"); -import ErrorReplies = require("../../ErrorReplies"); -import UserMessages = require("../../../../../shared/UserMessages"); -import { RedirectionMessage } from "../../../../../shared/RedirectionMessage"; -import Constants = require("../../../../../shared/constants"); - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response) - : BluebirdPromise { - - return new BluebirdPromise(function (resolve, reject) { - let redirectUrl: string = "/"; - if (vars.config.default_redirection_url) { - redirectUrl = vars.config.default_redirection_url; - } - vars.logger.debug(req, "Request redirection to \"%s\".", redirectUrl); - res.json({ - redirect: redirectUrl - } as RedirectionMessage); - return resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - }; -} diff --git a/themes/main/server/src/lib/routes/secondfactor/totp/constants.ts b/themes/main/server/src/lib/routes/secondfactor/totp/constants.ts deleted file mode 100644 index 7b5a1dcfe..000000000 --- a/themes/main/server/src/lib/routes/secondfactor/totp/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ - -export const CHALLENGE = "totp-register"; -export const TEMPLATE_NAME = "totp-register"; - diff --git a/themes/main/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts b/themes/main/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts deleted file mode 100644 index 78b8ea3ea..000000000 --- a/themes/main/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts +++ /dev/null @@ -1,116 +0,0 @@ -import Sinon = require("sinon"); -import RegistrationHandler from "./RegistrationHandler"; -import { Identity } from "../../../../../../types/Identity"; -import { UserDataStore } from "../../../../storage/UserDataStore"; -import BluebirdPromise = require("bluebird"); -import ExpressMock = require("../../../../stubs/express.spec"); -import { ServerVariablesMock, ServerVariablesMockBuilder } - from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; -import Assert = require("assert"); - -describe("routes/secondfactor/totp/identity/RegistrationHandler", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req = ExpressMock.RequestMock(); - req.session = { - auth: { - userid: "user", - email: "user@example.com", - first_factor: true, - second_factor: false, - identity_check: { - userid: "user", - challenge: "totp-register" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.produceIdentityValidationTokenStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.consumeIdentityValidationTokenStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.saveTOTPSecretStub - .returns(BluebirdPromise.resolve({})); - - res = ExpressMock.ResponseMock(); - }); - - describe("test totp registration pre validation", function () { - it("should fail if first_factor has not been passed", function () { - req.session.auth.first_factor = false; - return new RegistrationHandler(vars.logger, vars.userDataStore, - vars.totpHandler, vars.config.totp) - .preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject(new Error("It should fail")); - }) - .catch(function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if userid is missing", function (done) { - req.session.auth.first_factor = false; - req.session.auth.userid = undefined; - - new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler, - vars.config.totp) - .preValidationInit(req as any) - .catch(function (err: Error) { - done(); - }); - }); - - it("should fail if email is missing", function (done) { - req.session.auth.first_factor = false; - req.session.auth.email = undefined; - - new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler, - vars.config.totp) - .preValidationInit(req as any) - .catch(function (err: Error) { - done(); - }); - }); - - it("should succeed if first factor passed, userid and email are provided", - function () { - return new RegistrationHandler(vars.logger, vars.userDataStore, - vars.totpHandler, vars.config.totp) - .preValidationInit(req as any); - }); - }); - - describe("test totp registration post validation", function () { - it("should generate a secret using userId as label and issuer defined in config", function () { - vars.config.totp = { - issuer: "issuer" - }; - return new RegistrationHandler(vars.logger, vars.userDataStore, - vars.totpHandler, vars.config.totp) - .postValidationResponse(req as any, res as any) - .then(function() { - Assert(mocks.totpHandler.generateStub.calledWithExactly("user", "issuer")); - }); - }); - }); -}); diff --git a/themes/main/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts b/themes/main/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts deleted file mode 100644 index b39b6d045..000000000 --- a/themes/main/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts +++ /dev/null @@ -1,112 +0,0 @@ - -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import objectPath = require("object-path"); - -import { Identity } from "../../../../../../types/Identity"; -import { IdentityValidable } from "../../../../IdentityValidable"; -import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; -import Constants = require("../constants"); -import Endpoints = require("../../../../../../../shared/api"); -import ErrorReplies = require("../../../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import FirstFactorValidator = require("../../../../FirstFactorValidator"); -import { IRequestLogger } from "../../../../logging/IRequestLogger"; -import { IUserDataStore } from "../../../../storage/IUserDataStore"; -import { ITotpHandler } from "../../../../authentication/totp/ITotpHandler"; -import { TOTPSecret } from "../../../../../../types/TOTPSecret"; -import { TotpConfiguration } from "../../../../configuration/schema/TotpConfiguration"; - - -export default class RegistrationHandler implements IdentityValidable { - private logger: IRequestLogger; - private userDataStore: IUserDataStore; - private totp: ITotpHandler; - private configuration: TotpConfiguration; - - constructor(logger: IRequestLogger, - userDataStore: IUserDataStore, - totp: ITotpHandler, configuration: TotpConfiguration) { - this.logger = logger; - this.userDataStore = userDataStore; - this.totp = totp; - this.configuration = configuration; - } - - challenge(): string { - return Constants.CHALLENGE; - } - - private retrieveIdentity(req: express.Request): BluebirdPromise { - const that = this; - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, that.logger); - const userid = authSession.userid; - const email = authSession.email; - - if (!(userid && email)) { - return reject(new Error("User ID or email is missing")); - } - - const identity = { - email: email, - userid: userid - }; - return resolve(identity); - }); - } - - preValidationInit(req: express.Request): BluebirdPromise { - const that = this; - return FirstFactorValidator.validate(req, this.logger) - .then(function () { - return that.retrieveIdentity(req); - }); - } - - preValidationResponse(req: express.Request, res: express.Response) { - res.render(PRE_VALIDATION_TEMPLATE); - } - - postValidationInit(req: express.Request) { - return FirstFactorValidator.validate(req, this.logger); - } - - postValidationResponse(req: express.Request, res: express.Response) - : BluebirdPromise { - const that = this; - let secret: TOTPSecret; - let userId: string; - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, that.logger); - userId = authSession.userid; - - if (authSession.identity_check.challenge != Constants.CHALLENGE - || !userId) - return reject(new Error("Bad challenge.")); - - resolve(); - }) - .then(function () { - secret = that.totp.generate(userId, - that.configuration.issuer); - that.logger.debug(req, "Save the TOTP secret in DB"); - return that.userDataStore.saveTOTPSecret(userId, secret); - }) - .then(function () { - AuthenticationSessionHandler.reset(req); - - res.render(Constants.TEMPLATE_NAME, { - base32_secret: secret.base32, - otpauth_url: secret.otpauth_url, - login_endpoint: Endpoints.FIRST_FACTOR_GET - }); - }) - .catch(ErrorReplies.replyWithError200(req, res, that.logger, UserMessages.OPERATION_FAILED)); - } - - mailSubject(): string { - return "Set up Authelia's one-time password"; - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts b/themes/main/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts deleted file mode 100644 index 70a20d39d..000000000 --- a/themes/main/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import Sinon = require("sinon"); -import Assert = require("assert"); -import Exceptions = require("../../../../Exceptions"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import SignPost = require("./post"); -import { ServerVariables } from "../../../../ServerVariables"; - -import ExpressMock = require("../../../../stubs/express.spec"); -import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; -import { Level } from "../../../../authentication/Level"; - -describe("routes/secondfactor/totp/sign/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let authSession: AuthenticationSession; - let vars: ServerVariables; - let mocks: ServerVariablesMock; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - vars = s.variables; - mocks = s.mocks; - const app_get = Sinon.stub(); - req = { - originalUrl: "/api/totp-register", - app: {}, - body: { - token: "abc" - }, - session: {}, - query: { - redirect: "http://redirect" - } - }; - res = ExpressMock.ResponseMock(); - - const doc = { - userid: "user", - secret: { - base32: "ABCDEF" - } - }; - mocks.userDataStore.retrieveTOTPSecretStub.returns(BluebirdPromise.resolve(doc)); - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - authSession.userid = "user"; - authSession.authentication_level = Level.ONE_FACTOR; - }); - - - it("should send status code 200 when totp is valid", function () { - mocks.totpHandler.validateStub.returns(true); - return SignPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(authSession.authentication_level, Level.TWO_FACTOR); - return BluebirdPromise.resolve(); - }); - }); - - it("should send error message when totp is not valid", function () { - mocks.totpHandler.validateStub.returns(false); - return SignPost.default(vars)(req as any, res as any) - .then(function () { - Assert.notEqual(authSession.authentication_level, Level.TWO_FACTOR); - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); -}); - diff --git a/themes/main/server/src/lib/routes/secondfactor/totp/sign/post.ts b/themes/main/server/src/lib/routes/secondfactor/totp/sign/post.ts deleted file mode 100644 index 34a276d12..000000000 --- a/themes/main/server/src/lib/routes/secondfactor/totp/sign/post.ts +++ /dev/null @@ -1,42 +0,0 @@ -import Bluebird = require("bluebird"); -import Express = require("express"); - -import { TOTPSecretDocument } from "../../../../storage/TOTPSecretDocument"; -import Endpoints = require("../../../../../../../shared/api"); -import Redirect from "../../redirect"; -import ErrorReplies = require("../../../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { ServerVariables } from "../../../../ServerVariables"; -import { Level } from "../../../../authentication/Level"; - -const UNAUTHORIZED_MESSAGE = "Unauthorized access"; - -export default function (vars: ServerVariables) { - function handler(req: Express.Request, res: Express.Response): Bluebird { - let authSession: AuthenticationSession; - const token = req.body.token; - - return new Bluebird(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - vars.logger.info(req, "Initiate TOTP validation for user \"%s\".", authSession.userid); - resolve(); - }) - .then(function () { - return vars.userDataStore.retrieveTOTPSecret(authSession.userid); - }) - .then(function (doc: TOTPSecretDocument) { - if (!vars.totpHandler.validate(token, doc.secret.base32)) - return Bluebird.reject(new Error("Invalid TOTP token.")); - - vars.logger.debug(req, "TOTP validation succeeded."); - authSession.authentication_level = Level.TWO_FACTOR; - Redirect(vars)(req, res); - return Bluebird.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - return handler; -} diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts deleted file mode 100644 index 7f16c0ee8..000000000 --- a/themes/main/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts +++ /dev/null @@ -1,11 +0,0 @@ - -import util = require("util"); -import express = require("express"); - -function extract_app_id(req: express.Request): string { - return util.format("https://%s", req.headers.host); -} - -export = { - extract_app_id: extract_app_id -}; \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts deleted file mode 100644 index a54bfbfe6..000000000 --- a/themes/main/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts +++ /dev/null @@ -1,96 +0,0 @@ -import Sinon = require("sinon"); -import Assert = require("assert"); -import BluebirdPromise = require("bluebird"); - -import { Identity } from "../../../../../../types/Identity"; -import RegistrationHandler from "./RegistrationHandler"; -import ExpressMock = require("../../../../stubs/express.spec"); -import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; - -describe("routes/secondfactor/u2f/identity/RegistrationHandler", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req = ExpressMock.RequestMock(); - req.app = {}; - req.session = { - auth: { - userid: "user", - email: "user@example.com", - first_factor: true, - second_factor: false - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - - res = ExpressMock.ResponseMock(); - res.send = Sinon.spy(); - res.json = Sinon.spy(); - res.status = Sinon.spy(); - }); - - describe("test u2f registration check", test_registration_check); - - function test_registration_check() { - it("should fail if first_factor has not been passed", function () { - req.session.auth.first_factor = false; - return new RegistrationHandler(vars.logger).preValidationInit(req as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if userid is missing", function () { - req.session.auth.first_factor = false; - req.session.auth.userid = undefined; - - return new RegistrationHandler(vars.logger).preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here")); - }, - function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if email is missing", function () { - req.session.auth.first_factor = false; - req.session.auth.email = undefined; - - return new RegistrationHandler(vars.logger).preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here")); - }, - function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should succeed if first factor passed, userid and email are provided", function () { - req.session.auth.first_factor = true; - req.session.auth.email = "admin@example.com"; - req.session.auth.userid = "user"; - return new RegistrationHandler(vars.logger).preValidationInit(req as any); - }); - } -}); diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts deleted file mode 100644 index bc4713c77..000000000 --- a/themes/main/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts +++ /dev/null @@ -1,73 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import objectPath = require("object-path"); - -import { IdentityValidable } from "../../../../IdentityValidable"; -import { Identity } from "../../../../../../types/Identity"; -import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; -import FirstFactorValidator = require("../../../../FirstFactorValidator"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { IRequestLogger } from "../../../../logging/IRequestLogger"; - -const CHALLENGE = "u2f-register"; -const MAIL_SUBJECT = "Register your security key with Authelia"; - -const POST_VALIDATION_TEMPLATE_NAME = "u2f-register"; - - -export default class RegistrationHandler implements IdentityValidable { - private logger: IRequestLogger; - - constructor(logger: IRequestLogger) { - this.logger = logger; - } - - challenge(): string { - return CHALLENGE; - } - - private retrieveIdentity(req: express.Request): BluebirdPromise { - const that = this; - return new BluebirdPromise(function(resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, that.logger); - const userid = authSession.userid; - const email = authSession.email; - - if (!(userid && email)) { - return reject(new Error("User ID or email is missing")); - } - - const identity = { - email: email, - userid: userid - }; - return resolve(identity); - }); - } - - preValidationInit(req: express.Request): BluebirdPromise { - const that = this; - return FirstFactorValidator.validate(req, this.logger) - .then(function () { - return that.retrieveIdentity(req); - }); - } - - preValidationResponse(req: express.Request, res: express.Response) { - res.render(PRE_VALIDATION_TEMPLATE); - } - - postValidationInit(req: express.Request) { - return FirstFactorValidator.validate(req, this.logger); - } - - postValidationResponse(req: express.Request, res: express.Response) { - res.render(POST_VALIDATION_TEMPLATE_NAME); - } - - mailSubject(): string { - return MAIL_SUBJECT; - } -} - diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts deleted file mode 100644 index de3347a21..000000000 --- a/themes/main/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts +++ /dev/null @@ -1,146 +0,0 @@ - -import sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import assert = require("assert"); -import U2FRegisterPost = require("./post"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import ExpressMock = require("../../../../stubs/express.spec"); -import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; - - -describe("routes/secondfactor/u2f/register/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - let authSession: AuthenticationSession; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.originalUrl = "/api/xxxx"; - req.app = {}; - req.session = { - auth: { - userid: "user", - first_factor: true, - second_factor: false, - identity_check: { - challenge: "u2f-register", - userid: "user" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - }); - - describe("test registration", test_registration); - - - function test_registration() { - it("should save u2f meta and return status code 200", function () { - const expectedStatus = { - keyHandle: "keyHandle", - publicKey: "pbk", - certificate: "cert" - }; - mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve(expectedStatus)); - - authSession.register_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { - assert.equal("user", mocks.userDataStore.saveU2FRegistrationStub.getCall(0).args[0]); - assert.equal(authSession.identity_check, undefined); - }); - }); - - it("should return error message on finishRegistration error", function () { - mocks.u2f.checkRegistrationStub.returns({ errorCode: 500 }); - - authSession.register_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); - - it("should return error message when register_request is not provided", function () { - mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); - authSession.register_request = undefined; - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); - - it("should return error message when no auth request has been initiated", function () { - mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); - authSession.register_request = undefined; - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); - - it("should return error message when identity has not been verified", function () { - authSession.identity_check = undefined; - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); - } -}); - diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/register/post.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/register/post.ts deleted file mode 100644 index 7296ccbe5..000000000 --- a/themes/main/server/src/lib/routes/secondfactor/u2f/register/post.ts +++ /dev/null @@ -1,64 +0,0 @@ - -import { UserDataStore } from "../../../../storage/UserDataStore"; -import objectPath = require("object-path"); -import u2f_common = require("../U2FCommon"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import U2f = require("u2f"); -import { U2FRegistration } from "../../../../../../types/U2FRegistration"; -import redirect from "../../redirect"; -import ErrorReplies = require("../../../../ErrorReplies"); -import { ServerVariables } from "../../../../ServerVariables"; -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; - - -export default function (vars: ServerVariables) { - function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const appid = u2f_common.extract_app_id(req); - const registrationResponse: U2f.RegistrationData = req.body; - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - const registrationRequest = authSession.register_request; - - if (!registrationRequest) { - return reject(new Error("No registration request")); - } - - if (!authSession.identity_check - || authSession.identity_check.challenge != "u2f-register") { - return reject(new Error("Bad challenge for registration request")); - } - - vars.logger.info(req, "Finishing registration"); - vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); - vars.logger.debug(req, "RegistrationResponse = %s", JSON.stringify(registrationResponse)); - - return resolve(vars.u2f.checkRegistration(registrationRequest, registrationResponse)); - }) - .then(function (u2fResult: U2f.RegistrationResult | U2f.Error): BluebirdPromise { - if (objectPath.has(u2fResult, "errorCode")) - return BluebirdPromise.reject(new Error("Error while registering.")); - - const registrationResult: U2f.RegistrationResult = u2fResult as U2f.RegistrationResult; - vars.logger.info(req, "Store registration and reply"); - vars.logger.debug(req, "RegistrationResult = %s", JSON.stringify(registrationResult)); - const registration: U2FRegistration = { - keyHandle: registrationResult.keyHandle, - publicKey: registrationResult.publicKey - }; - return vars.userDataStore.saveU2FRegistration(authSession.userid, appid, registration); - }) - .then(function () { - authSession.identity_check = undefined; - redirect(vars)(req, res); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - return handler; -} diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts deleted file mode 100644 index a207c9109..000000000 --- a/themes/main/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ - -import sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import Assert = require("assert"); -import U2FRegisterRequestGet = require("./get"); -import ExpressMock = require("../../../../stubs/express.spec"); -import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; - -describe("routes/secondfactor/u2f/register_request/get", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.originalUrl = "/api/xxxx"; - req.app = {}; - req.session = { - auth: { - userid: "user", - first_factor: true, - second_factor: false, - identity_check: { - challenge: "u2f-register", - userid: "user" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - }); - - describe("test registration request", () => { - it("should send back the registration request and save it in the session", function () { - const expectedRequest = { - test: "abc" - }; - mocks.u2f.requestStub.returns(BluebirdPromise.resolve(expectedRequest)); - return U2FRegisterRequestGet.default(vars)(req as any, res as any) - .then(function () { - Assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); - }); - }); - - it("should return internal error on registration request", function () { - res.send = sinon.spy(); - const user_key_container = {}; - mocks.u2f.requestStub.returns(BluebirdPromise.reject("Internal error")); - return U2FRegisterRequestGet.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - }); - }); - - it("should return forbidden if identity has not been verified", function () { - req.session.auth.identity_check = undefined; - return U2FRegisterRequestGet.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(403, res.status.getCall(0).args[0]); - }); - }); - }); -}); - diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/register_request/get.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/register_request/get.ts deleted file mode 100644 index f611af933..000000000 --- a/themes/main/server/src/lib/routes/secondfactor/u2f/register_request/get.ts +++ /dev/null @@ -1,43 +0,0 @@ - -import { UserDataStore } from "../../../../storage/UserDataStore"; - -import objectPath = require("object-path"); -import u2f_common = require("../U2FCommon"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import U2f = require("u2f"); -import ErrorReplies = require("../../../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { ServerVariables } from "../../../../ServerVariables"; - -export default function (vars: ServerVariables) { - function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const appid: string = u2f_common.extract_app_id(req); - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - if (!authSession.identity_check - || authSession.identity_check.challenge != "u2f-register") { - res.status(403); - res.send(); - return reject(new Error("Bad challenge.")); - } - - vars.logger.info(req, "Starting registration for appId '%s'", appid); - return resolve(vars.u2f.request(appid)); - }) - .then(function (registrationRequest: U2f.Request) { - vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); - authSession.register_request = registrationRequest; - res.json(registrationRequest); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - - return handler; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts deleted file mode 100644 index 9b137e66d..000000000 --- a/themes/main/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts +++ /dev/null @@ -1,101 +0,0 @@ - -import sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import Assert = require("assert"); -import U2FSignPost = require("./post"); -import { ServerVariables } from "../../../../ServerVariables"; -import winston = require("winston"); - -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; -import ExpressMock = require("../../../../stubs/express.spec"); -import U2FMock = require("../../../../stubs/u2f.spec"); -import U2f = require("u2f"); -import { Level } from "../../../../authentication/Level"; - -describe("routes/secondfactor/u2f/sign/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.app = {}; - req.originalUrl = "/api/xxxx"; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req.session = { - auth: { - userid: "user", - authentication_level: Level.ONE_FACTOR, - identity_check: { - challenge: "u2f-register", - userid: "user" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const options = { - inMemoryOnly: true - }; - - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - }); - - it("should return status code 204", function () { - const expectedStatus = { - keyHandle: "keyHandle", - publicKey: "pbk", - certificate: "cert" - }; - mocks.u2f.checkSignatureStub.returns(expectedStatus); - - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ - registration: { - publicKey: "PUBKEY" - } - })); - - req.session.auth.sign_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - return U2FSignPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(req.session.auth.authentication_level, Level.TWO_FACTOR); - }); - }); - - it("should return unauthorized error on registration request internal error", function () { - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ - registration: { - publicKey: "PUBKEY" - } - })); - mocks.u2f.checkSignatureStub.returns({ errorCode: 500 }); - - req.session.auth.sign_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - return U2FSignPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], - { error: "Operation failed." }); - }); - }); -}); - diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/sign/post.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/sign/post.ts deleted file mode 100644 index 7ee711c2c..000000000 --- a/themes/main/server/src/lib/routes/secondfactor/u2f/sign/post.ts +++ /dev/null @@ -1,57 +0,0 @@ - -import objectPath = require("object-path"); -import u2f_common = require("../U2FCommon"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import { UserDataStore } from "../../../../storage/UserDataStore"; -import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; -import { Winston } from "../../../../../../types/Dependencies"; -import U2f = require("u2f"); -import exceptions = require("../../../../Exceptions"); -import redirect from "../../redirect"; -import ErrorReplies = require("../../../../ErrorReplies"); -import { ServerVariables } from "../../../../ServerVariables"; -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import { Level } from "../../../../authentication/Level"; - -export default function (vars: ServerVariables) { - function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const appId = u2f_common.extract_app_id(req); - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - if (!authSession.sign_request) { - const err = new Error("No sign request"); - ErrorReplies.replyWithError401(req, res, vars.logger)(err); - return reject(err); - } - resolve(); - }) - .then(function () { - const userid = authSession.userid; - return vars.userDataStore.retrieveU2FRegistration(userid, appId); - }) - .then(function (doc: U2FRegistrationDocument): BluebirdPromise { - const signRequest = authSession.sign_request; - const signData: U2f.SignatureData = req.body; - vars.logger.info(req, "Finish authentication"); - return BluebirdPromise.resolve(vars.u2f.checkSignature(signRequest, signData, doc.registration.publicKey)); - }) - .then(function (result: U2f.SignatureResult | U2f.Error): BluebirdPromise { - if (objectPath.has(result, "errorCode")) - return BluebirdPromise.reject(new Error("Error while signing")); - vars.logger.info(req, "Successful authentication"); - authSession.authentication_level = Level.TWO_FACTOR; - redirect(vars)(req, res); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - - return handler; -} - diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts deleted file mode 100644 index dd52b27e0..000000000 --- a/themes/main/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ - -import sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import assert = require("assert"); -import U2FSignRequestGet = require("./get"); -import ExpressMock = require("../../../../stubs/express.spec"); -import { Request } from "u2f"; -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; - -import { SignMessage } from "../../../../../../../shared/SignMessage"; - -describe("routes/secondfactor/u2f/sign_request/get", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.originalUrl = "/api/xxxx"; - req.app = {}; - req.session = { - auth: { - userid: "user", - first_factor: true, - second_factor: false, - identity_check: { - challenge: "u2f-register", - userid: "user" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - }); - - it("should send back the sign request and save it in the session", function () { - const expectedRequest: Request = { - version: "U2F_V2", - appId: 'app', - challenge: 'challenge!' - }; - mocks.u2f.requestStub.returns(expectedRequest); - mocks.userDataStore.retrieveU2FRegistrationStub - .returns(BluebirdPromise.resolve({ - registration: { - keyHandle: "KeyHandle" - } - })); - - return U2FSignRequestGet.default(vars)(req as any, res as any) - .then(() => { - assert.deepEqual(expectedRequest, req.session.auth.sign_request); - assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); - }); - }); -}); - diff --git a/themes/main/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts b/themes/main/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts deleted file mode 100644 index 9e93dde06..000000000 --- a/themes/main/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts +++ /dev/null @@ -1,42 +0,0 @@ - -import u2f_common = require("../../../secondfactor/u2f/U2FCommon"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; -import exceptions = require("../../../../Exceptions"); -import ErrorReplies = require("../../../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { ServerVariables } from "../../../../ServerVariables"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; - -export default function (vars: ServerVariables) { - function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const appId = u2f_common.extract_app_id(req); - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - resolve(); - }) - .then(function () { - return vars.userDataStore.retrieveU2FRegistration(authSession.userid, appId); - }) - .then(function (doc: U2FRegistrationDocument): BluebirdPromise { - if (!doc) - return BluebirdPromise.reject(new exceptions.AccessDeniedError("No U2F registration document found.")); - - const appId: string = u2f_common.extract_app_id(req); - vars.logger.info(req, "Start authentication of app '%s'", appId); - vars.logger.debug(req, "AppId = %s, keyHandle = %s", appId, JSON.stringify(doc.registration.keyHandle)); - - const request = vars.u2f.request(appId, doc.registration.keyHandle); - authSession.sign_request = request; - res.json(request); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - return handler; -} diff --git a/themes/main/server/src/lib/routes/verify/access_control.ts b/themes/main/server/src/lib/routes/verify/access_control.ts deleted file mode 100644 index 136239aeb..000000000 --- a/themes/main/server/src/lib/routes/verify/access_control.ts +++ /dev/null @@ -1,51 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import Util = require("util"); - -import Exceptions = require("../../Exceptions"); - -import { Level as AuthorizationLevel } from "../../authorization/Level"; -import { Level as AuthenticationLevel } from "../../authentication/Level"; -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { ServerVariables } from "../../ServerVariables"; - -function isAuthorized( - authorization: AuthorizationLevel, - authentication: AuthenticationLevel): boolean { - - if (authorization == AuthorizationLevel.BYPASS) { - return true; - } else if (authorization == AuthorizationLevel.ONE_FACTOR && - authentication >= AuthenticationLevel.ONE_FACTOR) { - return true; - } else if (authorization == AuthorizationLevel.TWO_FACTOR && - authentication >= AuthenticationLevel.TWO_FACTOR) { - return true; - } - return false; -} - -export default function ( - req: Express.Request, - vars: ServerVariables, - domain: string, resource: string, - user: string, groups: string[], - authenticationLevel: AuthenticationLevel) { - - return new BluebirdPromise(function (resolve, reject) { - const authorizationLevel = vars.authorizer - .authorization({domain, resource}, {user, groups}); - - if (!isAuthorized(authorizationLevel, authenticationLevel)) { - if (authorizationLevel == AuthorizationLevel.DENY) { - reject(new Exceptions.NotAuthorizedError( - Util.format("User %s is not authorized to access %s%s", user, domain, resource))); - return; - } - reject(new Exceptions.NotAuthenticatedError(Util.format( - "User '%s' is not sufficiently authorized to access %s%s.", user, domain, resource))); - return; - } - resolve(); - }); -} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/verify/get.spec.ts b/themes/main/server/src/lib/routes/verify/get.spec.ts deleted file mode 100644 index 67cf19fb6..000000000 --- a/themes/main/server/src/lib/routes/verify/get.spec.ts +++ /dev/null @@ -1,320 +0,0 @@ - -import Assert = require("assert"); -import BluebirdPromise = require("bluebird"); -import Express = require("express"); -import Sinon = require("sinon"); -import winston = require("winston"); - -import VerifyGet = require("./get"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../types/AuthenticationSession"; -import ExpressMock = require("../../stubs/express.spec"); -import { ServerVariables } from "../../ServerVariables"; -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../ServerVariablesMockBuilder.spec"; -import { Level } from "../../authentication/Level"; -import { Level as AuthorizationLevel } from "../../authorization/Level"; - -describe("routes/verify/get", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - let authSession: AuthenticationSession; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - res = ExpressMock.ResponseMock(); - req.originalUrl = "/api/xxxx"; - req.query = { - redirect: "undefined" - }; - AuthenticationSessionHandler.reset(req as any); - req.headers["x-original-url"] = "https://secret.example.com/"; - const s = ServerVariablesMockBuilder.build(false); - mocks = s.mocks; - vars = s.variables; - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - }); - - describe("with session cookie", function () { - it("should be already authenticated", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser"); - Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); - Assert.equal(204, res.status.getCall(0).args[0]); - }); - }); - - function test_session(_authSession: AuthenticationSession, status_code: number) { - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert.equal(status_code, res.status.getCall(0).args[0]); - }); - } - - function test_non_authenticated_401(authSession: AuthenticationSession) { - return test_session(authSession, 401); - } - - function test_unauthorized_403(authSession: AuthenticationSession) { - return test_session(authSession, 403); - } - - function test_authorized(authSession: AuthenticationSession) { - return test_session(authSession, 204); - } - - describe("given user tries to access a 2-factor endpoint", function () { - before(function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - }); - - describe("given different cases of session", function () { - it("should not be authenticated when second factor is missing", function () { - return test_non_authenticated_401({ - keep_me_logged_in: false, - userid: "user", - authentication_level: Level.ONE_FACTOR, - email: undefined, - groups: [], - last_activity_datetime: new Date().getTime() - }); - }); - - it("should not be authenticated when userid is missing", function () { - return test_non_authenticated_401({ - keep_me_logged_in: false, - userid: undefined, - authentication_level: Level.TWO_FACTOR, - email: undefined, - groups: [], - last_activity_datetime: new Date().getTime() - }); - }); - - it("should not be authenticated when level is insufficient", function () { - return test_non_authenticated_401({ - keep_me_logged_in: false, - userid: "user", - authentication_level: Level.NOT_AUTHENTICATED, - email: undefined, - groups: [], - last_activity_datetime: new Date().getTime() - }); - }); - - it("should not be authenticated when session has not be initiated", function () { - return test_non_authenticated_401(undefined); - }); - - it("should not be authenticated when domain is not allowed for user", function () { - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - req.headers["x-original-url"] = "https://test.example.com/"; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.DENY); - - return test_unauthorized_403({ - keep_me_logged_in: false, - authentication_level: Level.TWO_FACTOR, - userid: "user", - groups: ["group1", "group2"], - email: undefined, - last_activity_datetime: new Date().getTime() - }); - }); - }); - }); - - describe("given user tries to access a single factor endpoint", function () { - beforeEach(function () { - req.headers["x-original-url"] = "https://redirect.url/"; - }); - - it("should be authenticated when first factor is validated", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); - authSession.authentication_level = Level.ONE_FACTOR; - authSession.userid = "user1"; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWith(204)); - Assert(res.send.calledOnce); - }); - }); - - it("should be rejected with 401 when not authenticated", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); - authSession.authentication_level = Level.NOT_AUTHENTICATED; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWith(401)); - }); - }); - }); - - describe("inactivity period", function () { - it("should update last inactivity period on requests on /api/verify", function () { - mocks.config.session.inactivity = 200000; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - const currentTime = new Date().getTime() - 1000; - AuthenticationSessionHandler.reset(req as any); - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - authSession.last_activity_datetime = currentTime; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - return AuthenticationSessionHandler.get(req as any, vars.logger); - }) - .then(function (authSession) { - Assert(authSession.last_activity_datetime > currentTime); - }); - }); - - it("should reset session when max inactivity period has been reached", function () { - mocks.config.session.inactivity = 1; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - const currentTime = new Date().getTime() - 1000; - AuthenticationSessionHandler.reset(req as any); - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - authSession.last_activity_datetime = currentTime; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - return AuthenticationSessionHandler.get(req as any, vars.logger); - }) - .then(function (authSession) { - Assert.equal(authSession.authentication_level, Level.NOT_AUTHENTICATED); - Assert.equal(authSession.userid, undefined); - }); - }); - }); - }); - - describe("response type 401 | 302", function() { - it("should return error code 401", function() { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( - "Invalid credentials")); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should redirect to provided redirection url", function() { - const REDIRECT_URL = "http://redirection_url.com"; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( - "Invalid credentials")); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - req.query["rd"] = REDIRECT_URL; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.redirect.calledWithExactly(REDIRECT_URL)); - }); - }); - }); - - describe("with basic auth", function () { - it("should authenticate correctly", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.returns({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "john"); - Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); - Assert.equal(204, res.status.getCall(0).args[0]); - }); - }); - - it("should fail when endpoint is protected by two factors", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.config.access_control.rules = [{ - domain: "secret.example.com", - policy: "two_factor" - }]; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when base64 token is not valid", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic i_m*not_a_base64*token"; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when base64 token has not format user:psswd", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzOmJhZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when bad user password is provided", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( - "Invalid credentials")); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when resource is restricted", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - }); -}); - diff --git a/themes/main/server/src/lib/routes/verify/get.ts b/themes/main/server/src/lib/routes/verify/get.ts deleted file mode 100644 index f73861696..000000000 --- a/themes/main/server/src/lib/routes/verify/get.ts +++ /dev/null @@ -1,91 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Express = require("express"); -import Exceptions = require("../../Exceptions"); -import ErrorReplies = require("../../ErrorReplies"); -import { ServerVariables } from "../../ServerVariables"; -import GetWithSessionCookieMethod from "./get_session_cookie"; -import GetWithBasicAuthMethod from "./get_basic_auth"; -import Constants = require("../../../../../shared/constants"); -import ObjectPath = require("object-path"); - -import { AuthenticationSessionHandler } - from "../../AuthenticationSessionHandler"; -import { AuthenticationSession } - from "../../../../types/AuthenticationSession"; - -const REMOTE_USER = "Remote-User"; -const REMOTE_GROUPS = "Remote-Groups"; - - -function verifyWithSelectedMethod(req: Express.Request, res: Express.Response, - vars: ServerVariables, authSession: AuthenticationSession) - : () => BluebirdPromise<{ username: string, groups: string[] }> { - return function () { - const authorization: string = "" + req.headers["proxy-authorization"]; - if (authorization && authorization.startsWith("Basic ")) - return GetWithBasicAuthMethod(req, res, vars, authorization); - - return GetWithSessionCookieMethod(req, res, vars, authSession); - }; -} - -function setRedirectHeader(req: Express.Request, res: Express.Response) { - return function () { - const originalUrl = ObjectPath.get( - req, "headers.x-original-url"); - res.set("Redirect", originalUrl); - return BluebirdPromise.resolve(); - }; -} - -function setUserAndGroupsHeaders(res: Express.Response) { - return function (u: { username: string, groups: string[] }) { - res.setHeader(REMOTE_USER, u.username); - res.setHeader(REMOTE_GROUPS, u.groups.join(",")); - return BluebirdPromise.resolve(); - }; -} - -function replyWith200(res: Express.Response) { - return function () { - res.status(204); - res.send(); - }; -} - -function getRedirectParam(req: Express.Request) { - return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : undefined; -} - -export default function (vars: ServerVariables) { - return function (req: Express.Request, res: Express.Response) - : BluebirdPromise { - let authSession: AuthenticationSession; - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - resolve(); - }) - .then(setRedirectHeader(req, res)) - .then(verifyWithSelectedMethod(req, res, vars, authSession)) - .then(setUserAndGroupsHeaders(res)) - .then(replyWith200(res)) - // The user is authenticated but has restricted access -> 403 - .catch(Exceptions.NotAuthorizedError, - ErrorReplies.replyWithError403(req, res, vars.logger)) - .catch(Exceptions.NotAuthenticatedError, - ErrorReplies.replyWithError401(req, res, vars.logger)) - // The user is not yet authenticated -> 401 - .catch((err) => { - const redirectUrl = getRedirectParam(req); - if (redirectUrl) { - ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err); - } - else { - ErrorReplies.replyWithError401(req, res, vars.logger)(err); - } - }); - }; -} - diff --git a/themes/main/server/src/lib/routes/verify/get_basic_auth.ts b/themes/main/server/src/lib/routes/verify/get_basic_auth.ts deleted file mode 100644 index af23c76c9..000000000 --- a/themes/main/server/src/lib/routes/verify/get_basic_auth.ts +++ /dev/null @@ -1,55 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import ObjectPath = require("object-path"); -import { ServerVariables } from "../../ServerVariables"; -import { AuthenticationSession } - from "../../../../types/AuthenticationSession"; -import AccessControl from "./access_control"; -import { URLDecomposer } from "../../utils/URLDecomposer"; -import { Level } from "../../authentication/Level"; - -export default function (req: Express.Request, res: Express.Response, - vars: ServerVariables, authorizationHeader: string) - : BluebirdPromise<{ username: string, groups: string[] }> { - let username: string; - const uri = ObjectPath.get(req, "headers.x-original-url"); - const urlDecomposition = URLDecomposer.fromUrl(uri); - - return BluebirdPromise.resolve() - .then(() => { - const base64Re = new RegExp("^Basic ((?:[A-Za-z0-9+/]{4})*" + - "(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)$"); - const isTokenValidBase64 = base64Re.test(authorizationHeader); - - if (!isTokenValidBase64) { - return BluebirdPromise.reject(new Error("No valid base64 token found in the header")); - } - - const tokenMatches = authorizationHeader.match(base64Re); - const base64Token = tokenMatches[1]; - const decodedToken = Buffer.from(base64Token, "base64").toString(); - const splittedToken = decodedToken.split(":"); - - if (splittedToken.length != 2) { - return BluebirdPromise.reject(new Error( - "The authorization token is invalid. Expecting 'userid:password'")); - } - - username = splittedToken[0]; - const password = splittedToken[1]; - return vars.usersDatabase.checkUserPassword(username, password); - }) - .then(function (groupsAndEmails) { - return AccessControl(req, vars, urlDecomposition.domain, urlDecomposition.path, - username, groupsAndEmails.groups, Level.ONE_FACTOR) - .then(() => BluebirdPromise.resolve({ - username: username, - groups: groupsAndEmails.groups - })); - }) - .catch(function (err: Error) { - return BluebirdPromise.reject( - new Error("Unable to authenticate the user with basic auth. Cause: " - + err.message)); - }); -} \ No newline at end of file diff --git a/themes/main/server/src/lib/routes/verify/get_session_cookie.ts b/themes/main/server/src/lib/routes/verify/get_session_cookie.ts deleted file mode 100644 index 070344812..000000000 --- a/themes/main/server/src/lib/routes/verify/get_session_cookie.ts +++ /dev/null @@ -1,78 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import Util = require("util"); -import ObjectPath = require("object-path"); - -import Exceptions = require("../../Exceptions"); -import { Configuration } from "../../configuration/schema/Configuration"; -import { ServerVariables } from "../../ServerVariables"; -import { IRequestLogger } from "../../logging/IRequestLogger"; -import { AuthenticationSession } - from "../../../../types/AuthenticationSession"; -import { AuthenticationSessionHandler } - from "../../AuthenticationSessionHandler"; -import AccessControl from "./access_control"; -import { URLDecomposer } from "../../utils/URLDecomposer"; - -function verify_inactivity(req: Express.Request, - authSession: AuthenticationSession, - configuration: Configuration, logger: IRequestLogger) - : BluebirdPromise { - - // If inactivity is not specified, then inactivity timeout does not apply - if (!configuration.session.inactivity || authSession.keep_me_logged_in) { - return BluebirdPromise.resolve(); - } - - const lastActivityTime = authSession.last_activity_datetime; - const currentTime = new Date().getTime(); - authSession.last_activity_datetime = currentTime; - - const inactivityPeriodMs = currentTime - lastActivityTime; - logger.debug(req, "Inactivity period was %s s and max period was %s.", - inactivityPeriodMs / 1000, configuration.session.inactivity / 1000); - if (inactivityPeriodMs < configuration.session.inactivity) { - return BluebirdPromise.resolve(); - } - - logger.debug(req, "Session has been reset after too long inactivity period."); - AuthenticationSessionHandler.reset(req); - return BluebirdPromise.reject(new Error("Inactivity period exceeded.")); -} - -export default function (req: Express.Request, res: Express.Response, - vars: ServerVariables, authSession: AuthenticationSession) - : BluebirdPromise<{ username: string, groups: string[] }> { - - return BluebirdPromise.resolve() - .then(() => { - const username = authSession.userid; - const groups = authSession.groups; - - if (!authSession.userid) { - return BluebirdPromise.reject(new Exceptions.AccessDeniedError( - "userid is missing")); - } - - const originalUrl = ObjectPath.get( - req, "headers.x-original-url"); - const originalUri = - ObjectPath.get(req, "headers.x-original-uri"); - - const d = URLDecomposer.fromUrl(originalUrl); - vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", d.domain, - d.path, username, groups.join(",")); - return AccessControl(req, vars, d.domain, d.path, username, groups, - authSession.authentication_level); - }) - .then(() => { - return verify_inactivity(req, authSession, - vars.config, vars.logger); - }) - .then(() => { - return BluebirdPromise.resolve({ - username: authSession.userid, - groups: authSession.groups - }); - }); -} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/AuthenticationTraceDocument.d.ts b/themes/main/server/src/lib/storage/AuthenticationTraceDocument.d.ts deleted file mode 100644 index 69818c055..000000000 --- a/themes/main/server/src/lib/storage/AuthenticationTraceDocument.d.ts +++ /dev/null @@ -1,6 +0,0 @@ - -export interface AuthenticationTraceDocument { - userId: string; - date: Date; - isAuthenticationSuccessful: boolean; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/CollectionFactoryFactory.ts b/themes/main/server/src/lib/storage/CollectionFactoryFactory.ts deleted file mode 100644 index 92b29abfa..000000000 --- a/themes/main/server/src/lib/storage/CollectionFactoryFactory.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ICollectionFactory } from "./ICollectionFactory"; -import { NedbCollectionFactory } from "./nedb/NedbCollectionFactory"; -import { MongoCollectionFactory } from "./mongo/MongoCollectionFactory"; -import { IMongoClient } from "../connectors/mongo/IMongoClient"; - - -export class CollectionFactoryFactory { - static createNedb(options: Nedb.DataStoreOptions): ICollectionFactory { - return new NedbCollectionFactory(options); - } - - static createMongo(client: IMongoClient): ICollectionFactory { - return new MongoCollectionFactory(client); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/CollectionFactoryStub.spec.ts b/themes/main/server/src/lib/storage/CollectionFactoryStub.spec.ts deleted file mode 100644 index 17f8bb021..000000000 --- a/themes/main/server/src/lib/storage/CollectionFactoryStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Sinon = require("sinon"); -import { ICollection } from "./ICollection"; -import { ICollectionFactory } from "./ICollectionFactory"; - -export class CollectionFactoryStub implements ICollectionFactory { - buildStub: Sinon.SinonStub; - - constructor() { - this.buildStub = Sinon.stub(); - } - - build(collectionName: string): ICollection { - return this.buildStub(collectionName); - } -} diff --git a/themes/main/server/src/lib/storage/CollectionStub.spec.ts b/themes/main/server/src/lib/storage/CollectionStub.spec.ts deleted file mode 100644 index 42895d672..000000000 --- a/themes/main/server/src/lib/storage/CollectionStub.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Sinon = require("sinon"); -import { ICollection } from "./ICollection"; - -export class CollectionStub implements ICollection { - findStub: Sinon.SinonStub; - findOneStub: Sinon.SinonStub; - updateStub: Sinon.SinonStub; - removeStub: Sinon.SinonStub; - insertStub: Sinon.SinonStub; - - constructor() { - this.findStub = Sinon.stub(); - this.findOneStub = Sinon.stub(); - this.updateStub = Sinon.stub(); - this.removeStub = Sinon.stub(); - this.insertStub = Sinon.stub(); - } - - find(filter: any, sortKeys: any, count: number): BluebirdPromise { - return this.findStub(filter, sortKeys, count); - } - - findOne(filter: any): BluebirdPromise { - return this.findOneStub(filter); - } - - update(filter: any, document: any, options: any): BluebirdPromise { - return this.updateStub(filter, document, options); - } - - remove(filter: any): BluebirdPromise { - return this.removeStub(filter); - } - - insert(document: any): BluebirdPromise { - return this.insertStub(document); - } -} diff --git a/themes/main/server/src/lib/storage/ICollection.d.ts b/themes/main/server/src/lib/storage/ICollection.d.ts deleted file mode 100644 index caa6c2a87..000000000 --- a/themes/main/server/src/lib/storage/ICollection.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* istanbul ignore next */ -import BluebirdPromise = require("bluebird"); - -/* istanbul ignore next */ -export interface ICollection { - find(query: any, sortKeys: any, count: number): BluebirdPromise; - findOne(query: any): BluebirdPromise; - update(query: any, updateQuery: any, options?: any): BluebirdPromise; - remove(query: any): BluebirdPromise; - insert(document: any): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/ICollectionFactory.d.ts b/themes/main/server/src/lib/storage/ICollectionFactory.d.ts deleted file mode 100644 index 39eb42c77..000000000 --- a/themes/main/server/src/lib/storage/ICollectionFactory.d.ts +++ /dev/null @@ -1,6 +0,0 @@ - -import { ICollection } from "./ICollection"; - -export interface ICollectionFactory { - build(collectionName: string): ICollection; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/IUserDataStore.d.ts b/themes/main/server/src/lib/storage/IUserDataStore.d.ts deleted file mode 100644 index 81df482aa..000000000 --- a/themes/main/server/src/lib/storage/IUserDataStore.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import { TOTPSecretDocument } from "./TOTPSecretDocument"; -import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; -import { U2FRegistration } from "../../../types/U2FRegistration"; -import { TOTPSecret } from "../../../types/TOTPSecret"; -import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; -import { IdentityValidationDocument } from "./IdentityValidationDocument"; - -export interface IUserDataStore { - saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise; - retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise; - - saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; - retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise; - - produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise; - consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise; - - saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise; - retrieveTOTPSecret(userId: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/IdentityValidationDocument.d.ts b/themes/main/server/src/lib/storage/IdentityValidationDocument.d.ts deleted file mode 100644 index e7fd7b3f4..000000000 --- a/themes/main/server/src/lib/storage/IdentityValidationDocument.d.ts +++ /dev/null @@ -1,7 +0,0 @@ - -export interface IdentityValidationDocument { - userId: string; - token: string; - challenge: string; - maxDate: Date; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/TOTPSecretDocument.d.ts b/themes/main/server/src/lib/storage/TOTPSecretDocument.d.ts deleted file mode 100644 index a6c0bf9e6..000000000 --- a/themes/main/server/src/lib/storage/TOTPSecretDocument.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { TOTPSecret } from "../../../types/TOTPSecret"; - -export interface TOTPSecretDocument { - userid: string; - secret: TOTPSecret; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/U2FRegistrationDocument.d.ts b/themes/main/server/src/lib/storage/U2FRegistrationDocument.d.ts deleted file mode 100644 index efec6cb1d..000000000 --- a/themes/main/server/src/lib/storage/U2FRegistrationDocument.d.ts +++ /dev/null @@ -1,8 +0,0 @@ - -import { U2FRegistration } from "../../../types/U2FRegistration"; - -export interface U2FRegistrationDocument { - userId: string; - appId: string; - registration: U2FRegistration; -} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/UserDataStore.spec.ts b/themes/main/server/src/lib/storage/UserDataStore.spec.ts deleted file mode 100644 index 66fb85461..000000000 --- a/themes/main/server/src/lib/storage/UserDataStore.spec.ts +++ /dev/null @@ -1,264 +0,0 @@ - -import * as Assert from "assert"; -import * as Sinon from "sinon"; -import * as MockDate from "mockdate"; -import BluebirdPromise = require("bluebird"); - -import { UserDataStore } from "./UserDataStore"; -import { TOTPSecret } from "../../../types/TOTPSecret"; -import { U2FRegistration } from "../../../types/U2FRegistration"; -import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; -import { CollectionStub } from "./CollectionStub.spec"; -import { CollectionFactoryStub } from "./CollectionFactoryStub.spec"; - -describe("storage/UserDataStore", function () { - let factory: CollectionFactoryStub; - let collection: CollectionStub; - let userId: string; - let appId: string; - let totpSecret: TOTPSecret; - let u2fRegistration: U2FRegistration; - - beforeEach(function () { - factory = new CollectionFactoryStub(); - collection = new CollectionStub(); - - userId = "user"; - appId = "https://myappId"; - - totpSecret = { - ascii: "abc", - base32: "ABCDKZLEFZGREJK", - otpauth_url: "totp://test", - google_auth_qr: "dummy", - hex: "dummy", - qr_code_ascii: "dummy", - qr_code_base32: "dummy", - qr_code_hex: "dummy" - }; - - u2fRegistration = { - keyHandle: "KEY_HANDLE", - publicKey: "publickey" - }; - }); - - it("should correctly creates collections", function () { - new UserDataStore(factory); - - Assert.equal(4, factory.buildStub.callCount); - Assert(factory.buildStub.calledWith("authentication_traces")); - Assert(factory.buildStub.calledWith("identity_validation_tokens")); - Assert(factory.buildStub.calledWith("u2f_registrations")); - Assert(factory.buildStub.calledWith("totp_secrets")); - }); - - describe("TOTP secrets collection", function () { - it("should save a totp secret", function () { - factory.buildStub.returns(collection); - collection.updateStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.saveTOTPSecret(userId, totpSecret) - .then(function (doc) { - Assert(collection.updateStub.calledOnce); - Assert(collection.updateStub.calledWith({ userId: userId }, { - userId: userId, - secret: totpSecret - }, { upsert: true })); - return BluebirdPromise.resolve(); - }); - }); - - it("should retrieve a totp secret", function () { - factory.buildStub.returns(collection); - collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.retrieveTOTPSecret(userId) - .then(function (doc) { - Assert(collection.findOneStub.calledOnce); - Assert(collection.findOneStub.calledWith({ userId: userId })); - return BluebirdPromise.resolve(); - }); - }); - }); - - describe("U2F secrets collection", function () { - it("should save a U2F secret", function () { - factory.buildStub.returns(collection); - collection.updateStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.saveU2FRegistration(userId, appId, u2fRegistration) - .then(function (doc) { - Assert(collection.updateStub.calledOnce); - Assert(collection.updateStub.calledWith({ - userId: userId, - appId: appId - }, { - userId: userId, - appId: appId, - registration: u2fRegistration - }, { upsert: true })); - return BluebirdPromise.resolve(); - }); - }); - - it("should retrieve a U2F secret", function () { - factory.buildStub.returns(collection); - collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.retrieveU2FRegistration(userId, appId) - .then(function (doc) { - Assert(collection.findOneStub.calledOnce); - Assert(collection.findOneStub.calledWith({ - userId: userId, - appId: appId - })); - return BluebirdPromise.resolve(); - }); - }); - }); - - - describe("Regulator traces collection", function () { - it("should save a trace", function () { - factory.buildStub.returns(collection); - collection.insertStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.saveAuthenticationTrace(userId, true) - .then(function (doc) { - Assert(collection.insertStub.calledOnce); - Assert(collection.insertStub.calledWith({ - userId: userId, - date: Sinon.match.date, - isAuthenticationSuccessful: true - })); - return BluebirdPromise.resolve(); - }); - }); - - function should_retrieve_latest_authentication_traces(count: number) { - factory.buildStub.returns(collection); - collection.findStub.withArgs().returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.retrieveLatestAuthenticationTraces(userId, count) - .then(function (doc: AuthenticationTraceDocument[]) { - Assert(collection.findStub.calledOnce); - Assert(collection.findStub.calledWith({ - userId: userId, - }, { date: -1 }, count)); - return BluebirdPromise.resolve(); - }); - } - - it("should retrieve 3 latest failed authentication traces", function () { - should_retrieve_latest_authentication_traces(3); - }); - }); - - - describe("Identity validation collection", function () { - it("should save a identity validation token", function () { - factory.buildStub.returns(collection); - collection.insertStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - const maxAge = 400; - const token = "TOKEN"; - const challenge = "CHALLENGE"; - - return dataStore.produceIdentityValidationToken(userId, token, challenge, maxAge) - .then(function (doc) { - Assert(collection.insertStub.calledOnce); - Assert(collection.insertStub.calledWith({ - userId: userId, - token: token, - challenge: challenge, - maxDate: Sinon.match.date - })); - return BluebirdPromise.resolve(); - }); - }); - - it("should consume an identity token successfully", function () { - factory.buildStub.returns(collection); - - MockDate.set(100); - - const token = "TOKEN"; - const challenge = "CHALLENGE"; - - collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ - userId: "USER", - token: token, - challenge: challenge, - maxDate: new Date() - })); - collection.removeStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - MockDate.set(80); - - return dataStore.consumeIdentityValidationToken(token, challenge) - .then(function (doc) { - MockDate.reset(); - Assert(collection.findOneStub.calledOnce); - Assert(collection.findOneStub.calledWith({ - token: token, - challenge: challenge - })); - - Assert(collection.removeStub.calledOnce); - Assert(collection.removeStub.calledWith({ - token: token, - challenge: challenge - })); - return BluebirdPromise.resolve(); - }); - }); - - it("should consume an expired identity token", function () { - factory.buildStub.returns(collection); - - MockDate.set(0); - - const token = "TOKEN"; - const challenge = "CHALLENGE"; - - collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ - userId: "USER", - token: token, - challenge: challenge, - maxDate: new Date() - })); - - const dataStore = new UserDataStore(factory); - - MockDate.set(80000); - - return dataStore.consumeIdentityValidationToken(token, challenge) - .then(function () { return BluebirdPromise.reject(new Error("should not be here")); }) - .catch(function () { - MockDate.reset(); - Assert(collection.findOneStub.calledOnce); - Assert(collection.findOneStub.calledWith({ - token: token, - challenge: challenge - })); - return BluebirdPromise.resolve(); - }); - }); - }); -}); diff --git a/themes/main/server/src/lib/storage/UserDataStore.ts b/themes/main/server/src/lib/storage/UserDataStore.ts deleted file mode 100644 index 27b0cddbd..000000000 --- a/themes/main/server/src/lib/storage/UserDataStore.ts +++ /dev/null @@ -1,143 +0,0 @@ -import * as BluebirdPromise from "bluebird"; -import * as path from "path"; -import { IUserDataStore } from "./IUserDataStore"; -import { ICollection } from "./ICollection"; -import { ICollectionFactory } from "./ICollectionFactory"; -import { TOTPSecretDocument } from "./TOTPSecretDocument"; -import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; -import { U2FRegistration } from "../../../types/U2FRegistration"; -import { TOTPSecret } from "../../../types/TOTPSecret"; -import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; -import { IdentityValidationDocument } from "./IdentityValidationDocument"; - -// Constants - -const IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME = "identity_validation_tokens"; -const AUTHENTICATION_TRACES_COLLECTION_NAME = "authentication_traces"; - -const U2F_REGISTRATIONS_COLLECTION_NAME = "u2f_registrations"; -const TOTP_SECRETS_COLLECTION_NAME = "totp_secrets"; - - -export interface U2FRegistrationKey { - userId: string; - appId: string; -} - -// Source - -export class UserDataStore implements IUserDataStore { - private u2fSecretCollection: ICollection; - private identityCheckTokensCollection: ICollection; - private authenticationTracesCollection: ICollection; - private totpSecretCollection: ICollection; - - private collectionFactory: ICollectionFactory; - - constructor(collectionFactory: ICollectionFactory) { - this.collectionFactory = collectionFactory; - - this.u2fSecretCollection = this.collectionFactory.build(U2F_REGISTRATIONS_COLLECTION_NAME); - this.identityCheckTokensCollection = this.collectionFactory.build(IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME); - this.authenticationTracesCollection = this.collectionFactory.build(AUTHENTICATION_TRACES_COLLECTION_NAME); - this.totpSecretCollection = this.collectionFactory.build(TOTP_SECRETS_COLLECTION_NAME); - } - - saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { - const newDocument: U2FRegistrationDocument = { - userId: userId, - appId: appId, - registration: registration - }; - - const filter: U2FRegistrationKey = { - userId: userId, - appId: appId - }; - - return this.u2fSecretCollection.update(filter, newDocument, { upsert: true }); - } - - retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { - const filter: U2FRegistrationKey = { - userId: userId, - appId: appId - }; - return this.u2fSecretCollection.findOne(filter); - } - - saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { - const newDocument: AuthenticationTraceDocument = { - userId: userId, - date: new Date(), - isAuthenticationSuccessful: isAuthenticationSuccessful, - }; - - return this.authenticationTracesCollection.insert(newDocument); - } - - retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { - const q = { - userId: userId - }; - - return this.authenticationTracesCollection.find(q, { date: -1 }, count); - } - - produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { - const newDocument: IdentityValidationDocument = { - userId: userId, - token: token, - challenge: challenge, - maxDate: new Date(new Date().getTime() + maxAge) - }; - - return this.identityCheckTokensCollection.insert(newDocument); - } - - consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { - const that = this; - const filter = { - token: token, - challenge: challenge - }; - - let identityValidationDocument: IdentityValidationDocument; - - return this.identityCheckTokensCollection.findOne(filter) - .then(function (doc: IdentityValidationDocument) { - if (!doc) { - return BluebirdPromise.reject(new Error("Registration token does not exist")); - } - - identityValidationDocument = doc; - const current_date = new Date(); - if (current_date > doc.maxDate) - return BluebirdPromise.reject(new Error("Registration token is not valid anymore")); - - return that.identityCheckTokensCollection.remove(filter); - }) - .then(() => { - return BluebirdPromise.resolve(identityValidationDocument); - }); - } - - saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { - const doc = { - userId: userId, - secret: secret - }; - - const filter = { - userId: userId - }; - return this.totpSecretCollection.update(filter, doc, { upsert: true }); - } - - retrieveTOTPSecret(userId: string): BluebirdPromise { - const filter = { - userId: userId - }; - return this.totpSecretCollection.findOne(filter); - } -} diff --git a/themes/main/server/src/lib/storage/UserDataStoreStub.spec.ts b/themes/main/server/src/lib/storage/UserDataStoreStub.spec.ts deleted file mode 100644 index 5ea27a2de..000000000 --- a/themes/main/server/src/lib/storage/UserDataStoreStub.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); - -import { TOTPSecretDocument } from "./TOTPSecretDocument"; -import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; -import { U2FRegistration } from "../../../types/U2FRegistration"; -import { TOTPSecret } from "../../../types/TOTPSecret"; -import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; -import { IdentityValidationDocument } from "./IdentityValidationDocument"; -import { IUserDataStore } from "./IUserDataStore"; - -export class UserDataStoreStub implements IUserDataStore { - saveU2FRegistrationStub: Sinon.SinonStub; - retrieveU2FRegistrationStub: Sinon.SinonStub; - saveAuthenticationTraceStub: Sinon.SinonStub; - retrieveLatestAuthenticationTracesStub: Sinon.SinonStub; - produceIdentityValidationTokenStub: Sinon.SinonStub; - consumeIdentityValidationTokenStub: Sinon.SinonStub; - saveTOTPSecretStub: Sinon.SinonStub; - retrieveTOTPSecretStub: Sinon.SinonStub; - - constructor() { - this.saveU2FRegistrationStub = Sinon.stub(); - this.retrieveU2FRegistrationStub = Sinon.stub(); - this.saveAuthenticationTraceStub = Sinon.stub(); - this.retrieveLatestAuthenticationTracesStub = Sinon.stub(); - this.produceIdentityValidationTokenStub = Sinon.stub(); - this.consumeIdentityValidationTokenStub = Sinon.stub(); - this.saveTOTPSecretStub = Sinon.stub(); - this.retrieveTOTPSecretStub = Sinon.stub(); - } - - saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { - return this.saveU2FRegistrationStub(userId, appId, registration); - } - - retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { - return this.retrieveU2FRegistrationStub(userId, appId); - } - - saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { - return this.saveAuthenticationTraceStub(userId, isAuthenticationSuccessful); - } - - retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { - return this.retrieveLatestAuthenticationTracesStub(userId, count); - } - - produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { - return this.produceIdentityValidationTokenStub(userId, token, challenge, maxAge); - } - - consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { - return this.consumeIdentityValidationTokenStub(token, challenge); - } - - saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { - return this.saveTOTPSecretStub(userId, secret); - } - - retrieveTOTPSecret(userId: string): BluebirdPromise { - return this.retrieveTOTPSecretStub(userId); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/mongo/MongoCollection.spec.ts b/themes/main/server/src/lib/storage/mongo/MongoCollection.spec.ts deleted file mode 100644 index 74a773a1e..000000000 --- a/themes/main/server/src/lib/storage/mongo/MongoCollection.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -import Assert = require("assert"); -import Sinon = require("sinon"); -import MongoDB = require("mongodb"); -import BluebirdPromise = require("bluebird"); -import { MongoClientStub } from "../../connectors/mongo/MongoClientStub.spec"; -import { MongoCollection } from "./MongoCollection"; - -describe("storage/mongo/MongoCollection", function () { - let mongoCollectionStub: any; - let mongoClientStub: MongoClientStub; - let findStub: Sinon.SinonStub; - let findOneStub: Sinon.SinonStub; - let insertOneStub: Sinon.SinonStub; - let updateStub: Sinon.SinonStub; - let removeStub: Sinon.SinonStub; - let countStub: Sinon.SinonStub; - const COLLECTION_NAME = "collection"; - - before(function () { - mongoClientStub = new MongoClientStub(); - mongoCollectionStub = Sinon.createStubInstance(require("mongodb").Collection as any); - findStub = mongoCollectionStub.find as Sinon.SinonStub; - findOneStub = mongoCollectionStub.findOne as Sinon.SinonStub; - insertOneStub = mongoCollectionStub.insertOne as Sinon.SinonStub; - updateStub = mongoCollectionStub.update as Sinon.SinonStub; - removeStub = mongoCollectionStub.remove as Sinon.SinonStub; - countStub = mongoCollectionStub.count as Sinon.SinonStub; - mongoClientStub.collectionStub.returns( - BluebirdPromise.resolve(mongoCollectionStub) - ); - }); - - describe("find", function () { - it("should find a document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - findStub.returns({ - sort: Sinon.stub().returns({ - limit: Sinon.stub().returns({ - toArray: Sinon.stub().returns(BluebirdPromise.resolve([])) - }) - }) - }); - - return collection.find({ key: "KEY" }) - .then(function () { - Assert(findStub.calledWith({ key: "KEY" })); - }); - }); - }); - - describe("findOne", function () { - it("should find one document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - findOneStub.returns(BluebirdPromise.resolve({})); - - return collection.findOne({ key: "KEY" }) - .then(function () { - Assert(findOneStub.calledWith({ key: "KEY" })); - }); - }); - }); - - describe("insert", function () { - it("should insert a document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - insertOneStub.returns(BluebirdPromise.resolve({})); - - return collection.insert({ key: "KEY" }) - .then(function () { - Assert(insertOneStub.calledWith({ key: "KEY" })); - }); - }); - }); - - describe("update", function () { - it("should update a document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - updateStub.returns(BluebirdPromise.resolve({})); - - return collection.update({ key: "KEY" }, { key: "KEY", value: 1 }) - .then(function () { - Assert(updateStub.calledWith({ key: "KEY" }, { key: "KEY", value: 1 })); - }); - }); - }); - - describe("remove", function () { - it("should remove a document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - removeStub.returns(BluebirdPromise.resolve({})); - - return collection.remove({ key: "KEY" }) - .then(function () { - Assert(removeStub.calledWith({ key: "KEY" })); - }); - }); - }); - - describe("count", function () { - it("should count documents in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - countStub.returns(BluebirdPromise.resolve({})); - - return collection.count({ key: "KEY" }) - .then(function () { - Assert(countStub.calledWith({ key: "KEY" })); - }); - }); - }); -}); diff --git a/themes/main/server/src/lib/storage/mongo/MongoCollection.ts b/themes/main/server/src/lib/storage/mongo/MongoCollection.ts deleted file mode 100644 index 9771389f1..000000000 --- a/themes/main/server/src/lib/storage/mongo/MongoCollection.ts +++ /dev/null @@ -1,50 +0,0 @@ -import Bluebird = require("bluebird"); -import { ICollection } from "../ICollection"; -import MongoDB = require("mongodb"); -import { IMongoClient } from "../../connectors/mongo/IMongoClient"; - - -export class MongoCollection implements ICollection { - private mongoClient: IMongoClient; - private collectionName: string; - - constructor(collectionName: string, mongoClient: IMongoClient) { - this.collectionName = collectionName; - this.mongoClient = mongoClient; - } - - private collection(): Bluebird { - return this.mongoClient.collection(this.collectionName); - } - - find(query: any, sortKeys?: any, count?: number): Bluebird { - return this.collection() - .then((collection) => collection.find(query).sort(sortKeys).limit(count)) - .then((query) => query.toArray()); - } - - findOne(query: any): Bluebird { - return this.collection() - .then((collection) => collection.findOne(query)); - } - - update(query: any, updateQuery: any, options?: any): Bluebird { - return this.collection() - .then((collection) => collection.update(query, updateQuery, options)); - } - - remove(query: any): Bluebird { - return this.collection() - .then((collection) => collection.remove(query)); - } - - insert(document: any): Bluebird { - return this.collection() - .then((collection) => collection.insertOne(document)); - } - - count(query: any): Bluebird { - return this.collection() - .then((collection) => collection.count(query)); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts b/themes/main/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts deleted file mode 100644 index bd959cacb..000000000 --- a/themes/main/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Assert = require("assert"); -import Sinon = require("sinon"); -import { MongoClientStub } from "../../connectors/mongo/MongoClientStub.spec"; -import { MongoCollectionFactory } from "./MongoCollectionFactory"; - -describe("storage/mongo/MongoCollectionFactory", function () { - let mongoClient: MongoClientStub; - - before(function() { - mongoClient = new MongoClientStub(); - }); - - describe("create", function () { - it("should create a collection", function () { - const COLLECTION_NAME = "COLLECTION_NAME"; - - const factory = new MongoCollectionFactory(mongoClient); - Assert(factory.build(COLLECTION_NAME)); - }); - }); -}); diff --git a/themes/main/server/src/lib/storage/mongo/MongoCollectionFactory.ts b/themes/main/server/src/lib/storage/mongo/MongoCollectionFactory.ts deleted file mode 100644 index 14a8262c9..000000000 --- a/themes/main/server/src/lib/storage/mongo/MongoCollectionFactory.ts +++ /dev/null @@ -1,19 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import { ICollection } from "../ICollection"; -import { ICollectionFactory } from "../ICollectionFactory"; -import { MongoCollection } from "./MongoCollection"; -import path = require("path"); -import MongoDB = require("mongodb"); -import { IMongoClient } from "../../connectors/mongo/IMongoClient"; - -export class MongoCollectionFactory implements ICollectionFactory { - private mongoClient: IMongoClient; - - constructor(mongoClient: IMongoClient) { - this.mongoClient = mongoClient; - } - - build(collectionName: string): ICollection { - return new MongoCollection(collectionName, this.mongoClient); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/nedb/NedbCollection.spec.ts b/themes/main/server/src/lib/storage/nedb/NedbCollection.spec.ts deleted file mode 100644 index a69962b67..000000000 --- a/themes/main/server/src/lib/storage/nedb/NedbCollection.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -import Sinon = require("sinon"); -import Assert = require("assert"); - -import { NedbCollection } from "./NedbCollection"; - -describe("storage/nedb/NedbCollection", function () { - describe("insert", function () { - it("should insert one entry", function () { - const nedbOptions = { - inMemoryOnly: true - }; - const collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou" }); - - return collection.count({}).then(function (count: number) { - Assert.equal(1, count); - }); - }); - - it("should insert three entries", function () { - const nedbOptions = { - inMemoryOnly: true - }; - const collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou" }); - collection.insert({ key: "hello" }); - collection.insert({ key: "hey" }); - - return collection.count({}).then(function (count: number) { - Assert.equal(3, count); - }); - }); - }); - - describe("find", function () { - let collection: NedbCollection; - before(function () { - const nedbOptions = { - inMemoryOnly: true - }; - collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou", value: 1 }); - collection.insert({ key: "hello" }); - collection.insert({ key: "hey" }); - collection.insert({ key: "coucou", value: 2 }); - }); - - it("should find one hello", function () { - return collection.find({ key: "hello" }, { key: 1 }) - .then(function (docs: { key: string }[]) { - Assert.equal(1, docs.length); - Assert(docs[0].key == "hello"); - }); - }); - - it("should find two coucou", function () { - return collection.find({ key: "coucou" }, { value: 1 }) - .then(function (docs: { value: number }[]) { - Assert.equal(2, docs.length); - }); - }); - }); - - describe("findOne", function () { - let collection: NedbCollection; - before(function () { - const nedbOptions = { - inMemoryOnly: true - }; - collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou", value: 1 }); - collection.insert({ key: "coucou", value: 1 }); - collection.insert({ key: "coucou", value: 1 }); - collection.insert({ key: "coucou", value: 1 }); - }); - - it("should find two coucou", function () { - const doc = { key: "coucou", value: 1 }; - return collection.count(doc) - .then(function (count: number) { - Assert.equal(4, count); - return collection.findOne(doc); - }); - }); - }); - - describe("update", function () { - let collection: NedbCollection; - before(function () { - const nedbOptions = { - inMemoryOnly: true - }; - collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou", value: 1 }); - }); - - it("should update the value", function () { - return collection.update({ key: "coucou" }, { key: "coucou", value: 2 }, { multi: true }) - .then(function () { - return collection.find({ key: "coucou" }); - }) - .then(function (docs: { key: string, value: number }[]) { - Assert.equal(1, docs.length); - Assert.equal(2, docs[0].value); - }); - }); - }); - - describe("update", function () { - let collection: NedbCollection; - before(function () { - const nedbOptions = { - inMemoryOnly: true - }; - collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou" }); - collection.insert({ key: "hello" }); - }); - - it("should update the value", function () { - return collection.remove({ key: "coucou" }) - .then(function () { - return collection.count({}); - }) - .then(function (count: number) { - Assert.equal(1, count); - }); - }); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/nedb/NedbCollection.ts b/themes/main/server/src/lib/storage/nedb/NedbCollection.ts deleted file mode 100644 index 88a93ad05..000000000 --- a/themes/main/server/src/lib/storage/nedb/NedbCollection.ts +++ /dev/null @@ -1,47 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import { ICollection } from "../ICollection"; -import Nedb = require("nedb"); - -declare module "nedb" { - export class NedbAsync extends Nedb { - constructor(pathOrOptions?: string | Nedb.DataStoreOptions); - updateAsync(query: any, updateQuery: any, options?: Nedb.UpdateOptions): BluebirdPromise; - findOneAsync(query: any): BluebirdPromise; - insertAsync(newDoc: T): BluebirdPromise; - removeAsync(query: any): BluebirdPromise; - countAsync(query: any): BluebirdPromise; - } -} - -export class NedbCollection implements ICollection { - private collection: Nedb.NedbAsync; - - constructor(options: Nedb.DataStoreOptions) { - this.collection = BluebirdPromise.promisifyAll(new Nedb(options)) as Nedb.NedbAsync; - } - - find(query: any, sortKeys?: any, count?: number): BluebirdPromise { - const q = this.collection.find(query).sort(sortKeys).limit(count); - return BluebirdPromise.promisify(q.exec, { context: q })(); - } - - findOne(query: any): BluebirdPromise { - return this.collection.findOneAsync(query); - } - - update(query: any, updateQuery: any, options?: any): BluebirdPromise { - return this.collection.updateAsync(query, updateQuery, options); - } - - remove(query: any): BluebirdPromise { - return this.collection.removeAsync(query); - } - - insert(document: any): BluebirdPromise { - return this.collection.insertAsync(document); - } - - count(query: any): BluebirdPromise { - return this.collection.countAsync(query); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts b/themes/main/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts deleted file mode 100644 index da90c661f..000000000 --- a/themes/main/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Sinon = require("sinon"); -import Assert = require("assert"); - -import { NedbCollectionFactory } from "./NedbCollectionFactory"; - -describe("storage/nedb/NedbCollectionFactory", function() { - it("should create a nedb collection", function() { - const nedbOptions = { - inMemoryOnly: true - }; - const factory = new NedbCollectionFactory(nedbOptions); - - const collection = factory.build("mycollection"); - Assert(collection); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/storage/nedb/NedbCollectionFactory.ts b/themes/main/server/src/lib/storage/nedb/NedbCollectionFactory.ts deleted file mode 100644 index 49c4dc853..000000000 --- a/themes/main/server/src/lib/storage/nedb/NedbCollectionFactory.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ICollection } from "../ICollection"; -import { ICollectionFactory } from "../ICollectionFactory"; -import { NedbCollection } from "./NedbCollection"; -import path = require("path"); -import Nedb = require("nedb"); - -export interface NedbOptions { - inMemoryOnly?: boolean; - directory?: string; -} - -export class NedbCollectionFactory implements ICollectionFactory { - private options: Nedb.DataStoreOptions; - - constructor(options: Nedb.DataStoreOptions) { - this.options = options; - } - - build(collectionName: string): ICollection { - const datastoreOptions: Nedb.DataStoreOptions = { - inMemoryOnly: this.options.inMemoryOnly || false, - autoload: true, - filename: (this.options.filename) ? path.resolve(this.options.filename, collectionName) : undefined - }; - - return new NedbCollection(datastoreOptions); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/stubs/express.spec.ts b/themes/main/server/src/lib/stubs/express.spec.ts deleted file mode 100644 index 48f15d7e1..000000000 --- a/themes/main/server/src/lib/stubs/express.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ - -import sinon = require("sinon"); -import express = require("express"); - -export interface RequestMock { - app?: any; - body?: any; - session?: any; - headers?: any; - get?: any; - query?: any; - originalUrl: string; -} - -export interface ResponseMock { - send: sinon.SinonStub | sinon.SinonSpy; - sendStatus: sinon.SinonStub; - sendFile: sinon.SinonStub; - sendfile: sinon.SinonStub; - status: sinon.SinonStub | sinon.SinonSpy; - json: sinon.SinonStub | sinon.SinonSpy; - links: sinon.SinonStub; - jsonp: sinon.SinonStub; - download: sinon.SinonStub; - contentType: sinon.SinonStub; - type: sinon.SinonStub; - format: sinon.SinonStub; - attachment: sinon.SinonStub; - set: sinon.SinonStub; - header: sinon.SinonStub; - headersSent: boolean; - get: sinon.SinonStub; - clearCookie: sinon.SinonStub; - cookie: sinon.SinonStub; - location: sinon.SinonStub; - redirect: sinon.SinonStub | sinon.SinonSpy; - render: sinon.SinonStub | sinon.SinonSpy; - locals: sinon.SinonStub; - charset: string; - vary: sinon.SinonStub; - app: any; - write: sinon.SinonStub; - writeContinue: sinon.SinonStub; - writeHead: sinon.SinonStub; - statusCode: number; - statusMessage: string; - setHeader: sinon.SinonStub; - setTimeout: sinon.SinonStub; - sendDate: boolean; - getHeader: sinon.SinonStub; -} - -export function RequestMock(): RequestMock { - return { - originalUrl: "/non-api/xxx", - app: { - get: sinon.stub() - }, - headers: { - "x-forwarded-for": "127.0.0.1" - }, - session: {} - }; -} -export function ResponseMock(): ResponseMock { - return { - send: sinon.stub(), - status: sinon.stub(), - json: sinon.stub(), - sendStatus: sinon.stub(), - links: sinon.stub(), - jsonp: sinon.stub(), - sendFile: sinon.stub(), - sendfile: sinon.stub(), - download: sinon.stub(), - contentType: sinon.stub(), - type: sinon.stub(), - format: sinon.stub(), - attachment: sinon.stub(), - set: sinon.stub(), - header: sinon.stub(), - headersSent: true, - get: sinon.stub(), - clearCookie: sinon.stub(), - cookie: sinon.stub(), - location: sinon.stub(), - redirect: sinon.stub(), - render: sinon.stub(), - locals: sinon.stub(), - charset: "utf-8", - vary: sinon.stub(), - app: sinon.stub(), - write: sinon.stub(), - writeContinue: sinon.stub(), - writeHead: sinon.stub(), - statusCode: 200, - statusMessage: "message", - setHeader: sinon.stub(), - setTimeout: sinon.stub(), - sendDate: true, - getHeader: sinon.stub() - }; -} diff --git a/themes/main/server/src/lib/stubs/ldapjs.spec.ts b/themes/main/server/src/lib/stubs/ldapjs.spec.ts deleted file mode 100644 index 045c0e11b..000000000 --- a/themes/main/server/src/lib/stubs/ldapjs.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ - -import Sinon = require("sinon"); - -export class LdapjsMock { - createClientStub: sinon.SinonStub; - - constructor() { - this.createClientStub = Sinon.stub(); - } - - createClient(params: any) { - return this.createClientStub(params); - } -} - -export class LdapjsClientMock { - bindStub: sinon.SinonStub; - unbindStub: sinon.SinonStub; - searchStub: sinon.SinonStub; - modifyStub: sinon.SinonStub; - onStub: sinon.SinonStub; - - constructor() { - this.bindStub = Sinon.stub(); - this.unbindStub = Sinon.stub(); - this.searchStub = Sinon.stub(); - this.modifyStub = Sinon.stub(); - this.onStub = Sinon.stub(); - } - - bind() { - return this.bindStub(); - } - - unbind() { - return this.unbindStub(); - } - - search() { - return this.searchStub(); - } - - modify() { - return this.modifyStub(); - } - - on() { - return this.onStub(); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/stubs/speakeasy.spec.ts b/themes/main/server/src/lib/stubs/speakeasy.spec.ts deleted file mode 100644 index 023614dcd..000000000 --- a/themes/main/server/src/lib/stubs/speakeasy.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ - -import sinon = require("sinon"); - -export = { - totp: sinon.stub(), - generateSecret: sinon.stub() -}; diff --git a/themes/main/server/src/lib/stubs/u2f.spec.ts b/themes/main/server/src/lib/stubs/u2f.spec.ts deleted file mode 100644 index 234b28c11..000000000 --- a/themes/main/server/src/lib/stubs/u2f.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ - -import sinon = require("sinon"); - -export interface U2FMock { - request: sinon.SinonStub; - checkSignature: sinon.SinonStub; - checkRegistration: sinon.SinonStub; -} - -export function U2FMock(): U2FMock { - return { - request: sinon.stub(), - checkSignature: sinon.stub(), - checkRegistration: sinon.stub() - }; -} diff --git a/themes/main/server/src/lib/utils/HashGenerator.spec.ts b/themes/main/server/src/lib/utils/HashGenerator.spec.ts deleted file mode 100644 index f19619a65..000000000 --- a/themes/main/server/src/lib/utils/HashGenerator.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Assert = require("assert"); -import { HashGenerator } from "./HashGenerator"; - -describe("utils/HashGenerator", function () { - it("should compute correct ssha512 (password)", function () { - return HashGenerator.ssha512("password", 500000, "jgiCMRyGXzoqpxS3") - .then(function (hash: string) { - Assert.equal(hash, "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"); - }); - }); - - it("should compute correct ssha512 (test)", function () { - return HashGenerator.ssha512("test", 500000, "abcdefghijklmnop") - .then(function (hash: string) { - Assert.equal(hash, "{CRYPT}$6$rounds=500000$abcdefghijklmnop$sTlNGf0VO/HTQIOXemmaBbV28HUch/qhWOA1/4dsDj6CDQYhUgXbYSPL6gccAsWMr2zD5fFWwhKmPdG.yxphs."); - }); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/utils/HashGenerator.ts b/themes/main/server/src/lib/utils/HashGenerator.ts deleted file mode 100644 index e67de32b7..000000000 --- a/themes/main/server/src/lib/utils/HashGenerator.ts +++ /dev/null @@ -1,23 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import RandomString = require("randomstring"); -import Util = require("util"); -const crypt = require("crypt3"); - -export class HashGenerator { - static ssha512( - password: string, - rounds: number = 500000, - salt?: string): BluebirdPromise { - const saltSize = 16; - // $6 means SHA512 - const _salt = Util.format("$6$rounds=%d$%s", rounds, - (salt) ? salt : RandomString.generate(16)); - - const cryptAsync = BluebirdPromise.promisify(crypt); - - return cryptAsync(password, _salt) - .then(function (hash: string) { - return BluebirdPromise.resolve(Util.format("{CRYPT}%s", hash)); - }); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/utils/ObjectCloner.ts b/themes/main/server/src/lib/utils/ObjectCloner.ts deleted file mode 100644 index 3e125d749..000000000 --- a/themes/main/server/src/lib/utils/ObjectCloner.ts +++ /dev/null @@ -1,6 +0,0 @@ - -export class ObjectCloner { - static clone(obj: any): any { - return JSON.parse(JSON.stringify(obj)); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/utils/SafeRedirection.spec.ts b/themes/main/server/src/lib/utils/SafeRedirection.spec.ts deleted file mode 100644 index 4126949fd..000000000 --- a/themes/main/server/src/lib/utils/SafeRedirection.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import Assert = require("assert"); -import Sinon = require("sinon"); -import { SafeRedirector } from "./SafeRedirection"; - -describe("web_server/middlewares/SafeRedirection", () => { - describe("Url is in protected domain", () => { - before(() => { - this.redirector = new SafeRedirector("example.com"); - this.res = {redirect: Sinon.stub()}; - }); - - it("should redirect to provided url", () => { - this.redirector.redirectOrElse(this.res, - "https://mysubdomain.example.com:8080/abc", - "https://authelia.example.com"); - Assert(this.res.redirect.calledWith("https://mysubdomain.example.com:8080/abc")); - }); - - it("should redirect to default url when wrong domain", () => { - this.redirector.redirectOrElse(this.res, - "https://mysubdomain.domain.rtf:8080/abc", - "https://authelia.example.com"); - Assert(this.res.redirect.calledWith("https://authelia.example.com")); - }); - - it("should redirect to default url when not terminating by domain", () => { - this.redirector.redirectOrElse(this.res, - "https://mysubdomain.example.com.rtf:8080/abc", - "https://authelia.example.com"); - Assert(this.res.redirect.calledWith("https://authelia.example.com")); - }); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/utils/SafeRedirection.ts b/themes/main/server/src/lib/utils/SafeRedirection.ts deleted file mode 100644 index 9e6a32e0c..000000000 --- a/themes/main/server/src/lib/utils/SafeRedirection.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Express = require("express"); -import { DomainExtractor } from "../../../../shared/DomainExtractor"; -import { BelongToDomain } from "../../../../shared/BelongToDomain"; - - -export class SafeRedirector { - private domain: string; - - constructor(domain: string) { - this.domain = domain; - } - - redirectOrElse( - res: Express.Response, - url: string, - defaultUrl: string): void { - if (BelongToDomain(url, this.domain)) { - res.redirect(url); - } - res.redirect(defaultUrl); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/utils/URLDecomposer.spec.ts b/themes/main/server/src/lib/utils/URLDecomposer.spec.ts deleted file mode 100644 index cbb038738..000000000 --- a/themes/main/server/src/lib/utils/URLDecomposer.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { URLDecomposer } from "./URLDecomposer"; -import Assert = require("assert"); - -describe("utils/URLDecomposer", function () { - describe("test fromUrl", function () { - it("should return domain from https url", function () { - const d = URLDecomposer.fromUrl("https://www.example.com/test/abc"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/test/abc"); - }); - - it("should return domain from http url", function () { - const d = URLDecomposer.fromUrl("http://www.example.com/test/abc"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/test/abc"); - }); - - it("should return domain when url contains port", function () { - const d = URLDecomposer.fromUrl("https://www.example.com:8080/test/abc"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/test/abc"); - }); - - it("should return default path when no path provided", function () { - const d = URLDecomposer.fromUrl("https://www.example.com:8080"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/"); - }); - - it("should return default path when provided", function () { - const d = URLDecomposer.fromUrl("https://www.example.com:8080/"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/"); - }); - - it("should return undefined when does not match", function () { - const d = URLDecomposer.fromUrl("https:///abc/test"); - Assert.equal(d, undefined); - }); - - it("should return undefined when does not match", function () { - const d = URLDecomposer.fromUrl("https:///abc/test"); - Assert.equal(d, undefined); - }); - }); -}); \ No newline at end of file diff --git a/themes/main/server/src/lib/utils/URLDecomposer.ts b/themes/main/server/src/lib/utils/URLDecomposer.ts deleted file mode 100644 index 9bdf2e9d2..000000000 --- a/themes/main/server/src/lib/utils/URLDecomposer.ts +++ /dev/null @@ -1,15 +0,0 @@ -export class URLDecomposer { - static fromUrl(url: string): {domain: string, path: string} { - if (!url) return; - const match = url.match(/https?:\/\/([a-z0-9_.-]+)(:[0-9]+)?(.*)/); - - if (!match) return; - - if (match[1] && !match[3]) { - return {domain: match[1], path: "/"}; - } else if (match[1] && match[3]) { - return {domain: match[1], path: match[3]}; - } - return; - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/web_server/Configurator.ts b/themes/main/server/src/lib/web_server/Configurator.ts deleted file mode 100644 index 6e404874a..000000000 --- a/themes/main/server/src/lib/web_server/Configurator.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Configuration } from "../configuration/schema/Configuration"; -import { GlobalDependencies } from "../../../types/Dependencies"; -import { SessionConfigurationBuilder } from - "../configuration/SessionConfigurationBuilder"; -import Path = require("path"); -import Express = require("express"); -import * as BodyParser from "body-parser"; -import { RestApi } from "./RestApi"; -import { WithHeadersLogged } from "./middlewares/WithHeadersLogged"; -import { ServerVariables } from "../ServerVariables"; -import Helmet = require("helmet"); - -const addRequestId = require("express-request-id")(); - -// Constants -const TRUST_PROXY = "trust proxy"; -const X_POWERED_BY = "x-powered-by"; -const VIEWS = "views"; -const VIEW_ENGINE = "view engine"; -const PUG = "pug"; - -export class Configurator { - static configure(config: Configuration, - app: Express.Application, - vars: ServerVariables, - deps: GlobalDependencies): void { - const viewsDirectory = Path.resolve(__dirname, "../../views"); - const publicHtmlDirectory = Path.resolve(__dirname, "../../public_html"); - - const expressSessionOptions = SessionConfigurationBuilder.build(config, deps); - - app.use(Express.static(publicHtmlDirectory)); - app.use(BodyParser.urlencoded({ extended: false })); - app.use(BodyParser.json()); - app.use(deps.session(expressSessionOptions)); - app.use(addRequestId); - app.use(WithHeadersLogged.middleware(vars.logger)); - app.disable(X_POWERED_BY); - app.enable(TRUST_PROXY); - app.use(Helmet()); - - app.set(VIEWS, viewsDirectory); - app.set(VIEW_ENGINE, PUG); - - RestApi.setup(app, vars); - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/web_server/RestApi.ts b/themes/main/server/src/lib/web_server/RestApi.ts deleted file mode 100644 index 9144a15b9..000000000 --- a/themes/main/server/src/lib/web_server/RestApi.ts +++ /dev/null @@ -1,125 +0,0 @@ -import Express = require("express"); - -import FirstFactorGet = require("../routes/firstfactor/get"); -import SecondFactorGet = require("../routes/secondfactor/get"); - -import FirstFactorPost = require("../routes/firstfactor/post"); -import LogoutGet = require("../routes/logout/get"); -import VerifyGet = require("../routes/verify/get"); -import TOTPSignGet = require("../routes/secondfactor/totp/sign/post"); - -import IdentityCheckMiddleware = require("../IdentityCheckMiddleware"); - -import TOTPRegistrationIdentityHandler from "../routes/secondfactor/totp/identity/RegistrationHandler"; -import U2FRegistrationIdentityHandler from "../routes/secondfactor/u2f/identity/RegistrationHandler"; -import ResetPasswordIdentityHandler from "../routes/password-reset/identity/PasswordResetHandler"; - -import U2FSignPost = require("../routes/secondfactor/u2f/sign/post"); -import U2FSignRequestGet = require("../routes/secondfactor/u2f/sign_request/get"); - -import U2FRegisterPost = require("../routes/secondfactor/u2f/register/post"); -import U2FRegisterRequestGet = require("../routes/secondfactor/u2f/register_request/get"); - -import ResetPasswordFormPost = require("../routes/password-reset/form/post"); -import ResetPasswordRequestPost = require("../routes/password-reset/request/get"); - -import Error401Get = require("../routes/error/401/get"); -import Error403Get = require("../routes/error/403/get"); -import Error404Get = require("../routes/error/404/get"); - -import LoggedIn = require("../routes/loggedin/get"); - -import { ServerVariables } from "../ServerVariables"; -import Endpoints = require("../../../../shared/api"); -import { RequireValidatedFirstFactor } from "./middlewares/RequireValidatedFirstFactor"; - -function setupTotp(app: Express.Application, vars: ServerVariables) { - app.post(Endpoints.SECOND_FACTOR_TOTP_POST, - RequireValidatedFirstFactor.middleware(vars.logger), - TOTPSignGet.default(vars)); - - app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, - RequireValidatedFirstFactor.middleware(vars.logger)); - - app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, - RequireValidatedFirstFactor.middleware(vars.logger)); - - IdentityCheckMiddleware.register(app, - Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, - Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, - new TOTPRegistrationIdentityHandler(vars.logger, - vars.userDataStore, vars.totpHandler, vars.config.totp), - vars); -} - -function setupU2f(app: Express.Application, vars: ServerVariables) { - app.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, - RequireValidatedFirstFactor.middleware(vars.logger), - U2FSignRequestGet.default(vars)); - - app.post(Endpoints.SECOND_FACTOR_U2F_SIGN_POST, - RequireValidatedFirstFactor.middleware(vars.logger), - U2FSignPost.default(vars)); - - app.get(Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, - RequireValidatedFirstFactor.middleware(vars.logger), - U2FRegisterRequestGet.default(vars)); - - app.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, - RequireValidatedFirstFactor.middleware(vars.logger), - U2FRegisterPost.default(vars)); - - app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, - RequireValidatedFirstFactor.middleware(vars.logger)); - - app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, - RequireValidatedFirstFactor.middleware(vars.logger)); - - IdentityCheckMiddleware.register(app, - Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, - Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, - new U2FRegistrationIdentityHandler(vars.logger), vars); -} - -function setupResetPassword(app: Express.Application, vars: ServerVariables) { - IdentityCheckMiddleware.register(app, - Endpoints.RESET_PASSWORD_IDENTITY_START_GET, - Endpoints.RESET_PASSWORD_IDENTITY_FINISH_GET, - new ResetPasswordIdentityHandler(vars.logger, vars.usersDatabase), - vars); - - app.get(Endpoints.RESET_PASSWORD_REQUEST_GET, - ResetPasswordRequestPost.default); - app.post(Endpoints.RESET_PASSWORD_FORM_POST, - ResetPasswordFormPost.default(vars)); -} - -function setupErrors(app: Express.Application, vars: ServerVariables) { - app.get(Endpoints.ERROR_401_GET, Error401Get.default(vars)); - app.get(Endpoints.ERROR_403_GET, Error403Get.default(vars)); - app.get(Endpoints.ERROR_404_GET, Error404Get.default); -} - -export class RestApi { - static setup(app: Express.Application, vars: ServerVariables): void { - app.get(Endpoints.FIRST_FACTOR_GET, FirstFactorGet.default(vars)); - - app.get(Endpoints.SECOND_FACTOR_GET, - RequireValidatedFirstFactor.middleware(vars.logger), - SecondFactorGet.default(vars)); - - app.get(Endpoints.LOGOUT_GET, LogoutGet.default(vars)); - - app.get(Endpoints.VERIFY_GET, VerifyGet.default(vars)); - app.post(Endpoints.FIRST_FACTOR_POST, FirstFactorPost.default(vars)); - - setupTotp(app, vars); - setupU2f(app, vars); - setupResetPassword(app, vars); - setupErrors(app, vars); - - app.get(Endpoints.LOGGED_IN, - RequireValidatedFirstFactor.middleware(vars.logger), - LoggedIn.default(vars)); - } -} diff --git a/themes/main/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts b/themes/main/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts deleted file mode 100644 index ecfd75765..000000000 --- a/themes/main/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts +++ /dev/null @@ -1,27 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import ErrorReplies = require("../../ErrorReplies"); -import { IRequestLogger } from "../../logging/IRequestLogger"; -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import Exceptions = require("../../Exceptions"); -import { Level } from "../../authentication/Level"; - -export class RequireValidatedFirstFactor { - static middleware(logger: IRequestLogger) { - return function (req: Express.Request, res: Express.Response, - next: Express.NextFunction): BluebirdPromise { - - 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.")); - - next(); - resolve(); - }) - .catch(ErrorReplies.replyWithError401(req, res, logger)); - }; - } -} \ No newline at end of file diff --git a/themes/main/server/src/lib/web_server/middlewares/WithHeadersLogged.ts b/themes/main/server/src/lib/web_server/middlewares/WithHeadersLogged.ts deleted file mode 100644 index 139db1141..000000000 --- a/themes/main/server/src/lib/web_server/middlewares/WithHeadersLogged.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Express = require("express"); -import { IRequestLogger } from "../../logging/IRequestLogger"; - -export class WithHeadersLogged { - static middleware(logger: IRequestLogger) { - return function (req: Express.Request, res: Express.Response, - next: Express.NextFunction): void { - logger.debug(req, "Headers = %s", JSON.stringify(req.headers)); - next(); - }; - } -} \ No newline at end of file diff --git a/themes/main/server/test/requests.ts b/themes/main/server/test/requests.ts deleted file mode 100644 index 93fa0de47..000000000 --- a/themes/main/server/test/requests.ts +++ /dev/null @@ -1,94 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import request = require("request"); -import assert = require("assert"); -import express = require("express"); -import nodemailer = require("nodemailer"); -import Endpoints = require("../../shared/api"); - -declare module "request" { - export interface RequestAPI { - getAsync(uri: string, options?: RequiredUriUrl): BluebirdPromise; - getAsync(uri: string): BluebirdPromise; - getAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; - - postAsync(uri: string, options?: CoreOptions): BluebirdPromise; - postAsync(uri: string): BluebirdPromise; - postAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; - } -} - -const requestAsync: typeof request = BluebirdPromise.promisifyAll(request) as typeof request; - -export = function (port: number) { - const PORT = port; - const BASE_URL = "http://localhost:" + PORT; - - function execute_totp(jar: request.CookieJar, token: string) { - return requestAsync.postAsync({ - url: BASE_URL + Endpoints.SECOND_FACTOR_TOTP_POST, - jar: jar, - form: { - token: token - } - }); - } - - function execute_u2f_authentication(jar: request.CookieJar) { - return requestAsync.getAsync({ - url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, - jar: jar - }) - .then(function (res: request.RequestResponse) { - assert.equal(res.statusCode, 200); - return requestAsync.postAsync({ - url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_POST, - jar: jar, - form: { - } - }); - }); - } - - function execute_verification(jar: request.CookieJar) { - return requestAsync.getAsync({ url: BASE_URL + Endpoints.VERIFY_GET, jar: jar }); - } - - function execute_login(jar: request.CookieJar) { - return requestAsync.getAsync({ url: BASE_URL + Endpoints.FIRST_FACTOR_GET, jar: jar }); - } - - function execute_first_factor(jar: request.CookieJar) { - return requestAsync.postAsync({ - url: BASE_URL + Endpoints.FIRST_FACTOR_POST, - jar: jar, - form: { - username: "test_ok", - password: "password" - } - }); - } - - function execute_failing_first_factor(jar: request.CookieJar) { - return requestAsync.postAsync({ - url: BASE_URL + Endpoints.FIRST_FACTOR_POST, - jar: jar, - form: { - username: "test_nok", - password: "password" - } - }); - } - - return { - login: execute_login, - verify: execute_verification, - u2f_authentication: execute_u2f_authentication, - first_factor: execute_first_factor, - failing_first_factor: execute_failing_first_factor, - totp: execute_totp, - }; -}; - diff --git a/themes/main/server/tsconfig.json b/themes/main/server/tsconfig.json deleted file mode 100644 index ebe98c5ed..000000000 --- a/themes/main/server/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "moduleResolution": "node", - "noImplicitAny": true, - "sourceMap": true, - "removeComments": true, - "outDir": "../dist", - "baseUrl": ".", - "paths": { - "*": [ - "./types/*", - "../shared/types/*" - ] - } - }, - "exclude": [ - "src/**/*.spec.ts" - ] -} diff --git a/themes/main/server/tslint.json b/themes/main/server/tslint.json deleted file mode 100644 index c2c1b7501..000000000 --- a/themes/main/server/tslint.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "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 - } -} diff --git a/themes/main/server/types/.directory b/themes/main/server/types/.directory deleted file mode 100644 index 63f8e11d5..000000000 --- a/themes/main/server/types/.directory +++ /dev/null @@ -1,4 +0,0 @@ -[Dolphin] -Timestamp=2018,12,17,20,58,28 -Version=3 -ViewMode=1 diff --git a/themes/main/server/types/AuthenticationSession.ts b/themes/main/server/types/AuthenticationSession.ts deleted file mode 100644 index bbed0e715..000000000 --- a/themes/main/server/types/AuthenticationSession.ts +++ /dev/null @@ -1,18 +0,0 @@ -import U2f = require("u2f"); -import { Level } from "../src/lib/authentication/Level"; - -export interface AuthenticationSession { - userid: string; - authentication_level: Level; - keep_me_logged_in: boolean; - last_activity_datetime: number; - identity_check?: { - challenge: string; - userid: string; - }; - register_request?: U2f.Request; - sign_request?: U2f.Request; - email: string; - groups: string[]; - redirect?: string; -} \ No newline at end of file diff --git a/themes/main/server/types/Dependencies.ts b/themes/main/server/types/Dependencies.ts deleted file mode 100644 index f20404dbc..000000000 --- a/themes/main/server/types/Dependencies.ts +++ /dev/null @@ -1,29 +0,0 @@ -import winston = require("winston"); -import speakeasy = require("speakeasy"); -import nodemailer = require("nodemailer"); -import session = require("express-session"); -import nedb = require("nedb"); -import ldapjs = require("ldapjs"); -import u2f = require("u2f"); -import RedisSession = require("connect-redis"); -import Redis = require("redis"); - -export type Speakeasy = typeof speakeasy; -export type Winston = typeof winston; -export type Session = typeof session; -export type Nedb = typeof nedb; -export type Ldapjs = typeof ldapjs; -export type U2f = typeof u2f; -export type ConnectRedis = typeof RedisSession; -export type Redis = typeof Redis; - -export interface GlobalDependencies { - u2f: U2f; - ldapjs: Ldapjs; - session: Session; - Redis: Redis; - ConnectRedis: ConnectRedis; - winston: Winston; - speakeasy: Speakeasy; - nedb: Nedb; -} \ No newline at end of file diff --git a/themes/main/server/types/Identity.ts b/themes/main/server/types/Identity.ts deleted file mode 100644 index e985984e2..000000000 --- a/themes/main/server/types/Identity.ts +++ /dev/null @@ -1,6 +0,0 @@ - - -export interface Identity { - userid: string; - email: string; -} \ No newline at end of file diff --git a/themes/main/server/types/TOTPSecret.ts b/themes/main/server/types/TOTPSecret.ts deleted file mode 100644 index d6775f2f0..000000000 --- a/themes/main/server/types/TOTPSecret.ts +++ /dev/null @@ -1,11 +0,0 @@ - -export interface TOTPSecret { - ascii: string; - hex: string; - base32: string; - qr_code_ascii: string; - qr_code_hex: string; - qr_code_base32: string; - google_auth_qr: string; - otpauth_url: string; - } \ No newline at end of file diff --git a/themes/main/server/types/U2FRegistration.ts b/themes/main/server/types/U2FRegistration.ts deleted file mode 100644 index b6080af07..000000000 --- a/themes/main/server/types/U2FRegistration.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface U2FRegistration { - keyHandle: string; - publicKey: string; -} \ No newline at end of file diff --git a/themes/main/server/types/dovehash.d.ts b/themes/main/server/types/dovehash.d.ts deleted file mode 100644 index c354609c0..000000000 --- a/themes/main/server/types/dovehash.d.ts +++ /dev/null @@ -1,4 +0,0 @@ - -declare module "dovehash" { - function encode(algo: string, text: string): string; -} \ No newline at end of file diff --git a/themes/main/server/types/speakeasy.d.ts b/themes/main/server/types/speakeasy.d.ts deleted file mode 100644 index 6ea06948b..000000000 --- a/themes/main/server/types/speakeasy.d.ts +++ /dev/null @@ -1,96 +0,0 @@ -declare module "speakeasy" { - export = speakeasy - - interface SharedOptions { - encoding?: string - algorithm?: string - } - - interface DigestOptions extends SharedOptions { - secret: string - counter: number - } - - interface HOTPOptions extends SharedOptions { - secret: string - counter: number - digest?: Buffer - digits?: number - } - - interface HOTPVerifyOptions extends SharedOptions { - secret: string - token: string - counter: number - digits?: number - window?: number - } - - interface TOTPOptions extends SharedOptions { - secret: string - time?: number - step?: number - epoch?: number - counter?: number - digits?: number - } - - interface TOTPVerifyOptions extends SharedOptions { - secret: string - token: string - time?: number - step?: number - epoch?: number - counter?: number - digits?: number - window?: number - } - - interface GenerateSecretOptions { - length?: number - symbols?: boolean - otpauth_url?: boolean - name?: string - issuer?: string - } - - interface GeneratedSecret { - ascii: string - hex: string - base32: string - qr_code_ascii: string - qr_code_hex: string - qr_code_base32: string - google_auth_qr: string - otpauth_url: string - } - - interface OTPAuthURLOptions extends SharedOptions { - secret: string - label: string - type?: string - counter?: number - issuer?: string - digits?: number - period?: number - } - - interface Speakeasy { - digest: (options: DigestOptions) => Buffer - hotp: { - (options: HOTPOptions): string, - verifyDelta: (options: HOTPVerifyOptions) => boolean, - verify: (options: HOTPVerifyOptions) => boolean, - } - totp: { - (options: TOTPOptions): string - verifyDelta: (options: TOTPVerifyOptions) => boolean, - verify: (options: TOTPVerifyOptions) => boolean, - } - generateSecret: (options?: GenerateSecretOptions) => GeneratedSecret - generateSecretASCII: (length?: number, symbols?: boolean) => string - otpauthURL: (options: OTPAuthURLOptions) => string - } - - const speakeasy: Speakeasy -} \ No newline at end of file diff --git a/themes/matrix/client/src/css/.directory b/themes/matrix/client/src/css/.directory old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/css/00-bootstrap.min.css b/themes/matrix/client/src/css/00-bootstrap.min.css old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/css/01-main.css b/themes/matrix/client/src/css/01-main.css old mode 100644 new mode 100755 index 318b90e25..e62ff8dd0 --- a/themes/matrix/client/src/css/01-main.css +++ b/themes/matrix/client/src/css/01-main.css @@ -2,7 +2,7 @@ body { /*background-image: url("//*img//*LargeTriangles.svg");*/ /*background-image: url("//*img//*RandomizedPattern.svg");*/ /*background-image: url("//*img//*background.svg");*/ - background-color:#000000;*/ + background-color:#000000; } canvas{ position:absolute; diff --git a/themes/matrix/client/src/css/02-login.css b/themes/matrix/client/src/css/02-login.css old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/css/03-errors.css b/themes/matrix/client/src/css/03-errors.css old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/css/03-password-reset-form.css b/themes/matrix/client/src/css/03-password-reset-form.css old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/css/03-password-reset-request.css b/themes/matrix/client/src/css/03-password-reset-request.css old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/css/03-totp-register.css b/themes/matrix/client/src/css/03-totp-register.css old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/css/03-u2f-register.css b/themes/matrix/client/src/css/03-u2f-register.css old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/img/background.jpg b/themes/matrix/client/src/img/background.jpg old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/img/icon.png b/themes/matrix/client/src/img/icon.png old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/img/mail.png b/themes/matrix/client/src/img/mail.png old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/img/matrix_circle_128x128.png b/themes/matrix/client/src/img/matrix_circle_128x128.png old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/img/notifications/.directory b/themes/matrix/client/src/img/notifications/.directory old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/img/notifications/error.png b/themes/matrix/client/src/img/notifications/error.png old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/img/notifications/info.png b/themes/matrix/client/src/img/notifications/info.png old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/img/notifications/success.png b/themes/matrix/client/src/img/notifications/success.png old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/img/notifications/warning.png b/themes/matrix/client/src/img/notifications/warning.png old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/img/padlock.png b/themes/matrix/client/src/img/padlock.png old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/img/password_white.png b/themes/matrix/client/src/img/password_white.png old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/img/pendrive.png b/themes/matrix/client/src/img/pendrive.png old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/img/stores/.directory b/themes/matrix/client/src/img/stores/.directory old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/img/stores/applestore-badge.svg b/themes/matrix/client/src/img/stores/applestore-badge.svg old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/img/stores/googleplay-badge.svg b/themes/matrix/client/src/img/stores/googleplay-badge.svg old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/img/success.png b/themes/matrix/client/src/img/success.png old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/img/user.png b/themes/matrix/client/src/img/user.png old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/img/warning.png b/themes/matrix/client/src/img/warning.png old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/index.ts b/themes/matrix/client/src/index.ts deleted file mode 100644 index 802004a8f..000000000 --- a/themes/matrix/client/src/index.ts +++ /dev/null @@ -1,34 +0,0 @@ - -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 () { - (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, (global as any).u2f); - 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, (global as any).u2f); - 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); -})(); diff --git a/themes/matrix/client/src/lib/GetPromised.ts b/themes/matrix/client/src/lib/GetPromised.ts deleted file mode 100644 index 779139654..000000000 --- a/themes/matrix/client/src/lib/GetPromised.ts +++ /dev/null @@ -1,14 +0,0 @@ -import BluebirdPromise = require("bluebird"); - -export default function ($: JQueryStatic, url: string, data: Object, fn: any, - dataType: string): BluebirdPromise { - return new BluebirdPromise((resolve, reject) => { - $.get(url, {}, undefined, dataType) - .done((data: any) => { - resolve(data); - }) - .fail((xhr: JQueryXHR, textStatus: string) => { - reject(textStatus); - }); - }); -} \ No newline at end of file diff --git a/themes/matrix/client/src/lib/INotifier.ts b/themes/matrix/client/src/lib/INotifier.ts deleted file mode 100644 index df947538f..000000000 --- a/themes/matrix/client/src/lib/INotifier.ts +++ /dev/null @@ -1,14 +0,0 @@ - -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; -} \ No newline at end of file diff --git a/themes/matrix/client/src/lib/Notifier.ts b/themes/matrix/client/src/lib/Notifier.ts deleted file mode 100644 index c0252b9b9..000000000 --- a/themes/matrix/client/src/lib/Notifier.ts +++ /dev/null @@ -1,83 +0,0 @@ - - -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('status %s\ - %s', 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); - } -} \ No newline at end of file diff --git a/themes/matrix/client/src/lib/QueryParametersRetriever.ts b/themes/matrix/client/src/lib/QueryParametersRetriever.ts deleted file mode 100644 index a529adb6e..000000000 --- a/themes/matrix/client/src/lib/QueryParametersRetriever.ts +++ /dev/null @@ -1,12 +0,0 @@ - -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, " ")); - } -} \ No newline at end of file diff --git a/themes/matrix/client/src/lib/SafeRedirect.ts b/themes/matrix/client/src/lib/SafeRedirect.ts deleted file mode 100644 index 7e7684b8a..000000000 --- a/themes/matrix/client/src/lib/SafeRedirect.ts +++ /dev/null @@ -1,10 +0,0 @@ -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(); -} \ No newline at end of file diff --git a/themes/matrix/client/src/lib/firstfactor/FirstFactorValidator.ts b/themes/matrix/client/src/lib/firstfactor/FirstFactorValidator.ts deleted file mode 100644 index eaa496fdd..000000000 --- a/themes/matrix/client/src/lib/firstfactor/FirstFactorValidator.ts +++ /dev/null @@ -1,46 +0,0 @@ - -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 { - return new BluebirdPromise(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)); - }); - }); -} diff --git a/themes/matrix/client/src/lib/firstfactor/UISelectors.ts b/themes/matrix/client/src/lib/firstfactor/UISelectors.ts deleted file mode 100644 index 0e971b3c3..000000000 --- a/themes/matrix/client/src/lib/firstfactor/UISelectors.ts +++ /dev/null @@ -1,5 +0,0 @@ - -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"; diff --git a/themes/matrix/client/src/lib/firstfactor/index.ts b/themes/matrix/client/src/lib/firstfactor/index.ts deleted file mode 100644 index 24affee2d..000000000 --- a/themes/matrix/client/src/lib/firstfactor/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -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); - }); -} - diff --git a/themes/matrix/client/src/lib/reset-password/constants.ts b/themes/matrix/client/src/lib/reset-password/constants.ts deleted file mode 100644 index d48d4e67d..000000000 --- a/themes/matrix/client/src/lib/reset-password/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export const FORM_SELECTOR = ".form-signin"; \ No newline at end of file diff --git a/themes/matrix/client/src/lib/reset-password/reset-password-form.ts b/themes/matrix/client/src/lib/reset-password/reset-password-form.ts deleted file mode 100644 index b94279cde..000000000 --- a/themes/matrix/client/src/lib/reset-password/reset-password-form.ts +++ /dev/null @@ -1,57 +0,0 @@ -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); - }); -} diff --git a/themes/matrix/client/src/lib/reset-password/reset-password-request.ts b/themes/matrix/client/src/lib/reset-password/reset-password-request.ts deleted file mode 100644 index 846226d75..000000000 --- a/themes/matrix/client/src/lib/reset-password/reset-password-request.ts +++ /dev/null @@ -1,56 +0,0 @@ - -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); - }); -} - diff --git a/themes/matrix/client/src/lib/secondfactor/TOTPValidator.ts b/themes/matrix/client/src/lib/secondfactor/TOTPValidator.ts deleted file mode 100644 index 5394139a6..000000000 --- a/themes/matrix/client/src/lib/secondfactor/TOTPValidator.ts +++ /dev/null @@ -1,28 +0,0 @@ - -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 { - return new BluebirdPromise(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)); - }); - }); -} \ No newline at end of file diff --git a/themes/matrix/client/src/lib/secondfactor/U2FValidator.ts b/themes/matrix/client/src/lib/secondfactor/U2FValidator.ts deleted file mode 100644 index 5812922f7..000000000 --- a/themes/matrix/client/src/lib/secondfactor/U2FValidator.ts +++ /dev/null @@ -1,42 +0,0 @@ -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 { - return new BluebirdPromise(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 { - 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, $); - }); -} diff --git a/themes/matrix/client/src/lib/secondfactor/constants.ts b/themes/matrix/client/src/lib/secondfactor/constants.ts deleted file mode 100644 index 50bba7571..000000000 --- a/themes/matrix/client/src/lib/secondfactor/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ - -export const TOTP_FORM_SELECTOR = ".form-signin.totp"; -export const TOTP_TOKEN_SELECTOR = ".form-signin #token"; diff --git a/themes/matrix/client/src/lib/secondfactor/index.ts b/themes/matrix/client/src/lib/secondfactor/index.ts deleted file mode 100644 index 279723dce..000000000 --- a/themes/matrix/client/src/lib/secondfactor/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -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); - }); -} \ No newline at end of file diff --git a/themes/matrix/client/src/lib/totp-register/totp-register.ts b/themes/matrix/client/src/lib/totp-register/totp-register.ts deleted file mode 100644 index 6a9aa7ee0..000000000 --- a/themes/matrix/client/src/lib/totp-register/totp-register.ts +++ /dev/null @@ -1,11 +0,0 @@ - -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); -} diff --git a/themes/matrix/client/src/lib/totp-register/ui-selector.ts b/themes/matrix/client/src/lib/totp-register/ui-selector.ts deleted file mode 100644 index 9d43fabea..000000000 --- a/themes/matrix/client/src/lib/totp-register/ui-selector.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export const QRCODE_ID_SELECTOR = "#qrcode"; \ No newline at end of file diff --git a/themes/matrix/client/src/lib/u2f-register/u2f-register.ts b/themes/matrix/client/src/lib/u2f-register/u2f-register.ts deleted file mode 100644 index abf40ee05..000000000 --- a/themes/matrix/client/src/lib/u2f-register/u2f-register.ts +++ /dev/null @@ -1,56 +0,0 @@ - -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 { - return new BluebirdPromise(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 { - 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); - }); - }); -} diff --git a/themes/matrix/client/src/thirdparties/matrix.js b/themes/matrix/client/src/thirdparties/matrix.js old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/thirdparties/qrcode.min.js b/themes/matrix/client/src/thirdparties/qrcode.min.js old mode 100644 new mode 100755 diff --git a/themes/matrix/client/src/thirdparties/u2f-api.js b/themes/matrix/client/src/thirdparties/u2f-api.js old mode 100644 new mode 100755 diff --git a/themes/matrix/client/test/Notifier.test.ts b/themes/matrix/client/test/Notifier.test.ts deleted file mode 100644 index 70bfea146..000000000 --- a/themes/matrix/client/test/Notifier.test.ts +++ /dev/null @@ -1,71 +0,0 @@ - -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"); - }); -}); \ No newline at end of file diff --git a/themes/matrix/client/test/firstfactor/FirstFactorValidator.test.ts b/themes/matrix/client/test/firstfactor/FirstFactorValidator.test.ts deleted file mode 100644 index ac8353278..000000000 --- a/themes/matrix/client/test/firstfactor/FirstFactorValidator.test.ts +++ /dev/null @@ -1,44 +0,0 @@ - -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", "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", "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."); - }); - }); -}); \ No newline at end of file diff --git a/themes/matrix/client/test/mocks/NotifierStub.ts b/themes/matrix/client/test/mocks/NotifierStub.ts deleted file mode 100644 index 9c268d66d..000000000 --- a/themes/matrix/client/test/mocks/NotifierStub.ts +++ /dev/null @@ -1,33 +0,0 @@ - -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(); - } -} \ No newline at end of file diff --git a/themes/matrix/client/test/mocks/jquery.ts b/themes/matrix/client/test/mocks/jquery.ts deleted file mode 100644 index 273f90861..000000000 --- a/themes/matrix/client/test/mocks/jquery.ts +++ /dev/null @@ -1,59 +0,0 @@ - -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() - }; -} diff --git a/themes/matrix/client/test/mocks/u2f-api.ts b/themes/matrix/client/test/mocks/u2f-api.ts deleted file mode 100644 index d123f6a95..000000000 --- a/themes/matrix/client/test/mocks/u2f-api.ts +++ /dev/null @@ -1,14 +0,0 @@ - -import sinon = require("sinon"); - -export interface U2FApiMock { - sign: sinon.SinonStub; - register: sinon.SinonStub; -} - -export function U2FApiMock(): U2FApiMock { - return { - sign: sinon.stub(), - register: sinon.stub() - }; -} \ No newline at end of file diff --git a/themes/matrix/client/test/secondfactor/TOTPValidator.test.ts b/themes/matrix/client/test/secondfactor/TOTPValidator.test.ts deleted file mode 100644 index 5dd6f15c3..000000000 --- a/themes/matrix/client/test/secondfactor/TOTPValidator.test.ts +++ /dev/null @@ -1,37 +0,0 @@ - -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(); - }); - }); -}); \ No newline at end of file diff --git a/themes/matrix/client/test/totp-register/totp-register.test.ts b/themes/matrix/client/test/totp-register/totp-register.test.ts deleted file mode 100644 index 86fc455a1..000000000 --- a/themes/matrix/client/test/totp-register/totp-register.test.ts +++ /dev/null @@ -1,31 +0,0 @@ - -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); - }); -}); \ No newline at end of file diff --git a/themes/matrix/client/tsconfig.json b/themes/matrix/client/tsconfig.json deleted file mode 100644 index 0bb4d62ff..000000000 --- a/themes/matrix/client/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "moduleResolution": "node", - "noImplicitAny": true, - "sourceMap": true, - "removeComments": true, - "outDir": "../dist", - "baseUrl": ".", - "paths": { - "*": [ - "./types/*", - "../shared/types/*" - ] - } - }, - "include": [ - "src/**/*" - ], - "exclude": [ - "test/**/*" - ] -} diff --git a/themes/matrix/client/tslint.json b/themes/matrix/client/tslint.json deleted file mode 100644 index c2c1b7501..000000000 --- a/themes/matrix/client/tslint.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "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 - } -} diff --git a/themes/matrix/server/.directory b/themes/matrix/server/.directory old mode 100644 new mode 100755 diff --git a/themes/matrix/server/src/index.ts b/themes/matrix/server/src/index.ts deleted file mode 100755 index fcbf4d022..000000000 --- a/themes/matrix/server/src/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -#! /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 "); - 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); diff --git a/themes/matrix/server/src/lib/.directory b/themes/matrix/server/src/lib/.directory deleted file mode 100644 index 006b379ad..000000000 --- a/themes/matrix/server/src/lib/.directory +++ /dev/null @@ -1,4 +0,0 @@ -[Dolphin] -Timestamp=2018,12,17,20,59,13 -Version=3 -ViewMode=1 diff --git a/themes/matrix/server/src/lib/AuthenticationSessionHandler.ts b/themes/matrix/server/src/lib/AuthenticationSessionHandler.ts deleted file mode 100644 index 57361bf8a..000000000 --- a/themes/matrix/server/src/lib/AuthenticationSessionHandler.ts +++ /dev/null @@ -1,45 +0,0 @@ - - -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; - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/ErrorReplies.ts b/themes/matrix/server/src/lib/ErrorReplies.ts deleted file mode 100644 index f1c5f4fd1..000000000 --- a/themes/matrix/server/src/lib/ErrorReplies.ts +++ /dev/null @@ -1,49 +0,0 @@ -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 }); -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/Exceptions.ts b/themes/matrix/server/src/lib/Exceptions.ts deleted file mode 100644 index 83fa4eb6b..000000000 --- a/themes/matrix/server/src/lib/Exceptions.ts +++ /dev/null @@ -1,88 +0,0 @@ - -export class LdapSearchError extends Error { - constructor(message?: string) { - super(message); - this.name = "LdapSearchError"; - (Object).setPrototypeOf(this, LdapSearchError.prototype); - } -} - -export class LdapBindError extends Error { - constructor(message?: string) { - super(message); - this.name = "LdapBindError"; - (Object).setPrototypeOf(this, LdapBindError.prototype); - } -} - -export class LdapError extends Error { - constructor(message?: string) { - super(message); - this.name = "LdapError"; - (Object).setPrototypeOf(this, LdapError.prototype); - } -} - -export class IdentityError extends Error { - constructor(message?: string) { - super(message); - this.name = "IdentityError"; - (Object).setPrototypeOf(this, IdentityError.prototype); - } -} - -export class AccessDeniedError extends Error { - constructor(message?: string) { - super(message); - this.name = "AccessDeniedError"; - (Object).setPrototypeOf(this, AccessDeniedError.prototype); - } -} - -export class AuthenticationRegulationError extends Error { - constructor(message?: string) { - super(message); - this.name = "AuthenticationRegulationError"; - (Object).setPrototypeOf(this, AuthenticationRegulationError.prototype); - } -} - -export class InvalidTOTPError extends Error { - constructor(message?: string) { - super(message); - this.name = "InvalidTOTPError"; - (Object).setPrototypeOf(this, InvalidTOTPError.prototype); - } -} - -export class NotAuthenticatedError extends Error { - constructor(message?: string) { - super(message); - this.name = "NotAuthenticatedError"; - (Object).setPrototypeOf(this, NotAuthenticatedError.prototype); - } -} - -export class NotAuthorizedError extends Error { - constructor(message?: string) { - super(message); - this.name = "NotAuthanticatedError"; - (Object).setPrototypeOf(this, NotAuthorizedError.prototype); - } -} - -export class FirstFactorValidationError extends Error { - constructor(message?: string) { - super(message); - this.name = "FirstFactorValidationError"; - (Object).setPrototypeOf(this, FirstFactorValidationError.prototype); - } -} - -export class SecondFactorValidationError extends Error { - constructor(message?: string) { - super(message); - this.name = "SecondFactorValidationError"; - (Object).setPrototypeOf(this, FirstFactorValidationError.prototype); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/FirstFactorValidator.ts b/themes/matrix/server/src/lib/FirstFactorValidator.ts deleted file mode 100644 index 231060002..000000000 --- a/themes/matrix/server/src/lib/FirstFactorValidator.ts +++ /dev/null @@ -1,20 +0,0 @@ - -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 { - 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(); - }); -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/IdentityCheckMiddleware.spec.ts b/themes/matrix/server/src/lib/IdentityCheckMiddleware.spec.ts deleted file mode 100644 index 842ed6bcb..000000000 --- a/themes/matrix/server/src/lib/IdentityCheckMiddleware.spec.ts +++ /dev/null @@ -1,176 +0,0 @@ - -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")); - }); - }); - }); -}); diff --git a/themes/matrix/server/src/lib/IdentityCheckMiddleware.ts b/themes/matrix/server/src/lib/IdentityCheckMiddleware.ts deleted file mode 100644 index e72ea4db1..000000000 --- a/themes/matrix/server/src/lib/IdentityCheckMiddleware.ts +++ /dev/null @@ -1,138 +0,0 @@ -import objectPath = require("object-path"); -import randomstring = require("randomstring"); -import BluebirdPromise = require("bluebird"); -import util = require("util"); -import Exceptions = require("./Exceptions"); -import fs = require("fs"); -import ejs = require("ejs"); -import { 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 { - - 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 { - 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 { - 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 { - - let authSession: AuthenticationSession; - const identityToken = objectPath.get( - 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 { - 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)); - }; -} diff --git a/themes/matrix/server/src/lib/IdentityCheckPreValidationTemplate.ts b/themes/matrix/server/src/lib/IdentityCheckPreValidationTemplate.ts deleted file mode 100644 index 0161ce406..000000000 --- a/themes/matrix/server/src/lib/IdentityCheckPreValidationTemplate.ts +++ /dev/null @@ -1,3 +0,0 @@ - - -export const PRE_VALIDATION_TEMPLATE = "need-identity-validation"; \ No newline at end of file diff --git a/themes/matrix/server/src/lib/IdentityValidable.ts b/themes/matrix/server/src/lib/IdentityValidable.ts deleted file mode 100644 index 075580c9e..000000000 --- a/themes/matrix/server/src/lib/IdentityValidable.ts +++ /dev/null @@ -1,19 +0,0 @@ -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; - postValidationInit(req: Express.Request): Bluebird; - - // 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; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/IdentityValidableStub.spec.ts b/themes/matrix/server/src/lib/IdentityValidableStub.spec.ts deleted file mode 100644 index 20a977140..000000000 --- a/themes/matrix/server/src/lib/IdentityValidableStub.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ - -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 { - return this.preValidationInitStub(req); - } - - postValidationInit(req: Express.Request): Bluebird { - 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(); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/Server.spec.ts b/themes/matrix/server/src/lib/Server.spec.ts deleted file mode 100644 index 365163254..000000000 --- a/themes/matrix/server/src/lib/Server.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ - -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(); - }); - }); -}); diff --git a/themes/matrix/server/src/lib/Server.ts b/themes/matrix/server/src/lib/Server.ts deleted file mode 100644 index 4090f6294..000000000 --- a/themes/matrix/server/src/lib/Server.ts +++ /dev/null @@ -1,93 +0,0 @@ -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 { - 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((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 { - 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(); - } -} - diff --git a/themes/matrix/server/src/lib/ServerVariables.ts b/themes/matrix/server/src/lib/ServerVariables.ts deleted file mode 100644 index cd3dd6dc8..000000000 --- a/themes/matrix/server/src/lib/ServerVariables.ts +++ /dev/null @@ -1,21 +0,0 @@ -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; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/ServerVariablesInitializer.ts b/themes/matrix/server/src/lib/ServerVariablesInitializer.ts deleted file mode 100644 index df79238cc..000000000 --- a/themes/matrix/server/src/lib/ServerVariablesInitializer.ts +++ /dev/null @@ -1,116 +0,0 @@ - -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 { - 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 { - - 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); - }); - } -} diff --git a/themes/matrix/server/src/lib/ServerVariablesMockBuilder.spec.ts b/themes/matrix/server/src/lib/ServerVariablesMockBuilder.spec.ts deleted file mode 100644 index 7874702a0..000000000 --- a/themes/matrix/server/src/lib/ServerVariablesMockBuilder.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -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 - }; - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/Level.ts b/themes/matrix/server/src/lib/authentication/Level.ts deleted file mode 100644 index 57b6a2346..000000000 --- a/themes/matrix/server/src/lib/authentication/Level.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum Level { - NOT_AUTHENTICATED = 0, - ONE_FACTOR = 1, - TWO_FACTOR = 2 -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/GroupsAndEmails.ts b/themes/matrix/server/src/lib/authentication/backends/GroupsAndEmails.ts deleted file mode 100644 index 3434ba66f..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/GroupsAndEmails.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface GroupsAndEmails { - groups: string[]; - emails: string[]; -} diff --git a/themes/matrix/server/src/lib/authentication/backends/IUsersDatabase.ts b/themes/matrix/server/src/lib/authentication/backends/IUsersDatabase.ts deleted file mode 100644 index d7fa13b7a..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/IUsersDatabase.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Bluebird = require("bluebird"); - -import { GroupsAndEmails } from "./GroupsAndEmails"; - -export interface IUsersDatabase { - checkUserPassword(username: string, password: string): Bluebird; - getEmails(username: string): Bluebird; - getGroups(username: string): Bluebird; - updatePassword(username: string, newPassword: string): Bluebird; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts b/themes/matrix/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts deleted file mode 100644 index 19341a5dd..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -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 { - return this.checkUserPasswordStub(username, password); - } - - getEmails(username: string): Bluebird { - return this.getEmailsStub(username); - } - - getGroups(username: string): Bluebird { - return this.getGroupsStub(username); - } - - updatePassword(username: string, newPassword: string): Bluebird { - return this.updatePasswordStub(username, newPassword); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts b/themes/matrix/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts deleted file mode 100644 index a258a78f7..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts +++ /dev/null @@ -1,224 +0,0 @@ -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()); - }); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/file/FileUsersDatabase.ts b/themes/matrix/server/src/lib/authentication/backends/file/FileUsersDatabase.ts deleted file mode 100644 index d34dde21e..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/file/FileUsersDatabase.ts +++ /dev/null @@ -1,182 +0,0 @@ -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 { - return new Bluebird((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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - return this.readDatabase() - .then((database) => { - return this.checkUserExists(database, username) - .then(() => this.retrieveEmails(database, username)); - }); - } - - getGroups(username: string): Bluebird { - return this.readDatabase() - .then((database) => { - return this.checkUserExists(database, username) - .then(() => this.retrieveGroups(database, username)); - }); - } - - updatePassword(username: string, newPassword: string): Bluebird { - return this.readDatabase() - .then((database) => { - return this.checkUserExists(database, username) - .then(() => this.replacePassword(database, username, newPassword)); - }); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/file/ReadWriteQueue.ts b/themes/matrix/server/src/lib/authentication/backends/file/ReadWriteQueue.ts deleted file mode 100644 index 957ddaec5..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/file/ReadWriteQueue.ts +++ /dev/null @@ -1,60 +0,0 @@ -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(); - } - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/ISession.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/ISession.ts deleted file mode 100644 index da2c74433..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/ldap/ISession.ts +++ /dev/null @@ -1,12 +0,0 @@ - -import BluebirdPromise = require("bluebird"); - -export interface ISession { - open(): BluebirdPromise; - close(): BluebirdPromise; - - searchUserDn(username: string): BluebirdPromise; - searchEmails(username: string): BluebirdPromise; - searchGroups(username: string): BluebirdPromise; - modifyPassword(username: string, newPassword: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/ISessionFactory.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/ISessionFactory.ts deleted file mode 100644 index 014d1eea5..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/ldap/ISessionFactory.ts +++ /dev/null @@ -1,6 +0,0 @@ - -import { ISession } from "./ISession"; - -export interface ISessionFactory { - create(userDN: string, password: string): ISession; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts deleted file mode 100644 index f4a6e6306..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts +++ /dev/null @@ -1,386 +0,0 @@ -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); - }) - }); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts deleted file mode 100644 index edda62ec6..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts +++ /dev/null @@ -1,107 +0,0 @@ -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 = (session: ISession) => Bluebird; - -export class LdapUsersDatabase implements IUsersDatabase { - private sessionFactory: ISessionFactory; - private configuration: LdapConfiguration; - - constructor( - sessionFactory: ISessionFactory, - configuration: LdapConfiguration) { - this.sessionFactory = sessionFactory; - this.configuration = configuration; - } - - private withSession( - username: string, - password: string, - cb: SessionCallback): Bluebird { - const session = this.sessionFactory.create(username, password); - return session.open() - .then(() => cb(session)) - .finally(() => session.close()); - } - - checkUserPassword(username: string, password: string): Bluebird { - const that = this; - function verifyUserPassword(userDN: string) { - return that.withSession( - 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 { - 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 { - 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 { - 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)); - }); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts deleted file mode 100644 index 9dedfcb7c..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -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"); - }); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/SafeSession.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/SafeSession.ts deleted file mode 100644 index 572209066..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/ldap/SafeSession.ts +++ /dev/null @@ -1,62 +0,0 @@ -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 { - return this.sesion.open(); - } - - close(): BluebirdPromise { - return this.sesion.close(); - } - - searchGroups(username: string): BluebirdPromise { - 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 { - 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 { - 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 { - try { - const sanitizedUsername = Sanitizer.sanitize(username); - return this.sesion.modifyPassword(sanitizedUsername, newPassword); - } - catch (e) { - return BluebirdPromise.reject(new Error(SPECIAL_CHAR_USED_MESSAGE)); - } - } -} diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts deleted file mode 100644 index 9dd33fed4..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -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 { 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); - }); -}); diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/Sanitizer.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/Sanitizer.ts deleted file mode 100644 index be74132ad..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/ldap/Sanitizer.ts +++ /dev/null @@ -1,25 +0,0 @@ - -// 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; - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/Session.spec.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/Session.spec.ts deleted file mode 100644 index d55f6a805..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/ldap/Session.spec.ts +++ /dev/null @@ -1,127 +0,0 @@ - -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"]); - }); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/Session.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/Session.ts deleted file mode 100644 index e0284b3c4..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/ldap/Session.ts +++ /dev/null @@ -1,156 +0,0 @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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(); - }); - } -} diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/SessionFactory.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/SessionFactory.ts deleted file mode 100644 index 0b6c4bff3..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/ldap/SessionFactory.ts +++ /dev/null @@ -1,37 +0,0 @@ -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 - ) - ); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts deleted file mode 100644 index face3930d..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts deleted file mode 100644 index 5faf2ba18..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -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 { - return this.openStub(); - } - - close(): Bluebird { - return this.closeStub(); - } - - searchUserDn(username: string): Bluebird { - return this.searchUserDnStub(username); - } - - searchEmails(username: string): Bluebird { - return this.searchEmailsStub(username); - } - - searchGroups(username: string): Bluebird { - return this.searchGroupsStub(username); - } - - modifyPassword(username: string, newPassword: string): Bluebird { - return this.modifyPasswordStub(username, newPassword); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/connector/Connector.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/connector/Connector.ts deleted file mode 100644 index 2542ea7f9..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/ldap/connector/Connector.ts +++ /dev/null @@ -1,69 +0,0 @@ -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; - unbindAsync(): Bluebird; - searchAsync(base: string, query: LdapJs.SearchOptions): Bluebird; - modifyAsync(userdn: string, change: LdapJs.Change): Bluebird; -} - -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 { - return this.client.bindAsync(username, password); - } - - unbindAsync(): Bluebird { - return this.client.unbindAsync(); - } - - searchAsync(base: string, query: any): Bluebird { - const that = this; - return this.client.searchAsync(base, query) - .then(function (res: EventEmitter) { - const doc: SearchEntry[] = []; - return new Bluebird((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 { - return this.client.modifyAsync(dn, changeRequest); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts deleted file mode 100644 index 61fef07a4..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts +++ /dev/null @@ -1,18 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts deleted file mode 100644 index d11fa6386..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -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(); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts deleted file mode 100644 index 0b78225bf..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -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 { - return this.bindAsyncStub(username, password); - } - - unbindAsync(): BluebirdPromise { - return this.unbindAsyncStub(); - } - - searchAsync(base: string, query: any): BluebirdPromise { - return this.searchAsyncStub(base, query); - } - - modifyAsync(dn: string, changeRequest: any): BluebirdPromise { - return this.modifyAsyncStub(dn, changeRequest); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/connector/IConnector.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/connector/IConnector.ts deleted file mode 100644 index 1e63ab193..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/ldap/connector/IConnector.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Bluebird = require("bluebird"); -import EventEmitter = require("events"); - -export interface IConnector { - bindAsync(username: string, password: string): Bluebird; - unbindAsync(): Bluebird; - searchAsync(base: string, query: any): Bluebird; - modifyAsync(dn: string, changeRequest: any): Bluebird; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts b/themes/matrix/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts deleted file mode 100644 index f9ed65eff..000000000 --- a/themes/matrix/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { IConnector } from "./IConnector"; - -export interface IConnectorFactory { - create(): IConnector; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/totp/ITotpHandler.ts b/themes/matrix/server/src/lib/authentication/totp/ITotpHandler.ts deleted file mode 100644 index d600d31e4..000000000 --- a/themes/matrix/server/src/lib/authentication/totp/ITotpHandler.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { TOTPSecret } from "../../../../types/TOTPSecret"; - -export interface ITotpHandler { - generate(label: string, issuer: string): TOTPSecret; - validate(token: string, secret: string): boolean; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/totp/TotpHandler.spec.ts b/themes/matrix/server/src/lib/authentication/totp/TotpHandler.spec.ts deleted file mode 100644 index 67cffa638..000000000 --- a/themes/matrix/server/src/lib/authentication/totp/TotpHandler.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ - -import { TotpHandler } from "./TotpHandler"; -import Sinon = require("sinon"); -import Speakeasy = require("speakeasy"); -import Assert = require("assert"); - -describe("authentication/totp/TotpHandler", function() { - let totpValidator: TotpHandler; - let validateStub: Sinon.SinonStub; - - beforeEach(() => { - validateStub = Sinon.stub(Speakeasy.totp, "verify"); - totpValidator = new TotpHandler(Speakeasy); - }); - - afterEach(function() { - validateStub.restore(); - }); - - it("should validate the TOTP token", function() { - const totp_secret = "NBD2ZV64R9UV1O7K"; - const token = "token"; - validateStub.withArgs({ - secret: totp_secret, - token: token, - encoding: "base32", - window: 1 - }).returns(true); - Assert(totpValidator.validate(token, totp_secret)); - }); - - it("should not validate a wrong TOTP token", function() { - const totp_secret = "NBD2ZV64R9UV1O7K"; - const token = "wrong token"; - validateStub.returns(false); - Assert(!totpValidator.validate(token, totp_secret)); - }); -}); - diff --git a/themes/matrix/server/src/lib/authentication/totp/TotpHandler.ts b/themes/matrix/server/src/lib/authentication/totp/TotpHandler.ts deleted file mode 100644 index dfab502a7..000000000 --- a/themes/matrix/server/src/lib/authentication/totp/TotpHandler.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ITotpHandler } from "./ITotpHandler"; -import { TOTPSecret } from "../../../../types/TOTPSecret"; -import Speakeasy = require("speakeasy"); - -const TOTP_ENCODING = "base32"; -const WINDOW: number = 1; - -export class TotpHandler implements ITotpHandler { - private speakeasy: typeof Speakeasy; - - constructor(speakeasy: typeof Speakeasy) { - this.speakeasy = speakeasy; - } - - generate(label: string, issuer: string): TOTPSecret { - const secret = this.speakeasy.generateSecret({ - otpauth_url: false - }) as TOTPSecret; - - secret.otpauth_url = this.speakeasy.otpauthURL({ - secret: secret.ascii, - label: label, - issuer: issuer - }); - return secret; - } - - validate(token: string, secret: string): boolean { - return this.speakeasy.totp.verify({ - secret: secret, - encoding: TOTP_ENCODING, - token: token, - window: WINDOW - }); - } -} diff --git a/themes/matrix/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts b/themes/matrix/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts deleted file mode 100644 index ea93330d9..000000000 --- a/themes/matrix/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import { ITotpHandler } from "./ITotpHandler"; -import { TOTPSecret } from "../../../../types/TOTPSecret"; - -export class TotpHandlerStub implements ITotpHandler { - generateStub: Sinon.SinonStub; - validateStub: Sinon.SinonStub; - - constructor() { - this.generateStub = Sinon.stub(); - this.validateStub = Sinon.stub(); - } - - generate(label: string, issuer: string): TOTPSecret { - return this.generateStub(label, issuer); - } - - validate(token: string, secret: string): boolean { - return this.validateStub(token, secret); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/u2f/IU2fHandler.ts b/themes/matrix/server/src/lib/authentication/u2f/IU2fHandler.ts deleted file mode 100644 index b9b7d6f26..000000000 --- a/themes/matrix/server/src/lib/authentication/u2f/IU2fHandler.ts +++ /dev/null @@ -1,9 +0,0 @@ -import U2f = require("u2f"); - -export interface IU2fHandler { - request(appId: string, keyHandle?: string): U2f.Request; - checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) - : U2f.RegistrationResult | U2f.Error; - checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) - : U2f.SignatureResult | U2f.Error; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authentication/u2f/U2fHandler.ts b/themes/matrix/server/src/lib/authentication/u2f/U2fHandler.ts deleted file mode 100644 index bf3891e5b..000000000 --- a/themes/matrix/server/src/lib/authentication/u2f/U2fHandler.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { IU2fHandler } from "./IU2fHandler"; -import U2f = require("u2f"); - -export class U2fHandler implements IU2fHandler { - private u2f: typeof U2f; - - constructor(u2f: typeof U2f) { - this.u2f = u2f; - } - - request(appId: string, keyHandle?: string): U2f.Request { - return this.u2f.request(appId, keyHandle); - } - - checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) - : U2f.RegistrationResult | U2f.Error { - return this.u2f.checkRegistration(registrationRequest, registrationResponse); - } - - checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) - : U2f.SignatureResult | U2f.Error { - return this.u2f.checkSignature(signatureRequest, signatureResponse, publicKey); - } -} diff --git a/themes/matrix/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts b/themes/matrix/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts deleted file mode 100644 index 135d7eb06..000000000 --- a/themes/matrix/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import U2f = require("u2f"); -import { IU2fHandler } from "./IU2fHandler"; - - -export class U2fHandlerStub implements IU2fHandler { - requestStub: Sinon.SinonStub; - checkRegistrationStub: Sinon.SinonStub; - checkSignatureStub: Sinon.SinonStub; - - constructor() { - this.requestStub = Sinon.stub(); - this.checkRegistrationStub = Sinon.stub(); - this.checkSignatureStub = Sinon.stub(); - } - - request(appId: string, keyHandle?: string): U2f.Request { - return this.requestStub(appId, keyHandle); - } - - checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) - : U2f.RegistrationResult | U2f.Error { - return this.checkRegistrationStub(registrationRequest, registrationResponse); - } - - checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) - : U2f.SignatureResult | U2f.Error { - return this.checkSignatureStub(signatureRequest, signatureResponse, publicKey); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authorization/Authorizer.spec.ts b/themes/matrix/server/src/lib/authorization/Authorizer.spec.ts deleted file mode 100644 index 586814042..000000000 --- a/themes/matrix/server/src/lib/authorization/Authorizer.spec.ts +++ /dev/null @@ -1,372 +0,0 @@ - -import Assert = require("assert"); -import winston = require("winston"); -import { Authorizer } from "./Authorizer"; -import { ACLConfiguration, ACLRule } from "../configuration/schema/AclConfiguration"; -import { Level } from "./Level"; - -describe("authorization/Authorizer", function () { - let authorizer: Authorizer; - let configuration: ACLConfiguration; - - describe("configuration is null", function() { - it("should allow access to anything, anywhere for anybody", function() { - configuration = undefined; - authorizer = new Authorizer(configuration, winston); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1", "group2"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "admin.example.com", resource: "/"}, {user: "user3", groups: ["group3"]}), Level.BYPASS); - }); - }); - - describe("configuration is not null", function () { - beforeEach(function () { - configuration = { - default_policy: "deny", - rules: [] - }; - authorizer = new Authorizer(configuration, winston); - }); - - describe("check access control with default policy to deny", function () { - beforeEach(function () { - configuration.default_policy = "deny"; - }); - - it("should deny access when no rule is provided", function () { - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should control access when multiple domain matcher is provided", function () { - configuration.rules = [{ - domain: "*.mail.example.com", - policy: "two_factor", - subject: "user:user1", - resources: [".*"] - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should allow access to all resources when resources is not provided", function () { - configuration.rules = [{ - domain: "*.mail.example.com", - policy: "two_factor", - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - describe("check user rules", function () { - it("should allow access when user has a matching allowing rule", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: [".*"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should deny to other users", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: [".*"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user2", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); - }); - - it("should allow user access only to specific resources", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: ["/private/.*", "^/begin", "/end$"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/middle/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/begin"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/not/begin"}, {user: "user1", groups: ["group1"]}), Level.DENY); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end/x"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should allow access to multiple domains", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: [".*"], - subject: "user:user1" - }, { - domain: "home1.example.com", - policy: "one_factor", - resources: [".*"], - subject: "user:user1" - }, { - domain: "home2.example.com", - policy: "deny", - resources: [".*"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home1.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); - Assert.equal(authorizer.authorization({domain: "home2.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home3.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should apply rules in order", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "one_factor", - resources: ["/my/private/resource"], - subject: "user:user1" - }, { - domain: "home.example.com", - policy: "deny", - resources: ["^/my/private/.*"], - subject: "user:user1" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/my/.*"], - subject: "user:user1" - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/poney"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/duck"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/resource"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); - }); - }); - - describe("check group rules", function () { - it("should allow access when user is in group having a matching allowing rule", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: ["^/$"], - subject: "group:group1" - }, { - domain: "home.example.com", - policy: "one_factor", - resources: ["^/test$"], - subject: "group:group2" - }, { - domain: "home.example.com", - policy: "deny", - resources: ["^/private$"], - subject: "group:group2" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.ONE_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); - }); - }); - }); - - describe("check any rules", function () { - it("should control access when any rules are defined", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "bypass", - resources: ["^/public$"] - }, { - domain: "home.example.com", - policy: "deny", - resources: ["^/private$"] - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, - {user: "user4", groups: ["group5"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, - {user: "user4", groups: ["group5"]}), Level.DENY); - }); - }); - - describe("check access control with default policy to allow", function () { - beforeEach(function () { - configuration.default_policy = "bypass"; - }); - - it("should allow access to anything when no rule is provided", function () { - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - }); - - it("should deny access to one resource when defined", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "deny", - resources: ["/test"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - }); - }); - - describe("check access control with complete use case", function () { - beforeEach(function () { - configuration.default_policy = "deny"; - }); - - it("should control access of multiple user (real use case)", function () { - // Let say we have three users: admin, john, harry. - // admin is in groups ["admins"] - // john is in groups ["dev", "admin-private"] - // harry is in groups ["dev"] - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: ["^/public$", "^/$"] - }, { - domain: "home.example.com", - policy: "two_factor", - resources: [".*"], - subject: "group:admins" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/private/?.*"], - subject: "group:admin-private" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/private/john$"], - subject: "user:john" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/private/harry"], - subject: "user:harry" - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); - }); - - it("should allow when allowed at group level and denied at user level", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "deny", - resources: ["^/dev/bob$"], - subject: "user:john" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"], - subject: "group:dev" - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); - }); - - it("should allow access when allowed at 'any' level and denied at user level", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "deny", - resources: ["^/dev/bob$"], - subject: "user:john" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"] - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); - }); - - it("should allow access when allowed at 'any' level and denied at group level", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "deny", - resources: ["^/dev/bob$"], - subject: "group:dev" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"] - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); - }); - - it("should respect rules precedence", function () { - // the priority from least to most is 'default_policy', 'all', 'group', 'user' - // and the first rules in each category as a lower priority than the latest. - // You can think of it that way: they override themselves inside each category. - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"], - subject: "user:john" - }, { - domain: "home.example.com", - policy: "deny", - resources: ["^/dev/bob$"], - subject: "group:dev" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"] - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - }); - }); - }); -}); diff --git a/themes/matrix/server/src/lib/authorization/Authorizer.ts b/themes/matrix/server/src/lib/authorization/Authorizer.ts deleted file mode 100644 index 889b7ec20..000000000 --- a/themes/matrix/server/src/lib/authorization/Authorizer.ts +++ /dev/null @@ -1,85 +0,0 @@ - -import { ACLConfiguration, ACLPolicy, ACLRule } from "../configuration/schema/AclConfiguration"; -import { IAuthorizer } from "./IAuthorizer"; -import { Winston } from "../../../types/Dependencies"; -import { MultipleDomainMatcher } from "./MultipleDomainMatcher"; -import { Level } from "./Level"; -import { Object } from "./Object"; -import { Subject } from "./Subject"; - -function MatchDomain(actualDomain: string) { - return function (rule: ACLRule): boolean { - return MultipleDomainMatcher.match(actualDomain, rule.domain); - }; -} - -function MatchResource(actualResource: string) { - return function (rule: ACLRule): boolean { - // If resources key is not provided, the rule applies to all resources. - if (!rule.resources) return true; - - for (let i = 0; i < rule.resources.length; ++i) { - const regexp = new RegExp(rule.resources[i]); - if (regexp.test(actualResource)) return true; - } - return false; - }; -} - -function MatchSubject(subject: Subject) { - return (rule: ACLRule) => { - // If no subject, matches anybody - if (!rule.subject) return true; - - if (rule.subject.startsWith("user:")) { - const ruleUser = rule.subject.split(":")[1]; - if (subject.user == ruleUser) return true; - } - - if (rule.subject.startsWith("group:")) { - const ruleGroup = rule.subject.split(":")[1]; - if (subject.groups.indexOf(ruleGroup) > -1) return true; - } - return false; - }; -} - -export class Authorizer implements IAuthorizer { - private logger: Winston; - private readonly configuration: ACLConfiguration; - - constructor(configuration: ACLConfiguration, logger_: Winston) { - this.logger = logger_; - this.configuration = configuration; - } - - private getMatchingRules(object: Object, subject: Subject): ACLRule[] { - const rules = this.configuration.rules; - if (!rules) return []; - return rules - .filter(MatchDomain(object.domain)) - .filter(MatchResource(object.resource)) - .filter(MatchSubject(subject)); - } - - private ruleToLevel(policy: string): Level { - if (policy == "bypass") { - return Level.BYPASS; - } else if (policy == "one_factor") { - return Level.ONE_FACTOR; - } else if (policy == "two_factor") { - return Level.TWO_FACTOR; - } - return Level.DENY; - } - - authorization(object: Object, subject: Subject): Level { - if (!this.configuration) return Level.BYPASS; - - const rules = this.getMatchingRules(object, subject); - - return (rules.length > 0) - ? this.ruleToLevel(rules[0].policy) // extract the policy of the first matching rule - : this.ruleToLevel(this.configuration.default_policy); // otherwise use the default policy - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authorization/AuthorizerStub.spec.ts b/themes/matrix/server/src/lib/authorization/AuthorizerStub.spec.ts deleted file mode 100644 index 9bd6f4a85..000000000 --- a/themes/matrix/server/src/lib/authorization/AuthorizerStub.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Sinon = require("sinon"); -import { IAuthorizer } from "./IAuthorizer"; -import { Level } from "./Level"; -import { Object } from "./Object"; -import { Subject } from "./Subject"; - -export class AuthorizerStub implements IAuthorizer { - authorizationMock: Sinon.SinonStub; - - constructor() { - this.authorizationMock = Sinon.stub(); - } - - authorization(object: Object, subject: Subject): Level { - return this.authorizationMock(object, subject); - } -} diff --git a/themes/matrix/server/src/lib/authorization/IAuthorizer.ts b/themes/matrix/server/src/lib/authorization/IAuthorizer.ts deleted file mode 100644 index fe7ba367a..000000000 --- a/themes/matrix/server/src/lib/authorization/IAuthorizer.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Level } from "./Level"; -import { Subject } from "./Subject"; -import { Object } from "./Object"; - -export interface IAuthorizer { - authorization(object: Object, subject: Subject): Level; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authorization/Level.ts b/themes/matrix/server/src/lib/authorization/Level.ts deleted file mode 100644 index d12802610..000000000 --- a/themes/matrix/server/src/lib/authorization/Level.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum Level { - BYPASS = 0, - ONE_FACTOR = 1, - TWO_FACTOR = 2, - DENY = 3 -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authorization/MultipleDomainMatcher.ts b/themes/matrix/server/src/lib/authorization/MultipleDomainMatcher.ts deleted file mode 100644 index 64c647a4a..000000000 --- a/themes/matrix/server/src/lib/authorization/MultipleDomainMatcher.ts +++ /dev/null @@ -1,12 +0,0 @@ - -export class MultipleDomainMatcher { - static match(domain: string, pattern: string): boolean { - if (pattern.startsWith("*") && - domain.endsWith(pattern.substr(1))) { - return true; - } - else if (domain == pattern) { - return true; - } - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authorization/Object.ts b/themes/matrix/server/src/lib/authorization/Object.ts deleted file mode 100644 index 5411b0d24..000000000 --- a/themes/matrix/server/src/lib/authorization/Object.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface Object { - domain: string; - resource: string; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/authorization/Subject.ts b/themes/matrix/server/src/lib/authorization/Subject.ts deleted file mode 100644 index 310d6b4c0..000000000 --- a/themes/matrix/server/src/lib/authorization/Subject.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface Subject { - user: string; - groups: string[]; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/ConfigurationParser.spec.ts b/themes/matrix/server/src/lib/configuration/ConfigurationParser.spec.ts deleted file mode 100644 index 60c0f6182..000000000 --- a/themes/matrix/server/src/lib/configuration/ConfigurationParser.spec.ts +++ /dev/null @@ -1,171 +0,0 @@ -import * as Assert from "assert"; -import { Configuration } from "./schema/Configuration"; -import { ACLConfiguration } from "./schema/AclConfiguration"; -import { ConfigurationParser } from "./ConfigurationParser"; - -describe("configuration/ConfigurationParser", function () { - function buildYamlConfig(): Configuration { - const yaml_config: Configuration = { - port: 8080, - authentication_backend: { - ldap: { - url: "http://ldap", - base_dn: "dc=example,dc=com", - additional_users_dn: "ou=users", - additional_groups_dn: "ou=groups", - user: "user", - password: "pass" - }, - }, - session: { - domain: "example.com", - secret: "secret", - expiration: 40000 - }, - storage: { - local: { - path: "/mydirectory" - } - }, - regulation: { - max_retries: 3, - find_time: 5 * 60, - ban_time: 5 * 60 - }, - logs_level: "debug", - notifier: { - email: { - username: "user", - password: "password", - sender: "admin@example.com", - service: "gmail" - } - } - }; - return yaml_config; - } - - describe("port", function () { - it("should read the port from the yaml file", function () { - const yaml_config = buildYamlConfig(); - yaml_config.port = 7070; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.port, 7070); - }); - - it("should default the port to 8080 if not provided", function () { - const yaml_config = buildYamlConfig(); - delete yaml_config.port; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.port, 8080); - }); - }); - - describe("test session configuration", function() { - it("should get the session attributes", function () { - const yaml_config = buildYamlConfig(); - yaml_config.session = { - domain: "example.com", - secret: "secret", - expiration: 3600, - inactivity: 4000 - }; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.session.domain, "example.com"); - Assert.equal(config.session.secret, "secret"); - Assert.equal(config.session.expiration, 3600); - Assert.equal(config.session.inactivity, 4000); - }); - - it("should be ok not specifying inactivity", function () { - const yaml_config = buildYamlConfig(); - yaml_config.session = { - domain: "example.com", - secret: "secret", - expiration: 3600 - }; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.session.domain, "example.com"); - Assert.equal(config.session.secret, "secret"); - Assert.equal(config.session.expiration, 3600); - Assert.equal(config.session.inactivity, undefined); - }); - }); - - it("should get the log level", function () { - const yaml_config = buildYamlConfig(); - yaml_config.logs_level = "debug"; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.logs_level, "debug"); - }); - - it("should get the notifier config", function () { - const userConfig = buildYamlConfig(); - userConfig.notifier = { - email: { - username: "user", - password: "pass", - sender: "admin@example.com", - service: "gmail" - } - }; - const config = ConfigurationParser.parse(userConfig); - Assert.deepEqual(config.notifier, { - email: { - username: "user", - password: "pass", - sender: "admin@example.com", - service: "gmail" - } - }); - }); - - describe("access_control", function() { - it("should adapt access_control when it is already ok", function () { - const userConfig = buildYamlConfig(); - userConfig.access_control = { - default_policy: "deny", - rules: [{ - domain: "www.example.com", - policy: "two_factor", - subject: "user:user" - }, { - domain: "public.example.com", - policy: "two_factor" - }] - }; - const config = ConfigurationParser.parse(userConfig); - Assert.deepEqual(config.access_control, { - default_policy: "deny", - rules: [{ - domain: "www.example.com", - policy: "two_factor", - subject: "user:user" - }, { - domain: "public.example.com", - policy: "two_factor" - }] - } as ACLConfiguration); - }); - - - it("should adapt access_control when it is empty", function () { - const userConfig = buildYamlConfig(); - userConfig.access_control = {} as any; - const config = ConfigurationParser.parse(userConfig); - Assert.deepEqual(config.access_control, { - default_policy: "bypass", - rules: [] - }); - }); - }); - - describe("default_redirection_url", function() { - it("should parse default_redirection_url", function() { - const userConfig = buildYamlConfig(); - userConfig.default_redirection_url = "dummy_url"; - const config = ConfigurationParser.parse(userConfig); - Assert.deepEqual(config.default_redirection_url, "dummy_url"); - }); - }); -}); diff --git a/themes/matrix/server/src/lib/configuration/ConfigurationParser.ts b/themes/matrix/server/src/lib/configuration/ConfigurationParser.ts deleted file mode 100644 index d92d163c4..000000000 --- a/themes/matrix/server/src/lib/configuration/ConfigurationParser.ts +++ /dev/null @@ -1,39 +0,0 @@ - -import * as ObjectPath from "object-path"; -import { Configuration, complete } from "./schema/Configuration"; -import Ajv = require("ajv"); -import Path = require("path"); -import Util = require("util"); - -export class ConfigurationParser { - private static parseTypes(configuration: Configuration): string[] { - const schema = require(Path.resolve(__dirname, "./Configuration.schema.json")); - const ajv = new Ajv({ - allErrors: true, - missingRefs: "fail" - }); - ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json")); - const valid = ajv.validate(schema, configuration); - if (!valid) - return ajv.errors.map( - (e: Ajv.ErrorObject) => { return ajv.errorsText([e]); }); - return []; - } - - static parse(configuration: Configuration): Configuration { - const validationErrors = this.parseTypes(configuration); - if (validationErrors.length > 0) { - validationErrors.forEach((e: string) => { console.log(e); }); - throw new Error("Malformed configuration (schema). Please double-check your configuration file."); - } - - const [newConfiguration, completionErrors] = complete(configuration); - - if (completionErrors.length > 0) { - completionErrors.forEach((e: string) => { console.log(e); }); - throw new Error("Malformed configuration (validator). Please double-check your configuration file."); - } - return newConfiguration; - } -} - diff --git a/themes/matrix/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts b/themes/matrix/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts deleted file mode 100644 index d4a3093ee..000000000 --- a/themes/matrix/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { SessionConfigurationBuilder } from "./SessionConfigurationBuilder"; -import { Configuration } from "./schema/Configuration"; -import { GlobalDependencies } from "../../../types/Dependencies"; - -import ExpressSession = require("express-session"); -import ConnectRedis = require("connect-redis"); -import Sinon = require("sinon"); -import Assert = require("assert"); - -describe("configuration/SessionConfigurationBuilder", function () { - const configuration: Configuration = { - access_control: { - default_policy: "deny", - rules: [] - }, - totp: { - issuer: "authelia.com" - }, - authentication_backend: { - ldap: { - url: "ldap://ldap", - user: "user", - base_dn: "dc=example,dc=com", - password: "password", - additional_groups_dn: "ou=groups", - additional_users_dn: "ou=users", - group_name_attribute: "", - groups_filter: "", - mail_attribute: "", - users_filter: "" - }, - }, - logs_level: "debug", - notifier: { - filesystem: { - filename: "/test" - } - }, - port: 8080, - session: { - name: "authelia_session", - domain: "example.com", - expiration: 3600, - secret: "secret" - }, - regulation: { - max_retries: 3, - ban_time: 5 * 60, - find_time: 5 * 60 - }, - storage: { - local: { - in_memory: true - } - } - }; - - const deps: GlobalDependencies = { - ConnectRedis: Sinon.spy() as any, - ldapjs: Sinon.spy() as any, - nedb: Sinon.spy() as any, - session: Sinon.spy() as any, - speakeasy: Sinon.spy() as any, - u2f: Sinon.spy() as any, - winston: Sinon.spy() as any, - Redis: Sinon.spy() as any - }; - - it("should return session options without redis options", function () { - const options = SessionConfigurationBuilder.build(configuration, deps); - const expectedOptions = { - name: "authelia_session", - secret: "secret", - resave: false, - saveUninitialized: true, - cookie: { - secure: true, - httpOnly: true, - maxAge: 3600, - domain: "example.com" - } - }; - - Assert.deepEqual(expectedOptions, options); - }); - - it("should return session options with redis options", function () { - configuration.session["redis"] = { - host: "redis.example.com", - port: 6379 - }; - const RedisStoreMock = Sinon.spy(); - const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); - - deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; - deps.Redis = { - createClient: Sinon.mock().returns(redisClient) - } as any; - - const options = SessionConfigurationBuilder.build(configuration, deps); - - const expectedOptions: ExpressSession.SessionOptions = { - secret: "secret", - resave: false, - saveUninitialized: true, - name: "authelia_session", - cookie: { - secure: true, - httpOnly: true, - maxAge: 3600, - domain: "example.com" - }, - store: Sinon.match.object as any - }; - - Assert((deps.ConnectRedis as Sinon.SinonStub).calledWith(deps.session)); - Assert.equal(options.secret, expectedOptions.secret); - Assert.equal(options.resave, expectedOptions.resave); - Assert.equal(options.saveUninitialized, expectedOptions.saveUninitialized); - Assert.deepEqual(options.cookie, expectedOptions.cookie); - Assert(options.store != undefined); - }); - - it("should return session options with redis password", function () { - configuration.session["redis"] = { - host: "redis.example.com", - port: 6379, - password: "authelia_pass" - }; - const RedisStoreMock = Sinon.spy(); - const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); - const createClientStub = Sinon.stub(); - - deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; - deps.Redis = { - createClient: createClientStub - } as any; - - createClientStub.returns(redisClient); - - const options = SessionConfigurationBuilder.build(configuration, deps); - - Assert(createClientStub.calledWith({ - host: "redis.example.com", - port: 6379, - password: "authelia_pass" - })); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/SessionConfigurationBuilder.ts b/themes/matrix/server/src/lib/configuration/SessionConfigurationBuilder.ts deleted file mode 100644 index 6ce643d9d..000000000 --- a/themes/matrix/server/src/lib/configuration/SessionConfigurationBuilder.ts +++ /dev/null @@ -1,52 +0,0 @@ -import ExpressSession = require("express-session"); -import Redis = require("redis"); - -import { Configuration } from "./schema/Configuration"; -import { GlobalDependencies } from "../../../types/Dependencies"; -import { RedisStoreOptions } from "connect-redis"; - -export class SessionConfigurationBuilder { - - static build(configuration: Configuration, deps: GlobalDependencies): ExpressSession.SessionOptions { - const sessionOptions: ExpressSession.SessionOptions = { - name: configuration.session.name, - secret: configuration.session.secret, - resave: false, - saveUninitialized: true, - cookie: { - secure: true, - httpOnly: true, - maxAge: configuration.session.expiration, - domain: configuration.session.domain - }, - }; - - if (configuration.session.redis) { - let redisOptions; - const options: Redis.ClientOpts = { - host: configuration.session.redis.host, - port: configuration.session.redis.port - }; - - if (configuration.session.redis.password) { - options["password"] = configuration.session.redis.password; - } - const client = deps.Redis.createClient(options); - - client.on("error", function (err: Error) { - console.error("Redis error:", err); - }); - - redisOptions = { - client: client, - logErrors: true - }; - - if (redisOptions) { - const RedisStore = deps.ConnectRedis(deps.session); - sessionOptions.store = new RedisStore(redisOptions); - } - } - return sessionOptions; - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/AclConfiguration.spec.ts b/themes/matrix/server/src/lib/configuration/schema/AclConfiguration.spec.ts deleted file mode 100644 index d1e2a03a2..000000000 --- a/themes/matrix/server/src/lib/configuration/schema/AclConfiguration.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ACLConfiguration, complete } from "./AclConfiguration"; -import Assert = require("assert"); - -describe("configuration/schema/AclConfiguration", function() { - it("should complete ACLConfiguration", function() { - const configuration: ACLConfiguration = {}; - const [newConfiguration, errors] = complete(configuration); - - Assert.deepEqual(newConfiguration.default_policy, "bypass"); - Assert.deepEqual(newConfiguration.rules, []); - }); - - it("should return errors when subject is not good", function() { - const configuration: ACLConfiguration = { - default_policy: "deny", - rules: [{ - domain: "dev.example.com", - subject: "user:abc", - policy: "bypass" - }, { - domain: "dev.example.com", - subject: "user:def", - policy: "bypass" - }, { - domain: "dev.example.com", - subject: "badkey:abc", - policy: "bypass" - }] - }; - const [newConfiguration, errors] = complete(configuration); - - Assert.deepEqual(errors, ["Rule 2 has wrong subject. It should be starting with user: or group:."]); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/AclConfiguration.ts b/themes/matrix/server/src/lib/configuration/schema/AclConfiguration.ts deleted file mode 100644 index 40401dd64..000000000 --- a/themes/matrix/server/src/lib/configuration/schema/AclConfiguration.ts +++ /dev/null @@ -1,41 +0,0 @@ - -export type ACLPolicy = "deny" | "bypass" | "one_factor" | "two_factor"; - -export type ACLRule = { - domain: string; - resources?: string[]; - subject?: string; - policy: ACLPolicy; -}; - -export interface ACLConfiguration { - default_policy?: ACLPolicy; - rules?: ACLRule[]; -} - -export function complete(configuration: ACLConfiguration): [ACLConfiguration, string[]] { - const newConfiguration: ACLConfiguration = (configuration) - ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.default_policy) { - newConfiguration.default_policy = "bypass"; - } - - if (!newConfiguration.rules) { - newConfiguration.rules = []; - } - - if (newConfiguration.rules.length > 0) { - const errors: string[] = []; - newConfiguration.rules.forEach((r, idx) => { - if (r.subject && !r.subject.match(/^(user|group):[a-zA-Z0-9]+$/)) { - errors.push(`Rule ${idx} has wrong subject. It should be starting with user: or group:.`); - } - }); - if (errors.length > 0) { - return [newConfiguration, errors]; - } - } - - return [newConfiguration, []]; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts b/themes/matrix/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts deleted file mode 100644 index 3ca86381c..000000000 --- a/themes/matrix/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { AuthenticationBackendConfiguration, complete } from "./AuthenticationBackendConfiguration"; -import Assert = require("assert"); - -describe("configuration/schema/AuthenticationBackendConfiguration", function() { - it("should ensure there is at least one key", function() { - const configuration: AuthenticationBackendConfiguration = {} as any; - const [newConfiguration, error] = complete(configuration); - - Assert.equal(error, "Authentication backend must have one of the following keys:`ldap` or `file`"); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts b/themes/matrix/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts deleted file mode 100644 index 7f77f894f..000000000 --- a/themes/matrix/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { LdapConfiguration } from "./LdapConfiguration"; -import { FileUsersDatabaseConfiguration } from "./FileUsersDatabaseConfiguration"; - -export interface AuthenticationBackendConfiguration { - ldap?: LdapConfiguration; - file?: FileUsersDatabaseConfiguration; -} - -export function complete( - configuration: AuthenticationBackendConfiguration) - : [AuthenticationBackendConfiguration, string] { - - const newConfiguration: AuthenticationBackendConfiguration = (configuration) - ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (Object.keys(newConfiguration).length != 1) { - return [ - newConfiguration, - "Authentication backend must have one of the following keys:" + - "`ldap` or `file`" - ]; - } - - return [newConfiguration, undefined]; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/Configuration.ts b/themes/matrix/server/src/lib/configuration/schema/Configuration.ts deleted file mode 100644 index 8d16a5fb2..000000000 --- a/themes/matrix/server/src/lib/configuration/schema/Configuration.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ACLConfiguration, complete as AclConfigurationComplete } from "./AclConfiguration"; -import { AuthenticationBackendConfiguration, complete as AuthenticationBackendComplete } from "./AuthenticationBackendConfiguration"; -import { NotifierConfiguration, complete as NotifierConfigurationComplete } from "./NotifierConfiguration"; -import { RegulationConfiguration, complete as RegulationConfigurationComplete } from "./RegulationConfiguration"; -import { SessionConfiguration, complete as SessionConfigurationComplete } from "./SessionConfiguration"; -import { StorageConfiguration, complete as StorageConfigurationComplete } from "./StorageConfiguration"; -import { TotpConfiguration, complete as TotpConfigurationComplete } from "./TotpConfiguration"; - -export interface Configuration { - access_control?: ACLConfiguration; - authentication_backend: AuthenticationBackendConfiguration; - default_redirection_url?: string; - logs_level?: string; - notifier?: NotifierConfiguration; - port?: number; - regulation?: RegulationConfiguration; - session?: SessionConfiguration; - storage?: StorageConfiguration; - totp?: TotpConfiguration; -} - -export function complete( - configuration: Configuration): - [Configuration, string[]] { - - const newConfiguration: Configuration = JSON.parse( - JSON.stringify(configuration)); - const errors: string[] = []; - - const [acls, aclsErrors] = AclConfigurationComplete( - newConfiguration.access_control); - - newConfiguration.access_control = acls; - if (aclsErrors.length > 0) { - errors.concat(aclsErrors); - } - - const [backend, error] = - AuthenticationBackendComplete( - newConfiguration.authentication_backend); - - if (error) errors.push(error); - newConfiguration.authentication_backend = backend; - - if (!newConfiguration.logs_level) { - newConfiguration.logs_level = "info"; - } - - const [notifier, notifierError] = NotifierConfigurationComplete( - newConfiguration.notifier); - newConfiguration.notifier = notifier; - if (notifierError) errors.push(notifierError); - - if (!newConfiguration.port) { - newConfiguration.port = 8080; - } - - newConfiguration.regulation = RegulationConfigurationComplete( - newConfiguration.regulation); - newConfiguration.session = SessionConfigurationComplete( - newConfiguration.session); - newConfiguration.storage = StorageConfigurationComplete( - newConfiguration.storage); - newConfiguration.totp = TotpConfigurationComplete( - newConfiguration.totp); - - return [newConfiguration, errors]; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts b/themes/matrix/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts deleted file mode 100644 index d19002ba9..000000000 --- a/themes/matrix/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts +++ /dev/null @@ -1,4 +0,0 @@ - -export interface FileUsersDatabaseConfiguration { - path: string; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/LdapConfiguration.spec.ts b/themes/matrix/server/src/lib/configuration/schema/LdapConfiguration.spec.ts deleted file mode 100644 index cc73d1085..000000000 --- a/themes/matrix/server/src/lib/configuration/schema/LdapConfiguration.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Assert = require("assert"); -import { LdapConfiguration, complete } from "./LdapConfiguration"; - -describe("configuration/schema/AuthenticationMethodsConfiguration", function() { - it("should ensure at least one key is provided", function() { - const configuration: LdapConfiguration = { - url: "ldap.example.com", - base_dn: "dc=example,dc=com", - user: "admin", - password: "password" - }; - const newConfiguration = complete(configuration); - - Assert.deepEqual(newConfiguration, { - url: "ldap.example.com", - base_dn: "dc=example,dc=com", - user: "admin", - password: "password", - users_filter: "cn={0}", - group_name_attribute: "cn", - groups_filter: "member={dn}", - mail_attribute: "mail" - }); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/LdapConfiguration.ts b/themes/matrix/server/src/lib/configuration/schema/LdapConfiguration.ts deleted file mode 100644 index 5dacb9390..000000000 --- a/themes/matrix/server/src/lib/configuration/schema/LdapConfiguration.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Util = require("util"); - -export interface LdapConfiguration { - url: string; - base_dn: string; - - additional_users_dn?: string; - users_filter?: string; - - additional_groups_dn?: string; - groups_filter?: string; - - group_name_attribute?: string; - mail_attribute?: string; - - user: string; // admin username - password: string; // admin password -} - -export function complete(configuration: LdapConfiguration): LdapConfiguration { - const newConfiguration: LdapConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.users_filter) { - newConfiguration.users_filter = "cn={0}"; - } - - if (!newConfiguration.groups_filter) { - newConfiguration.groups_filter = "member={dn}"; - } - - if (!newConfiguration.group_name_attribute) { - newConfiguration.group_name_attribute = "cn"; - } - - if (!newConfiguration.mail_attribute) { - newConfiguration.mail_attribute = "mail"; - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts b/themes/matrix/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts deleted file mode 100644 index 6c576e8e0..000000000 --- a/themes/matrix/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Assert = require("assert"); -import { NotifierConfiguration, complete } from "./NotifierConfiguration"; - -describe("configuration/schema/NotifierConfiguration", function() { - it("should use a default notifier when none is provided", function() { - const configuration: NotifierConfiguration = {}; - const [newConfiguration, error] = complete(configuration); - - Assert.deepEqual(newConfiguration.filesystem, {filename: "/tmp/authelia/notification.txt"}); - }); - - it("should ensure correct key is provided", function() { - const configuration = { - abc: "badvalue" - }; - const [newConfiguration, error] = complete(configuration as any); - - Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"); - }); - - it("should ensure there is no more than one key", function() { - const configuration: NotifierConfiguration = { - smtp: { - host: "smtp.example.com", - port: 25, - secure: false, - sender: "test@example.com" - }, - email: { - username: "test", - password: "test", - sender: "test@example.com", - service: "gmail" - } - }; - const [newConfiguration, error] = complete(configuration); - - Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/NotifierConfiguration.ts b/themes/matrix/server/src/lib/configuration/schema/NotifierConfiguration.ts deleted file mode 100644 index 7bcce15c2..000000000 --- a/themes/matrix/server/src/lib/configuration/schema/NotifierConfiguration.ts +++ /dev/null @@ -1,45 +0,0 @@ - -export interface EmailNotifierConfiguration { - username: string; - password: string; - sender: string; - service: string; -} - -export interface SmtpNotifierConfiguration { - username?: string; - password?: string; - host: string; - port: number; - secure: boolean; - sender: string; -} - -export interface FileSystemNotifierConfiguration { - filename: string; -} - -export interface NotifierConfiguration { - email?: EmailNotifierConfiguration; - smtp?: SmtpNotifierConfiguration; - filesystem?: FileSystemNotifierConfiguration; -} - -export function complete(configuration: NotifierConfiguration): [NotifierConfiguration, string] { - const newConfiguration: NotifierConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (Object.keys(newConfiguration).length == 0) - newConfiguration.filesystem = { filename: "/tmp/authelia/notification.txt" }; - - const ERROR = "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"; - - if (Object.keys(newConfiguration).length != 1) - return [newConfiguration, ERROR]; - - const key = Object.keys(newConfiguration)[0]; - - if (key != "filesystem" && key != "smtp" && key != "email") - return [newConfiguration, ERROR]; - - return [newConfiguration, undefined]; -} diff --git a/themes/matrix/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts b/themes/matrix/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts deleted file mode 100644 index dce2caf4e..000000000 --- a/themes/matrix/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Assert = require("assert"); -import { RegulationConfiguration, complete } from "./RegulationConfiguration"; - -describe("configuration/schema/RegulationConfiguration", function() { - it("should return default regulation configuration", function() { - const configuration: RegulationConfiguration = {}; - const newConfiguration = complete(configuration); - - Assert.equal(newConfiguration.ban_time, 300); - Assert.equal(newConfiguration.find_time, 120); - Assert.equal(newConfiguration.max_retries, 3); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/RegulationConfiguration.ts b/themes/matrix/server/src/lib/configuration/schema/RegulationConfiguration.ts deleted file mode 100644 index 117463f43..000000000 --- a/themes/matrix/server/src/lib/configuration/schema/RegulationConfiguration.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface RegulationConfiguration { - max_retries?: number; - find_time?: number; - ban_time?: number; -} - -export function complete(configuration: RegulationConfiguration): RegulationConfiguration { - const newConfiguration: RegulationConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.max_retries) { - newConfiguration.max_retries = 3; - } - - if (!newConfiguration.find_time) { - newConfiguration.find_time = 120; // seconds - } - - if (!newConfiguration.ban_time) { - newConfiguration.ban_time = 300; // seconds - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/SessionConfiguration.spec.ts b/themes/matrix/server/src/lib/configuration/schema/SessionConfiguration.spec.ts deleted file mode 100644 index e54010837..000000000 --- a/themes/matrix/server/src/lib/configuration/schema/SessionConfiguration.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Assert = require("assert"); -import { SessionConfiguration, complete } from "./SessionConfiguration"; - -describe("configuration/schema/SessionConfiguration", function() { - it("should return default regulation configuration", function() { - const configuration: SessionConfiguration = { - domain: "example.com", - secret: "unsecure_secret" - }; - const newConfiguration = complete(configuration); - - Assert.equal(newConfiguration.name, 'authelia_session'); - Assert.equal(newConfiguration.expiration, 3600000); - Assert.equal(newConfiguration.inactivity, undefined); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/SessionConfiguration.ts b/themes/matrix/server/src/lib/configuration/schema/SessionConfiguration.ts deleted file mode 100644 index 2c88bb215..000000000 --- a/themes/matrix/server/src/lib/configuration/schema/SessionConfiguration.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface SessionRedisOptions { - host: string; - port: number; - password?: string; -} - -export interface SessionConfiguration { - name?: string; - domain: string; - secret: string; - expiration?: number; - inactivity?: number; - redis?: SessionRedisOptions; -} - -export function complete(configuration: SessionConfiguration): SessionConfiguration { - const newConfiguration: SessionConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.name) { - newConfiguration.name = "authelia_session"; - } - - if (!newConfiguration.expiration) { - newConfiguration.expiration = 3600000; // 1 hour - } - - if (!newConfiguration.inactivity) { - newConfiguration.inactivity = undefined; // disabled - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/StorageConfiguration.spec.ts b/themes/matrix/server/src/lib/configuration/schema/StorageConfiguration.spec.ts deleted file mode 100644 index 9d02a11b6..000000000 --- a/themes/matrix/server/src/lib/configuration/schema/StorageConfiguration.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Assert = require("assert"); -import { StorageConfiguration, complete } from "./StorageConfiguration"; - -describe("configuration/schema/StorageConfiguration", function() { - it("should return default regulation configuration", function() { - const configuration: StorageConfiguration = {}; - const newConfiguration = complete(configuration); - - Assert.deepEqual(newConfiguration, { - local: { - in_memory: true - } - }); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/StorageConfiguration.ts b/themes/matrix/server/src/lib/configuration/schema/StorageConfiguration.ts deleted file mode 100644 index 47e356ef4..000000000 --- a/themes/matrix/server/src/lib/configuration/schema/StorageConfiguration.ts +++ /dev/null @@ -1,30 +0,0 @@ -export interface MongoStorageConfiguration { - url: string; - database: string; - auth?: { - username: string; - password: string; - }; -} - -export interface LocalStorageConfiguration { - path?: string; - in_memory?: boolean; -} - -export interface StorageConfiguration { - local?: LocalStorageConfiguration; - mongo?: MongoStorageConfiguration; -} - -export function complete(configuration: StorageConfiguration): StorageConfiguration { - const newConfiguration: StorageConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.local && !newConfiguration.mongo) { - newConfiguration.local = { - in_memory: true - }; - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/TotpConfiguration.ts b/themes/matrix/server/src/lib/configuration/schema/TotpConfiguration.ts deleted file mode 100644 index 683135639..000000000 --- a/themes/matrix/server/src/lib/configuration/schema/TotpConfiguration.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface TotpConfiguration { - issuer: string; -} - -export function complete(configuration: TotpConfiguration): TotpConfiguration { - const newConfiguration: TotpConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.issuer) { - newConfiguration.issuer = "authelia.com"; - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts b/themes/matrix/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts deleted file mode 100644 index 8008b4833..000000000 --- a/themes/matrix/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts +++ /dev/null @@ -1,9 +0,0 @@ - -export interface UserInfo { - username: string; - password_hash: string; - email: string; - groups?: string[]; -} - -export type UserDatabaseConfiguration = UserInfo[]; \ No newline at end of file diff --git a/themes/matrix/server/src/lib/connectors/mongo/IMongoClient.d.ts b/themes/matrix/server/src/lib/connectors/mongo/IMongoClient.d.ts deleted file mode 100644 index 36cb4b8bf..000000000 --- a/themes/matrix/server/src/lib/connectors/mongo/IMongoClient.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import MongoDB = require("mongodb"); -import Bluebird = require("bluebird"); - -export interface IMongoClient { - collection(name: string): Bluebird -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/connectors/mongo/MongoClient.spec.ts b/themes/matrix/server/src/lib/connectors/mongo/MongoClient.spec.ts deleted file mode 100644 index ca0c68593..000000000 --- a/themes/matrix/server/src/lib/connectors/mongo/MongoClient.spec.ts +++ /dev/null @@ -1,119 +0,0 @@ -import Assert = require("assert"); -import Bluebird = require("bluebird"); -import MongoDB = require("mongodb"); -import Sinon = require("sinon"); - -import { MongoClient } from "./MongoClient"; -import { GlobalLoggerStub } from "../../logging/GlobalLoggerStub.spec"; -import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration"; - -describe("connectors/mongo/MongoClient", function () { - let MongoClientStub: any; - let mongoClientStub: any; - let mongoDatabaseStub: any; - let logger: GlobalLoggerStub = new GlobalLoggerStub(); - - const configuration: MongoStorageConfiguration = { - url: "mongo://url", - database: "databasename" - }; - - describe("connection", () => { - before(() => { - mongoClientStub = { - db: Sinon.stub() - }; - mongoDatabaseStub = { - on: Sinon.stub(), - collection: Sinon.stub() - } - MongoClientStub = Sinon.stub( - MongoDB.MongoClient, "connect"); - MongoClientStub.yields( - undefined, mongoClientStub); - mongoClientStub.db.returns( - mongoDatabaseStub); - }); - - after(() => { - MongoClientStub.restore(); - }); - - it("should use credentials from configuration", () => { - configuration.auth = { - username: "authelia", - password: "authelia_pass" - }; - - const client = new MongoClient(configuration, logger); - return client.collection("test") - .then(() => { - Assert(MongoClientStub.calledWith("mongo://url", { - auth: { - user: "authelia", - password: "authelia_pass" - } - })) - }); - }); - }); - - describe("collection", () => { - before(function() { - mongoClientStub = { - db: Sinon.stub() - }; - mongoDatabaseStub = { - on: Sinon.stub(), - collection: Sinon.stub() - } - }); - - describe("Connection to mongo is ok", function() { - before(function () { - MongoClientStub = Sinon.stub( - MongoDB.MongoClient, "connect"); - MongoClientStub.yields( - undefined, mongoClientStub); - mongoClientStub.db.returns( - mongoDatabaseStub); - }); - - after(function () { - MongoClientStub.restore(); - }); - - it("should create a collection", function () { - const COLLECTION_NAME = "mycollection"; - const client = new MongoClient(configuration, logger); - - mongoDatabaseStub.collection.returns("COL"); - return client.collection(COLLECTION_NAME) - .then((collection) => mongoDatabaseStub.collection.calledWith(COLLECTION_NAME)); - }); - }); - - describe("Connection to mongo is broken", function() { - before(function () { - MongoClientStub = Sinon.stub( - MongoDB.MongoClient, "connect"); - MongoClientStub.yields( - new Error("Failed connection"), undefined); - }); - - after(function () { - MongoClientStub.restore(); - }); - - it("should fail creating the collection", function() { - const COLLECTION_NAME = "mycollection"; - const client = new MongoClient(configuration, logger); - - mongoDatabaseStub.collection.returns("COL"); - return client.collection(COLLECTION_NAME) - .then((collection) => Bluebird.reject(new Error("should not be here."))) - .catch((err) => Bluebird.resolve()); - }); - }) - }); -}); diff --git a/themes/matrix/server/src/lib/connectors/mongo/MongoClient.ts b/themes/matrix/server/src/lib/connectors/mongo/MongoClient.ts deleted file mode 100644 index d15731e97..000000000 --- a/themes/matrix/server/src/lib/connectors/mongo/MongoClient.ts +++ /dev/null @@ -1,76 +0,0 @@ - -import MongoDB = require("mongodb"); -import { IMongoClient } from "./IMongoClient"; -import Bluebird = require("bluebird"); -import { AUTHENTICATION_FAILED } from "../../../../../shared/UserMessages"; -import { IGlobalLogger } from "../../logging/IGlobalLogger"; -import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration"; - -export class MongoClient implements IMongoClient { - private configuration: MongoStorageConfiguration; - - private database: MongoDB.Db; - private client: MongoDB.MongoClient; - private logger: IGlobalLogger; - - constructor( - configuration: MongoStorageConfiguration, - logger: IGlobalLogger) { - - this.configuration = configuration; - this.logger = logger; - } - - connect(): Bluebird { - const that = this; - const options: MongoDB.MongoClientOptions = {}; - if (that.configuration.auth) { - options["auth"] = { - user: that.configuration.auth.username, - password: that.configuration.auth.password - }; - } - - return new Bluebird((resolve, reject) => { - MongoDB.MongoClient.connect( - this.configuration.url, - options, - function(err, client) { - if (err) { - reject(err); - return; - } - resolve(client); - }); - }) - .then(function (client: MongoDB.MongoClient) { - that.database = client.db(that.configuration.database); - that.database.on("close", () => { - that.logger.info("[MongoClient] Lost connection."); - }); - that.database.on("reconnect", () => { - that.logger.info("[MongoClient] Reconnected."); - }); - that.client = client; - }); - } - - close(): Bluebird { - if (this.client) { - this.client.close(); - this.database = undefined; - this.client = undefined; - } - return Bluebird.resolve(); - } - - collection(name: string): Bluebird { - if (!this.client) { - const that = this; - return this.connect() - .then(() => Bluebird.resolve(that.database.collection(name))); - } - - return Bluebird.resolve(this.database.collection(name)); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/connectors/mongo/MongoClientStub.spec.ts b/themes/matrix/server/src/lib/connectors/mongo/MongoClientStub.spec.ts deleted file mode 100644 index 1cfd48e32..000000000 --- a/themes/matrix/server/src/lib/connectors/mongo/MongoClientStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Sinon = require("sinon"); -import MongoDB = require("mongodb"); -import Bluebird = require("bluebird"); -import { IMongoClient } from "../../../../src/lib/connectors/mongo/IMongoClient"; - -export class MongoClientStub implements IMongoClient { - public collectionStub: Sinon.SinonStub; - - constructor() { - this.collectionStub = Sinon.stub(); - } - - collection(name: string): Bluebird { - return this.collectionStub(name); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/logging/GlobalLogger.ts b/themes/matrix/server/src/lib/logging/GlobalLogger.ts deleted file mode 100644 index 4da7acf49..000000000 --- a/themes/matrix/server/src/lib/logging/GlobalLogger.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { IGlobalLogger } from "./IGlobalLogger"; -import Util = require("util"); -import Express = require("express"); -import Winston = require("winston"); - -declare module "express" { - interface Request { - id: string; - } -} - -export class GlobalLogger implements IGlobalLogger { - private winston: typeof Winston; - constructor(winston: typeof Winston) { - this.winston = winston; - } - - private buildMessage(message: string, ...args: any[]): string { - return Util.format("date='%s' message='%s'", new Date(), - Util.format(message, ...args)); - } - - info(message: string, ...args: any[]): void { - this.winston.info(this.buildMessage(message, ...args)); - } - - debug(message: string, ...args: any[]): void { - this.winston.debug(this.buildMessage(message, ...args)); - } - - error(message: string, ...args: any[]): void { - this.winston.debug(this.buildMessage(message, ...args)); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/logging/GlobalLoggerStub.spec.ts b/themes/matrix/server/src/lib/logging/GlobalLoggerStub.spec.ts deleted file mode 100644 index d4bb13710..000000000 --- a/themes/matrix/server/src/lib/logging/GlobalLoggerStub.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Sinon = require("sinon"); -import { GlobalLogger } from "./GlobalLogger"; -import Winston = require("winston"); -import Express = require("express"); -import { IGlobalLogger } from "./IGlobalLogger"; - -export class GlobalLoggerStub implements IGlobalLogger { - infoStub: Sinon.SinonStub; - debugStub: Sinon.SinonStub; - errorStub: Sinon.SinonStub; - private globalLogger: IGlobalLogger; - - constructor(enableLogging?: boolean) { - this.infoStub = Sinon.stub(); - this.debugStub = Sinon.stub(); - this.errorStub = Sinon.stub(); - if (enableLogging) - this.globalLogger = new GlobalLogger(Winston); - } - - info(message: string, ...args: any[]): void { - if (this.globalLogger) - this.globalLogger.info(message, ...args); - this.infoStub(message, ...args); - } - - debug(message: string, ...args: any[]): void { - if (this.globalLogger) - this.globalLogger.info(message, ...args); - this.debugStub(message, ...args); - } - - error(message: string, ...args: any[]): void { - if (this.globalLogger) - this.globalLogger.info(message, ...args); - this.errorStub(message, ...args); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/logging/IGlobalLogger.ts b/themes/matrix/server/src/lib/logging/IGlobalLogger.ts deleted file mode 100644 index 548515ec7..000000000 --- a/themes/matrix/server/src/lib/logging/IGlobalLogger.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface IGlobalLogger { - info(message: string, ...args: any[]): void; - debug(message: string, ...args: any[]): void; - error(message: string, ...args: any[]): void; -} diff --git a/themes/matrix/server/src/lib/logging/IRequestLogger.ts b/themes/matrix/server/src/lib/logging/IRequestLogger.ts deleted file mode 100644 index 126a601fe..000000000 --- a/themes/matrix/server/src/lib/logging/IRequestLogger.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Express = require("express"); - -export interface IRequestLogger { - info(req: Express.Request, message: string, ...args: any[]): void; - debug(req: Express.Request, message: string, ...args: any[]): void; - error(req: Express.Request, message: string, ...args: any[]): void; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/logging/RequestLogger.ts b/themes/matrix/server/src/lib/logging/RequestLogger.ts deleted file mode 100644 index c45c66018..000000000 --- a/themes/matrix/server/src/lib/logging/RequestLogger.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { IRequestLogger } from "./IRequestLogger"; -import Util = require("util"); -import Express = require("express"); -import Winston = require("winston"); - -declare module "express" { - interface Request { - id: string; - } -} - -export class RequestLogger implements IRequestLogger { - private winston: typeof Winston; - - constructor(winston: typeof Winston) { - this.winston = winston; - } - - private formatHeader(req: Express.Request) { - const clientIP = req.ip; // The IP of the original client going through the proxy chain. - return Util.format("date='%s' method='%s', path='%s' requestId='%s' sessionId='%s' ip='%s'", - new Date(), req.method, req.path, req.id, req.sessionID, clientIP); - } - - private formatBody(message: string) { - return Util.format("message='%s'", message); - } - - private formatMessage(req: Express.Request, message: string) { - return Util.format("%s %s", this.formatHeader(req), - this.formatBody(message)); - } - - info(req: Express.Request, message: string, ...args: any[]): void { - this.winston.info(this.formatMessage(req, message), ...args); - } - - debug(req: Express.Request, message: string, ...args: any[]): void { - this.winston.debug(this.formatMessage(req, message), ...args); - } - - error(req: Express.Request, message: string, ...args: any[]): void { - this.winston.error(this.formatMessage(req, message), ...args); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/logging/RequestLoggerStub.spec.ts b/themes/matrix/server/src/lib/logging/RequestLoggerStub.spec.ts deleted file mode 100644 index b0e375210..000000000 --- a/themes/matrix/server/src/lib/logging/RequestLoggerStub.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { IRequestLogger } from "./IRequestLogger"; -import Sinon = require("sinon"); -import { RequestLogger } from "./RequestLogger"; -import Winston = require("winston"); -import Express = require("express"); - -export class RequestLoggerStub implements IRequestLogger { - infoStub: Sinon.SinonStub; - debugStub: Sinon.SinonStub; - errorStub: Sinon.SinonStub; - private requestLogger: RequestLogger; - - constructor(enableLogging?: boolean) { - this.infoStub = Sinon.stub(); - this.debugStub = Sinon.stub(); - this.errorStub = Sinon.stub(); - if (enableLogging) - this.requestLogger = new RequestLogger(Winston); - } - - info(req: Express.Request, message: string, ...args: any[]): void { - if (this.requestLogger) - this.requestLogger.info(req, message, ...args); - this.infoStub(req, message, ...args); - } - - debug(req: Express.Request, message: string, ...args: any[]): void { - if (this.requestLogger) - this.requestLogger.info(req, message, ...args); - this.debugStub(req, message, ...args); - } - - error(req: Express.Request, message: string, ...args: any[]): void { - if (this.requestLogger) - this.requestLogger.info(req, message, ...args); - this.errorStub(req, message, ...args); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/AbstractEmailNotifier.ts b/themes/matrix/server/src/lib/notifiers/AbstractEmailNotifier.ts deleted file mode 100644 index 198e4e5db..000000000 --- a/themes/matrix/server/src/lib/notifiers/AbstractEmailNotifier.ts +++ /dev/null @@ -1,23 +0,0 @@ - -import { INotifier } from "../notifiers/INotifier"; -import { Identity } from "../../../types/Identity"; - -import Fs = require("fs"); -import Path = require("path"); -import Ejs = require("ejs"); -import BluebirdPromise = require("bluebird"); - -const email_template = Fs.readFileSync(Path.join(__dirname, "../../resources/email-template.ejs"), "UTF-8"); - -export abstract class AbstractEmailNotifier implements INotifier { - notify(to: string, subject: string, link: string): BluebirdPromise { - const d = { - url: link, - button_title: "Continue", - title: subject - }; - return this.sendEmail(to, subject, Ejs.render(email_template, d)); - } - - abstract sendEmail(to: string, subject: string, content: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/EmailNotifier.spec.ts b/themes/matrix/server/src/lib/notifiers/EmailNotifier.spec.ts deleted file mode 100644 index 8211bbc02..000000000 --- a/themes/matrix/server/src/lib/notifiers/EmailNotifier.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as sinon from "sinon"; -import * as Assert from "assert"; -import BluebirdPromise = require("bluebird"); - -import { MailSenderStub } from "./MailSenderStub.spec"; -import EmailNotifier = require("./EmailNotifier"); - - -describe("notifiers/EmailNotifier", function () { - it("should send an email to given user", function () { - const mailSender = new MailSenderStub(); - const options = { - username: "user_gmail", - password: "pass_gmail", - sender: "admin@example.com", - service: "gmail" - }; - - mailSender.sendStub.returns(BluebirdPromise.resolve()); - const sender = new EmailNotifier.EmailNotifier(options, mailSender); - const subject = "subject"; - const url = "http://test.com"; - - return sender.notify("user@example.com", subject, url) - .then(function () { - Assert.equal(mailSender.sendStub.getCall(0).args[0].to, "user@example.com"); - Assert.equal(mailSender.sendStub.getCall(0).args[0].subject, "subject"); - return BluebirdPromise.resolve(); - }); - }); - - it("should fail while sending an email", function () { - const mailSender = new MailSenderStub(); - const options = { - username: "user_gmail", - password: "pass_gmail", - sender: "admin@example.com", - service: "gmail" - }; - - mailSender.sendStub.returns(BluebirdPromise.reject(new Error("Failed to send mail"))); - const sender = new EmailNotifier.EmailNotifier(options, mailSender); - const subject = "subject"; - const url = "http://test.com"; - - return sender.notify("user@example.com", subject, url) - .then(function () { - return BluebirdPromise.reject(new Error()); - }, function() { - Assert.equal(mailSender.sendStub.getCall(0).args[0].from, "admin@example.com"); - return BluebirdPromise.resolve(); - }); - }); -}); diff --git a/themes/matrix/server/src/lib/notifiers/EmailNotifier.ts b/themes/matrix/server/src/lib/notifiers/EmailNotifier.ts deleted file mode 100644 index 4df7c861e..000000000 --- a/themes/matrix/server/src/lib/notifiers/EmailNotifier.ts +++ /dev/null @@ -1,27 +0,0 @@ - -import * as BluebirdPromise from "bluebird"; - -import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; -import { EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; -import { IMailSender } from "./IMailSender"; - -export class EmailNotifier extends AbstractEmailNotifier { - private mailSender: IMailSender; - private sender: string; - - constructor(options: EmailNotifierConfiguration, mailSender: IMailSender) { - super(); - this.mailSender = mailSender; - this.sender = options.sender; - } - - sendEmail(to: string, subject: string, content: string) { - const mailOptions = { - from: this.sender, - to: to, - subject: subject, - html: content - }; - return this.mailSender.send(mailOptions); - } -} diff --git a/themes/matrix/server/src/lib/notifiers/FileSystemNotifier.ts b/themes/matrix/server/src/lib/notifiers/FileSystemNotifier.ts deleted file mode 100644 index 23f6242c4..000000000 --- a/themes/matrix/server/src/lib/notifiers/FileSystemNotifier.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as BluebirdPromise from "bluebird"; -import * as util from "util"; -import * as Fs from "fs"; -import { INotifier } from "./INotifier"; -import { Identity } from "../../../types/Identity"; - -import { FileSystemNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; - -export class FileSystemNotifier implements INotifier { - private filename: string; - - constructor(options: FileSystemNotifierConfiguration) { - this.filename = options.filename; - } - - notify(to: string, subject: string, link: string): BluebirdPromise { - const content = util.format("Date: %s\nEmail: %s\nSubject: %s\nLink: %s", - new Date().toString(), to, subject, link); - const writeFilePromised: any = BluebirdPromise.promisify(Fs.writeFile); - return writeFilePromised(this.filename, content); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/IMailSender.ts b/themes/matrix/server/src/lib/notifiers/IMailSender.ts deleted file mode 100644 index 34ac464a8..000000000 --- a/themes/matrix/server/src/lib/notifiers/IMailSender.ts +++ /dev/null @@ -1,6 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Nodemailer = require("nodemailer"); - -export interface IMailSender { - send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/IMailSenderBuilder.ts b/themes/matrix/server/src/lib/notifiers/IMailSenderBuilder.ts deleted file mode 100644 index 36d4dcdf7..000000000 --- a/themes/matrix/server/src/lib/notifiers/IMailSenderBuilder.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IMailSender } from "./IMailSender"; -import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; - -export interface IMailSenderBuilder { - buildEmail(options: EmailNotifierConfiguration): IMailSender; - buildSmtp(options: SmtpNotifierConfiguration): IMailSender; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/INotifier.ts b/themes/matrix/server/src/lib/notifiers/INotifier.ts deleted file mode 100644 index b9a6b138c..000000000 --- a/themes/matrix/server/src/lib/notifiers/INotifier.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as BluebirdPromise from "bluebird"; - -export interface INotifier { - notify(to: string, subject: string, link: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/MailSender.ts b/themes/matrix/server/src/lib/notifiers/MailSender.ts deleted file mode 100644 index 536a88e63..000000000 --- a/themes/matrix/server/src/lib/notifiers/MailSender.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { IMailSender } from "./IMailSender"; -import Nodemailer = require("nodemailer"); -import NodemailerDirectTransport = require("nodemailer-direct-transport"); -import NodemailerSmtpTransport = require("nodemailer-smtp-transport"); -import BluebirdPromise = require("bluebird"); - -export class MailSender implements IMailSender { - private transporter: Nodemailer.Transporter; - - constructor(options: NodemailerDirectTransport.DirectOptions | - NodemailerSmtpTransport.SmtpOptions, nodemailer: typeof Nodemailer) { - this.transporter = nodemailer.createTransport(options); - } - - verify(): BluebirdPromise { - const that = this; - return new BluebirdPromise(function (resolve, reject) { - that.transporter.verify(function (error: Error, success: any) { - if (error) { - reject(new Error("Unable to connect to SMTP server. \ - Please check the service is running and your credentials are correct.")); - return; - } - resolve(); - }); - }); - } - - send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise { - const that = this; - return new BluebirdPromise(function (resolve, reject) { - that.transporter.sendMail(mailOptions, (error: Error, - data: Nodemailer.SentMessageInfo) => { - if (error) { - reject(new Error("Error while sending email: " + error.message)); - return; - } - resolve(); - }); - }); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/MailSenderBuilder.spec.ts b/themes/matrix/server/src/lib/notifiers/MailSenderBuilder.spec.ts deleted file mode 100644 index 41e0db426..000000000 --- a/themes/matrix/server/src/lib/notifiers/MailSenderBuilder.spec.ts +++ /dev/null @@ -1,67 +0,0 @@ - -import { MailSenderBuilder } from ".//MailSenderBuilder"; -import Nodemailer = require("nodemailer"); -import Sinon = require("sinon"); -import Assert = require("assert"); - -describe("notifiers/MailSenderBuilder", function() { - let createTransportStub: Sinon.SinonStub; - beforeEach(function() { - createTransportStub = Sinon.stub(Nodemailer, "createTransport"); - }); - - afterEach(function() { - createTransportStub.restore(); - }); - - it("should create a email mail sender", function() { - const mailSenderBuilder = new MailSenderBuilder(Nodemailer); - mailSenderBuilder.buildEmail({ - username: "user_gmail", - password: "pass_gmail", - sender: "admin@example.com", - service: "gmail" - }); - Assert.equal(createTransportStub.getCall(0).args[0].auth.user, "user_gmail"); - Assert.equal(createTransportStub.getCall(0).args[0].auth.pass, "pass_gmail"); - Assert.equal(createTransportStub.getCall(0).args[0].service, "gmail"); - }); - - describe("build smtp mail sender", function() { - it("should create a smtp mail sender with authenticated user", function() { - const mailSenderBuilder = new MailSenderBuilder(Nodemailer); - mailSenderBuilder.buildSmtp({ - host: "mail.example.com", - password: "password", - port: 25, - secure: true, - username: "user", - sender: "admin@example.com" - }); - Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { - host: "mail.example.com", - auth: { - pass: "password", - user: "user" - }, - port: 25, - secure: true, - }); - }); - - it("should create a smtp mail sender with anonymous user", function() { - const mailSenderBuilder = new MailSenderBuilder(Nodemailer); - mailSenderBuilder.buildSmtp({ - host: "mail.example.com", - port: 25, - secure: true, - sender: "admin@example.com" - }); - Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { - host: "mail.example.com", - port: 25, - secure: true, - }); - }); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/MailSenderBuilder.ts b/themes/matrix/server/src/lib/notifiers/MailSenderBuilder.ts deleted file mode 100644 index 1d06be52f..000000000 --- a/themes/matrix/server/src/lib/notifiers/MailSenderBuilder.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { IMailSender } from "./IMailSender"; -import { IMailSenderBuilder } from "./IMailSenderBuilder"; -import { MailSender } from "./MailSender"; -import Nodemailer = require("nodemailer"); -import NodemailerSmtpTransport = require("nodemailer-smtp-transport"); -import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; - -export class MailSenderBuilder implements IMailSenderBuilder { - private nodemailer: typeof Nodemailer; - - constructor(nodemailer: typeof Nodemailer) { - this.nodemailer = nodemailer; - } - - buildEmail(options: EmailNotifierConfiguration): IMailSender { - const emailOptions = { - service: options.service, - auth: { - user: options.username, - pass: options.password - } - }; - return new MailSender(emailOptions, this.nodemailer); - } - - buildSmtp(options: SmtpNotifierConfiguration): IMailSender { - const smtpOptions: NodemailerSmtpTransport.SmtpOptions = { - host: options.host, - port: options.port, - secure: options.secure, // upgrade later with STARTTLS - }; - - if (options.username && options.password) { - smtpOptions.auth = { - user: options.username, - pass: options.password - }; - } - - return new MailSender(smtpOptions, this.nodemailer); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts b/themes/matrix/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts deleted file mode 100644 index 5b76f6e56..000000000 --- a/themes/matrix/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { IMailSenderBuilder } from "../../../src/lib/notifiers/IMailSenderBuilder"; -import BluebirdPromise = require("bluebird"); -import Nodemailer = require("nodemailer"); -import Sinon = require("sinon"); -import { IMailSender } from "../../../src/lib/notifiers/IMailSender"; -import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../../../src/lib/configuration/schema/NotifierConfiguration"; - -export class MailSenderBuilderStub implements IMailSenderBuilder { - buildEmailStub: Sinon.SinonStub; - buildSmtpStub: Sinon.SinonStub; - - constructor() { - this.buildEmailStub = Sinon.stub(); - this.buildSmtpStub = Sinon.stub(); - } - - buildEmail(options: EmailNotifierConfiguration): IMailSender { - return this.buildEmailStub(options); - } - - buildSmtp(options: SmtpNotifierConfiguration): IMailSender { - return this.buildSmtpStub(options); - } - -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/MailSenderStub.spec.ts b/themes/matrix/server/src/lib/notifiers/MailSenderStub.spec.ts deleted file mode 100644 index d57c458fc..000000000 --- a/themes/matrix/server/src/lib/notifiers/MailSenderStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { IMailSender } from "../../../src/lib/notifiers/IMailSender"; -import BluebirdPromise = require("bluebird"); -import Nodemailer = require("nodemailer"); -import Sinon = require("sinon"); - -export class MailSenderStub implements IMailSender { - sendStub: Sinon.SinonStub; - - constructor() { - this.sendStub = Sinon.stub(); - } - - send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise { - return this.sendStub(mailOptions); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/NotifierFactory.spec.ts b/themes/matrix/server/src/lib/notifiers/NotifierFactory.spec.ts deleted file mode 100644 index f15e7667f..000000000 --- a/themes/matrix/server/src/lib/notifiers/NotifierFactory.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ - -import * as sinon from "sinon"; -import * as BluebirdPromise from "bluebird"; -import * as assert from "assert"; - -import { NotifierFactory } from "./NotifierFactory"; -import { EmailNotifier } from "./EmailNotifier"; -import { SmtpNotifier } from "./SmtpNotifier"; -import { MailSenderBuilderStub } from "./MailSenderBuilderStub.spec"; - - -describe("notifiers/NotifierFactory", function () { - let mailSenderBuilderStub: MailSenderBuilderStub; - it("should build a Email Notifier", function () { - const options = { - email: { - username: "abc", - password: "password", - sender: "admin@example.com", - service: "gmail" - } - }; - mailSenderBuilderStub = new MailSenderBuilderStub(); - assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof EmailNotifier); - }); - - it("should build a SMTP Notifier", function () { - const options = { - smtp: { - username: "user", - password: "pass", - secure: true, - host: "localhost", - port: 25, - sender: "admin@example.com" - } - }; - - mailSenderBuilderStub = new MailSenderBuilderStub(); - assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof SmtpNotifier); - }); -}); diff --git a/themes/matrix/server/src/lib/notifiers/NotifierFactory.ts b/themes/matrix/server/src/lib/notifiers/NotifierFactory.ts deleted file mode 100644 index a89155feb..000000000 --- a/themes/matrix/server/src/lib/notifiers/NotifierFactory.ts +++ /dev/null @@ -1,33 +0,0 @@ - -import { NotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; -import Nodemailer = require("nodemailer"); -import { INotifier } from "./INotifier"; - -import { FileSystemNotifier } from "./FileSystemNotifier"; -import { EmailNotifier } from "./EmailNotifier"; -import { SmtpNotifier } from "./SmtpNotifier"; -import { IMailSender } from "./IMailSender"; -import { IMailSenderBuilder } from "./IMailSenderBuilder"; - -export class NotifierFactory { - static build(options: NotifierConfiguration, mailSenderBuilder: IMailSenderBuilder): INotifier { - if ("email" in options) { - const mailSender = mailSenderBuilder.buildEmail(options.email); - return new EmailNotifier(options.email, mailSender); - } - else if ("smtp" in options) { - const mailSender = mailSenderBuilder.buildSmtp(options.smtp); - return new SmtpNotifier(options.smtp, mailSender); - } - else if ("filesystem" in options) { - return new FileSystemNotifier(options.filesystem); - } - else { - throw new Error("No available notifier option detected."); - } - } -} - - - - diff --git a/themes/matrix/server/src/lib/notifiers/NotifierStub.spec.ts b/themes/matrix/server/src/lib/notifiers/NotifierStub.spec.ts deleted file mode 100644 index f99231b50..000000000 --- a/themes/matrix/server/src/lib/notifiers/NotifierStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); - -import { INotifier } from "./INotifier"; - -export class NotifierStub implements INotifier { - notifyStub: Sinon.SinonStub; - - constructor() { - this.notifyStub = Sinon.stub(); - } - - notify(to: string, subject: string, link: string): BluebirdPromise { - return this.notifyStub(to, subject, link); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/notifiers/SmtpNotifier.ts b/themes/matrix/server/src/lib/notifiers/SmtpNotifier.ts deleted file mode 100644 index f93a6d4a0..000000000 --- a/themes/matrix/server/src/lib/notifiers/SmtpNotifier.ts +++ /dev/null @@ -1,30 +0,0 @@ - - -import * as BluebirdPromise from "bluebird"; - -import { IMailSender } from "./IMailSender"; -import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; -import { SmtpNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; - -export class SmtpNotifier extends AbstractEmailNotifier { - private mailSender: IMailSender; - private sender: string; - - constructor(options: SmtpNotifierConfiguration, - mailSender: IMailSender) { - super(); - this.mailSender = mailSender; - this.sender = options.sender; - } - - sendEmail(to: string, subject: string, content: string) { - const mailOptions = { - from: this.sender, - to: to, - subject: subject, - html: content - }; - const that = this; - return this.mailSender.send(mailOptions); - } -} diff --git a/themes/matrix/server/src/lib/regulation/IRegulator.ts b/themes/matrix/server/src/lib/regulation/IRegulator.ts deleted file mode 100644 index c49425b24..000000000 --- a/themes/matrix/server/src/lib/regulation/IRegulator.ts +++ /dev/null @@ -1,6 +0,0 @@ -import BluebirdPromise = require("bluebird"); - -export interface IRegulator { - mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; - regulate(userId: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/regulation/Regulator.spec.ts b/themes/matrix/server/src/lib/regulation/Regulator.spec.ts deleted file mode 100644 index f9c6e6086..000000000 --- a/themes/matrix/server/src/lib/regulation/Regulator.spec.ts +++ /dev/null @@ -1,186 +0,0 @@ - -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import Assert = require("assert"); - -import { Regulator } from "./Regulator"; -import MockDate = require("mockdate"); -import exceptions = require("../Exceptions"); -import { UserDataStoreStub } from "../storage/UserDataStoreStub.spec"; - -describe("regulation/Regulator", function () { - const USER1 = "USER1"; - const USER2 = "USER2"; - let userDataStoreStub: UserDataStoreStub; - - beforeEach(function () { - userDataStoreStub = new UserDataStoreStub(); - const dataStore: { [userId: string]: { userId: string, date: Date, isAuthenticationSuccessful: boolean }[] } = { - [USER1]: [], - [USER2]: [] - }; - - userDataStoreStub.saveAuthenticationTraceStub.callsFake(function (userId, isAuthenticationSuccessful) { - dataStore[userId].unshift({ - userId: userId, - date: new Date(), - isAuthenticationSuccessful: isAuthenticationSuccessful, - }); - return BluebirdPromise.resolve(); - }); - - userDataStoreStub.retrieveLatestAuthenticationTracesStub.callsFake(function (userId, count) { - const ret = (dataStore[userId].length <= count) ? dataStore[userId] : dataStore[userId].slice(0, 3); - return BluebirdPromise.resolve(ret); - }); - }); - - afterEach(function () { - MockDate.reset(); - }); - - function markAuthenticationAt(regulator: Regulator, user: string, time: string, success: boolean) { - MockDate.set(time); - return regulator.mark(user, success); - } - - it("should mark 2 authentication and regulate (accept)", function () { - const regulator = new Regulator(userDataStoreStub, 3, 10, 10); - - return regulator.mark(USER1, false) - .then(function () { - return regulator.mark(USER1, true); - }) - .then(function () { - return regulator.regulate(USER1); - }); - }); - - it("should mark 3 authentications and regulate (reject)", function () { - const regulator = new Regulator(userDataStoreStub, 3, 10, 10); - - return regulator.mark(USER1, false) - .then(function () { - return regulator.mark(USER1, false); - }) - .then(function () { - return regulator.mark(USER1, false); - }) - .then(function () { - return regulator.regulate(USER1); - }) - .then(function () { return BluebirdPromise.reject(new Error("should not be here!")); }) - .catch(exceptions.AuthenticationRegulationError, function () { - return BluebirdPromise.resolve(); - }); - }); - - it("should mark 1 failed, 1 successful and 1 failed authentications within minimum time and regulate (accept)", function () { - const regulator = new Regulator(userDataStoreStub, 3, 60, 30); - - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", true); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:20", false); - }) - .then(function () { - return regulator.regulate(USER1); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:30", false); - }) - .then(function () { - return regulator.regulate(USER1); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:39", false); - }) - .then(function () { - return regulator.regulate(USER1); - }) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here!")); - }, - function () { - return BluebirdPromise.resolve(); - }); - }); - - it("should regulate user if number of failures is greater than 3 in allowed time lapse", function () { - function markAuthentications(regulator: Regulator, user: string) { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:45", false); - }) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:01:05", false); - }) - .then(function () { - return regulator.regulate(user); - }); - } - - const regulator1 = new Regulator(userDataStoreStub, 3, 60, 60); - const regulator2 = new Regulator(userDataStoreStub, 3, 2 * 60, 60); - - const p1 = markAuthentications(regulator1, USER1); - const p2 = markAuthentications(regulator2, USER2); - - return BluebirdPromise.join(p1, p2) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here...")); - }, function () { - Assert(p1.isFulfilled()); - Assert(p2.isRejected()); - }); - }); - - it("should user wait after regulation to authenticate again", function () { - function markAuthentications(regulator: Regulator, user: string) { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:10", false); - }) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:15", false); - }) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:25", false); - }) - .then(function () { - MockDate.set("1/2/2000 00:00:54"); - return regulator.regulate(user); - }) - .then(function () { - return BluebirdPromise.reject(new Error("should fail at this time")); - }, function () { - MockDate.set("1/2/2000 00:00:56"); - return regulator.regulate(user); - }); - } - - const regulator = new Regulator(userDataStoreStub, 4, 30, 30); - return markAuthentications(regulator, USER1); - }); - - it("should disable regulation when max_retries is set to 0", function () { - const maxRetries = 0; - const regulator = new Regulator(userDataStoreStub, maxRetries, 60, 30); - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", false); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:15", false); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:25", false); - }) - .then(function () { - MockDate.set("1/2/2000 00:00:26"); - return regulator.regulate(USER1); - }); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/regulation/Regulator.ts b/themes/matrix/server/src/lib/regulation/Regulator.ts deleted file mode 100644 index 1037a6a17..000000000 --- a/themes/matrix/server/src/lib/regulation/Regulator.ts +++ /dev/null @@ -1,55 +0,0 @@ - -import * as BluebirdPromise from "bluebird"; -import exceptions = require("../Exceptions"); -import { IUserDataStore } from "../storage/IUserDataStore"; -import { AuthenticationTraceDocument } from "../storage/AuthenticationTraceDocument"; -import { IRegulator } from "./IRegulator"; - -export class Regulator implements IRegulator { - private userDataStore: IUserDataStore; - private banTime: number; - private findTime: number; - private maxRetries: number; - - constructor(userDataStore: any, maxRetries: number, findTime: number, banTime: number) { - this.userDataStore = userDataStore; - this.banTime = banTime; - this.findTime = findTime; - this.maxRetries = maxRetries; - } - - // Mark authentication - mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { - return this.userDataStore.saveAuthenticationTrace(userId, isAuthenticationSuccessful); - } - - regulate(userId: string): BluebirdPromise { - const that = this; - - if (that.maxRetries <= 0) return BluebirdPromise.resolve(); - - return this.userDataStore.retrieveLatestAuthenticationTraces(userId, that.maxRetries) - .then((docs: AuthenticationTraceDocument[]) => { - // less than the max authorized number of authentication in time range, thus authorizing access - if (docs.length < that.maxRetries) return BluebirdPromise.resolve(); - - const numberOfFailedAuth = docs - .map(function (d: AuthenticationTraceDocument) { return d.isAuthenticationSuccessful == false ? 1 : 0; }) - .reduce(function (acc, v) { return acc + v; }, 0); - - if (numberOfFailedAuth < this.maxRetries) return BluebirdPromise.resolve(); - - const newestDocument = docs[0]; - const oldestDocument = docs[that.maxRetries - 1]; - - const authenticationsTimeRangeInSeconds = (newestDocument.date.getTime() - oldestDocument.date.getTime()) / 1000; - const tooManyAuthInTimelapse = (authenticationsTimeRangeInSeconds < this.findTime); - const stillInBannedTimeRange = (new Date(new Date().getTime() - this.banTime * 1000) < newestDocument.date); - - if (tooManyAuthInTimelapse && stillInBannedTimeRange) - throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes."); - - return BluebirdPromise.resolve(); - }); - } -} diff --git a/themes/matrix/server/src/lib/regulation/RegulatorStub.spec.ts b/themes/matrix/server/src/lib/regulation/RegulatorStub.spec.ts deleted file mode 100644 index ca8a00fb1..000000000 --- a/themes/matrix/server/src/lib/regulation/RegulatorStub.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Bluebird = require("bluebird"); -import Sinon = require("sinon"); -import { IRegulator } from "./IRegulator"; - - -export class RegulatorStub implements IRegulator { - markStub: Sinon.SinonStub; - regulateStub: Sinon.SinonStub; - - constructor() { - this.markStub = Sinon.stub(); - this.regulateStub = Sinon.stub(); - } - - mark(userId: string, isAuthenticationSuccessful: boolean): Bluebird { - return this.markStub(userId, isAuthenticationSuccessful); - } - - regulate(userId: string): Bluebird { - return this.regulateStub(userId); - } -} diff --git a/themes/matrix/server/src/lib/routes/error/401/get.spec.ts b/themes/matrix/server/src/lib/routes/error/401/get.spec.ts deleted file mode 100644 index 9fdac9c3c..000000000 --- a/themes/matrix/server/src/lib/routes/error/401/get.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import Sinon = require("sinon"); -import Express = require("express"); -import Assert = require("assert"); -import Get401 from "./get"; -import { ServerVariables } from "../../../ServerVariables"; -import { ServerVariablesMockBuilder, ServerVariablesMock } - from "../../../ServerVariablesMockBuilder.spec"; - -describe("routes/error/401/get", function () { - let vars: ServerVariables; - let mocks: ServerVariablesMock; - let req: any; - let res: any; - let renderSpy: Sinon.SinonSpy; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - vars = s.variables; - mocks = s.mocks; - - renderSpy = Sinon.spy(); - req = { - headers: {} - }; - res = { - render: renderSpy - }; - }); - - it("should set redirection url to the default redirection url", function () { - vars.config.default_redirection_url = "http://default-redirection"; - return Get401(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/401", { - redirection_url: "http://default-redirection" - })); - }); - }); - - it("should set redirection url to the referer", function () { - req.headers["referer"] = "http://redirection"; - return Get401(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/401", { - redirection_url: "http://redirection" - })); - }); - }); - - it("should render without redirecting the user", function () { - return Get401(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/401", { - redirection_url: undefined - })); - }); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/error/401/get.ts b/themes/matrix/server/src/lib/routes/error/401/get.ts deleted file mode 100644 index ca4a3963d..000000000 --- a/themes/matrix/server/src/lib/routes/error/401/get.ts +++ /dev/null @@ -1,15 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import redirector from "../redirector"; -import { ServerVariables } from "../../../ServerVariables"; - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response): BluebirdPromise { - const redirectionUrl = redirector(req, vars); - res.render("errors/401", { - redirection_url: redirectionUrl - }); - return BluebirdPromise.resolve(); - }; -} diff --git a/themes/matrix/server/src/lib/routes/error/403/get.spec.ts b/themes/matrix/server/src/lib/routes/error/403/get.spec.ts deleted file mode 100644 index 22eb84853..000000000 --- a/themes/matrix/server/src/lib/routes/error/403/get.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import Sinon = require("sinon"); -import Express = require("express"); -import Assert = require("assert"); -import Get403 from "./get"; -import { ServerVariables } from "../../../ServerVariables"; -import { ServerVariablesMockBuilder, ServerVariablesMock } - from "../../../ServerVariablesMockBuilder.spec"; - -describe("routes/error/403/get", function () { - let vars: ServerVariables; - let mocks: ServerVariablesMock; - let req: any; - let res: any; - let renderSpy: Sinon.SinonSpy; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - vars = s.variables; - mocks = s.mocks; - - renderSpy = Sinon.spy(); - req = { - headers: {} - }; - res = { - render: renderSpy - }; - }); - - it("should set redirection url to the default redirection url", function () { - vars.config.default_redirection_url = "http://default-redirection"; - return Get403(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/403", { - redirection_url: "http://default-redirection" - })); - }); - }); - - it("should set redirection url to the referer", function () { - req.headers["referer"] = "http://redirection"; - return Get403(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/403", { - redirection_url: "http://redirection" - })); - }); - }); - - it("should render without redirecting the user", function () { - return Get403(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/403", { - redirection_url: undefined - })); - }); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/error/403/get.ts b/themes/matrix/server/src/lib/routes/error/403/get.ts deleted file mode 100644 index 3ab0319e5..000000000 --- a/themes/matrix/server/src/lib/routes/error/403/get.ts +++ /dev/null @@ -1,15 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import redirector from "../redirector"; -import { ServerVariables } from "../../../ServerVariables"; - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response): BluebirdPromise { - const redirectionUrl = redirector(req, vars); - res.render("errors/403", { - redirection_url: redirectionUrl - }); - return BluebirdPromise.resolve(); - }; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/error/404/get.spec.ts b/themes/matrix/server/src/lib/routes/error/404/get.spec.ts deleted file mode 100644 index 73e4e6cef..000000000 --- a/themes/matrix/server/src/lib/routes/error/404/get.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Sinon = require("sinon"); -import Express = require("express"); -import Assert = require("assert"); -import Get404 from "./get"; - -describe("routes/error/404/get", function () { - it("should render the page", function () { - const req = {} as Express.Request; - const res = { - render: Sinon.stub() - }; - - return Get404(req, res as any) - .then(function () { - Assert(res.render.calledOnce); - Assert(res.render.calledWith("errors/404")); - }); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/error/404/get.ts b/themes/matrix/server/src/lib/routes/error/404/get.ts deleted file mode 100644 index 6693b6fc8..000000000 --- a/themes/matrix/server/src/lib/routes/error/404/get.ts +++ /dev/null @@ -1,8 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import express = require("express"); - -export default function (req: express.Request, res: express.Response): BluebirdPromise { - res.render("errors/404"); - return BluebirdPromise.resolve(); -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/error/redirector.ts b/themes/matrix/server/src/lib/routes/error/redirector.ts deleted file mode 100644 index b1a3ccc11..000000000 --- a/themes/matrix/server/src/lib/routes/error/redirector.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Express = require("express"); -import { ServerVariables } from "../../ServerVariables"; - -export default function (req: Express.Request, vars: ServerVariables): string { - let redirectionUrl: string; - - if (req.headers && req.headers["referer"]) - redirectionUrl = "" + req.headers["referer"]; - else if (vars.config.default_redirection_url) - redirectionUrl = vars.config.default_redirection_url; - - return redirectionUrl; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/firstfactor/get.ts b/themes/matrix/server/src/lib/routes/firstfactor/get.ts deleted file mode 100644 index d94f656c8..000000000 --- a/themes/matrix/server/src/lib/routes/firstfactor/get.ts +++ /dev/null @@ -1,72 +0,0 @@ - -import express = require("express"); -import Endpoints = require("../../../../../shared/api"); -import BluebirdPromise = require("bluebird"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import Constants = require("../../../../../shared/constants"); -import Util = require("util"); -import { ServerVariables } from "../../ServerVariables"; -import { SafeRedirector } from "../../utils/SafeRedirection"; -import { Level } from "../../authentication/Level"; - -function getRedirectParam( - req: express.Request) { - return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : undefined; -} - -function redirectToSecondFactorPage( - req: express.Request, - res: express.Response) { - - const redirectUrl = getRedirectParam(req); - if (!redirectUrl) - res.redirect(Endpoints.SECOND_FACTOR_GET); - else - res.redirect( - Util.format("%s?%s=%s", - Endpoints.SECOND_FACTOR_GET, - Constants.REDIRECT_QUERY_PARAM, - redirectUrl)); -} - -function redirectToService( - req: express.Request, - res: express.Response, - redirector: SafeRedirector) { - const redirectUrl = getRedirectParam(req); - if (!redirectUrl) { - res.redirect(Endpoints.LOGGED_IN); - } else { - redirector.redirectOrElse(res, redirectUrl, Endpoints.LOGGED_IN); - } -} - -function renderFirstFactor( - res: express.Response) { - - res.render("firstfactor", { - first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST, - reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET - }); -} - -export default function ( - vars: ServerVariables) { - - const redirector = new SafeRedirector(vars.config.session.domain); - return function (req: express.Request, res: express.Response): BluebirdPromise { - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, vars.logger); - if (authSession.authentication_level == Level.ONE_FACTOR) { - redirectToSecondFactorPage(req, res); - } else if (authSession.authentication_level == Level.TWO_FACTOR) { - redirectToService(req, res, redirector); - } else { - renderFirstFactor(res); - } - resolve(); - }); - }; -} diff --git a/themes/matrix/server/src/lib/routes/firstfactor/post.spec.ts b/themes/matrix/server/src/lib/routes/firstfactor/post.spec.ts deleted file mode 100644 index e1d078cdd..000000000 --- a/themes/matrix/server/src/lib/routes/firstfactor/post.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ - -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import Assert = require("assert"); -import FirstFactorPost = require("./post"); -import exceptions = require("../../Exceptions"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../types/AuthenticationSession"; -import Endpoints = require("../../../../../shared/api"); -import AuthenticationRegulatorMock = require("../../regulation/RegulatorStub.spec"); -import ExpressMock = require("../../stubs/express.spec"); -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../ServerVariables"; - -describe("routes/firstfactor/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let emails: string[]; - let groups: string[]; - let vars: ServerVariables; - let mocks: ServerVariablesMock; - let authSession: AuthenticationSession; - - beforeEach(function () { - emails = ["test_ok@example.com"]; - groups = ["group1", "group2" ]; - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - mocks.authorizer.authorizationMock.returns(true); - mocks.regulator.regulateStub.returns(BluebirdPromise.resolve()); - mocks.regulator.markStub.returns(BluebirdPromise.resolve()); - - req = { - originalUrl: "/api/firstfactor", - body: { - username: "username", - password: "password" - }, - query: { - redirect: "http://redirect.url" - }, - session: { - cookie: {} - }, - headers: { - host: "home.example.com" - } - }; - - res = ExpressMock.ResponseMock(); - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - }); - - it("should reply with 204 if success", function () { - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.resolve({ - emails: emails, - groups: groups - })); - return FirstFactorPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal("username", authSession.userid); - Assert(res.send.calledOnce); - }); - }); - - describe("keep me logged in", () => { - beforeEach(() => { - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.resolve({ - emails: emails, - groups: groups - })); - req.body.keepMeLoggedIn = "true"; - return FirstFactorPost.default(vars)(req as any, res as any); - }); - - it("should set keep_me_logged_in session variable to true", function () { - Assert.equal(authSession.keep_me_logged_in, true); - }); - - it("should set cookie maxAge to one year", function () { - Assert.equal(req.session.cookie.maxAge, 365 * 24 * 60 * 60 * 1000); - }); - }); - - it("should retrieve email from LDAP", function () { - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.resolve([{ mail: ["test@example.com"] }])); - return FirstFactorPost.default(vars)(req as any, res as any); - }); - - it("should set first email address as user session variable", function () { - const emails = ["test_ok@example.com"]; - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.resolve({ - emails: emails, - groups: groups - })); - - return FirstFactorPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal("test_ok@example.com", authSession.email); - }); - }); - - it("should return error message when LDAP authenticator throws", function () { - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.reject(new exceptions.LdapBindError("Bad credentials"))); - - return FirstFactorPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.equal(mocks.regulator.markStub.getCall(0).args[0], "username"); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - }); - }); - - it("should return error message when regulator rejects authentication", function () { - const err = new exceptions.AuthenticationRegulationError("Authentication regulation..."); - mocks.regulator.regulateStub.returns(BluebirdPromise.reject(err)); - return FirstFactorPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - }); - }); -}); - - diff --git a/themes/matrix/server/src/lib/routes/firstfactor/post.ts b/themes/matrix/server/src/lib/routes/firstfactor/post.ts deleted file mode 100644 index 565681d6a..000000000 --- a/themes/matrix/server/src/lib/routes/firstfactor/post.ts +++ /dev/null @@ -1,101 +0,0 @@ - -import Exceptions = require("../../Exceptions"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import Endpoint = require("../../../../../shared/api"); -import ErrorReplies = require("../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import Constants = require("../../../../../shared/constants"); -import UserMessages = require("../../../../../shared/UserMessages"); -import { ServerVariables } from "../../ServerVariables"; -import { AuthenticationSession } from "../../../../types/AuthenticationSession"; -import { GroupsAndEmails } from "../../authentication/backends/GroupsAndEmails"; -import { Level as AuthenticationLevel } from "../../authentication/Level"; -import { Level as AuthorizationLevel } from "../../authorization/Level"; -import { URLDecomposer } from "../../utils/URLDecomposer"; - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response) - : BluebirdPromise { - const username: string = req.body.username; - const password: string = req.body.password; - const keepMeLoggedIn: boolean = req.body.keepMeLoggedIn && - req.body.keepMeLoggedIn === "true"; - let authSession: AuthenticationSession; - - if (keepMeLoggedIn) { - // Stay connected for 1 year. - vars.logger.debug(req, "User requested to stay logged in for one year."); - req.session.cookie.maxAge = 365 * 24 * 60 * 60 * 1000; - } - - return BluebirdPromise.resolve() - .then(function () { - if (!username || !password) { - return BluebirdPromise.reject(new Error("No username or password.")); - } - vars.logger.info(req, "Starting authentication of user \"%s\"", username); - authSession = AuthenticationSessionHandler.get(req, vars.logger); - return vars.regulator.regulate(username); - }) - .then(function () { - vars.logger.info(req, "No regulation applied."); - return vars.usersDatabase.checkUserPassword(username, password); - }) - .then(function (groupsAndEmails: GroupsAndEmails) { - vars.logger.info(req, - "LDAP binding successful. Retrieved information about user are %s", - JSON.stringify(groupsAndEmails)); - authSession.userid = username; - authSession.keep_me_logged_in = keepMeLoggedIn; - authSession.authentication_level = AuthenticationLevel.ONE_FACTOR; - const redirectUrl: string = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined" - // Fuck, don't know why it is a string! - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : ""; - - const emails: string[] = groupsAndEmails.emails; - const groups: string[] = groupsAndEmails.groups; - const decomposition = URLDecomposer.fromUrl(redirectUrl); - const authorizationLevel = (decomposition) - ? vars.authorizer.authorization( - {domain: decomposition.domain, resource: decomposition.path}, - {user: username, groups: groups}) - : AuthorizationLevel.TWO_FACTOR; - - if (emails.length > 0) - authSession.email = emails[0]; - authSession.groups = groups; - - vars.logger.debug(req, "Mark successful authentication to regulator."); - vars.regulator.mark(username, true); - - if (authorizationLevel <= AuthorizationLevel.ONE_FACTOR) { - let newRedirectionUrl: string = redirectUrl; - if (!newRedirectionUrl) - newRedirectionUrl = Endpoint.LOGGED_IN; - res.send({ - redirect: newRedirectionUrl - }); - vars.logger.debug(req, "Redirect to '%s'", redirectUrl); - } - else { - let newRedirectUrl = Endpoint.SECOND_FACTOR_GET; - if (redirectUrl) { - newRedirectUrl += "?" + Constants.REDIRECT_QUERY_PARAM + "=" - + redirectUrl; - } - vars.logger.debug(req, "Redirect to '%s'", newRedirectUrl); - res.send({ - redirect: newRedirectUrl - }); - } - return BluebirdPromise.resolve(); - }) - .catch(Exceptions.LdapBindError, function (err: Error) { - vars.regulator.mark(username, false); - return ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)(err); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)); - }; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/loggedin/get.ts b/themes/matrix/server/src/lib/routes/loggedin/get.ts deleted file mode 100644 index 283a041b1..000000000 --- a/themes/matrix/server/src/lib/routes/loggedin/get.ts +++ /dev/null @@ -1,23 +0,0 @@ -import Express = require("express"); -import Endpoints = require("../../../../../shared/api"); -import BluebirdPromise = require("bluebird"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { ServerVariables } from "../../ServerVariables"; -import ErrorReplies = require("../../ErrorReplies"); - -export default function (vars: ServerVariables) { - function handler(req: Express.Request, res: Express.Response): BluebirdPromise { - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, vars.logger); - res.render("already-logged-in", { - logout_endpoint: Endpoints.LOGOUT_GET, - username: authSession.userid, - redirection_url: vars.config.default_redirection_url - }); - resolve(); - }) - .catch(ErrorReplies.replyWithError401(req, res, vars.logger)); - } - - return handler; -} diff --git a/themes/matrix/server/src/lib/routes/logout/get.ts b/themes/matrix/server/src/lib/routes/logout/get.ts deleted file mode 100644 index 4d5112146..000000000 --- a/themes/matrix/server/src/lib/routes/logout/get.ts +++ /dev/null @@ -1,20 +0,0 @@ - -import express = require("express"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import Constants = require("../../../../../shared/constants"); -import { ServerVariables } from "../../ServerVariables"; - -function getRedirectParam(req: express.Request) { - return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : undefined; -} - -export default function (vars: ServerVariables) { - return function(req: express.Request, res: express.Response) { - const redirect_param = getRedirectParam(req); - const redirect_url = redirect_param || "/"; - AuthenticationSessionHandler.reset(req); - res.redirect(redirect_url); - }; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/password-reset/constants.ts b/themes/matrix/server/src/lib/routes/password-reset/constants.ts deleted file mode 100644 index 5c639e92a..000000000 --- a/themes/matrix/server/src/lib/routes/password-reset/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export const CHALLENGE = "reset-password"; \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/password-reset/form/post.spec.ts b/themes/matrix/server/src/lib/routes/password-reset/form/post.spec.ts deleted file mode 100644 index ed029c906..000000000 --- a/themes/matrix/server/src/lib/routes/password-reset/form/post.spec.ts +++ /dev/null @@ -1,122 +0,0 @@ - -import PasswordResetFormPost = require("./post"); -import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; -import { UserDataStore } from "../../../storage/UserDataStore"; -import Sinon = require("sinon"); -import Assert = require("assert"); -import BluebirdPromise = require("bluebird"); -import ExpressMock = require("../../../stubs/express.spec"); -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../ServerVariables"; -import { Level } from "../../../authentication/Level"; - -describe("routes/password-reset/form/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let vars: ServerVariables; - let mocks: ServerVariablesMock; - let authSession: AuthenticationSession; - - beforeEach(function () { - req = { - originalUrl: "/api/password-reset", - body: { - userid: "user" - }, - session: {}, - headers: { - host: "localhost" - } - }; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - - mocks.config.authentication_backend.ldap = { - url: "ldap://ldapjs", - mail_attribute: "mail", - user: "user", - password: "password", - additional_users_dn: "ou=users", - additional_groups_dn: "ou=groups", - base_dn: "dc=example,dc=com", - users_filter: "user", - group_name_attribute: "cn", - groups_filter: "groups" - }; - - res = ExpressMock.ResponseMock(); - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - authSession.userid = "user"; - authSession.email = "user@example.com"; - authSession.authentication_level = Level.ONE_FACTOR; - }); - - describe("test reset password post", () => { - it("should update the password and reset auth_session for reauthentication", function () { - req.body = {}; - req.body.password = "new-password"; - - mocks.usersDatabase.updatePasswordStub.returns(BluebirdPromise.resolve()); - - authSession.identity_check = { - userid: "user", - challenge: "reset-password" - }; - return PasswordResetFormPost.default(vars)(req as any, res as any) - .then(function () { - return AuthenticationSessionHandler.get(req as any, vars.logger); - }).then(function (_authSession) { - Assert.equal(res.status.getCall(0).args[0], 204); - Assert.equal(_authSession.authentication_level, Level.NOT_AUTHENTICATED); - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if identity_challenge does not exist", function () { - authSession.identity_check = { - userid: "user", - challenge: undefined - }; - return PasswordResetFormPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "An error occurred during password reset. Your password has not been changed." - }); - }); - }); - - it("should fail when ldap fails", function () { - req.body = {}; - req.body.password = "new-password"; - - mocks.usersDatabase.updatePasswordStub - .returns(BluebirdPromise.reject("Internal error with LDAP")); - - authSession.identity_check = { - challenge: "reset-password", - userid: "user" - }; - return PasswordResetFormPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "An error occurred during password reset. Your password has not been changed." - }); - return BluebirdPromise.resolve(); - }); - }); - }); -}); diff --git a/themes/matrix/server/src/lib/routes/password-reset/form/post.ts b/themes/matrix/server/src/lib/routes/password-reset/form/post.ts deleted file mode 100644 index fccd7471b..000000000 --- a/themes/matrix/server/src/lib/routes/password-reset/form/post.ts +++ /dev/null @@ -1,50 +0,0 @@ - -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import objectPath = require("object-path"); -import exceptions = require("../../../Exceptions"); -import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; -import ErrorReplies = require("../../../ErrorReplies"); -import UserMessages = require("../../../../../../shared/UserMessages"); -import { ServerVariables } from "../../../ServerVariables"; - -import Constants = require("./../constants"); - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const newPassword = objectPath.get(req, "body.password"); - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - if (!authSession.identity_check) { - reject(new Error("No identity check initiated")); - return; - } - - vars.logger.info(req, "User %s wants to reset his/her password.", - authSession.identity_check.userid); - vars.logger.debug(req, "Challenge %s", authSession.identity_check.challenge); - - if (authSession.identity_check.challenge != Constants.CHALLENGE) { - reject(new Error("Bad challenge.")); - return; - } - resolve(); - }) - .then(function () { - return vars.usersDatabase.updatePassword(authSession.identity_check.userid, newPassword); - }) - .then(function () { - vars.logger.info(req, "Password reset for user '%s'", - authSession.identity_check.userid); - AuthenticationSessionHandler.reset(req); - res.status(204); - res.send(); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.RESET_PASSWORD_FAILED)); - }; -} diff --git a/themes/matrix/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts b/themes/matrix/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts deleted file mode 100644 index ac6a41754..000000000 --- a/themes/matrix/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts +++ /dev/null @@ -1,92 +0,0 @@ - -import PasswordResetHandler - from "./PasswordResetHandler"; -import { UserDataStore } from "../../../storage/UserDataStore"; -import Sinon = require("sinon"); -import winston = require("winston"); -import assert = require("assert"); -import BluebirdPromise = require("bluebird"); -import ExpressMock = require("../../../stubs/express.spec"); -import { ServerVariablesMock, ServerVariablesMockBuilder } - from "../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../ServerVariables"; - -describe("routes/password-reset/identity/PasswordResetHandler", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - req = { - originalUrl: "/non-api/xxx", - query: { - userid: "user" - }, - session: { - auth: { - userid: "user", - email: "user@example.com", - first_factor: true, - second_factor: false - } - }, - headers: { - host: "localhost" - } - }; - - const options = { - inMemoryOnly: true - }; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - mocks.userDataStore.saveU2FRegistrationStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.produceIdentityValidationTokenStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.consumeIdentityValidationTokenStub - .returns(BluebirdPromise.resolve({})); - res = ExpressMock.ResponseMock(); - }); - - describe("test reset password identity pre check", () => { - it("should fail when no userid is provided", function () { - req.query.userid = undefined; - const handler = new PasswordResetHandler(vars.logger, - vars.usersDatabase); - return handler.preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject("It should fail"); - }) - .catch(function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if ldap fail", function () { - mocks.usersDatabase.getEmailsStub - .returns(BluebirdPromise.reject("Internal error")); - new PasswordResetHandler(vars.logger, vars.usersDatabase) - .preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here")); - }, - function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should returns identity when ldap replies", function () { - mocks.usersDatabase.getEmailsStub - .returns(BluebirdPromise.resolve(["test@example.com"])); - return new PasswordResetHandler(vars.logger, vars.usersDatabase) - .preValidationInit(req as any); - }); - }); -}); diff --git a/themes/matrix/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts b/themes/matrix/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts deleted file mode 100644 index 42ae92cda..000000000 --- a/themes/matrix/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts +++ /dev/null @@ -1,69 +0,0 @@ -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import objectPath = require("object-path"); - -import exceptions = require("../../../Exceptions"); -import { Identity } from "../../../../../types/Identity"; -import { IdentityValidable } from "../../../IdentityValidable"; -import { PRE_VALIDATION_TEMPLATE } from "../../../IdentityCheckPreValidationTemplate"; -import Constants = require("../constants"); -import { IRequestLogger } from "../../../logging/IRequestLogger"; -import { IUsersDatabase } from "../../../authentication/backends/IUsersDatabase"; - -export const TEMPLATE_NAME = "password-reset-form"; - -export default class PasswordResetHandler implements IdentityValidable { - private logger: IRequestLogger; - private usersDatabase: IUsersDatabase; - - constructor(logger: IRequestLogger, usersDatabase: IUsersDatabase) { - this.logger = logger; - this.usersDatabase = usersDatabase; - } - - challenge(): string { - return Constants.CHALLENGE; - } - - preValidationInit(req: express.Request): BluebirdPromise { - const that = this; - const userid: string = - objectPath.get(req, "query.userid"); - return BluebirdPromise.resolve() - .then(function () { - that.logger.debug(req, "User '%s' requested a password reset", userid); - if (!userid) - return BluebirdPromise.reject( - new exceptions.AccessDeniedError("No user id provided")); - - return that.usersDatabase.getEmails(userid); - }) - .then(function (emails: string[]) { - if (!emails && emails.length <= 0) throw new Error("No email found"); - const identity = { - email: emails[0], - userid: userid - }; - return BluebirdPromise.resolve(identity); - }) - .catch(function (err: Error) { - return BluebirdPromise.reject(new exceptions.IdentityError(err.message)); - }); - } - - preValidationResponse(req: express.Request, res: express.Response) { - res.render(PRE_VALIDATION_TEMPLATE); - } - - postValidationInit(req: express.Request) { - return BluebirdPromise.resolve(); - } - - postValidationResponse(req: express.Request, res: express.Response) { - res.render(TEMPLATE_NAME); - } - - mailSubject(): string { - return "Reset your password"; - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/password-reset/request/get.ts b/themes/matrix/server/src/lib/routes/password-reset/request/get.ts deleted file mode 100644 index 8f3ae2b4b..000000000 --- a/themes/matrix/server/src/lib/routes/password-reset/request/get.ts +++ /dev/null @@ -1,13 +0,0 @@ - -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import objectPath = require("object-path"); -import exceptions = require("../../../Exceptions"); - -import Constants = require("./../constants"); - -const TEMPLATE_NAME = "password-reset-request"; - -export default function (req: express.Request, res: express.Response) { - res.render(TEMPLATE_NAME); -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/secondfactor/get.spec.ts b/themes/matrix/server/src/lib/routes/secondfactor/get.spec.ts deleted file mode 100644 index 6c77e1f69..000000000 --- a/themes/matrix/server/src/lib/routes/secondfactor/get.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import SecondFactorGet from "./get"; -import { ServerVariablesMockBuilder, ServerVariablesMock } - from "../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../ServerVariables"; -import Sinon = require("sinon"); -import ExpressMock = require("../../stubs/express.spec"); -import Assert = require("assert"); -import Endpoints = require("../../../../../shared/api"); -import BluebirdPromise = require("bluebird"); - -describe("routes/secondfactor/get", function () { - let mocks: ServerVariablesMock; - let vars: ServerVariables; - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req = ExpressMock.RequestMock(); - res = ExpressMock.ResponseMock(); - - req.session = { - auth: { - userid: "user", - first_factor: true, - second_factor: false - } - }; - }); - - describe("test rendering", function () { - it("should render second factor page", function () { - req.session.auth.second_factor = false; - return SecondFactorGet(vars)(req as any, res as any) - .then(function () { - Assert(res.render.calledWith("secondfactor")); - return BluebirdPromise.resolve(); - }); - }); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/secondfactor/get.ts b/themes/matrix/server/src/lib/routes/secondfactor/get.ts deleted file mode 100644 index 9f6deb4c6..000000000 --- a/themes/matrix/server/src/lib/routes/secondfactor/get.ts +++ /dev/null @@ -1,28 +0,0 @@ - -import Express = require("express"); -import Endpoints = require("../../../../../shared/api"); -import BluebirdPromise = require("bluebird"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { ServerVariables } from "../../ServerVariables"; - -const TEMPLATE_NAME = "secondfactor"; - -export default function (vars: ServerVariables) { - function handler(req: Express.Request, res: Express.Response) - : BluebirdPromise { - - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, vars.logger); - - res.render(TEMPLATE_NAME, { - username: authSession.userid, - totp_identity_start_endpoint: - Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, - u2f_identity_start_endpoint: - Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET - }); - resolve(); - }); - } - return handler; -} diff --git a/themes/matrix/server/src/lib/routes/secondfactor/redirect.spec.ts b/themes/matrix/server/src/lib/routes/secondfactor/redirect.spec.ts deleted file mode 100644 index ea66e6dca..000000000 --- a/themes/matrix/server/src/lib/routes/secondfactor/redirect.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import Redirect from "./redirect"; -import ExpressMock = require("../../stubs/express.spec"); -import { ServerVariablesMockBuilder, ServerVariablesMock } -from "../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../ServerVariables"; -import Assert = require("assert"); - -describe("routes/secondfactor/redirect", function() { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req = ExpressMock.RequestMock(); - res = ExpressMock.ResponseMock(); - }); - - it("should redirect to default_redirection_url", function() { - vars.config.default_redirection_url = "http://default_redirection_url"; - Redirect(vars)(req as any, res as any) - .then(function() { - Assert(res.json.calledWith({ - redirect: "http://default_redirection_url" - })); - }); - }); - - it("should redirect to /", function() { - Redirect(vars)(req as any, res as any) - .then(function() { - Assert(res.json.calledWith({ - redirect: "/" - })); - }); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/secondfactor/redirect.ts b/themes/matrix/server/src/lib/routes/secondfactor/redirect.ts deleted file mode 100644 index 5d84d9ebf..000000000 --- a/themes/matrix/server/src/lib/routes/secondfactor/redirect.ts +++ /dev/null @@ -1,30 +0,0 @@ - -import express = require("express"); -import objectPath = require("object-path"); -import Endpoints = require("../../../../../shared/api"); -import { ServerVariables } from "../../ServerVariables"; -import BluebirdPromise = require("bluebird"); -import ErrorReplies = require("../../ErrorReplies"); -import UserMessages = require("../../../../../shared/UserMessages"); -import { RedirectionMessage } from "../../../../../shared/RedirectionMessage"; -import Constants = require("../../../../../shared/constants"); - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response) - : BluebirdPromise { - - return new BluebirdPromise(function (resolve, reject) { - let redirectUrl: string = "/"; - if (vars.config.default_redirection_url) { - redirectUrl = vars.config.default_redirection_url; - } - vars.logger.debug(req, "Request redirection to \"%s\".", redirectUrl); - res.json({ - redirect: redirectUrl - } as RedirectionMessage); - return resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - }; -} diff --git a/themes/matrix/server/src/lib/routes/secondfactor/totp/constants.ts b/themes/matrix/server/src/lib/routes/secondfactor/totp/constants.ts deleted file mode 100644 index 7b5a1dcfe..000000000 --- a/themes/matrix/server/src/lib/routes/secondfactor/totp/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ - -export const CHALLENGE = "totp-register"; -export const TEMPLATE_NAME = "totp-register"; - diff --git a/themes/matrix/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts b/themes/matrix/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts deleted file mode 100644 index 78b8ea3ea..000000000 --- a/themes/matrix/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts +++ /dev/null @@ -1,116 +0,0 @@ -import Sinon = require("sinon"); -import RegistrationHandler from "./RegistrationHandler"; -import { Identity } from "../../../../../../types/Identity"; -import { UserDataStore } from "../../../../storage/UserDataStore"; -import BluebirdPromise = require("bluebird"); -import ExpressMock = require("../../../../stubs/express.spec"); -import { ServerVariablesMock, ServerVariablesMockBuilder } - from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; -import Assert = require("assert"); - -describe("routes/secondfactor/totp/identity/RegistrationHandler", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req = ExpressMock.RequestMock(); - req.session = { - auth: { - userid: "user", - email: "user@example.com", - first_factor: true, - second_factor: false, - identity_check: { - userid: "user", - challenge: "totp-register" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.produceIdentityValidationTokenStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.consumeIdentityValidationTokenStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.saveTOTPSecretStub - .returns(BluebirdPromise.resolve({})); - - res = ExpressMock.ResponseMock(); - }); - - describe("test totp registration pre validation", function () { - it("should fail if first_factor has not been passed", function () { - req.session.auth.first_factor = false; - return new RegistrationHandler(vars.logger, vars.userDataStore, - vars.totpHandler, vars.config.totp) - .preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject(new Error("It should fail")); - }) - .catch(function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if userid is missing", function (done) { - req.session.auth.first_factor = false; - req.session.auth.userid = undefined; - - new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler, - vars.config.totp) - .preValidationInit(req as any) - .catch(function (err: Error) { - done(); - }); - }); - - it("should fail if email is missing", function (done) { - req.session.auth.first_factor = false; - req.session.auth.email = undefined; - - new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler, - vars.config.totp) - .preValidationInit(req as any) - .catch(function (err: Error) { - done(); - }); - }); - - it("should succeed if first factor passed, userid and email are provided", - function () { - return new RegistrationHandler(vars.logger, vars.userDataStore, - vars.totpHandler, vars.config.totp) - .preValidationInit(req as any); - }); - }); - - describe("test totp registration post validation", function () { - it("should generate a secret using userId as label and issuer defined in config", function () { - vars.config.totp = { - issuer: "issuer" - }; - return new RegistrationHandler(vars.logger, vars.userDataStore, - vars.totpHandler, vars.config.totp) - .postValidationResponse(req as any, res as any) - .then(function() { - Assert(mocks.totpHandler.generateStub.calledWithExactly("user", "issuer")); - }); - }); - }); -}); diff --git a/themes/matrix/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts b/themes/matrix/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts deleted file mode 100644 index b39b6d045..000000000 --- a/themes/matrix/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts +++ /dev/null @@ -1,112 +0,0 @@ - -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import objectPath = require("object-path"); - -import { Identity } from "../../../../../../types/Identity"; -import { IdentityValidable } from "../../../../IdentityValidable"; -import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; -import Constants = require("../constants"); -import Endpoints = require("../../../../../../../shared/api"); -import ErrorReplies = require("../../../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import FirstFactorValidator = require("../../../../FirstFactorValidator"); -import { IRequestLogger } from "../../../../logging/IRequestLogger"; -import { IUserDataStore } from "../../../../storage/IUserDataStore"; -import { ITotpHandler } from "../../../../authentication/totp/ITotpHandler"; -import { TOTPSecret } from "../../../../../../types/TOTPSecret"; -import { TotpConfiguration } from "../../../../configuration/schema/TotpConfiguration"; - - -export default class RegistrationHandler implements IdentityValidable { - private logger: IRequestLogger; - private userDataStore: IUserDataStore; - private totp: ITotpHandler; - private configuration: TotpConfiguration; - - constructor(logger: IRequestLogger, - userDataStore: IUserDataStore, - totp: ITotpHandler, configuration: TotpConfiguration) { - this.logger = logger; - this.userDataStore = userDataStore; - this.totp = totp; - this.configuration = configuration; - } - - challenge(): string { - return Constants.CHALLENGE; - } - - private retrieveIdentity(req: express.Request): BluebirdPromise { - const that = this; - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, that.logger); - const userid = authSession.userid; - const email = authSession.email; - - if (!(userid && email)) { - return reject(new Error("User ID or email is missing")); - } - - const identity = { - email: email, - userid: userid - }; - return resolve(identity); - }); - } - - preValidationInit(req: express.Request): BluebirdPromise { - const that = this; - return FirstFactorValidator.validate(req, this.logger) - .then(function () { - return that.retrieveIdentity(req); - }); - } - - preValidationResponse(req: express.Request, res: express.Response) { - res.render(PRE_VALIDATION_TEMPLATE); - } - - postValidationInit(req: express.Request) { - return FirstFactorValidator.validate(req, this.logger); - } - - postValidationResponse(req: express.Request, res: express.Response) - : BluebirdPromise { - const that = this; - let secret: TOTPSecret; - let userId: string; - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, that.logger); - userId = authSession.userid; - - if (authSession.identity_check.challenge != Constants.CHALLENGE - || !userId) - return reject(new Error("Bad challenge.")); - - resolve(); - }) - .then(function () { - secret = that.totp.generate(userId, - that.configuration.issuer); - that.logger.debug(req, "Save the TOTP secret in DB"); - return that.userDataStore.saveTOTPSecret(userId, secret); - }) - .then(function () { - AuthenticationSessionHandler.reset(req); - - res.render(Constants.TEMPLATE_NAME, { - base32_secret: secret.base32, - otpauth_url: secret.otpauth_url, - login_endpoint: Endpoints.FIRST_FACTOR_GET - }); - }) - .catch(ErrorReplies.replyWithError200(req, res, that.logger, UserMessages.OPERATION_FAILED)); - } - - mailSubject(): string { - return "Set up Authelia's one-time password"; - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts b/themes/matrix/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts deleted file mode 100644 index 70a20d39d..000000000 --- a/themes/matrix/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import Sinon = require("sinon"); -import Assert = require("assert"); -import Exceptions = require("../../../../Exceptions"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import SignPost = require("./post"); -import { ServerVariables } from "../../../../ServerVariables"; - -import ExpressMock = require("../../../../stubs/express.spec"); -import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; -import { Level } from "../../../../authentication/Level"; - -describe("routes/secondfactor/totp/sign/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let authSession: AuthenticationSession; - let vars: ServerVariables; - let mocks: ServerVariablesMock; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - vars = s.variables; - mocks = s.mocks; - const app_get = Sinon.stub(); - req = { - originalUrl: "/api/totp-register", - app: {}, - body: { - token: "abc" - }, - session: {}, - query: { - redirect: "http://redirect" - } - }; - res = ExpressMock.ResponseMock(); - - const doc = { - userid: "user", - secret: { - base32: "ABCDEF" - } - }; - mocks.userDataStore.retrieveTOTPSecretStub.returns(BluebirdPromise.resolve(doc)); - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - authSession.userid = "user"; - authSession.authentication_level = Level.ONE_FACTOR; - }); - - - it("should send status code 200 when totp is valid", function () { - mocks.totpHandler.validateStub.returns(true); - return SignPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(authSession.authentication_level, Level.TWO_FACTOR); - return BluebirdPromise.resolve(); - }); - }); - - it("should send error message when totp is not valid", function () { - mocks.totpHandler.validateStub.returns(false); - return SignPost.default(vars)(req as any, res as any) - .then(function () { - Assert.notEqual(authSession.authentication_level, Level.TWO_FACTOR); - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); -}); - diff --git a/themes/matrix/server/src/lib/routes/secondfactor/totp/sign/post.ts b/themes/matrix/server/src/lib/routes/secondfactor/totp/sign/post.ts deleted file mode 100644 index 34a276d12..000000000 --- a/themes/matrix/server/src/lib/routes/secondfactor/totp/sign/post.ts +++ /dev/null @@ -1,42 +0,0 @@ -import Bluebird = require("bluebird"); -import Express = require("express"); - -import { TOTPSecretDocument } from "../../../../storage/TOTPSecretDocument"; -import Endpoints = require("../../../../../../../shared/api"); -import Redirect from "../../redirect"; -import ErrorReplies = require("../../../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { ServerVariables } from "../../../../ServerVariables"; -import { Level } from "../../../../authentication/Level"; - -const UNAUTHORIZED_MESSAGE = "Unauthorized access"; - -export default function (vars: ServerVariables) { - function handler(req: Express.Request, res: Express.Response): Bluebird { - let authSession: AuthenticationSession; - const token = req.body.token; - - return new Bluebird(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - vars.logger.info(req, "Initiate TOTP validation for user \"%s\".", authSession.userid); - resolve(); - }) - .then(function () { - return vars.userDataStore.retrieveTOTPSecret(authSession.userid); - }) - .then(function (doc: TOTPSecretDocument) { - if (!vars.totpHandler.validate(token, doc.secret.base32)) - return Bluebird.reject(new Error("Invalid TOTP token.")); - - vars.logger.debug(req, "TOTP validation succeeded."); - authSession.authentication_level = Level.TWO_FACTOR; - Redirect(vars)(req, res); - return Bluebird.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - return handler; -} diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts deleted file mode 100644 index 7f16c0ee8..000000000 --- a/themes/matrix/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts +++ /dev/null @@ -1,11 +0,0 @@ - -import util = require("util"); -import express = require("express"); - -function extract_app_id(req: express.Request): string { - return util.format("https://%s", req.headers.host); -} - -export = { - extract_app_id: extract_app_id -}; \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts deleted file mode 100644 index a54bfbfe6..000000000 --- a/themes/matrix/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts +++ /dev/null @@ -1,96 +0,0 @@ -import Sinon = require("sinon"); -import Assert = require("assert"); -import BluebirdPromise = require("bluebird"); - -import { Identity } from "../../../../../../types/Identity"; -import RegistrationHandler from "./RegistrationHandler"; -import ExpressMock = require("../../../../stubs/express.spec"); -import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; - -describe("routes/secondfactor/u2f/identity/RegistrationHandler", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req = ExpressMock.RequestMock(); - req.app = {}; - req.session = { - auth: { - userid: "user", - email: "user@example.com", - first_factor: true, - second_factor: false - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - - res = ExpressMock.ResponseMock(); - res.send = Sinon.spy(); - res.json = Sinon.spy(); - res.status = Sinon.spy(); - }); - - describe("test u2f registration check", test_registration_check); - - function test_registration_check() { - it("should fail if first_factor has not been passed", function () { - req.session.auth.first_factor = false; - return new RegistrationHandler(vars.logger).preValidationInit(req as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if userid is missing", function () { - req.session.auth.first_factor = false; - req.session.auth.userid = undefined; - - return new RegistrationHandler(vars.logger).preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here")); - }, - function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if email is missing", function () { - req.session.auth.first_factor = false; - req.session.auth.email = undefined; - - return new RegistrationHandler(vars.logger).preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here")); - }, - function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should succeed if first factor passed, userid and email are provided", function () { - req.session.auth.first_factor = true; - req.session.auth.email = "admin@example.com"; - req.session.auth.userid = "user"; - return new RegistrationHandler(vars.logger).preValidationInit(req as any); - }); - } -}); diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts deleted file mode 100644 index bc4713c77..000000000 --- a/themes/matrix/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts +++ /dev/null @@ -1,73 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import objectPath = require("object-path"); - -import { IdentityValidable } from "../../../../IdentityValidable"; -import { Identity } from "../../../../../../types/Identity"; -import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; -import FirstFactorValidator = require("../../../../FirstFactorValidator"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { IRequestLogger } from "../../../../logging/IRequestLogger"; - -const CHALLENGE = "u2f-register"; -const MAIL_SUBJECT = "Register your security key with Authelia"; - -const POST_VALIDATION_TEMPLATE_NAME = "u2f-register"; - - -export default class RegistrationHandler implements IdentityValidable { - private logger: IRequestLogger; - - constructor(logger: IRequestLogger) { - this.logger = logger; - } - - challenge(): string { - return CHALLENGE; - } - - private retrieveIdentity(req: express.Request): BluebirdPromise { - const that = this; - return new BluebirdPromise(function(resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, that.logger); - const userid = authSession.userid; - const email = authSession.email; - - if (!(userid && email)) { - return reject(new Error("User ID or email is missing")); - } - - const identity = { - email: email, - userid: userid - }; - return resolve(identity); - }); - } - - preValidationInit(req: express.Request): BluebirdPromise { - const that = this; - return FirstFactorValidator.validate(req, this.logger) - .then(function () { - return that.retrieveIdentity(req); - }); - } - - preValidationResponse(req: express.Request, res: express.Response) { - res.render(PRE_VALIDATION_TEMPLATE); - } - - postValidationInit(req: express.Request) { - return FirstFactorValidator.validate(req, this.logger); - } - - postValidationResponse(req: express.Request, res: express.Response) { - res.render(POST_VALIDATION_TEMPLATE_NAME); - } - - mailSubject(): string { - return MAIL_SUBJECT; - } -} - diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts deleted file mode 100644 index de3347a21..000000000 --- a/themes/matrix/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts +++ /dev/null @@ -1,146 +0,0 @@ - -import sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import assert = require("assert"); -import U2FRegisterPost = require("./post"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import ExpressMock = require("../../../../stubs/express.spec"); -import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; - - -describe("routes/secondfactor/u2f/register/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - let authSession: AuthenticationSession; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.originalUrl = "/api/xxxx"; - req.app = {}; - req.session = { - auth: { - userid: "user", - first_factor: true, - second_factor: false, - identity_check: { - challenge: "u2f-register", - userid: "user" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - }); - - describe("test registration", test_registration); - - - function test_registration() { - it("should save u2f meta and return status code 200", function () { - const expectedStatus = { - keyHandle: "keyHandle", - publicKey: "pbk", - certificate: "cert" - }; - mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve(expectedStatus)); - - authSession.register_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { - assert.equal("user", mocks.userDataStore.saveU2FRegistrationStub.getCall(0).args[0]); - assert.equal(authSession.identity_check, undefined); - }); - }); - - it("should return error message on finishRegistration error", function () { - mocks.u2f.checkRegistrationStub.returns({ errorCode: 500 }); - - authSession.register_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); - - it("should return error message when register_request is not provided", function () { - mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); - authSession.register_request = undefined; - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); - - it("should return error message when no auth request has been initiated", function () { - mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); - authSession.register_request = undefined; - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); - - it("should return error message when identity has not been verified", function () { - authSession.identity_check = undefined; - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); - } -}); - diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/register/post.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/register/post.ts deleted file mode 100644 index 7296ccbe5..000000000 --- a/themes/matrix/server/src/lib/routes/secondfactor/u2f/register/post.ts +++ /dev/null @@ -1,64 +0,0 @@ - -import { UserDataStore } from "../../../../storage/UserDataStore"; -import objectPath = require("object-path"); -import u2f_common = require("../U2FCommon"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import U2f = require("u2f"); -import { U2FRegistration } from "../../../../../../types/U2FRegistration"; -import redirect from "../../redirect"; -import ErrorReplies = require("../../../../ErrorReplies"); -import { ServerVariables } from "../../../../ServerVariables"; -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; - - -export default function (vars: ServerVariables) { - function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const appid = u2f_common.extract_app_id(req); - const registrationResponse: U2f.RegistrationData = req.body; - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - const registrationRequest = authSession.register_request; - - if (!registrationRequest) { - return reject(new Error("No registration request")); - } - - if (!authSession.identity_check - || authSession.identity_check.challenge != "u2f-register") { - return reject(new Error("Bad challenge for registration request")); - } - - vars.logger.info(req, "Finishing registration"); - vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); - vars.logger.debug(req, "RegistrationResponse = %s", JSON.stringify(registrationResponse)); - - return resolve(vars.u2f.checkRegistration(registrationRequest, registrationResponse)); - }) - .then(function (u2fResult: U2f.RegistrationResult | U2f.Error): BluebirdPromise { - if (objectPath.has(u2fResult, "errorCode")) - return BluebirdPromise.reject(new Error("Error while registering.")); - - const registrationResult: U2f.RegistrationResult = u2fResult as U2f.RegistrationResult; - vars.logger.info(req, "Store registration and reply"); - vars.logger.debug(req, "RegistrationResult = %s", JSON.stringify(registrationResult)); - const registration: U2FRegistration = { - keyHandle: registrationResult.keyHandle, - publicKey: registrationResult.publicKey - }; - return vars.userDataStore.saveU2FRegistration(authSession.userid, appid, registration); - }) - .then(function () { - authSession.identity_check = undefined; - redirect(vars)(req, res); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - return handler; -} diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts deleted file mode 100644 index a207c9109..000000000 --- a/themes/matrix/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ - -import sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import Assert = require("assert"); -import U2FRegisterRequestGet = require("./get"); -import ExpressMock = require("../../../../stubs/express.spec"); -import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; - -describe("routes/secondfactor/u2f/register_request/get", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.originalUrl = "/api/xxxx"; - req.app = {}; - req.session = { - auth: { - userid: "user", - first_factor: true, - second_factor: false, - identity_check: { - challenge: "u2f-register", - userid: "user" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - }); - - describe("test registration request", () => { - it("should send back the registration request and save it in the session", function () { - const expectedRequest = { - test: "abc" - }; - mocks.u2f.requestStub.returns(BluebirdPromise.resolve(expectedRequest)); - return U2FRegisterRequestGet.default(vars)(req as any, res as any) - .then(function () { - Assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); - }); - }); - - it("should return internal error on registration request", function () { - res.send = sinon.spy(); - const user_key_container = {}; - mocks.u2f.requestStub.returns(BluebirdPromise.reject("Internal error")); - return U2FRegisterRequestGet.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - }); - }); - - it("should return forbidden if identity has not been verified", function () { - req.session.auth.identity_check = undefined; - return U2FRegisterRequestGet.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(403, res.status.getCall(0).args[0]); - }); - }); - }); -}); - diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/register_request/get.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/register_request/get.ts deleted file mode 100644 index f611af933..000000000 --- a/themes/matrix/server/src/lib/routes/secondfactor/u2f/register_request/get.ts +++ /dev/null @@ -1,43 +0,0 @@ - -import { UserDataStore } from "../../../../storage/UserDataStore"; - -import objectPath = require("object-path"); -import u2f_common = require("../U2FCommon"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import U2f = require("u2f"); -import ErrorReplies = require("../../../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { ServerVariables } from "../../../../ServerVariables"; - -export default function (vars: ServerVariables) { - function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const appid: string = u2f_common.extract_app_id(req); - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - if (!authSession.identity_check - || authSession.identity_check.challenge != "u2f-register") { - res.status(403); - res.send(); - return reject(new Error("Bad challenge.")); - } - - vars.logger.info(req, "Starting registration for appId '%s'", appid); - return resolve(vars.u2f.request(appid)); - }) - .then(function (registrationRequest: U2f.Request) { - vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); - authSession.register_request = registrationRequest; - res.json(registrationRequest); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - - return handler; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts deleted file mode 100644 index 9b137e66d..000000000 --- a/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts +++ /dev/null @@ -1,101 +0,0 @@ - -import sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import Assert = require("assert"); -import U2FSignPost = require("./post"); -import { ServerVariables } from "../../../../ServerVariables"; -import winston = require("winston"); - -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; -import ExpressMock = require("../../../../stubs/express.spec"); -import U2FMock = require("../../../../stubs/u2f.spec"); -import U2f = require("u2f"); -import { Level } from "../../../../authentication/Level"; - -describe("routes/secondfactor/u2f/sign/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.app = {}; - req.originalUrl = "/api/xxxx"; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req.session = { - auth: { - userid: "user", - authentication_level: Level.ONE_FACTOR, - identity_check: { - challenge: "u2f-register", - userid: "user" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const options = { - inMemoryOnly: true - }; - - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - }); - - it("should return status code 204", function () { - const expectedStatus = { - keyHandle: "keyHandle", - publicKey: "pbk", - certificate: "cert" - }; - mocks.u2f.checkSignatureStub.returns(expectedStatus); - - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ - registration: { - publicKey: "PUBKEY" - } - })); - - req.session.auth.sign_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - return U2FSignPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(req.session.auth.authentication_level, Level.TWO_FACTOR); - }); - }); - - it("should return unauthorized error on registration request internal error", function () { - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ - registration: { - publicKey: "PUBKEY" - } - })); - mocks.u2f.checkSignatureStub.returns({ errorCode: 500 }); - - req.session.auth.sign_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - return U2FSignPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], - { error: "Operation failed." }); - }); - }); -}); - diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign/post.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign/post.ts deleted file mode 100644 index 7ee711c2c..000000000 --- a/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign/post.ts +++ /dev/null @@ -1,57 +0,0 @@ - -import objectPath = require("object-path"); -import u2f_common = require("../U2FCommon"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import { UserDataStore } from "../../../../storage/UserDataStore"; -import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; -import { Winston } from "../../../../../../types/Dependencies"; -import U2f = require("u2f"); -import exceptions = require("../../../../Exceptions"); -import redirect from "../../redirect"; -import ErrorReplies = require("../../../../ErrorReplies"); -import { ServerVariables } from "../../../../ServerVariables"; -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import { Level } from "../../../../authentication/Level"; - -export default function (vars: ServerVariables) { - function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const appId = u2f_common.extract_app_id(req); - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - if (!authSession.sign_request) { - const err = new Error("No sign request"); - ErrorReplies.replyWithError401(req, res, vars.logger)(err); - return reject(err); - } - resolve(); - }) - .then(function () { - const userid = authSession.userid; - return vars.userDataStore.retrieveU2FRegistration(userid, appId); - }) - .then(function (doc: U2FRegistrationDocument): BluebirdPromise { - const signRequest = authSession.sign_request; - const signData: U2f.SignatureData = req.body; - vars.logger.info(req, "Finish authentication"); - return BluebirdPromise.resolve(vars.u2f.checkSignature(signRequest, signData, doc.registration.publicKey)); - }) - .then(function (result: U2f.SignatureResult | U2f.Error): BluebirdPromise { - if (objectPath.has(result, "errorCode")) - return BluebirdPromise.reject(new Error("Error while signing")); - vars.logger.info(req, "Successful authentication"); - authSession.authentication_level = Level.TWO_FACTOR; - redirect(vars)(req, res); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - - return handler; -} - diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts deleted file mode 100644 index dd52b27e0..000000000 --- a/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ - -import sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import assert = require("assert"); -import U2FSignRequestGet = require("./get"); -import ExpressMock = require("../../../../stubs/express.spec"); -import { Request } from "u2f"; -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; - -import { SignMessage } from "../../../../../../../shared/SignMessage"; - -describe("routes/secondfactor/u2f/sign_request/get", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.originalUrl = "/api/xxxx"; - req.app = {}; - req.session = { - auth: { - userid: "user", - first_factor: true, - second_factor: false, - identity_check: { - challenge: "u2f-register", - userid: "user" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - }); - - it("should send back the sign request and save it in the session", function () { - const expectedRequest: Request = { - version: "U2F_V2", - appId: 'app', - challenge: 'challenge!' - }; - mocks.u2f.requestStub.returns(expectedRequest); - mocks.userDataStore.retrieveU2FRegistrationStub - .returns(BluebirdPromise.resolve({ - registration: { - keyHandle: "KeyHandle" - } - })); - - return U2FSignRequestGet.default(vars)(req as any, res as any) - .then(() => { - assert.deepEqual(expectedRequest, req.session.auth.sign_request); - assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); - }); - }); -}); - diff --git a/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts b/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts deleted file mode 100644 index 9e93dde06..000000000 --- a/themes/matrix/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts +++ /dev/null @@ -1,42 +0,0 @@ - -import u2f_common = require("../../../secondfactor/u2f/U2FCommon"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; -import exceptions = require("../../../../Exceptions"); -import ErrorReplies = require("../../../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { ServerVariables } from "../../../../ServerVariables"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; - -export default function (vars: ServerVariables) { - function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const appId = u2f_common.extract_app_id(req); - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - resolve(); - }) - .then(function () { - return vars.userDataStore.retrieveU2FRegistration(authSession.userid, appId); - }) - .then(function (doc: U2FRegistrationDocument): BluebirdPromise { - if (!doc) - return BluebirdPromise.reject(new exceptions.AccessDeniedError("No U2F registration document found.")); - - const appId: string = u2f_common.extract_app_id(req); - vars.logger.info(req, "Start authentication of app '%s'", appId); - vars.logger.debug(req, "AppId = %s, keyHandle = %s", appId, JSON.stringify(doc.registration.keyHandle)); - - const request = vars.u2f.request(appId, doc.registration.keyHandle); - authSession.sign_request = request; - res.json(request); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - return handler; -} diff --git a/themes/matrix/server/src/lib/routes/verify/access_control.ts b/themes/matrix/server/src/lib/routes/verify/access_control.ts deleted file mode 100644 index 136239aeb..000000000 --- a/themes/matrix/server/src/lib/routes/verify/access_control.ts +++ /dev/null @@ -1,51 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import Util = require("util"); - -import Exceptions = require("../../Exceptions"); - -import { Level as AuthorizationLevel } from "../../authorization/Level"; -import { Level as AuthenticationLevel } from "../../authentication/Level"; -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { ServerVariables } from "../../ServerVariables"; - -function isAuthorized( - authorization: AuthorizationLevel, - authentication: AuthenticationLevel): boolean { - - if (authorization == AuthorizationLevel.BYPASS) { - return true; - } else if (authorization == AuthorizationLevel.ONE_FACTOR && - authentication >= AuthenticationLevel.ONE_FACTOR) { - return true; - } else if (authorization == AuthorizationLevel.TWO_FACTOR && - authentication >= AuthenticationLevel.TWO_FACTOR) { - return true; - } - return false; -} - -export default function ( - req: Express.Request, - vars: ServerVariables, - domain: string, resource: string, - user: string, groups: string[], - authenticationLevel: AuthenticationLevel) { - - return new BluebirdPromise(function (resolve, reject) { - const authorizationLevel = vars.authorizer - .authorization({domain, resource}, {user, groups}); - - if (!isAuthorized(authorizationLevel, authenticationLevel)) { - if (authorizationLevel == AuthorizationLevel.DENY) { - reject(new Exceptions.NotAuthorizedError( - Util.format("User %s is not authorized to access %s%s", user, domain, resource))); - return; - } - reject(new Exceptions.NotAuthenticatedError(Util.format( - "User '%s' is not sufficiently authorized to access %s%s.", user, domain, resource))); - return; - } - resolve(); - }); -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/verify/get.spec.ts b/themes/matrix/server/src/lib/routes/verify/get.spec.ts deleted file mode 100644 index 67cf19fb6..000000000 --- a/themes/matrix/server/src/lib/routes/verify/get.spec.ts +++ /dev/null @@ -1,320 +0,0 @@ - -import Assert = require("assert"); -import BluebirdPromise = require("bluebird"); -import Express = require("express"); -import Sinon = require("sinon"); -import winston = require("winston"); - -import VerifyGet = require("./get"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../types/AuthenticationSession"; -import ExpressMock = require("../../stubs/express.spec"); -import { ServerVariables } from "../../ServerVariables"; -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../ServerVariablesMockBuilder.spec"; -import { Level } from "../../authentication/Level"; -import { Level as AuthorizationLevel } from "../../authorization/Level"; - -describe("routes/verify/get", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - let authSession: AuthenticationSession; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - res = ExpressMock.ResponseMock(); - req.originalUrl = "/api/xxxx"; - req.query = { - redirect: "undefined" - }; - AuthenticationSessionHandler.reset(req as any); - req.headers["x-original-url"] = "https://secret.example.com/"; - const s = ServerVariablesMockBuilder.build(false); - mocks = s.mocks; - vars = s.variables; - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - }); - - describe("with session cookie", function () { - it("should be already authenticated", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser"); - Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); - Assert.equal(204, res.status.getCall(0).args[0]); - }); - }); - - function test_session(_authSession: AuthenticationSession, status_code: number) { - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert.equal(status_code, res.status.getCall(0).args[0]); - }); - } - - function test_non_authenticated_401(authSession: AuthenticationSession) { - return test_session(authSession, 401); - } - - function test_unauthorized_403(authSession: AuthenticationSession) { - return test_session(authSession, 403); - } - - function test_authorized(authSession: AuthenticationSession) { - return test_session(authSession, 204); - } - - describe("given user tries to access a 2-factor endpoint", function () { - before(function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - }); - - describe("given different cases of session", function () { - it("should not be authenticated when second factor is missing", function () { - return test_non_authenticated_401({ - keep_me_logged_in: false, - userid: "user", - authentication_level: Level.ONE_FACTOR, - email: undefined, - groups: [], - last_activity_datetime: new Date().getTime() - }); - }); - - it("should not be authenticated when userid is missing", function () { - return test_non_authenticated_401({ - keep_me_logged_in: false, - userid: undefined, - authentication_level: Level.TWO_FACTOR, - email: undefined, - groups: [], - last_activity_datetime: new Date().getTime() - }); - }); - - it("should not be authenticated when level is insufficient", function () { - return test_non_authenticated_401({ - keep_me_logged_in: false, - userid: "user", - authentication_level: Level.NOT_AUTHENTICATED, - email: undefined, - groups: [], - last_activity_datetime: new Date().getTime() - }); - }); - - it("should not be authenticated when session has not be initiated", function () { - return test_non_authenticated_401(undefined); - }); - - it("should not be authenticated when domain is not allowed for user", function () { - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - req.headers["x-original-url"] = "https://test.example.com/"; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.DENY); - - return test_unauthorized_403({ - keep_me_logged_in: false, - authentication_level: Level.TWO_FACTOR, - userid: "user", - groups: ["group1", "group2"], - email: undefined, - last_activity_datetime: new Date().getTime() - }); - }); - }); - }); - - describe("given user tries to access a single factor endpoint", function () { - beforeEach(function () { - req.headers["x-original-url"] = "https://redirect.url/"; - }); - - it("should be authenticated when first factor is validated", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); - authSession.authentication_level = Level.ONE_FACTOR; - authSession.userid = "user1"; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWith(204)); - Assert(res.send.calledOnce); - }); - }); - - it("should be rejected with 401 when not authenticated", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); - authSession.authentication_level = Level.NOT_AUTHENTICATED; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWith(401)); - }); - }); - }); - - describe("inactivity period", function () { - it("should update last inactivity period on requests on /api/verify", function () { - mocks.config.session.inactivity = 200000; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - const currentTime = new Date().getTime() - 1000; - AuthenticationSessionHandler.reset(req as any); - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - authSession.last_activity_datetime = currentTime; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - return AuthenticationSessionHandler.get(req as any, vars.logger); - }) - .then(function (authSession) { - Assert(authSession.last_activity_datetime > currentTime); - }); - }); - - it("should reset session when max inactivity period has been reached", function () { - mocks.config.session.inactivity = 1; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - const currentTime = new Date().getTime() - 1000; - AuthenticationSessionHandler.reset(req as any); - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - authSession.last_activity_datetime = currentTime; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - return AuthenticationSessionHandler.get(req as any, vars.logger); - }) - .then(function (authSession) { - Assert.equal(authSession.authentication_level, Level.NOT_AUTHENTICATED); - Assert.equal(authSession.userid, undefined); - }); - }); - }); - }); - - describe("response type 401 | 302", function() { - it("should return error code 401", function() { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( - "Invalid credentials")); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should redirect to provided redirection url", function() { - const REDIRECT_URL = "http://redirection_url.com"; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( - "Invalid credentials")); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - req.query["rd"] = REDIRECT_URL; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.redirect.calledWithExactly(REDIRECT_URL)); - }); - }); - }); - - describe("with basic auth", function () { - it("should authenticate correctly", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.returns({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "john"); - Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); - Assert.equal(204, res.status.getCall(0).args[0]); - }); - }); - - it("should fail when endpoint is protected by two factors", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.config.access_control.rules = [{ - domain: "secret.example.com", - policy: "two_factor" - }]; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when base64 token is not valid", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic i_m*not_a_base64*token"; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when base64 token has not format user:psswd", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzOmJhZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when bad user password is provided", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( - "Invalid credentials")); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when resource is restricted", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - }); -}); - diff --git a/themes/matrix/server/src/lib/routes/verify/get.ts b/themes/matrix/server/src/lib/routes/verify/get.ts deleted file mode 100644 index f73861696..000000000 --- a/themes/matrix/server/src/lib/routes/verify/get.ts +++ /dev/null @@ -1,91 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Express = require("express"); -import Exceptions = require("../../Exceptions"); -import ErrorReplies = require("../../ErrorReplies"); -import { ServerVariables } from "../../ServerVariables"; -import GetWithSessionCookieMethod from "./get_session_cookie"; -import GetWithBasicAuthMethod from "./get_basic_auth"; -import Constants = require("../../../../../shared/constants"); -import ObjectPath = require("object-path"); - -import { AuthenticationSessionHandler } - from "../../AuthenticationSessionHandler"; -import { AuthenticationSession } - from "../../../../types/AuthenticationSession"; - -const REMOTE_USER = "Remote-User"; -const REMOTE_GROUPS = "Remote-Groups"; - - -function verifyWithSelectedMethod(req: Express.Request, res: Express.Response, - vars: ServerVariables, authSession: AuthenticationSession) - : () => BluebirdPromise<{ username: string, groups: string[] }> { - return function () { - const authorization: string = "" + req.headers["proxy-authorization"]; - if (authorization && authorization.startsWith("Basic ")) - return GetWithBasicAuthMethod(req, res, vars, authorization); - - return GetWithSessionCookieMethod(req, res, vars, authSession); - }; -} - -function setRedirectHeader(req: Express.Request, res: Express.Response) { - return function () { - const originalUrl = ObjectPath.get( - req, "headers.x-original-url"); - res.set("Redirect", originalUrl); - return BluebirdPromise.resolve(); - }; -} - -function setUserAndGroupsHeaders(res: Express.Response) { - return function (u: { username: string, groups: string[] }) { - res.setHeader(REMOTE_USER, u.username); - res.setHeader(REMOTE_GROUPS, u.groups.join(",")); - return BluebirdPromise.resolve(); - }; -} - -function replyWith200(res: Express.Response) { - return function () { - res.status(204); - res.send(); - }; -} - -function getRedirectParam(req: Express.Request) { - return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : undefined; -} - -export default function (vars: ServerVariables) { - return function (req: Express.Request, res: Express.Response) - : BluebirdPromise { - let authSession: AuthenticationSession; - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - resolve(); - }) - .then(setRedirectHeader(req, res)) - .then(verifyWithSelectedMethod(req, res, vars, authSession)) - .then(setUserAndGroupsHeaders(res)) - .then(replyWith200(res)) - // The user is authenticated but has restricted access -> 403 - .catch(Exceptions.NotAuthorizedError, - ErrorReplies.replyWithError403(req, res, vars.logger)) - .catch(Exceptions.NotAuthenticatedError, - ErrorReplies.replyWithError401(req, res, vars.logger)) - // The user is not yet authenticated -> 401 - .catch((err) => { - const redirectUrl = getRedirectParam(req); - if (redirectUrl) { - ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err); - } - else { - ErrorReplies.replyWithError401(req, res, vars.logger)(err); - } - }); - }; -} - diff --git a/themes/matrix/server/src/lib/routes/verify/get_basic_auth.ts b/themes/matrix/server/src/lib/routes/verify/get_basic_auth.ts deleted file mode 100644 index af23c76c9..000000000 --- a/themes/matrix/server/src/lib/routes/verify/get_basic_auth.ts +++ /dev/null @@ -1,55 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import ObjectPath = require("object-path"); -import { ServerVariables } from "../../ServerVariables"; -import { AuthenticationSession } - from "../../../../types/AuthenticationSession"; -import AccessControl from "./access_control"; -import { URLDecomposer } from "../../utils/URLDecomposer"; -import { Level } from "../../authentication/Level"; - -export default function (req: Express.Request, res: Express.Response, - vars: ServerVariables, authorizationHeader: string) - : BluebirdPromise<{ username: string, groups: string[] }> { - let username: string; - const uri = ObjectPath.get(req, "headers.x-original-url"); - const urlDecomposition = URLDecomposer.fromUrl(uri); - - return BluebirdPromise.resolve() - .then(() => { - const base64Re = new RegExp("^Basic ((?:[A-Za-z0-9+/]{4})*" + - "(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)$"); - const isTokenValidBase64 = base64Re.test(authorizationHeader); - - if (!isTokenValidBase64) { - return BluebirdPromise.reject(new Error("No valid base64 token found in the header")); - } - - const tokenMatches = authorizationHeader.match(base64Re); - const base64Token = tokenMatches[1]; - const decodedToken = Buffer.from(base64Token, "base64").toString(); - const splittedToken = decodedToken.split(":"); - - if (splittedToken.length != 2) { - return BluebirdPromise.reject(new Error( - "The authorization token is invalid. Expecting 'userid:password'")); - } - - username = splittedToken[0]; - const password = splittedToken[1]; - return vars.usersDatabase.checkUserPassword(username, password); - }) - .then(function (groupsAndEmails) { - return AccessControl(req, vars, urlDecomposition.domain, urlDecomposition.path, - username, groupsAndEmails.groups, Level.ONE_FACTOR) - .then(() => BluebirdPromise.resolve({ - username: username, - groups: groupsAndEmails.groups - })); - }) - .catch(function (err: Error) { - return BluebirdPromise.reject( - new Error("Unable to authenticate the user with basic auth. Cause: " - + err.message)); - }); -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/routes/verify/get_session_cookie.ts b/themes/matrix/server/src/lib/routes/verify/get_session_cookie.ts deleted file mode 100644 index 070344812..000000000 --- a/themes/matrix/server/src/lib/routes/verify/get_session_cookie.ts +++ /dev/null @@ -1,78 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import Util = require("util"); -import ObjectPath = require("object-path"); - -import Exceptions = require("../../Exceptions"); -import { Configuration } from "../../configuration/schema/Configuration"; -import { ServerVariables } from "../../ServerVariables"; -import { IRequestLogger } from "../../logging/IRequestLogger"; -import { AuthenticationSession } - from "../../../../types/AuthenticationSession"; -import { AuthenticationSessionHandler } - from "../../AuthenticationSessionHandler"; -import AccessControl from "./access_control"; -import { URLDecomposer } from "../../utils/URLDecomposer"; - -function verify_inactivity(req: Express.Request, - authSession: AuthenticationSession, - configuration: Configuration, logger: IRequestLogger) - : BluebirdPromise { - - // If inactivity is not specified, then inactivity timeout does not apply - if (!configuration.session.inactivity || authSession.keep_me_logged_in) { - return BluebirdPromise.resolve(); - } - - const lastActivityTime = authSession.last_activity_datetime; - const currentTime = new Date().getTime(); - authSession.last_activity_datetime = currentTime; - - const inactivityPeriodMs = currentTime - lastActivityTime; - logger.debug(req, "Inactivity period was %s s and max period was %s.", - inactivityPeriodMs / 1000, configuration.session.inactivity / 1000); - if (inactivityPeriodMs < configuration.session.inactivity) { - return BluebirdPromise.resolve(); - } - - logger.debug(req, "Session has been reset after too long inactivity period."); - AuthenticationSessionHandler.reset(req); - return BluebirdPromise.reject(new Error("Inactivity period exceeded.")); -} - -export default function (req: Express.Request, res: Express.Response, - vars: ServerVariables, authSession: AuthenticationSession) - : BluebirdPromise<{ username: string, groups: string[] }> { - - return BluebirdPromise.resolve() - .then(() => { - const username = authSession.userid; - const groups = authSession.groups; - - if (!authSession.userid) { - return BluebirdPromise.reject(new Exceptions.AccessDeniedError( - "userid is missing")); - } - - const originalUrl = ObjectPath.get( - req, "headers.x-original-url"); - const originalUri = - ObjectPath.get(req, "headers.x-original-uri"); - - const d = URLDecomposer.fromUrl(originalUrl); - vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", d.domain, - d.path, username, groups.join(",")); - return AccessControl(req, vars, d.domain, d.path, username, groups, - authSession.authentication_level); - }) - .then(() => { - return verify_inactivity(req, authSession, - vars.config, vars.logger); - }) - .then(() => { - return BluebirdPromise.resolve({ - username: authSession.userid, - groups: authSession.groups - }); - }); -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/AuthenticationTraceDocument.d.ts b/themes/matrix/server/src/lib/storage/AuthenticationTraceDocument.d.ts deleted file mode 100644 index 69818c055..000000000 --- a/themes/matrix/server/src/lib/storage/AuthenticationTraceDocument.d.ts +++ /dev/null @@ -1,6 +0,0 @@ - -export interface AuthenticationTraceDocument { - userId: string; - date: Date; - isAuthenticationSuccessful: boolean; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/CollectionFactoryFactory.ts b/themes/matrix/server/src/lib/storage/CollectionFactoryFactory.ts deleted file mode 100644 index 92b29abfa..000000000 --- a/themes/matrix/server/src/lib/storage/CollectionFactoryFactory.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ICollectionFactory } from "./ICollectionFactory"; -import { NedbCollectionFactory } from "./nedb/NedbCollectionFactory"; -import { MongoCollectionFactory } from "./mongo/MongoCollectionFactory"; -import { IMongoClient } from "../connectors/mongo/IMongoClient"; - - -export class CollectionFactoryFactory { - static createNedb(options: Nedb.DataStoreOptions): ICollectionFactory { - return new NedbCollectionFactory(options); - } - - static createMongo(client: IMongoClient): ICollectionFactory { - return new MongoCollectionFactory(client); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/CollectionFactoryStub.spec.ts b/themes/matrix/server/src/lib/storage/CollectionFactoryStub.spec.ts deleted file mode 100644 index 17f8bb021..000000000 --- a/themes/matrix/server/src/lib/storage/CollectionFactoryStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Sinon = require("sinon"); -import { ICollection } from "./ICollection"; -import { ICollectionFactory } from "./ICollectionFactory"; - -export class CollectionFactoryStub implements ICollectionFactory { - buildStub: Sinon.SinonStub; - - constructor() { - this.buildStub = Sinon.stub(); - } - - build(collectionName: string): ICollection { - return this.buildStub(collectionName); - } -} diff --git a/themes/matrix/server/src/lib/storage/CollectionStub.spec.ts b/themes/matrix/server/src/lib/storage/CollectionStub.spec.ts deleted file mode 100644 index 42895d672..000000000 --- a/themes/matrix/server/src/lib/storage/CollectionStub.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Sinon = require("sinon"); -import { ICollection } from "./ICollection"; - -export class CollectionStub implements ICollection { - findStub: Sinon.SinonStub; - findOneStub: Sinon.SinonStub; - updateStub: Sinon.SinonStub; - removeStub: Sinon.SinonStub; - insertStub: Sinon.SinonStub; - - constructor() { - this.findStub = Sinon.stub(); - this.findOneStub = Sinon.stub(); - this.updateStub = Sinon.stub(); - this.removeStub = Sinon.stub(); - this.insertStub = Sinon.stub(); - } - - find(filter: any, sortKeys: any, count: number): BluebirdPromise { - return this.findStub(filter, sortKeys, count); - } - - findOne(filter: any): BluebirdPromise { - return this.findOneStub(filter); - } - - update(filter: any, document: any, options: any): BluebirdPromise { - return this.updateStub(filter, document, options); - } - - remove(filter: any): BluebirdPromise { - return this.removeStub(filter); - } - - insert(document: any): BluebirdPromise { - return this.insertStub(document); - } -} diff --git a/themes/matrix/server/src/lib/storage/ICollection.d.ts b/themes/matrix/server/src/lib/storage/ICollection.d.ts deleted file mode 100644 index caa6c2a87..000000000 --- a/themes/matrix/server/src/lib/storage/ICollection.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* istanbul ignore next */ -import BluebirdPromise = require("bluebird"); - -/* istanbul ignore next */ -export interface ICollection { - find(query: any, sortKeys: any, count: number): BluebirdPromise; - findOne(query: any): BluebirdPromise; - update(query: any, updateQuery: any, options?: any): BluebirdPromise; - remove(query: any): BluebirdPromise; - insert(document: any): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/ICollectionFactory.d.ts b/themes/matrix/server/src/lib/storage/ICollectionFactory.d.ts deleted file mode 100644 index 39eb42c77..000000000 --- a/themes/matrix/server/src/lib/storage/ICollectionFactory.d.ts +++ /dev/null @@ -1,6 +0,0 @@ - -import { ICollection } from "./ICollection"; - -export interface ICollectionFactory { - build(collectionName: string): ICollection; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/IUserDataStore.d.ts b/themes/matrix/server/src/lib/storage/IUserDataStore.d.ts deleted file mode 100644 index 81df482aa..000000000 --- a/themes/matrix/server/src/lib/storage/IUserDataStore.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import { TOTPSecretDocument } from "./TOTPSecretDocument"; -import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; -import { U2FRegistration } from "../../../types/U2FRegistration"; -import { TOTPSecret } from "../../../types/TOTPSecret"; -import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; -import { IdentityValidationDocument } from "./IdentityValidationDocument"; - -export interface IUserDataStore { - saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise; - retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise; - - saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; - retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise; - - produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise; - consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise; - - saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise; - retrieveTOTPSecret(userId: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/IdentityValidationDocument.d.ts b/themes/matrix/server/src/lib/storage/IdentityValidationDocument.d.ts deleted file mode 100644 index e7fd7b3f4..000000000 --- a/themes/matrix/server/src/lib/storage/IdentityValidationDocument.d.ts +++ /dev/null @@ -1,7 +0,0 @@ - -export interface IdentityValidationDocument { - userId: string; - token: string; - challenge: string; - maxDate: Date; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/TOTPSecretDocument.d.ts b/themes/matrix/server/src/lib/storage/TOTPSecretDocument.d.ts deleted file mode 100644 index a6c0bf9e6..000000000 --- a/themes/matrix/server/src/lib/storage/TOTPSecretDocument.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { TOTPSecret } from "../../../types/TOTPSecret"; - -export interface TOTPSecretDocument { - userid: string; - secret: TOTPSecret; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/U2FRegistrationDocument.d.ts b/themes/matrix/server/src/lib/storage/U2FRegistrationDocument.d.ts deleted file mode 100644 index efec6cb1d..000000000 --- a/themes/matrix/server/src/lib/storage/U2FRegistrationDocument.d.ts +++ /dev/null @@ -1,8 +0,0 @@ - -import { U2FRegistration } from "../../../types/U2FRegistration"; - -export interface U2FRegistrationDocument { - userId: string; - appId: string; - registration: U2FRegistration; -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/UserDataStore.spec.ts b/themes/matrix/server/src/lib/storage/UserDataStore.spec.ts deleted file mode 100644 index 66fb85461..000000000 --- a/themes/matrix/server/src/lib/storage/UserDataStore.spec.ts +++ /dev/null @@ -1,264 +0,0 @@ - -import * as Assert from "assert"; -import * as Sinon from "sinon"; -import * as MockDate from "mockdate"; -import BluebirdPromise = require("bluebird"); - -import { UserDataStore } from "./UserDataStore"; -import { TOTPSecret } from "../../../types/TOTPSecret"; -import { U2FRegistration } from "../../../types/U2FRegistration"; -import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; -import { CollectionStub } from "./CollectionStub.spec"; -import { CollectionFactoryStub } from "./CollectionFactoryStub.spec"; - -describe("storage/UserDataStore", function () { - let factory: CollectionFactoryStub; - let collection: CollectionStub; - let userId: string; - let appId: string; - let totpSecret: TOTPSecret; - let u2fRegistration: U2FRegistration; - - beforeEach(function () { - factory = new CollectionFactoryStub(); - collection = new CollectionStub(); - - userId = "user"; - appId = "https://myappId"; - - totpSecret = { - ascii: "abc", - base32: "ABCDKZLEFZGREJK", - otpauth_url: "totp://test", - google_auth_qr: "dummy", - hex: "dummy", - qr_code_ascii: "dummy", - qr_code_base32: "dummy", - qr_code_hex: "dummy" - }; - - u2fRegistration = { - keyHandle: "KEY_HANDLE", - publicKey: "publickey" - }; - }); - - it("should correctly creates collections", function () { - new UserDataStore(factory); - - Assert.equal(4, factory.buildStub.callCount); - Assert(factory.buildStub.calledWith("authentication_traces")); - Assert(factory.buildStub.calledWith("identity_validation_tokens")); - Assert(factory.buildStub.calledWith("u2f_registrations")); - Assert(factory.buildStub.calledWith("totp_secrets")); - }); - - describe("TOTP secrets collection", function () { - it("should save a totp secret", function () { - factory.buildStub.returns(collection); - collection.updateStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.saveTOTPSecret(userId, totpSecret) - .then(function (doc) { - Assert(collection.updateStub.calledOnce); - Assert(collection.updateStub.calledWith({ userId: userId }, { - userId: userId, - secret: totpSecret - }, { upsert: true })); - return BluebirdPromise.resolve(); - }); - }); - - it("should retrieve a totp secret", function () { - factory.buildStub.returns(collection); - collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.retrieveTOTPSecret(userId) - .then(function (doc) { - Assert(collection.findOneStub.calledOnce); - Assert(collection.findOneStub.calledWith({ userId: userId })); - return BluebirdPromise.resolve(); - }); - }); - }); - - describe("U2F secrets collection", function () { - it("should save a U2F secret", function () { - factory.buildStub.returns(collection); - collection.updateStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.saveU2FRegistration(userId, appId, u2fRegistration) - .then(function (doc) { - Assert(collection.updateStub.calledOnce); - Assert(collection.updateStub.calledWith({ - userId: userId, - appId: appId - }, { - userId: userId, - appId: appId, - registration: u2fRegistration - }, { upsert: true })); - return BluebirdPromise.resolve(); - }); - }); - - it("should retrieve a U2F secret", function () { - factory.buildStub.returns(collection); - collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.retrieveU2FRegistration(userId, appId) - .then(function (doc) { - Assert(collection.findOneStub.calledOnce); - Assert(collection.findOneStub.calledWith({ - userId: userId, - appId: appId - })); - return BluebirdPromise.resolve(); - }); - }); - }); - - - describe("Regulator traces collection", function () { - it("should save a trace", function () { - factory.buildStub.returns(collection); - collection.insertStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.saveAuthenticationTrace(userId, true) - .then(function (doc) { - Assert(collection.insertStub.calledOnce); - Assert(collection.insertStub.calledWith({ - userId: userId, - date: Sinon.match.date, - isAuthenticationSuccessful: true - })); - return BluebirdPromise.resolve(); - }); - }); - - function should_retrieve_latest_authentication_traces(count: number) { - factory.buildStub.returns(collection); - collection.findStub.withArgs().returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.retrieveLatestAuthenticationTraces(userId, count) - .then(function (doc: AuthenticationTraceDocument[]) { - Assert(collection.findStub.calledOnce); - Assert(collection.findStub.calledWith({ - userId: userId, - }, { date: -1 }, count)); - return BluebirdPromise.resolve(); - }); - } - - it("should retrieve 3 latest failed authentication traces", function () { - should_retrieve_latest_authentication_traces(3); - }); - }); - - - describe("Identity validation collection", function () { - it("should save a identity validation token", function () { - factory.buildStub.returns(collection); - collection.insertStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - const maxAge = 400; - const token = "TOKEN"; - const challenge = "CHALLENGE"; - - return dataStore.produceIdentityValidationToken(userId, token, challenge, maxAge) - .then(function (doc) { - Assert(collection.insertStub.calledOnce); - Assert(collection.insertStub.calledWith({ - userId: userId, - token: token, - challenge: challenge, - maxDate: Sinon.match.date - })); - return BluebirdPromise.resolve(); - }); - }); - - it("should consume an identity token successfully", function () { - factory.buildStub.returns(collection); - - MockDate.set(100); - - const token = "TOKEN"; - const challenge = "CHALLENGE"; - - collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ - userId: "USER", - token: token, - challenge: challenge, - maxDate: new Date() - })); - collection.removeStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - MockDate.set(80); - - return dataStore.consumeIdentityValidationToken(token, challenge) - .then(function (doc) { - MockDate.reset(); - Assert(collection.findOneStub.calledOnce); - Assert(collection.findOneStub.calledWith({ - token: token, - challenge: challenge - })); - - Assert(collection.removeStub.calledOnce); - Assert(collection.removeStub.calledWith({ - token: token, - challenge: challenge - })); - return BluebirdPromise.resolve(); - }); - }); - - it("should consume an expired identity token", function () { - factory.buildStub.returns(collection); - - MockDate.set(0); - - const token = "TOKEN"; - const challenge = "CHALLENGE"; - - collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ - userId: "USER", - token: token, - challenge: challenge, - maxDate: new Date() - })); - - const dataStore = new UserDataStore(factory); - - MockDate.set(80000); - - return dataStore.consumeIdentityValidationToken(token, challenge) - .then(function () { return BluebirdPromise.reject(new Error("should not be here")); }) - .catch(function () { - MockDate.reset(); - Assert(collection.findOneStub.calledOnce); - Assert(collection.findOneStub.calledWith({ - token: token, - challenge: challenge - })); - return BluebirdPromise.resolve(); - }); - }); - }); -}); diff --git a/themes/matrix/server/src/lib/storage/UserDataStore.ts b/themes/matrix/server/src/lib/storage/UserDataStore.ts deleted file mode 100644 index 27b0cddbd..000000000 --- a/themes/matrix/server/src/lib/storage/UserDataStore.ts +++ /dev/null @@ -1,143 +0,0 @@ -import * as BluebirdPromise from "bluebird"; -import * as path from "path"; -import { IUserDataStore } from "./IUserDataStore"; -import { ICollection } from "./ICollection"; -import { ICollectionFactory } from "./ICollectionFactory"; -import { TOTPSecretDocument } from "./TOTPSecretDocument"; -import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; -import { U2FRegistration } from "../../../types/U2FRegistration"; -import { TOTPSecret } from "../../../types/TOTPSecret"; -import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; -import { IdentityValidationDocument } from "./IdentityValidationDocument"; - -// Constants - -const IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME = "identity_validation_tokens"; -const AUTHENTICATION_TRACES_COLLECTION_NAME = "authentication_traces"; - -const U2F_REGISTRATIONS_COLLECTION_NAME = "u2f_registrations"; -const TOTP_SECRETS_COLLECTION_NAME = "totp_secrets"; - - -export interface U2FRegistrationKey { - userId: string; - appId: string; -} - -// Source - -export class UserDataStore implements IUserDataStore { - private u2fSecretCollection: ICollection; - private identityCheckTokensCollection: ICollection; - private authenticationTracesCollection: ICollection; - private totpSecretCollection: ICollection; - - private collectionFactory: ICollectionFactory; - - constructor(collectionFactory: ICollectionFactory) { - this.collectionFactory = collectionFactory; - - this.u2fSecretCollection = this.collectionFactory.build(U2F_REGISTRATIONS_COLLECTION_NAME); - this.identityCheckTokensCollection = this.collectionFactory.build(IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME); - this.authenticationTracesCollection = this.collectionFactory.build(AUTHENTICATION_TRACES_COLLECTION_NAME); - this.totpSecretCollection = this.collectionFactory.build(TOTP_SECRETS_COLLECTION_NAME); - } - - saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { - const newDocument: U2FRegistrationDocument = { - userId: userId, - appId: appId, - registration: registration - }; - - const filter: U2FRegistrationKey = { - userId: userId, - appId: appId - }; - - return this.u2fSecretCollection.update(filter, newDocument, { upsert: true }); - } - - retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { - const filter: U2FRegistrationKey = { - userId: userId, - appId: appId - }; - return this.u2fSecretCollection.findOne(filter); - } - - saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { - const newDocument: AuthenticationTraceDocument = { - userId: userId, - date: new Date(), - isAuthenticationSuccessful: isAuthenticationSuccessful, - }; - - return this.authenticationTracesCollection.insert(newDocument); - } - - retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { - const q = { - userId: userId - }; - - return this.authenticationTracesCollection.find(q, { date: -1 }, count); - } - - produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { - const newDocument: IdentityValidationDocument = { - userId: userId, - token: token, - challenge: challenge, - maxDate: new Date(new Date().getTime() + maxAge) - }; - - return this.identityCheckTokensCollection.insert(newDocument); - } - - consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { - const that = this; - const filter = { - token: token, - challenge: challenge - }; - - let identityValidationDocument: IdentityValidationDocument; - - return this.identityCheckTokensCollection.findOne(filter) - .then(function (doc: IdentityValidationDocument) { - if (!doc) { - return BluebirdPromise.reject(new Error("Registration token does not exist")); - } - - identityValidationDocument = doc; - const current_date = new Date(); - if (current_date > doc.maxDate) - return BluebirdPromise.reject(new Error("Registration token is not valid anymore")); - - return that.identityCheckTokensCollection.remove(filter); - }) - .then(() => { - return BluebirdPromise.resolve(identityValidationDocument); - }); - } - - saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { - const doc = { - userId: userId, - secret: secret - }; - - const filter = { - userId: userId - }; - return this.totpSecretCollection.update(filter, doc, { upsert: true }); - } - - retrieveTOTPSecret(userId: string): BluebirdPromise { - const filter = { - userId: userId - }; - return this.totpSecretCollection.findOne(filter); - } -} diff --git a/themes/matrix/server/src/lib/storage/UserDataStoreStub.spec.ts b/themes/matrix/server/src/lib/storage/UserDataStoreStub.spec.ts deleted file mode 100644 index 5ea27a2de..000000000 --- a/themes/matrix/server/src/lib/storage/UserDataStoreStub.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); - -import { TOTPSecretDocument } from "./TOTPSecretDocument"; -import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; -import { U2FRegistration } from "../../../types/U2FRegistration"; -import { TOTPSecret } from "../../../types/TOTPSecret"; -import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; -import { IdentityValidationDocument } from "./IdentityValidationDocument"; -import { IUserDataStore } from "./IUserDataStore"; - -export class UserDataStoreStub implements IUserDataStore { - saveU2FRegistrationStub: Sinon.SinonStub; - retrieveU2FRegistrationStub: Sinon.SinonStub; - saveAuthenticationTraceStub: Sinon.SinonStub; - retrieveLatestAuthenticationTracesStub: Sinon.SinonStub; - produceIdentityValidationTokenStub: Sinon.SinonStub; - consumeIdentityValidationTokenStub: Sinon.SinonStub; - saveTOTPSecretStub: Sinon.SinonStub; - retrieveTOTPSecretStub: Sinon.SinonStub; - - constructor() { - this.saveU2FRegistrationStub = Sinon.stub(); - this.retrieveU2FRegistrationStub = Sinon.stub(); - this.saveAuthenticationTraceStub = Sinon.stub(); - this.retrieveLatestAuthenticationTracesStub = Sinon.stub(); - this.produceIdentityValidationTokenStub = Sinon.stub(); - this.consumeIdentityValidationTokenStub = Sinon.stub(); - this.saveTOTPSecretStub = Sinon.stub(); - this.retrieveTOTPSecretStub = Sinon.stub(); - } - - saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { - return this.saveU2FRegistrationStub(userId, appId, registration); - } - - retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { - return this.retrieveU2FRegistrationStub(userId, appId); - } - - saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { - return this.saveAuthenticationTraceStub(userId, isAuthenticationSuccessful); - } - - retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { - return this.retrieveLatestAuthenticationTracesStub(userId, count); - } - - produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { - return this.produceIdentityValidationTokenStub(userId, token, challenge, maxAge); - } - - consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { - return this.consumeIdentityValidationTokenStub(token, challenge); - } - - saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { - return this.saveTOTPSecretStub(userId, secret); - } - - retrieveTOTPSecret(userId: string): BluebirdPromise { - return this.retrieveTOTPSecretStub(userId); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/mongo/MongoCollection.spec.ts b/themes/matrix/server/src/lib/storage/mongo/MongoCollection.spec.ts deleted file mode 100644 index 74a773a1e..000000000 --- a/themes/matrix/server/src/lib/storage/mongo/MongoCollection.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -import Assert = require("assert"); -import Sinon = require("sinon"); -import MongoDB = require("mongodb"); -import BluebirdPromise = require("bluebird"); -import { MongoClientStub } from "../../connectors/mongo/MongoClientStub.spec"; -import { MongoCollection } from "./MongoCollection"; - -describe("storage/mongo/MongoCollection", function () { - let mongoCollectionStub: any; - let mongoClientStub: MongoClientStub; - let findStub: Sinon.SinonStub; - let findOneStub: Sinon.SinonStub; - let insertOneStub: Sinon.SinonStub; - let updateStub: Sinon.SinonStub; - let removeStub: Sinon.SinonStub; - let countStub: Sinon.SinonStub; - const COLLECTION_NAME = "collection"; - - before(function () { - mongoClientStub = new MongoClientStub(); - mongoCollectionStub = Sinon.createStubInstance(require("mongodb").Collection as any); - findStub = mongoCollectionStub.find as Sinon.SinonStub; - findOneStub = mongoCollectionStub.findOne as Sinon.SinonStub; - insertOneStub = mongoCollectionStub.insertOne as Sinon.SinonStub; - updateStub = mongoCollectionStub.update as Sinon.SinonStub; - removeStub = mongoCollectionStub.remove as Sinon.SinonStub; - countStub = mongoCollectionStub.count as Sinon.SinonStub; - mongoClientStub.collectionStub.returns( - BluebirdPromise.resolve(mongoCollectionStub) - ); - }); - - describe("find", function () { - it("should find a document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - findStub.returns({ - sort: Sinon.stub().returns({ - limit: Sinon.stub().returns({ - toArray: Sinon.stub().returns(BluebirdPromise.resolve([])) - }) - }) - }); - - return collection.find({ key: "KEY" }) - .then(function () { - Assert(findStub.calledWith({ key: "KEY" })); - }); - }); - }); - - describe("findOne", function () { - it("should find one document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - findOneStub.returns(BluebirdPromise.resolve({})); - - return collection.findOne({ key: "KEY" }) - .then(function () { - Assert(findOneStub.calledWith({ key: "KEY" })); - }); - }); - }); - - describe("insert", function () { - it("should insert a document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - insertOneStub.returns(BluebirdPromise.resolve({})); - - return collection.insert({ key: "KEY" }) - .then(function () { - Assert(insertOneStub.calledWith({ key: "KEY" })); - }); - }); - }); - - describe("update", function () { - it("should update a document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - updateStub.returns(BluebirdPromise.resolve({})); - - return collection.update({ key: "KEY" }, { key: "KEY", value: 1 }) - .then(function () { - Assert(updateStub.calledWith({ key: "KEY" }, { key: "KEY", value: 1 })); - }); - }); - }); - - describe("remove", function () { - it("should remove a document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - removeStub.returns(BluebirdPromise.resolve({})); - - return collection.remove({ key: "KEY" }) - .then(function () { - Assert(removeStub.calledWith({ key: "KEY" })); - }); - }); - }); - - describe("count", function () { - it("should count documents in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - countStub.returns(BluebirdPromise.resolve({})); - - return collection.count({ key: "KEY" }) - .then(function () { - Assert(countStub.calledWith({ key: "KEY" })); - }); - }); - }); -}); diff --git a/themes/matrix/server/src/lib/storage/mongo/MongoCollection.ts b/themes/matrix/server/src/lib/storage/mongo/MongoCollection.ts deleted file mode 100644 index 9771389f1..000000000 --- a/themes/matrix/server/src/lib/storage/mongo/MongoCollection.ts +++ /dev/null @@ -1,50 +0,0 @@ -import Bluebird = require("bluebird"); -import { ICollection } from "../ICollection"; -import MongoDB = require("mongodb"); -import { IMongoClient } from "../../connectors/mongo/IMongoClient"; - - -export class MongoCollection implements ICollection { - private mongoClient: IMongoClient; - private collectionName: string; - - constructor(collectionName: string, mongoClient: IMongoClient) { - this.collectionName = collectionName; - this.mongoClient = mongoClient; - } - - private collection(): Bluebird { - return this.mongoClient.collection(this.collectionName); - } - - find(query: any, sortKeys?: any, count?: number): Bluebird { - return this.collection() - .then((collection) => collection.find(query).sort(sortKeys).limit(count)) - .then((query) => query.toArray()); - } - - findOne(query: any): Bluebird { - return this.collection() - .then((collection) => collection.findOne(query)); - } - - update(query: any, updateQuery: any, options?: any): Bluebird { - return this.collection() - .then((collection) => collection.update(query, updateQuery, options)); - } - - remove(query: any): Bluebird { - return this.collection() - .then((collection) => collection.remove(query)); - } - - insert(document: any): Bluebird { - return this.collection() - .then((collection) => collection.insertOne(document)); - } - - count(query: any): Bluebird { - return this.collection() - .then((collection) => collection.count(query)); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts b/themes/matrix/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts deleted file mode 100644 index bd959cacb..000000000 --- a/themes/matrix/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Assert = require("assert"); -import Sinon = require("sinon"); -import { MongoClientStub } from "../../connectors/mongo/MongoClientStub.spec"; -import { MongoCollectionFactory } from "./MongoCollectionFactory"; - -describe("storage/mongo/MongoCollectionFactory", function () { - let mongoClient: MongoClientStub; - - before(function() { - mongoClient = new MongoClientStub(); - }); - - describe("create", function () { - it("should create a collection", function () { - const COLLECTION_NAME = "COLLECTION_NAME"; - - const factory = new MongoCollectionFactory(mongoClient); - Assert(factory.build(COLLECTION_NAME)); - }); - }); -}); diff --git a/themes/matrix/server/src/lib/storage/mongo/MongoCollectionFactory.ts b/themes/matrix/server/src/lib/storage/mongo/MongoCollectionFactory.ts deleted file mode 100644 index 14a8262c9..000000000 --- a/themes/matrix/server/src/lib/storage/mongo/MongoCollectionFactory.ts +++ /dev/null @@ -1,19 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import { ICollection } from "../ICollection"; -import { ICollectionFactory } from "../ICollectionFactory"; -import { MongoCollection } from "./MongoCollection"; -import path = require("path"); -import MongoDB = require("mongodb"); -import { IMongoClient } from "../../connectors/mongo/IMongoClient"; - -export class MongoCollectionFactory implements ICollectionFactory { - private mongoClient: IMongoClient; - - constructor(mongoClient: IMongoClient) { - this.mongoClient = mongoClient; - } - - build(collectionName: string): ICollection { - return new MongoCollection(collectionName, this.mongoClient); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/nedb/NedbCollection.spec.ts b/themes/matrix/server/src/lib/storage/nedb/NedbCollection.spec.ts deleted file mode 100644 index a69962b67..000000000 --- a/themes/matrix/server/src/lib/storage/nedb/NedbCollection.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -import Sinon = require("sinon"); -import Assert = require("assert"); - -import { NedbCollection } from "./NedbCollection"; - -describe("storage/nedb/NedbCollection", function () { - describe("insert", function () { - it("should insert one entry", function () { - const nedbOptions = { - inMemoryOnly: true - }; - const collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou" }); - - return collection.count({}).then(function (count: number) { - Assert.equal(1, count); - }); - }); - - it("should insert three entries", function () { - const nedbOptions = { - inMemoryOnly: true - }; - const collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou" }); - collection.insert({ key: "hello" }); - collection.insert({ key: "hey" }); - - return collection.count({}).then(function (count: number) { - Assert.equal(3, count); - }); - }); - }); - - describe("find", function () { - let collection: NedbCollection; - before(function () { - const nedbOptions = { - inMemoryOnly: true - }; - collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou", value: 1 }); - collection.insert({ key: "hello" }); - collection.insert({ key: "hey" }); - collection.insert({ key: "coucou", value: 2 }); - }); - - it("should find one hello", function () { - return collection.find({ key: "hello" }, { key: 1 }) - .then(function (docs: { key: string }[]) { - Assert.equal(1, docs.length); - Assert(docs[0].key == "hello"); - }); - }); - - it("should find two coucou", function () { - return collection.find({ key: "coucou" }, { value: 1 }) - .then(function (docs: { value: number }[]) { - Assert.equal(2, docs.length); - }); - }); - }); - - describe("findOne", function () { - let collection: NedbCollection; - before(function () { - const nedbOptions = { - inMemoryOnly: true - }; - collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou", value: 1 }); - collection.insert({ key: "coucou", value: 1 }); - collection.insert({ key: "coucou", value: 1 }); - collection.insert({ key: "coucou", value: 1 }); - }); - - it("should find two coucou", function () { - const doc = { key: "coucou", value: 1 }; - return collection.count(doc) - .then(function (count: number) { - Assert.equal(4, count); - return collection.findOne(doc); - }); - }); - }); - - describe("update", function () { - let collection: NedbCollection; - before(function () { - const nedbOptions = { - inMemoryOnly: true - }; - collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou", value: 1 }); - }); - - it("should update the value", function () { - return collection.update({ key: "coucou" }, { key: "coucou", value: 2 }, { multi: true }) - .then(function () { - return collection.find({ key: "coucou" }); - }) - .then(function (docs: { key: string, value: number }[]) { - Assert.equal(1, docs.length); - Assert.equal(2, docs[0].value); - }); - }); - }); - - describe("update", function () { - let collection: NedbCollection; - before(function () { - const nedbOptions = { - inMemoryOnly: true - }; - collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou" }); - collection.insert({ key: "hello" }); - }); - - it("should update the value", function () { - return collection.remove({ key: "coucou" }) - .then(function () { - return collection.count({}); - }) - .then(function (count: number) { - Assert.equal(1, count); - }); - }); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/nedb/NedbCollection.ts b/themes/matrix/server/src/lib/storage/nedb/NedbCollection.ts deleted file mode 100644 index 88a93ad05..000000000 --- a/themes/matrix/server/src/lib/storage/nedb/NedbCollection.ts +++ /dev/null @@ -1,47 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import { ICollection } from "../ICollection"; -import Nedb = require("nedb"); - -declare module "nedb" { - export class NedbAsync extends Nedb { - constructor(pathOrOptions?: string | Nedb.DataStoreOptions); - updateAsync(query: any, updateQuery: any, options?: Nedb.UpdateOptions): BluebirdPromise; - findOneAsync(query: any): BluebirdPromise; - insertAsync(newDoc: T): BluebirdPromise; - removeAsync(query: any): BluebirdPromise; - countAsync(query: any): BluebirdPromise; - } -} - -export class NedbCollection implements ICollection { - private collection: Nedb.NedbAsync; - - constructor(options: Nedb.DataStoreOptions) { - this.collection = BluebirdPromise.promisifyAll(new Nedb(options)) as Nedb.NedbAsync; - } - - find(query: any, sortKeys?: any, count?: number): BluebirdPromise { - const q = this.collection.find(query).sort(sortKeys).limit(count); - return BluebirdPromise.promisify(q.exec, { context: q })(); - } - - findOne(query: any): BluebirdPromise { - return this.collection.findOneAsync(query); - } - - update(query: any, updateQuery: any, options?: any): BluebirdPromise { - return this.collection.updateAsync(query, updateQuery, options); - } - - remove(query: any): BluebirdPromise { - return this.collection.removeAsync(query); - } - - insert(document: any): BluebirdPromise { - return this.collection.insertAsync(document); - } - - count(query: any): BluebirdPromise { - return this.collection.countAsync(query); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts b/themes/matrix/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts deleted file mode 100644 index da90c661f..000000000 --- a/themes/matrix/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Sinon = require("sinon"); -import Assert = require("assert"); - -import { NedbCollectionFactory } from "./NedbCollectionFactory"; - -describe("storage/nedb/NedbCollectionFactory", function() { - it("should create a nedb collection", function() { - const nedbOptions = { - inMemoryOnly: true - }; - const factory = new NedbCollectionFactory(nedbOptions); - - const collection = factory.build("mycollection"); - Assert(collection); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/storage/nedb/NedbCollectionFactory.ts b/themes/matrix/server/src/lib/storage/nedb/NedbCollectionFactory.ts deleted file mode 100644 index 49c4dc853..000000000 --- a/themes/matrix/server/src/lib/storage/nedb/NedbCollectionFactory.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ICollection } from "../ICollection"; -import { ICollectionFactory } from "../ICollectionFactory"; -import { NedbCollection } from "./NedbCollection"; -import path = require("path"); -import Nedb = require("nedb"); - -export interface NedbOptions { - inMemoryOnly?: boolean; - directory?: string; -} - -export class NedbCollectionFactory implements ICollectionFactory { - private options: Nedb.DataStoreOptions; - - constructor(options: Nedb.DataStoreOptions) { - this.options = options; - } - - build(collectionName: string): ICollection { - const datastoreOptions: Nedb.DataStoreOptions = { - inMemoryOnly: this.options.inMemoryOnly || false, - autoload: true, - filename: (this.options.filename) ? path.resolve(this.options.filename, collectionName) : undefined - }; - - return new NedbCollection(datastoreOptions); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/stubs/express.spec.ts b/themes/matrix/server/src/lib/stubs/express.spec.ts deleted file mode 100644 index 48f15d7e1..000000000 --- a/themes/matrix/server/src/lib/stubs/express.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ - -import sinon = require("sinon"); -import express = require("express"); - -export interface RequestMock { - app?: any; - body?: any; - session?: any; - headers?: any; - get?: any; - query?: any; - originalUrl: string; -} - -export interface ResponseMock { - send: sinon.SinonStub | sinon.SinonSpy; - sendStatus: sinon.SinonStub; - sendFile: sinon.SinonStub; - sendfile: sinon.SinonStub; - status: sinon.SinonStub | sinon.SinonSpy; - json: sinon.SinonStub | sinon.SinonSpy; - links: sinon.SinonStub; - jsonp: sinon.SinonStub; - download: sinon.SinonStub; - contentType: sinon.SinonStub; - type: sinon.SinonStub; - format: sinon.SinonStub; - attachment: sinon.SinonStub; - set: sinon.SinonStub; - header: sinon.SinonStub; - headersSent: boolean; - get: sinon.SinonStub; - clearCookie: sinon.SinonStub; - cookie: sinon.SinonStub; - location: sinon.SinonStub; - redirect: sinon.SinonStub | sinon.SinonSpy; - render: sinon.SinonStub | sinon.SinonSpy; - locals: sinon.SinonStub; - charset: string; - vary: sinon.SinonStub; - app: any; - write: sinon.SinonStub; - writeContinue: sinon.SinonStub; - writeHead: sinon.SinonStub; - statusCode: number; - statusMessage: string; - setHeader: sinon.SinonStub; - setTimeout: sinon.SinonStub; - sendDate: boolean; - getHeader: sinon.SinonStub; -} - -export function RequestMock(): RequestMock { - return { - originalUrl: "/non-api/xxx", - app: { - get: sinon.stub() - }, - headers: { - "x-forwarded-for": "127.0.0.1" - }, - session: {} - }; -} -export function ResponseMock(): ResponseMock { - return { - send: sinon.stub(), - status: sinon.stub(), - json: sinon.stub(), - sendStatus: sinon.stub(), - links: sinon.stub(), - jsonp: sinon.stub(), - sendFile: sinon.stub(), - sendfile: sinon.stub(), - download: sinon.stub(), - contentType: sinon.stub(), - type: sinon.stub(), - format: sinon.stub(), - attachment: sinon.stub(), - set: sinon.stub(), - header: sinon.stub(), - headersSent: true, - get: sinon.stub(), - clearCookie: sinon.stub(), - cookie: sinon.stub(), - location: sinon.stub(), - redirect: sinon.stub(), - render: sinon.stub(), - locals: sinon.stub(), - charset: "utf-8", - vary: sinon.stub(), - app: sinon.stub(), - write: sinon.stub(), - writeContinue: sinon.stub(), - writeHead: sinon.stub(), - statusCode: 200, - statusMessage: "message", - setHeader: sinon.stub(), - setTimeout: sinon.stub(), - sendDate: true, - getHeader: sinon.stub() - }; -} diff --git a/themes/matrix/server/src/lib/stubs/ldapjs.spec.ts b/themes/matrix/server/src/lib/stubs/ldapjs.spec.ts deleted file mode 100644 index 045c0e11b..000000000 --- a/themes/matrix/server/src/lib/stubs/ldapjs.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ - -import Sinon = require("sinon"); - -export class LdapjsMock { - createClientStub: sinon.SinonStub; - - constructor() { - this.createClientStub = Sinon.stub(); - } - - createClient(params: any) { - return this.createClientStub(params); - } -} - -export class LdapjsClientMock { - bindStub: sinon.SinonStub; - unbindStub: sinon.SinonStub; - searchStub: sinon.SinonStub; - modifyStub: sinon.SinonStub; - onStub: sinon.SinonStub; - - constructor() { - this.bindStub = Sinon.stub(); - this.unbindStub = Sinon.stub(); - this.searchStub = Sinon.stub(); - this.modifyStub = Sinon.stub(); - this.onStub = Sinon.stub(); - } - - bind() { - return this.bindStub(); - } - - unbind() { - return this.unbindStub(); - } - - search() { - return this.searchStub(); - } - - modify() { - return this.modifyStub(); - } - - on() { - return this.onStub(); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/stubs/speakeasy.spec.ts b/themes/matrix/server/src/lib/stubs/speakeasy.spec.ts deleted file mode 100644 index 023614dcd..000000000 --- a/themes/matrix/server/src/lib/stubs/speakeasy.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ - -import sinon = require("sinon"); - -export = { - totp: sinon.stub(), - generateSecret: sinon.stub() -}; diff --git a/themes/matrix/server/src/lib/stubs/u2f.spec.ts b/themes/matrix/server/src/lib/stubs/u2f.spec.ts deleted file mode 100644 index 234b28c11..000000000 --- a/themes/matrix/server/src/lib/stubs/u2f.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ - -import sinon = require("sinon"); - -export interface U2FMock { - request: sinon.SinonStub; - checkSignature: sinon.SinonStub; - checkRegistration: sinon.SinonStub; -} - -export function U2FMock(): U2FMock { - return { - request: sinon.stub(), - checkSignature: sinon.stub(), - checkRegistration: sinon.stub() - }; -} diff --git a/themes/matrix/server/src/lib/utils/HashGenerator.spec.ts b/themes/matrix/server/src/lib/utils/HashGenerator.spec.ts deleted file mode 100644 index f19619a65..000000000 --- a/themes/matrix/server/src/lib/utils/HashGenerator.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Assert = require("assert"); -import { HashGenerator } from "./HashGenerator"; - -describe("utils/HashGenerator", function () { - it("should compute correct ssha512 (password)", function () { - return HashGenerator.ssha512("password", 500000, "jgiCMRyGXzoqpxS3") - .then(function (hash: string) { - Assert.equal(hash, "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"); - }); - }); - - it("should compute correct ssha512 (test)", function () { - return HashGenerator.ssha512("test", 500000, "abcdefghijklmnop") - .then(function (hash: string) { - Assert.equal(hash, "{CRYPT}$6$rounds=500000$abcdefghijklmnop$sTlNGf0VO/HTQIOXemmaBbV28HUch/qhWOA1/4dsDj6CDQYhUgXbYSPL6gccAsWMr2zD5fFWwhKmPdG.yxphs."); - }); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/utils/HashGenerator.ts b/themes/matrix/server/src/lib/utils/HashGenerator.ts deleted file mode 100644 index e67de32b7..000000000 --- a/themes/matrix/server/src/lib/utils/HashGenerator.ts +++ /dev/null @@ -1,23 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import RandomString = require("randomstring"); -import Util = require("util"); -const crypt = require("crypt3"); - -export class HashGenerator { - static ssha512( - password: string, - rounds: number = 500000, - salt?: string): BluebirdPromise { - const saltSize = 16; - // $6 means SHA512 - const _salt = Util.format("$6$rounds=%d$%s", rounds, - (salt) ? salt : RandomString.generate(16)); - - const cryptAsync = BluebirdPromise.promisify(crypt); - - return cryptAsync(password, _salt) - .then(function (hash: string) { - return BluebirdPromise.resolve(Util.format("{CRYPT}%s", hash)); - }); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/utils/ObjectCloner.ts b/themes/matrix/server/src/lib/utils/ObjectCloner.ts deleted file mode 100644 index 3e125d749..000000000 --- a/themes/matrix/server/src/lib/utils/ObjectCloner.ts +++ /dev/null @@ -1,6 +0,0 @@ - -export class ObjectCloner { - static clone(obj: any): any { - return JSON.parse(JSON.stringify(obj)); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/utils/SafeRedirection.spec.ts b/themes/matrix/server/src/lib/utils/SafeRedirection.spec.ts deleted file mode 100644 index 4126949fd..000000000 --- a/themes/matrix/server/src/lib/utils/SafeRedirection.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import Assert = require("assert"); -import Sinon = require("sinon"); -import { SafeRedirector } from "./SafeRedirection"; - -describe("web_server/middlewares/SafeRedirection", () => { - describe("Url is in protected domain", () => { - before(() => { - this.redirector = new SafeRedirector("example.com"); - this.res = {redirect: Sinon.stub()}; - }); - - it("should redirect to provided url", () => { - this.redirector.redirectOrElse(this.res, - "https://mysubdomain.example.com:8080/abc", - "https://authelia.example.com"); - Assert(this.res.redirect.calledWith("https://mysubdomain.example.com:8080/abc")); - }); - - it("should redirect to default url when wrong domain", () => { - this.redirector.redirectOrElse(this.res, - "https://mysubdomain.domain.rtf:8080/abc", - "https://authelia.example.com"); - Assert(this.res.redirect.calledWith("https://authelia.example.com")); - }); - - it("should redirect to default url when not terminating by domain", () => { - this.redirector.redirectOrElse(this.res, - "https://mysubdomain.example.com.rtf:8080/abc", - "https://authelia.example.com"); - Assert(this.res.redirect.calledWith("https://authelia.example.com")); - }); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/utils/SafeRedirection.ts b/themes/matrix/server/src/lib/utils/SafeRedirection.ts deleted file mode 100644 index 9e6a32e0c..000000000 --- a/themes/matrix/server/src/lib/utils/SafeRedirection.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Express = require("express"); -import { DomainExtractor } from "../../../../shared/DomainExtractor"; -import { BelongToDomain } from "../../../../shared/BelongToDomain"; - - -export class SafeRedirector { - private domain: string; - - constructor(domain: string) { - this.domain = domain; - } - - redirectOrElse( - res: Express.Response, - url: string, - defaultUrl: string): void { - if (BelongToDomain(url, this.domain)) { - res.redirect(url); - } - res.redirect(defaultUrl); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/utils/URLDecomposer.spec.ts b/themes/matrix/server/src/lib/utils/URLDecomposer.spec.ts deleted file mode 100644 index cbb038738..000000000 --- a/themes/matrix/server/src/lib/utils/URLDecomposer.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { URLDecomposer } from "./URLDecomposer"; -import Assert = require("assert"); - -describe("utils/URLDecomposer", function () { - describe("test fromUrl", function () { - it("should return domain from https url", function () { - const d = URLDecomposer.fromUrl("https://www.example.com/test/abc"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/test/abc"); - }); - - it("should return domain from http url", function () { - const d = URLDecomposer.fromUrl("http://www.example.com/test/abc"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/test/abc"); - }); - - it("should return domain when url contains port", function () { - const d = URLDecomposer.fromUrl("https://www.example.com:8080/test/abc"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/test/abc"); - }); - - it("should return default path when no path provided", function () { - const d = URLDecomposer.fromUrl("https://www.example.com:8080"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/"); - }); - - it("should return default path when provided", function () { - const d = URLDecomposer.fromUrl("https://www.example.com:8080/"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/"); - }); - - it("should return undefined when does not match", function () { - const d = URLDecomposer.fromUrl("https:///abc/test"); - Assert.equal(d, undefined); - }); - - it("should return undefined when does not match", function () { - const d = URLDecomposer.fromUrl("https:///abc/test"); - Assert.equal(d, undefined); - }); - }); -}); \ No newline at end of file diff --git a/themes/matrix/server/src/lib/utils/URLDecomposer.ts b/themes/matrix/server/src/lib/utils/URLDecomposer.ts deleted file mode 100644 index 9bdf2e9d2..000000000 --- a/themes/matrix/server/src/lib/utils/URLDecomposer.ts +++ /dev/null @@ -1,15 +0,0 @@ -export class URLDecomposer { - static fromUrl(url: string): {domain: string, path: string} { - if (!url) return; - const match = url.match(/https?:\/\/([a-z0-9_.-]+)(:[0-9]+)?(.*)/); - - if (!match) return; - - if (match[1] && !match[3]) { - return {domain: match[1], path: "/"}; - } else if (match[1] && match[3]) { - return {domain: match[1], path: match[3]}; - } - return; - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/web_server/Configurator.ts b/themes/matrix/server/src/lib/web_server/Configurator.ts deleted file mode 100644 index 6e404874a..000000000 --- a/themes/matrix/server/src/lib/web_server/Configurator.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Configuration } from "../configuration/schema/Configuration"; -import { GlobalDependencies } from "../../../types/Dependencies"; -import { SessionConfigurationBuilder } from - "../configuration/SessionConfigurationBuilder"; -import Path = require("path"); -import Express = require("express"); -import * as BodyParser from "body-parser"; -import { RestApi } from "./RestApi"; -import { WithHeadersLogged } from "./middlewares/WithHeadersLogged"; -import { ServerVariables } from "../ServerVariables"; -import Helmet = require("helmet"); - -const addRequestId = require("express-request-id")(); - -// Constants -const TRUST_PROXY = "trust proxy"; -const X_POWERED_BY = "x-powered-by"; -const VIEWS = "views"; -const VIEW_ENGINE = "view engine"; -const PUG = "pug"; - -export class Configurator { - static configure(config: Configuration, - app: Express.Application, - vars: ServerVariables, - deps: GlobalDependencies): void { - const viewsDirectory = Path.resolve(__dirname, "../../views"); - const publicHtmlDirectory = Path.resolve(__dirname, "../../public_html"); - - const expressSessionOptions = SessionConfigurationBuilder.build(config, deps); - - app.use(Express.static(publicHtmlDirectory)); - app.use(BodyParser.urlencoded({ extended: false })); - app.use(BodyParser.json()); - app.use(deps.session(expressSessionOptions)); - app.use(addRequestId); - app.use(WithHeadersLogged.middleware(vars.logger)); - app.disable(X_POWERED_BY); - app.enable(TRUST_PROXY); - app.use(Helmet()); - - app.set(VIEWS, viewsDirectory); - app.set(VIEW_ENGINE, PUG); - - RestApi.setup(app, vars); - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/web_server/RestApi.ts b/themes/matrix/server/src/lib/web_server/RestApi.ts deleted file mode 100644 index 9144a15b9..000000000 --- a/themes/matrix/server/src/lib/web_server/RestApi.ts +++ /dev/null @@ -1,125 +0,0 @@ -import Express = require("express"); - -import FirstFactorGet = require("../routes/firstfactor/get"); -import SecondFactorGet = require("../routes/secondfactor/get"); - -import FirstFactorPost = require("../routes/firstfactor/post"); -import LogoutGet = require("../routes/logout/get"); -import VerifyGet = require("../routes/verify/get"); -import TOTPSignGet = require("../routes/secondfactor/totp/sign/post"); - -import IdentityCheckMiddleware = require("../IdentityCheckMiddleware"); - -import TOTPRegistrationIdentityHandler from "../routes/secondfactor/totp/identity/RegistrationHandler"; -import U2FRegistrationIdentityHandler from "../routes/secondfactor/u2f/identity/RegistrationHandler"; -import ResetPasswordIdentityHandler from "../routes/password-reset/identity/PasswordResetHandler"; - -import U2FSignPost = require("../routes/secondfactor/u2f/sign/post"); -import U2FSignRequestGet = require("../routes/secondfactor/u2f/sign_request/get"); - -import U2FRegisterPost = require("../routes/secondfactor/u2f/register/post"); -import U2FRegisterRequestGet = require("../routes/secondfactor/u2f/register_request/get"); - -import ResetPasswordFormPost = require("../routes/password-reset/form/post"); -import ResetPasswordRequestPost = require("../routes/password-reset/request/get"); - -import Error401Get = require("../routes/error/401/get"); -import Error403Get = require("../routes/error/403/get"); -import Error404Get = require("../routes/error/404/get"); - -import LoggedIn = require("../routes/loggedin/get"); - -import { ServerVariables } from "../ServerVariables"; -import Endpoints = require("../../../../shared/api"); -import { RequireValidatedFirstFactor } from "./middlewares/RequireValidatedFirstFactor"; - -function setupTotp(app: Express.Application, vars: ServerVariables) { - app.post(Endpoints.SECOND_FACTOR_TOTP_POST, - RequireValidatedFirstFactor.middleware(vars.logger), - TOTPSignGet.default(vars)); - - app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, - RequireValidatedFirstFactor.middleware(vars.logger)); - - app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, - RequireValidatedFirstFactor.middleware(vars.logger)); - - IdentityCheckMiddleware.register(app, - Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, - Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, - new TOTPRegistrationIdentityHandler(vars.logger, - vars.userDataStore, vars.totpHandler, vars.config.totp), - vars); -} - -function setupU2f(app: Express.Application, vars: ServerVariables) { - app.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, - RequireValidatedFirstFactor.middleware(vars.logger), - U2FSignRequestGet.default(vars)); - - app.post(Endpoints.SECOND_FACTOR_U2F_SIGN_POST, - RequireValidatedFirstFactor.middleware(vars.logger), - U2FSignPost.default(vars)); - - app.get(Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, - RequireValidatedFirstFactor.middleware(vars.logger), - U2FRegisterRequestGet.default(vars)); - - app.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, - RequireValidatedFirstFactor.middleware(vars.logger), - U2FRegisterPost.default(vars)); - - app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, - RequireValidatedFirstFactor.middleware(vars.logger)); - - app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, - RequireValidatedFirstFactor.middleware(vars.logger)); - - IdentityCheckMiddleware.register(app, - Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, - Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, - new U2FRegistrationIdentityHandler(vars.logger), vars); -} - -function setupResetPassword(app: Express.Application, vars: ServerVariables) { - IdentityCheckMiddleware.register(app, - Endpoints.RESET_PASSWORD_IDENTITY_START_GET, - Endpoints.RESET_PASSWORD_IDENTITY_FINISH_GET, - new ResetPasswordIdentityHandler(vars.logger, vars.usersDatabase), - vars); - - app.get(Endpoints.RESET_PASSWORD_REQUEST_GET, - ResetPasswordRequestPost.default); - app.post(Endpoints.RESET_PASSWORD_FORM_POST, - ResetPasswordFormPost.default(vars)); -} - -function setupErrors(app: Express.Application, vars: ServerVariables) { - app.get(Endpoints.ERROR_401_GET, Error401Get.default(vars)); - app.get(Endpoints.ERROR_403_GET, Error403Get.default(vars)); - app.get(Endpoints.ERROR_404_GET, Error404Get.default); -} - -export class RestApi { - static setup(app: Express.Application, vars: ServerVariables): void { - app.get(Endpoints.FIRST_FACTOR_GET, FirstFactorGet.default(vars)); - - app.get(Endpoints.SECOND_FACTOR_GET, - RequireValidatedFirstFactor.middleware(vars.logger), - SecondFactorGet.default(vars)); - - app.get(Endpoints.LOGOUT_GET, LogoutGet.default(vars)); - - app.get(Endpoints.VERIFY_GET, VerifyGet.default(vars)); - app.post(Endpoints.FIRST_FACTOR_POST, FirstFactorPost.default(vars)); - - setupTotp(app, vars); - setupU2f(app, vars); - setupResetPassword(app, vars); - setupErrors(app, vars); - - app.get(Endpoints.LOGGED_IN, - RequireValidatedFirstFactor.middleware(vars.logger), - LoggedIn.default(vars)); - } -} diff --git a/themes/matrix/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts b/themes/matrix/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts deleted file mode 100644 index ecfd75765..000000000 --- a/themes/matrix/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts +++ /dev/null @@ -1,27 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import ErrorReplies = require("../../ErrorReplies"); -import { IRequestLogger } from "../../logging/IRequestLogger"; -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import Exceptions = require("../../Exceptions"); -import { Level } from "../../authentication/Level"; - -export class RequireValidatedFirstFactor { - static middleware(logger: IRequestLogger) { - return function (req: Express.Request, res: Express.Response, - next: Express.NextFunction): BluebirdPromise { - - 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.")); - - next(); - resolve(); - }) - .catch(ErrorReplies.replyWithError401(req, res, logger)); - }; - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/lib/web_server/middlewares/WithHeadersLogged.ts b/themes/matrix/server/src/lib/web_server/middlewares/WithHeadersLogged.ts deleted file mode 100644 index 139db1141..000000000 --- a/themes/matrix/server/src/lib/web_server/middlewares/WithHeadersLogged.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Express = require("express"); -import { IRequestLogger } from "../../logging/IRequestLogger"; - -export class WithHeadersLogged { - static middleware(logger: IRequestLogger) { - return function (req: Express.Request, res: Express.Response, - next: Express.NextFunction): void { - logger.debug(req, "Headers = %s", JSON.stringify(req.headers)); - next(); - }; - } -} \ No newline at end of file diff --git a/themes/matrix/server/src/resources/email-template.ejs b/themes/matrix/server/src/resources/email-template.ejs old mode 100644 new mode 100755 diff --git a/themes/matrix/server/src/views/already-logged-in.pug b/themes/matrix/server/src/views/already-logged-in.pug old mode 100644 new mode 100755 diff --git a/themes/matrix/server/src/views/errors/.directory b/themes/matrix/server/src/views/errors/.directory old mode 100644 new mode 100755 diff --git a/themes/matrix/server/src/views/errors/401.pug b/themes/matrix/server/src/views/errors/401.pug old mode 100644 new mode 100755 diff --git a/themes/matrix/server/src/views/errors/403.pug b/themes/matrix/server/src/views/errors/403.pug old mode 100644 new mode 100755 diff --git a/themes/matrix/server/src/views/errors/404.pug b/themes/matrix/server/src/views/errors/404.pug old mode 100644 new mode 100755 diff --git a/themes/matrix/server/src/views/firstfactor.pug b/themes/matrix/server/src/views/firstfactor.pug old mode 100644 new mode 100755 diff --git a/themes/matrix/server/src/views/layout/layout.pug b/themes/matrix/server/src/views/layout/layout.pug old mode 100644 new mode 100755 diff --git a/themes/matrix/server/src/views/need-identity-validation.pug b/themes/matrix/server/src/views/need-identity-validation.pug old mode 100644 new mode 100755 diff --git a/themes/matrix/server/src/views/password-reset-form.pug b/themes/matrix/server/src/views/password-reset-form.pug old mode 100644 new mode 100755 diff --git a/themes/matrix/server/src/views/password-reset-request.pug b/themes/matrix/server/src/views/password-reset-request.pug old mode 100644 new mode 100755 diff --git a/themes/matrix/server/src/views/secondfactor.pug b/themes/matrix/server/src/views/secondfactor.pug old mode 100644 new mode 100755 diff --git a/themes/matrix/server/src/views/totp-register.pug b/themes/matrix/server/src/views/totp-register.pug old mode 100644 new mode 100755 diff --git a/themes/matrix/server/src/views/u2f-register.pug b/themes/matrix/server/src/views/u2f-register.pug old mode 100644 new mode 100755 diff --git a/themes/matrix/server/test/requests.ts b/themes/matrix/server/test/requests.ts deleted file mode 100644 index 93fa0de47..000000000 --- a/themes/matrix/server/test/requests.ts +++ /dev/null @@ -1,94 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import request = require("request"); -import assert = require("assert"); -import express = require("express"); -import nodemailer = require("nodemailer"); -import Endpoints = require("../../shared/api"); - -declare module "request" { - export interface RequestAPI { - getAsync(uri: string, options?: RequiredUriUrl): BluebirdPromise; - getAsync(uri: string): BluebirdPromise; - getAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; - - postAsync(uri: string, options?: CoreOptions): BluebirdPromise; - postAsync(uri: string): BluebirdPromise; - postAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; - } -} - -const requestAsync: typeof request = BluebirdPromise.promisifyAll(request) as typeof request; - -export = function (port: number) { - const PORT = port; - const BASE_URL = "http://localhost:" + PORT; - - function execute_totp(jar: request.CookieJar, token: string) { - return requestAsync.postAsync({ - url: BASE_URL + Endpoints.SECOND_FACTOR_TOTP_POST, - jar: jar, - form: { - token: token - } - }); - } - - function execute_u2f_authentication(jar: request.CookieJar) { - return requestAsync.getAsync({ - url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, - jar: jar - }) - .then(function (res: request.RequestResponse) { - assert.equal(res.statusCode, 200); - return requestAsync.postAsync({ - url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_POST, - jar: jar, - form: { - } - }); - }); - } - - function execute_verification(jar: request.CookieJar) { - return requestAsync.getAsync({ url: BASE_URL + Endpoints.VERIFY_GET, jar: jar }); - } - - function execute_login(jar: request.CookieJar) { - return requestAsync.getAsync({ url: BASE_URL + Endpoints.FIRST_FACTOR_GET, jar: jar }); - } - - function execute_first_factor(jar: request.CookieJar) { - return requestAsync.postAsync({ - url: BASE_URL + Endpoints.FIRST_FACTOR_POST, - jar: jar, - form: { - username: "test_ok", - password: "password" - } - }); - } - - function execute_failing_first_factor(jar: request.CookieJar) { - return requestAsync.postAsync({ - url: BASE_URL + Endpoints.FIRST_FACTOR_POST, - jar: jar, - form: { - username: "test_nok", - password: "password" - } - }); - } - - return { - login: execute_login, - verify: execute_verification, - u2f_authentication: execute_u2f_authentication, - first_factor: execute_first_factor, - failing_first_factor: execute_failing_first_factor, - totp: execute_totp, - }; -}; - diff --git a/themes/matrix/server/tsconfig.json b/themes/matrix/server/tsconfig.json deleted file mode 100644 index ebe98c5ed..000000000 --- a/themes/matrix/server/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "moduleResolution": "node", - "noImplicitAny": true, - "sourceMap": true, - "removeComments": true, - "outDir": "../dist", - "baseUrl": ".", - "paths": { - "*": [ - "./types/*", - "../shared/types/*" - ] - } - }, - "exclude": [ - "src/**/*.spec.ts" - ] -} diff --git a/themes/matrix/server/tslint.json b/themes/matrix/server/tslint.json deleted file mode 100644 index c2c1b7501..000000000 --- a/themes/matrix/server/tslint.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "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 - } -} diff --git a/themes/matrix/server/types/.directory b/themes/matrix/server/types/.directory deleted file mode 100644 index 1e65000ea..000000000 --- a/themes/matrix/server/types/.directory +++ /dev/null @@ -1,4 +0,0 @@ -[Dolphin] -Timestamp=2018,12,17,20,58,27 -Version=3 -ViewMode=1 diff --git a/themes/matrix/server/types/AuthenticationSession.ts b/themes/matrix/server/types/AuthenticationSession.ts deleted file mode 100644 index bbed0e715..000000000 --- a/themes/matrix/server/types/AuthenticationSession.ts +++ /dev/null @@ -1,18 +0,0 @@ -import U2f = require("u2f"); -import { Level } from "../src/lib/authentication/Level"; - -export interface AuthenticationSession { - userid: string; - authentication_level: Level; - keep_me_logged_in: boolean; - last_activity_datetime: number; - identity_check?: { - challenge: string; - userid: string; - }; - register_request?: U2f.Request; - sign_request?: U2f.Request; - email: string; - groups: string[]; - redirect?: string; -} \ No newline at end of file diff --git a/themes/matrix/server/types/Dependencies.ts b/themes/matrix/server/types/Dependencies.ts deleted file mode 100644 index f20404dbc..000000000 --- a/themes/matrix/server/types/Dependencies.ts +++ /dev/null @@ -1,29 +0,0 @@ -import winston = require("winston"); -import speakeasy = require("speakeasy"); -import nodemailer = require("nodemailer"); -import session = require("express-session"); -import nedb = require("nedb"); -import ldapjs = require("ldapjs"); -import u2f = require("u2f"); -import RedisSession = require("connect-redis"); -import Redis = require("redis"); - -export type Speakeasy = typeof speakeasy; -export type Winston = typeof winston; -export type Session = typeof session; -export type Nedb = typeof nedb; -export type Ldapjs = typeof ldapjs; -export type U2f = typeof u2f; -export type ConnectRedis = typeof RedisSession; -export type Redis = typeof Redis; - -export interface GlobalDependencies { - u2f: U2f; - ldapjs: Ldapjs; - session: Session; - Redis: Redis; - ConnectRedis: ConnectRedis; - winston: Winston; - speakeasy: Speakeasy; - nedb: Nedb; -} \ No newline at end of file diff --git a/themes/matrix/server/types/Identity.ts b/themes/matrix/server/types/Identity.ts deleted file mode 100644 index e985984e2..000000000 --- a/themes/matrix/server/types/Identity.ts +++ /dev/null @@ -1,6 +0,0 @@ - - -export interface Identity { - userid: string; - email: string; -} \ No newline at end of file diff --git a/themes/matrix/server/types/TOTPSecret.ts b/themes/matrix/server/types/TOTPSecret.ts deleted file mode 100644 index d6775f2f0..000000000 --- a/themes/matrix/server/types/TOTPSecret.ts +++ /dev/null @@ -1,11 +0,0 @@ - -export interface TOTPSecret { - ascii: string; - hex: string; - base32: string; - qr_code_ascii: string; - qr_code_hex: string; - qr_code_base32: string; - google_auth_qr: string; - otpauth_url: string; - } \ No newline at end of file diff --git a/themes/matrix/server/types/U2FRegistration.ts b/themes/matrix/server/types/U2FRegistration.ts deleted file mode 100644 index b6080af07..000000000 --- a/themes/matrix/server/types/U2FRegistration.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface U2FRegistration { - keyHandle: string; - publicKey: string; -} \ No newline at end of file diff --git a/themes/matrix/server/types/dovehash.d.ts b/themes/matrix/server/types/dovehash.d.ts deleted file mode 100644 index c354609c0..000000000 --- a/themes/matrix/server/types/dovehash.d.ts +++ /dev/null @@ -1,4 +0,0 @@ - -declare module "dovehash" { - function encode(algo: string, text: string): string; -} \ No newline at end of file diff --git a/themes/matrix/server/types/speakeasy.d.ts b/themes/matrix/server/types/speakeasy.d.ts deleted file mode 100644 index 6ea06948b..000000000 --- a/themes/matrix/server/types/speakeasy.d.ts +++ /dev/null @@ -1,96 +0,0 @@ -declare module "speakeasy" { - export = speakeasy - - interface SharedOptions { - encoding?: string - algorithm?: string - } - - interface DigestOptions extends SharedOptions { - secret: string - counter: number - } - - interface HOTPOptions extends SharedOptions { - secret: string - counter: number - digest?: Buffer - digits?: number - } - - interface HOTPVerifyOptions extends SharedOptions { - secret: string - token: string - counter: number - digits?: number - window?: number - } - - interface TOTPOptions extends SharedOptions { - secret: string - time?: number - step?: number - epoch?: number - counter?: number - digits?: number - } - - interface TOTPVerifyOptions extends SharedOptions { - secret: string - token: string - time?: number - step?: number - epoch?: number - counter?: number - digits?: number - window?: number - } - - interface GenerateSecretOptions { - length?: number - symbols?: boolean - otpauth_url?: boolean - name?: string - issuer?: string - } - - interface GeneratedSecret { - ascii: string - hex: string - base32: string - qr_code_ascii: string - qr_code_hex: string - qr_code_base32: string - google_auth_qr: string - otpauth_url: string - } - - interface OTPAuthURLOptions extends SharedOptions { - secret: string - label: string - type?: string - counter?: number - issuer?: string - digits?: number - period?: number - } - - interface Speakeasy { - digest: (options: DigestOptions) => Buffer - hotp: { - (options: HOTPOptions): string, - verifyDelta: (options: HOTPVerifyOptions) => boolean, - verify: (options: HOTPVerifyOptions) => boolean, - } - totp: { - (options: TOTPOptions): string - verifyDelta: (options: TOTPVerifyOptions) => boolean, - verify: (options: TOTPVerifyOptions) => boolean, - } - generateSecret: (options?: GenerateSecretOptions) => GeneratedSecret - generateSecretASCII: (length?: number, symbols?: boolean) => string - otpauthURL: (options: OTPAuthURLOptions) => string - } - - const speakeasy: Speakeasy -} \ No newline at end of file diff --git a/themes/squares/client/src/css/.directory b/themes/squares/client/src/css/.directory old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/css/00-bootstrap.min.css b/themes/squares/client/src/css/00-bootstrap.min.css old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/css/01-main.css b/themes/squares/client/src/css/01-main.css old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/css/02-login.css b/themes/squares/client/src/css/02-login.css old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/css/03-errors.css b/themes/squares/client/src/css/03-errors.css old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/css/03-password-reset-form.css b/themes/squares/client/src/css/03-password-reset-form.css old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/css/03-password-reset-request.css b/themes/squares/client/src/css/03-password-reset-request.css old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/css/03-totp-register.css b/themes/squares/client/src/css/03-totp-register.css old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/css/03-u2f-register.css b/themes/squares/client/src/css/03-u2f-register.css old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/LargeTriangles.svg b/themes/squares/client/src/img/LargeTriangles.svg old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/RandomizedPattern.svg b/themes/squares/client/src/img/RandomizedPattern.svg old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/background.jpg b/themes/squares/client/src/img/background.jpg old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/background.svg b/themes/squares/client/src/img/background.svg old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/icon.png b/themes/squares/client/src/img/icon.png old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/mail.png b/themes/squares/client/src/img/mail.png old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/matrix_circle_128x128.png b/themes/squares/client/src/img/matrix_circle_128x128.png old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/notifications/.directory b/themes/squares/client/src/img/notifications/.directory old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/notifications/error.png b/themes/squares/client/src/img/notifications/error.png old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/notifications/info.png b/themes/squares/client/src/img/notifications/info.png old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/notifications/success.png b/themes/squares/client/src/img/notifications/success.png old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/notifications/warning.png b/themes/squares/client/src/img/notifications/warning.png old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/padlock.png b/themes/squares/client/src/img/padlock.png old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/password_white.png b/themes/squares/client/src/img/password_white.png old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/pendrive.png b/themes/squares/client/src/img/pendrive.png old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/sharingan.png b/themes/squares/client/src/img/sharingan.png old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/stores/.directory b/themes/squares/client/src/img/stores/.directory old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/stores/applestore-badge.svg b/themes/squares/client/src/img/stores/applestore-badge.svg old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/stores/googleplay-badge.svg b/themes/squares/client/src/img/stores/googleplay-badge.svg old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/success.png b/themes/squares/client/src/img/success.png old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/user.png b/themes/squares/client/src/img/user.png old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/img/warning.png b/themes/squares/client/src/img/warning.png old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/index.ts b/themes/squares/client/src/index.ts deleted file mode 100644 index 802004a8f..000000000 --- a/themes/squares/client/src/index.ts +++ /dev/null @@ -1,34 +0,0 @@ - -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 () { - (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, (global as any).u2f); - 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, (global as any).u2f); - 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); -})(); diff --git a/themes/squares/client/src/lib/GetPromised.ts b/themes/squares/client/src/lib/GetPromised.ts deleted file mode 100644 index 779139654..000000000 --- a/themes/squares/client/src/lib/GetPromised.ts +++ /dev/null @@ -1,14 +0,0 @@ -import BluebirdPromise = require("bluebird"); - -export default function ($: JQueryStatic, url: string, data: Object, fn: any, - dataType: string): BluebirdPromise { - return new BluebirdPromise((resolve, reject) => { - $.get(url, {}, undefined, dataType) - .done((data: any) => { - resolve(data); - }) - .fail((xhr: JQueryXHR, textStatus: string) => { - reject(textStatus); - }); - }); -} \ No newline at end of file diff --git a/themes/squares/client/src/lib/INotifier.ts b/themes/squares/client/src/lib/INotifier.ts deleted file mode 100644 index df947538f..000000000 --- a/themes/squares/client/src/lib/INotifier.ts +++ /dev/null @@ -1,14 +0,0 @@ - -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; -} \ No newline at end of file diff --git a/themes/squares/client/src/lib/Notifier.ts b/themes/squares/client/src/lib/Notifier.ts deleted file mode 100644 index c0252b9b9..000000000 --- a/themes/squares/client/src/lib/Notifier.ts +++ /dev/null @@ -1,83 +0,0 @@ - - -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('status %s\ - %s', 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); - } -} \ No newline at end of file diff --git a/themes/squares/client/src/lib/QueryParametersRetriever.ts b/themes/squares/client/src/lib/QueryParametersRetriever.ts deleted file mode 100644 index a529adb6e..000000000 --- a/themes/squares/client/src/lib/QueryParametersRetriever.ts +++ /dev/null @@ -1,12 +0,0 @@ - -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, " ")); - } -} \ No newline at end of file diff --git a/themes/squares/client/src/lib/SafeRedirect.ts b/themes/squares/client/src/lib/SafeRedirect.ts deleted file mode 100644 index 7e7684b8a..000000000 --- a/themes/squares/client/src/lib/SafeRedirect.ts +++ /dev/null @@ -1,10 +0,0 @@ -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(); -} \ No newline at end of file diff --git a/themes/squares/client/src/lib/firstfactor/FirstFactorValidator.ts b/themes/squares/client/src/lib/firstfactor/FirstFactorValidator.ts deleted file mode 100644 index eaa496fdd..000000000 --- a/themes/squares/client/src/lib/firstfactor/FirstFactorValidator.ts +++ /dev/null @@ -1,46 +0,0 @@ - -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 { - return new BluebirdPromise(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)); - }); - }); -} diff --git a/themes/squares/client/src/lib/firstfactor/UISelectors.ts b/themes/squares/client/src/lib/firstfactor/UISelectors.ts deleted file mode 100644 index 0e971b3c3..000000000 --- a/themes/squares/client/src/lib/firstfactor/UISelectors.ts +++ /dev/null @@ -1,5 +0,0 @@ - -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"; diff --git a/themes/squares/client/src/lib/firstfactor/index.ts b/themes/squares/client/src/lib/firstfactor/index.ts deleted file mode 100644 index 24affee2d..000000000 --- a/themes/squares/client/src/lib/firstfactor/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -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); - }); -} - diff --git a/themes/squares/client/src/lib/reset-password/constants.ts b/themes/squares/client/src/lib/reset-password/constants.ts deleted file mode 100644 index d48d4e67d..000000000 --- a/themes/squares/client/src/lib/reset-password/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export const FORM_SELECTOR = ".form-signin"; \ No newline at end of file diff --git a/themes/squares/client/src/lib/reset-password/reset-password-form.ts b/themes/squares/client/src/lib/reset-password/reset-password-form.ts deleted file mode 100644 index b94279cde..000000000 --- a/themes/squares/client/src/lib/reset-password/reset-password-form.ts +++ /dev/null @@ -1,57 +0,0 @@ -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); - }); -} diff --git a/themes/squares/client/src/lib/reset-password/reset-password-request.ts b/themes/squares/client/src/lib/reset-password/reset-password-request.ts deleted file mode 100644 index 846226d75..000000000 --- a/themes/squares/client/src/lib/reset-password/reset-password-request.ts +++ /dev/null @@ -1,56 +0,0 @@ - -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); - }); -} - diff --git a/themes/squares/client/src/lib/secondfactor/TOTPValidator.ts b/themes/squares/client/src/lib/secondfactor/TOTPValidator.ts deleted file mode 100644 index 5394139a6..000000000 --- a/themes/squares/client/src/lib/secondfactor/TOTPValidator.ts +++ /dev/null @@ -1,28 +0,0 @@ - -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 { - return new BluebirdPromise(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)); - }); - }); -} \ No newline at end of file diff --git a/themes/squares/client/src/lib/secondfactor/U2FValidator.ts b/themes/squares/client/src/lib/secondfactor/U2FValidator.ts deleted file mode 100644 index 5812922f7..000000000 --- a/themes/squares/client/src/lib/secondfactor/U2FValidator.ts +++ /dev/null @@ -1,42 +0,0 @@ -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 { - return new BluebirdPromise(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 { - 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, $); - }); -} diff --git a/themes/squares/client/src/lib/secondfactor/constants.ts b/themes/squares/client/src/lib/secondfactor/constants.ts deleted file mode 100644 index 50bba7571..000000000 --- a/themes/squares/client/src/lib/secondfactor/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ - -export const TOTP_FORM_SELECTOR = ".form-signin.totp"; -export const TOTP_TOKEN_SELECTOR = ".form-signin #token"; diff --git a/themes/squares/client/src/lib/secondfactor/index.ts b/themes/squares/client/src/lib/secondfactor/index.ts deleted file mode 100644 index 279723dce..000000000 --- a/themes/squares/client/src/lib/secondfactor/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -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); - }); -} \ No newline at end of file diff --git a/themes/squares/client/src/lib/totp-register/totp-register.ts b/themes/squares/client/src/lib/totp-register/totp-register.ts deleted file mode 100644 index 6a9aa7ee0..000000000 --- a/themes/squares/client/src/lib/totp-register/totp-register.ts +++ /dev/null @@ -1,11 +0,0 @@ - -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); -} diff --git a/themes/squares/client/src/lib/totp-register/ui-selector.ts b/themes/squares/client/src/lib/totp-register/ui-selector.ts deleted file mode 100644 index 9d43fabea..000000000 --- a/themes/squares/client/src/lib/totp-register/ui-selector.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export const QRCODE_ID_SELECTOR = "#qrcode"; \ No newline at end of file diff --git a/themes/squares/client/src/lib/u2f-register/u2f-register.ts b/themes/squares/client/src/lib/u2f-register/u2f-register.ts deleted file mode 100644 index abf40ee05..000000000 --- a/themes/squares/client/src/lib/u2f-register/u2f-register.ts +++ /dev/null @@ -1,56 +0,0 @@ - -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 { - return new BluebirdPromise(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 { - 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); - }); - }); -} diff --git a/themes/squares/client/src/thirdparties/qrcode.min.js b/themes/squares/client/src/thirdparties/qrcode.min.js old mode 100644 new mode 100755 diff --git a/themes/squares/client/src/thirdparties/u2f-api.js b/themes/squares/client/src/thirdparties/u2f-api.js old mode 100644 new mode 100755 diff --git a/themes/squares/client/test/Notifier.test.ts b/themes/squares/client/test/Notifier.test.ts deleted file mode 100644 index 70bfea146..000000000 --- a/themes/squares/client/test/Notifier.test.ts +++ /dev/null @@ -1,71 +0,0 @@ - -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"); - }); -}); \ No newline at end of file diff --git a/themes/squares/client/test/firstfactor/FirstFactorValidator.test.ts b/themes/squares/client/test/firstfactor/FirstFactorValidator.test.ts deleted file mode 100644 index ac8353278..000000000 --- a/themes/squares/client/test/firstfactor/FirstFactorValidator.test.ts +++ /dev/null @@ -1,44 +0,0 @@ - -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", "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", "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."); - }); - }); -}); \ No newline at end of file diff --git a/themes/squares/client/test/mocks/NotifierStub.ts b/themes/squares/client/test/mocks/NotifierStub.ts deleted file mode 100644 index 9c268d66d..000000000 --- a/themes/squares/client/test/mocks/NotifierStub.ts +++ /dev/null @@ -1,33 +0,0 @@ - -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(); - } -} \ No newline at end of file diff --git a/themes/squares/client/test/mocks/jquery.ts b/themes/squares/client/test/mocks/jquery.ts deleted file mode 100644 index 273f90861..000000000 --- a/themes/squares/client/test/mocks/jquery.ts +++ /dev/null @@ -1,59 +0,0 @@ - -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() - }; -} diff --git a/themes/squares/client/test/mocks/u2f-api.ts b/themes/squares/client/test/mocks/u2f-api.ts deleted file mode 100644 index d123f6a95..000000000 --- a/themes/squares/client/test/mocks/u2f-api.ts +++ /dev/null @@ -1,14 +0,0 @@ - -import sinon = require("sinon"); - -export interface U2FApiMock { - sign: sinon.SinonStub; - register: sinon.SinonStub; -} - -export function U2FApiMock(): U2FApiMock { - return { - sign: sinon.stub(), - register: sinon.stub() - }; -} \ No newline at end of file diff --git a/themes/squares/client/test/secondfactor/TOTPValidator.test.ts b/themes/squares/client/test/secondfactor/TOTPValidator.test.ts deleted file mode 100644 index 5dd6f15c3..000000000 --- a/themes/squares/client/test/secondfactor/TOTPValidator.test.ts +++ /dev/null @@ -1,37 +0,0 @@ - -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(); - }); - }); -}); \ No newline at end of file diff --git a/themes/squares/client/test/totp-register/totp-register.test.ts b/themes/squares/client/test/totp-register/totp-register.test.ts deleted file mode 100644 index 86fc455a1..000000000 --- a/themes/squares/client/test/totp-register/totp-register.test.ts +++ /dev/null @@ -1,31 +0,0 @@ - -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); - }); -}); \ No newline at end of file diff --git a/themes/squares/client/tsconfig.json b/themes/squares/client/tsconfig.json deleted file mode 100644 index 0bb4d62ff..000000000 --- a/themes/squares/client/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "moduleResolution": "node", - "noImplicitAny": true, - "sourceMap": true, - "removeComments": true, - "outDir": "../dist", - "baseUrl": ".", - "paths": { - "*": [ - "./types/*", - "../shared/types/*" - ] - } - }, - "include": [ - "src/**/*" - ], - "exclude": [ - "test/**/*" - ] -} diff --git a/themes/squares/client/tslint.json b/themes/squares/client/tslint.json deleted file mode 100644 index c2c1b7501..000000000 --- a/themes/squares/client/tslint.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "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 - } -} diff --git a/themes/squares/server/.directory b/themes/squares/server/.directory old mode 100644 new mode 100755 diff --git a/themes/squares/server/src/index.ts b/themes/squares/server/src/index.ts deleted file mode 100755 index fcbf4d022..000000000 --- a/themes/squares/server/src/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -#! /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 "); - 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); diff --git a/themes/squares/server/src/lib/.directory b/themes/squares/server/src/lib/.directory deleted file mode 100644 index 006b379ad..000000000 --- a/themes/squares/server/src/lib/.directory +++ /dev/null @@ -1,4 +0,0 @@ -[Dolphin] -Timestamp=2018,12,17,20,59,13 -Version=3 -ViewMode=1 diff --git a/themes/squares/server/src/lib/AuthenticationSessionHandler.ts b/themes/squares/server/src/lib/AuthenticationSessionHandler.ts deleted file mode 100644 index 57361bf8a..000000000 --- a/themes/squares/server/src/lib/AuthenticationSessionHandler.ts +++ /dev/null @@ -1,45 +0,0 @@ - - -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; - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/ErrorReplies.ts b/themes/squares/server/src/lib/ErrorReplies.ts deleted file mode 100644 index f1c5f4fd1..000000000 --- a/themes/squares/server/src/lib/ErrorReplies.ts +++ /dev/null @@ -1,49 +0,0 @@ -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 }); -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/Exceptions.ts b/themes/squares/server/src/lib/Exceptions.ts deleted file mode 100644 index 83fa4eb6b..000000000 --- a/themes/squares/server/src/lib/Exceptions.ts +++ /dev/null @@ -1,88 +0,0 @@ - -export class LdapSearchError extends Error { - constructor(message?: string) { - super(message); - this.name = "LdapSearchError"; - (Object).setPrototypeOf(this, LdapSearchError.prototype); - } -} - -export class LdapBindError extends Error { - constructor(message?: string) { - super(message); - this.name = "LdapBindError"; - (Object).setPrototypeOf(this, LdapBindError.prototype); - } -} - -export class LdapError extends Error { - constructor(message?: string) { - super(message); - this.name = "LdapError"; - (Object).setPrototypeOf(this, LdapError.prototype); - } -} - -export class IdentityError extends Error { - constructor(message?: string) { - super(message); - this.name = "IdentityError"; - (Object).setPrototypeOf(this, IdentityError.prototype); - } -} - -export class AccessDeniedError extends Error { - constructor(message?: string) { - super(message); - this.name = "AccessDeniedError"; - (Object).setPrototypeOf(this, AccessDeniedError.prototype); - } -} - -export class AuthenticationRegulationError extends Error { - constructor(message?: string) { - super(message); - this.name = "AuthenticationRegulationError"; - (Object).setPrototypeOf(this, AuthenticationRegulationError.prototype); - } -} - -export class InvalidTOTPError extends Error { - constructor(message?: string) { - super(message); - this.name = "InvalidTOTPError"; - (Object).setPrototypeOf(this, InvalidTOTPError.prototype); - } -} - -export class NotAuthenticatedError extends Error { - constructor(message?: string) { - super(message); - this.name = "NotAuthenticatedError"; - (Object).setPrototypeOf(this, NotAuthenticatedError.prototype); - } -} - -export class NotAuthorizedError extends Error { - constructor(message?: string) { - super(message); - this.name = "NotAuthanticatedError"; - (Object).setPrototypeOf(this, NotAuthorizedError.prototype); - } -} - -export class FirstFactorValidationError extends Error { - constructor(message?: string) { - super(message); - this.name = "FirstFactorValidationError"; - (Object).setPrototypeOf(this, FirstFactorValidationError.prototype); - } -} - -export class SecondFactorValidationError extends Error { - constructor(message?: string) { - super(message); - this.name = "SecondFactorValidationError"; - (Object).setPrototypeOf(this, FirstFactorValidationError.prototype); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/FirstFactorValidator.ts b/themes/squares/server/src/lib/FirstFactorValidator.ts deleted file mode 100644 index 231060002..000000000 --- a/themes/squares/server/src/lib/FirstFactorValidator.ts +++ /dev/null @@ -1,20 +0,0 @@ - -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 { - 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(); - }); -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/IdentityCheckMiddleware.spec.ts b/themes/squares/server/src/lib/IdentityCheckMiddleware.spec.ts deleted file mode 100644 index 842ed6bcb..000000000 --- a/themes/squares/server/src/lib/IdentityCheckMiddleware.spec.ts +++ /dev/null @@ -1,176 +0,0 @@ - -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")); - }); - }); - }); -}); diff --git a/themes/squares/server/src/lib/IdentityCheckMiddleware.ts b/themes/squares/server/src/lib/IdentityCheckMiddleware.ts deleted file mode 100644 index e72ea4db1..000000000 --- a/themes/squares/server/src/lib/IdentityCheckMiddleware.ts +++ /dev/null @@ -1,138 +0,0 @@ -import objectPath = require("object-path"); -import randomstring = require("randomstring"); -import BluebirdPromise = require("bluebird"); -import util = require("util"); -import Exceptions = require("./Exceptions"); -import fs = require("fs"); -import ejs = require("ejs"); -import { 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 { - - 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 { - 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 { - 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 { - - let authSession: AuthenticationSession; - const identityToken = objectPath.get( - 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 { - 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)); - }; -} diff --git a/themes/squares/server/src/lib/IdentityCheckPreValidationTemplate.ts b/themes/squares/server/src/lib/IdentityCheckPreValidationTemplate.ts deleted file mode 100644 index 0161ce406..000000000 --- a/themes/squares/server/src/lib/IdentityCheckPreValidationTemplate.ts +++ /dev/null @@ -1,3 +0,0 @@ - - -export const PRE_VALIDATION_TEMPLATE = "need-identity-validation"; \ No newline at end of file diff --git a/themes/squares/server/src/lib/IdentityValidable.ts b/themes/squares/server/src/lib/IdentityValidable.ts deleted file mode 100644 index 075580c9e..000000000 --- a/themes/squares/server/src/lib/IdentityValidable.ts +++ /dev/null @@ -1,19 +0,0 @@ -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; - postValidationInit(req: Express.Request): Bluebird; - - // 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; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/IdentityValidableStub.spec.ts b/themes/squares/server/src/lib/IdentityValidableStub.spec.ts deleted file mode 100644 index 20a977140..000000000 --- a/themes/squares/server/src/lib/IdentityValidableStub.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ - -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 { - return this.preValidationInitStub(req); - } - - postValidationInit(req: Express.Request): Bluebird { - 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(); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/Server.spec.ts b/themes/squares/server/src/lib/Server.spec.ts deleted file mode 100644 index 365163254..000000000 --- a/themes/squares/server/src/lib/Server.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ - -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(); - }); - }); -}); diff --git a/themes/squares/server/src/lib/Server.ts b/themes/squares/server/src/lib/Server.ts deleted file mode 100644 index 4090f6294..000000000 --- a/themes/squares/server/src/lib/Server.ts +++ /dev/null @@ -1,93 +0,0 @@ -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 { - 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((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 { - 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(); - } -} - diff --git a/themes/squares/server/src/lib/ServerVariables.ts b/themes/squares/server/src/lib/ServerVariables.ts deleted file mode 100644 index cd3dd6dc8..000000000 --- a/themes/squares/server/src/lib/ServerVariables.ts +++ /dev/null @@ -1,21 +0,0 @@ -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; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/ServerVariablesInitializer.ts b/themes/squares/server/src/lib/ServerVariablesInitializer.ts deleted file mode 100644 index df79238cc..000000000 --- a/themes/squares/server/src/lib/ServerVariablesInitializer.ts +++ /dev/null @@ -1,116 +0,0 @@ - -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 { - 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 { - - 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); - }); - } -} diff --git a/themes/squares/server/src/lib/ServerVariablesMockBuilder.spec.ts b/themes/squares/server/src/lib/ServerVariablesMockBuilder.spec.ts deleted file mode 100644 index 7874702a0..000000000 --- a/themes/squares/server/src/lib/ServerVariablesMockBuilder.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -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 - }; - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/Level.ts b/themes/squares/server/src/lib/authentication/Level.ts deleted file mode 100644 index 57b6a2346..000000000 --- a/themes/squares/server/src/lib/authentication/Level.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum Level { - NOT_AUTHENTICATED = 0, - ONE_FACTOR = 1, - TWO_FACTOR = 2 -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/GroupsAndEmails.ts b/themes/squares/server/src/lib/authentication/backends/GroupsAndEmails.ts deleted file mode 100644 index 3434ba66f..000000000 --- a/themes/squares/server/src/lib/authentication/backends/GroupsAndEmails.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface GroupsAndEmails { - groups: string[]; - emails: string[]; -} diff --git a/themes/squares/server/src/lib/authentication/backends/IUsersDatabase.ts b/themes/squares/server/src/lib/authentication/backends/IUsersDatabase.ts deleted file mode 100644 index d7fa13b7a..000000000 --- a/themes/squares/server/src/lib/authentication/backends/IUsersDatabase.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Bluebird = require("bluebird"); - -import { GroupsAndEmails } from "./GroupsAndEmails"; - -export interface IUsersDatabase { - checkUserPassword(username: string, password: string): Bluebird; - getEmails(username: string): Bluebird; - getGroups(username: string): Bluebird; - updatePassword(username: string, newPassword: string): Bluebird; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts b/themes/squares/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts deleted file mode 100644 index 19341a5dd..000000000 --- a/themes/squares/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -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 { - return this.checkUserPasswordStub(username, password); - } - - getEmails(username: string): Bluebird { - return this.getEmailsStub(username); - } - - getGroups(username: string): Bluebird { - return this.getGroupsStub(username); - } - - updatePassword(username: string, newPassword: string): Bluebird { - return this.updatePasswordStub(username, newPassword); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts b/themes/squares/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts deleted file mode 100644 index a258a78f7..000000000 --- a/themes/squares/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts +++ /dev/null @@ -1,224 +0,0 @@ -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()); - }); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/file/FileUsersDatabase.ts b/themes/squares/server/src/lib/authentication/backends/file/FileUsersDatabase.ts deleted file mode 100644 index d34dde21e..000000000 --- a/themes/squares/server/src/lib/authentication/backends/file/FileUsersDatabase.ts +++ /dev/null @@ -1,182 +0,0 @@ -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 { - return new Bluebird((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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - return this.readDatabase() - .then((database) => { - return this.checkUserExists(database, username) - .then(() => this.retrieveEmails(database, username)); - }); - } - - getGroups(username: string): Bluebird { - return this.readDatabase() - .then((database) => { - return this.checkUserExists(database, username) - .then(() => this.retrieveGroups(database, username)); - }); - } - - updatePassword(username: string, newPassword: string): Bluebird { - return this.readDatabase() - .then((database) => { - return this.checkUserExists(database, username) - .then(() => this.replacePassword(database, username, newPassword)); - }); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/file/ReadWriteQueue.ts b/themes/squares/server/src/lib/authentication/backends/file/ReadWriteQueue.ts deleted file mode 100644 index 957ddaec5..000000000 --- a/themes/squares/server/src/lib/authentication/backends/file/ReadWriteQueue.ts +++ /dev/null @@ -1,60 +0,0 @@ -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(); - } - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/ISession.ts b/themes/squares/server/src/lib/authentication/backends/ldap/ISession.ts deleted file mode 100644 index da2c74433..000000000 --- a/themes/squares/server/src/lib/authentication/backends/ldap/ISession.ts +++ /dev/null @@ -1,12 +0,0 @@ - -import BluebirdPromise = require("bluebird"); - -export interface ISession { - open(): BluebirdPromise; - close(): BluebirdPromise; - - searchUserDn(username: string): BluebirdPromise; - searchEmails(username: string): BluebirdPromise; - searchGroups(username: string): BluebirdPromise; - modifyPassword(username: string, newPassword: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/ISessionFactory.ts b/themes/squares/server/src/lib/authentication/backends/ldap/ISessionFactory.ts deleted file mode 100644 index 014d1eea5..000000000 --- a/themes/squares/server/src/lib/authentication/backends/ldap/ISessionFactory.ts +++ /dev/null @@ -1,6 +0,0 @@ - -import { ISession } from "./ISession"; - -export interface ISessionFactory { - create(userDN: string, password: string): ISession; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts b/themes/squares/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts deleted file mode 100644 index f4a6e6306..000000000 --- a/themes/squares/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts +++ /dev/null @@ -1,386 +0,0 @@ -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); - }) - }); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts b/themes/squares/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts deleted file mode 100644 index edda62ec6..000000000 --- a/themes/squares/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts +++ /dev/null @@ -1,107 +0,0 @@ -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 = (session: ISession) => Bluebird; - -export class LdapUsersDatabase implements IUsersDatabase { - private sessionFactory: ISessionFactory; - private configuration: LdapConfiguration; - - constructor( - sessionFactory: ISessionFactory, - configuration: LdapConfiguration) { - this.sessionFactory = sessionFactory; - this.configuration = configuration; - } - - private withSession( - username: string, - password: string, - cb: SessionCallback): Bluebird { - const session = this.sessionFactory.create(username, password); - return session.open() - .then(() => cb(session)) - .finally(() => session.close()); - } - - checkUserPassword(username: string, password: string): Bluebird { - const that = this; - function verifyUserPassword(userDN: string) { - return that.withSession( - 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 { - 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 { - 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 { - 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)); - }); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts b/themes/squares/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts deleted file mode 100644 index 9dedfcb7c..000000000 --- a/themes/squares/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -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"); - }); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/SafeSession.ts b/themes/squares/server/src/lib/authentication/backends/ldap/SafeSession.ts deleted file mode 100644 index 572209066..000000000 --- a/themes/squares/server/src/lib/authentication/backends/ldap/SafeSession.ts +++ /dev/null @@ -1,62 +0,0 @@ -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 { - return this.sesion.open(); - } - - close(): BluebirdPromise { - return this.sesion.close(); - } - - searchGroups(username: string): BluebirdPromise { - 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 { - 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 { - 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 { - try { - const sanitizedUsername = Sanitizer.sanitize(username); - return this.sesion.modifyPassword(sanitizedUsername, newPassword); - } - catch (e) { - return BluebirdPromise.reject(new Error(SPECIAL_CHAR_USED_MESSAGE)); - } - } -} diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts b/themes/squares/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts deleted file mode 100644 index 9dd33fed4..000000000 --- a/themes/squares/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -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 { 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); - }); -}); diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/Sanitizer.ts b/themes/squares/server/src/lib/authentication/backends/ldap/Sanitizer.ts deleted file mode 100644 index be74132ad..000000000 --- a/themes/squares/server/src/lib/authentication/backends/ldap/Sanitizer.ts +++ /dev/null @@ -1,25 +0,0 @@ - -// 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; - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/Session.spec.ts b/themes/squares/server/src/lib/authentication/backends/ldap/Session.spec.ts deleted file mode 100644 index d55f6a805..000000000 --- a/themes/squares/server/src/lib/authentication/backends/ldap/Session.spec.ts +++ /dev/null @@ -1,127 +0,0 @@ - -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"]); - }); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/Session.ts b/themes/squares/server/src/lib/authentication/backends/ldap/Session.ts deleted file mode 100644 index e0284b3c4..000000000 --- a/themes/squares/server/src/lib/authentication/backends/ldap/Session.ts +++ /dev/null @@ -1,156 +0,0 @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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(); - }); - } -} diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/SessionFactory.ts b/themes/squares/server/src/lib/authentication/backends/ldap/SessionFactory.ts deleted file mode 100644 index 0b6c4bff3..000000000 --- a/themes/squares/server/src/lib/authentication/backends/ldap/SessionFactory.ts +++ /dev/null @@ -1,37 +0,0 @@ -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 - ) - ); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts b/themes/squares/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts deleted file mode 100644 index face3930d..000000000 --- a/themes/squares/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts b/themes/squares/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts deleted file mode 100644 index 5faf2ba18..000000000 --- a/themes/squares/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -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 { - return this.openStub(); - } - - close(): Bluebird { - return this.closeStub(); - } - - searchUserDn(username: string): Bluebird { - return this.searchUserDnStub(username); - } - - searchEmails(username: string): Bluebird { - return this.searchEmailsStub(username); - } - - searchGroups(username: string): Bluebird { - return this.searchGroupsStub(username); - } - - modifyPassword(username: string, newPassword: string): Bluebird { - return this.modifyPasswordStub(username, newPassword); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/connector/Connector.ts b/themes/squares/server/src/lib/authentication/backends/ldap/connector/Connector.ts deleted file mode 100644 index 2542ea7f9..000000000 --- a/themes/squares/server/src/lib/authentication/backends/ldap/connector/Connector.ts +++ /dev/null @@ -1,69 +0,0 @@ -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; - unbindAsync(): Bluebird; - searchAsync(base: string, query: LdapJs.SearchOptions): Bluebird; - modifyAsync(userdn: string, change: LdapJs.Change): Bluebird; -} - -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 { - return this.client.bindAsync(username, password); - } - - unbindAsync(): Bluebird { - return this.client.unbindAsync(); - } - - searchAsync(base: string, query: any): Bluebird { - const that = this; - return this.client.searchAsync(base, query) - .then(function (res: EventEmitter) { - const doc: SearchEntry[] = []; - return new Bluebird((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 { - return this.client.modifyAsync(dn, changeRequest); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts b/themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts deleted file mode 100644 index 61fef07a4..000000000 --- a/themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts +++ /dev/null @@ -1,18 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts b/themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts deleted file mode 100644 index d11fa6386..000000000 --- a/themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -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(); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts b/themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts deleted file mode 100644 index 0b78225bf..000000000 --- a/themes/squares/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -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 { - return this.bindAsyncStub(username, password); - } - - unbindAsync(): BluebirdPromise { - return this.unbindAsyncStub(); - } - - searchAsync(base: string, query: any): BluebirdPromise { - return this.searchAsyncStub(base, query); - } - - modifyAsync(dn: string, changeRequest: any): BluebirdPromise { - return this.modifyAsyncStub(dn, changeRequest); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/connector/IConnector.ts b/themes/squares/server/src/lib/authentication/backends/ldap/connector/IConnector.ts deleted file mode 100644 index 1e63ab193..000000000 --- a/themes/squares/server/src/lib/authentication/backends/ldap/connector/IConnector.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Bluebird = require("bluebird"); -import EventEmitter = require("events"); - -export interface IConnector { - bindAsync(username: string, password: string): Bluebird; - unbindAsync(): Bluebird; - searchAsync(base: string, query: any): Bluebird; - modifyAsync(dn: string, changeRequest: any): Bluebird; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts b/themes/squares/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts deleted file mode 100644 index f9ed65eff..000000000 --- a/themes/squares/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { IConnector } from "./IConnector"; - -export interface IConnectorFactory { - create(): IConnector; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/totp/ITotpHandler.ts b/themes/squares/server/src/lib/authentication/totp/ITotpHandler.ts deleted file mode 100644 index d600d31e4..000000000 --- a/themes/squares/server/src/lib/authentication/totp/ITotpHandler.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { TOTPSecret } from "../../../../types/TOTPSecret"; - -export interface ITotpHandler { - generate(label: string, issuer: string): TOTPSecret; - validate(token: string, secret: string): boolean; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/totp/TotpHandler.spec.ts b/themes/squares/server/src/lib/authentication/totp/TotpHandler.spec.ts deleted file mode 100644 index 67cffa638..000000000 --- a/themes/squares/server/src/lib/authentication/totp/TotpHandler.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ - -import { TotpHandler } from "./TotpHandler"; -import Sinon = require("sinon"); -import Speakeasy = require("speakeasy"); -import Assert = require("assert"); - -describe("authentication/totp/TotpHandler", function() { - let totpValidator: TotpHandler; - let validateStub: Sinon.SinonStub; - - beforeEach(() => { - validateStub = Sinon.stub(Speakeasy.totp, "verify"); - totpValidator = new TotpHandler(Speakeasy); - }); - - afterEach(function() { - validateStub.restore(); - }); - - it("should validate the TOTP token", function() { - const totp_secret = "NBD2ZV64R9UV1O7K"; - const token = "token"; - validateStub.withArgs({ - secret: totp_secret, - token: token, - encoding: "base32", - window: 1 - }).returns(true); - Assert(totpValidator.validate(token, totp_secret)); - }); - - it("should not validate a wrong TOTP token", function() { - const totp_secret = "NBD2ZV64R9UV1O7K"; - const token = "wrong token"; - validateStub.returns(false); - Assert(!totpValidator.validate(token, totp_secret)); - }); -}); - diff --git a/themes/squares/server/src/lib/authentication/totp/TotpHandler.ts b/themes/squares/server/src/lib/authentication/totp/TotpHandler.ts deleted file mode 100644 index dfab502a7..000000000 --- a/themes/squares/server/src/lib/authentication/totp/TotpHandler.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ITotpHandler } from "./ITotpHandler"; -import { TOTPSecret } from "../../../../types/TOTPSecret"; -import Speakeasy = require("speakeasy"); - -const TOTP_ENCODING = "base32"; -const WINDOW: number = 1; - -export class TotpHandler implements ITotpHandler { - private speakeasy: typeof Speakeasy; - - constructor(speakeasy: typeof Speakeasy) { - this.speakeasy = speakeasy; - } - - generate(label: string, issuer: string): TOTPSecret { - const secret = this.speakeasy.generateSecret({ - otpauth_url: false - }) as TOTPSecret; - - secret.otpauth_url = this.speakeasy.otpauthURL({ - secret: secret.ascii, - label: label, - issuer: issuer - }); - return secret; - } - - validate(token: string, secret: string): boolean { - return this.speakeasy.totp.verify({ - secret: secret, - encoding: TOTP_ENCODING, - token: token, - window: WINDOW - }); - } -} diff --git a/themes/squares/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts b/themes/squares/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts deleted file mode 100644 index ea93330d9..000000000 --- a/themes/squares/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import { ITotpHandler } from "./ITotpHandler"; -import { TOTPSecret } from "../../../../types/TOTPSecret"; - -export class TotpHandlerStub implements ITotpHandler { - generateStub: Sinon.SinonStub; - validateStub: Sinon.SinonStub; - - constructor() { - this.generateStub = Sinon.stub(); - this.validateStub = Sinon.stub(); - } - - generate(label: string, issuer: string): TOTPSecret { - return this.generateStub(label, issuer); - } - - validate(token: string, secret: string): boolean { - return this.validateStub(token, secret); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/u2f/IU2fHandler.ts b/themes/squares/server/src/lib/authentication/u2f/IU2fHandler.ts deleted file mode 100644 index b9b7d6f26..000000000 --- a/themes/squares/server/src/lib/authentication/u2f/IU2fHandler.ts +++ /dev/null @@ -1,9 +0,0 @@ -import U2f = require("u2f"); - -export interface IU2fHandler { - request(appId: string, keyHandle?: string): U2f.Request; - checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) - : U2f.RegistrationResult | U2f.Error; - checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) - : U2f.SignatureResult | U2f.Error; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authentication/u2f/U2fHandler.ts b/themes/squares/server/src/lib/authentication/u2f/U2fHandler.ts deleted file mode 100644 index bf3891e5b..000000000 --- a/themes/squares/server/src/lib/authentication/u2f/U2fHandler.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { IU2fHandler } from "./IU2fHandler"; -import U2f = require("u2f"); - -export class U2fHandler implements IU2fHandler { - private u2f: typeof U2f; - - constructor(u2f: typeof U2f) { - this.u2f = u2f; - } - - request(appId: string, keyHandle?: string): U2f.Request { - return this.u2f.request(appId, keyHandle); - } - - checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) - : U2f.RegistrationResult | U2f.Error { - return this.u2f.checkRegistration(registrationRequest, registrationResponse); - } - - checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) - : U2f.SignatureResult | U2f.Error { - return this.u2f.checkSignature(signatureRequest, signatureResponse, publicKey); - } -} diff --git a/themes/squares/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts b/themes/squares/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts deleted file mode 100644 index 135d7eb06..000000000 --- a/themes/squares/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import U2f = require("u2f"); -import { IU2fHandler } from "./IU2fHandler"; - - -export class U2fHandlerStub implements IU2fHandler { - requestStub: Sinon.SinonStub; - checkRegistrationStub: Sinon.SinonStub; - checkSignatureStub: Sinon.SinonStub; - - constructor() { - this.requestStub = Sinon.stub(); - this.checkRegistrationStub = Sinon.stub(); - this.checkSignatureStub = Sinon.stub(); - } - - request(appId: string, keyHandle?: string): U2f.Request { - return this.requestStub(appId, keyHandle); - } - - checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) - : U2f.RegistrationResult | U2f.Error { - return this.checkRegistrationStub(registrationRequest, registrationResponse); - } - - checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) - : U2f.SignatureResult | U2f.Error { - return this.checkSignatureStub(signatureRequest, signatureResponse, publicKey); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authorization/Authorizer.spec.ts b/themes/squares/server/src/lib/authorization/Authorizer.spec.ts deleted file mode 100644 index 586814042..000000000 --- a/themes/squares/server/src/lib/authorization/Authorizer.spec.ts +++ /dev/null @@ -1,372 +0,0 @@ - -import Assert = require("assert"); -import winston = require("winston"); -import { Authorizer } from "./Authorizer"; -import { ACLConfiguration, ACLRule } from "../configuration/schema/AclConfiguration"; -import { Level } from "./Level"; - -describe("authorization/Authorizer", function () { - let authorizer: Authorizer; - let configuration: ACLConfiguration; - - describe("configuration is null", function() { - it("should allow access to anything, anywhere for anybody", function() { - configuration = undefined; - authorizer = new Authorizer(configuration, winston); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1", "group2"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "admin.example.com", resource: "/"}, {user: "user3", groups: ["group3"]}), Level.BYPASS); - }); - }); - - describe("configuration is not null", function () { - beforeEach(function () { - configuration = { - default_policy: "deny", - rules: [] - }; - authorizer = new Authorizer(configuration, winston); - }); - - describe("check access control with default policy to deny", function () { - beforeEach(function () { - configuration.default_policy = "deny"; - }); - - it("should deny access when no rule is provided", function () { - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should control access when multiple domain matcher is provided", function () { - configuration.rules = [{ - domain: "*.mail.example.com", - policy: "two_factor", - subject: "user:user1", - resources: [".*"] - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should allow access to all resources when resources is not provided", function () { - configuration.rules = [{ - domain: "*.mail.example.com", - policy: "two_factor", - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - describe("check user rules", function () { - it("should allow access when user has a matching allowing rule", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: [".*"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should deny to other users", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: [".*"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user2", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); - }); - - it("should allow user access only to specific resources", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: ["/private/.*", "^/begin", "/end$"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/middle/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/begin"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/not/begin"}, {user: "user1", groups: ["group1"]}), Level.DENY); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end/x"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should allow access to multiple domains", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: [".*"], - subject: "user:user1" - }, { - domain: "home1.example.com", - policy: "one_factor", - resources: [".*"], - subject: "user:user1" - }, { - domain: "home2.example.com", - policy: "deny", - resources: [".*"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home1.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); - Assert.equal(authorizer.authorization({domain: "home2.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home3.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should apply rules in order", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "one_factor", - resources: ["/my/private/resource"], - subject: "user:user1" - }, { - domain: "home.example.com", - policy: "deny", - resources: ["^/my/private/.*"], - subject: "user:user1" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/my/.*"], - subject: "user:user1" - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/poney"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/duck"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/resource"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); - }); - }); - - describe("check group rules", function () { - it("should allow access when user is in group having a matching allowing rule", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: ["^/$"], - subject: "group:group1" - }, { - domain: "home.example.com", - policy: "one_factor", - resources: ["^/test$"], - subject: "group:group2" - }, { - domain: "home.example.com", - policy: "deny", - resources: ["^/private$"], - subject: "group:group2" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.ONE_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); - }); - }); - }); - - describe("check any rules", function () { - it("should control access when any rules are defined", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "bypass", - resources: ["^/public$"] - }, { - domain: "home.example.com", - policy: "deny", - resources: ["^/private$"] - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, - {user: "user4", groups: ["group5"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, - {user: "user4", groups: ["group5"]}), Level.DENY); - }); - }); - - describe("check access control with default policy to allow", function () { - beforeEach(function () { - configuration.default_policy = "bypass"; - }); - - it("should allow access to anything when no rule is provided", function () { - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - }); - - it("should deny access to one resource when defined", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "deny", - resources: ["/test"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - }); - }); - - describe("check access control with complete use case", function () { - beforeEach(function () { - configuration.default_policy = "deny"; - }); - - it("should control access of multiple user (real use case)", function () { - // Let say we have three users: admin, john, harry. - // admin is in groups ["admins"] - // john is in groups ["dev", "admin-private"] - // harry is in groups ["dev"] - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: ["^/public$", "^/$"] - }, { - domain: "home.example.com", - policy: "two_factor", - resources: [".*"], - subject: "group:admins" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/private/?.*"], - subject: "group:admin-private" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/private/john$"], - subject: "user:john" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/private/harry"], - subject: "user:harry" - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); - }); - - it("should allow when allowed at group level and denied at user level", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "deny", - resources: ["^/dev/bob$"], - subject: "user:john" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"], - subject: "group:dev" - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); - }); - - it("should allow access when allowed at 'any' level and denied at user level", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "deny", - resources: ["^/dev/bob$"], - subject: "user:john" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"] - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); - }); - - it("should allow access when allowed at 'any' level and denied at group level", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "deny", - resources: ["^/dev/bob$"], - subject: "group:dev" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"] - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); - }); - - it("should respect rules precedence", function () { - // the priority from least to most is 'default_policy', 'all', 'group', 'user' - // and the first rules in each category as a lower priority than the latest. - // You can think of it that way: they override themselves inside each category. - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"], - subject: "user:john" - }, { - domain: "home.example.com", - policy: "deny", - resources: ["^/dev/bob$"], - subject: "group:dev" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"] - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - }); - }); - }); -}); diff --git a/themes/squares/server/src/lib/authorization/Authorizer.ts b/themes/squares/server/src/lib/authorization/Authorizer.ts deleted file mode 100644 index 889b7ec20..000000000 --- a/themes/squares/server/src/lib/authorization/Authorizer.ts +++ /dev/null @@ -1,85 +0,0 @@ - -import { ACLConfiguration, ACLPolicy, ACLRule } from "../configuration/schema/AclConfiguration"; -import { IAuthorizer } from "./IAuthorizer"; -import { Winston } from "../../../types/Dependencies"; -import { MultipleDomainMatcher } from "./MultipleDomainMatcher"; -import { Level } from "./Level"; -import { Object } from "./Object"; -import { Subject } from "./Subject"; - -function MatchDomain(actualDomain: string) { - return function (rule: ACLRule): boolean { - return MultipleDomainMatcher.match(actualDomain, rule.domain); - }; -} - -function MatchResource(actualResource: string) { - return function (rule: ACLRule): boolean { - // If resources key is not provided, the rule applies to all resources. - if (!rule.resources) return true; - - for (let i = 0; i < rule.resources.length; ++i) { - const regexp = new RegExp(rule.resources[i]); - if (regexp.test(actualResource)) return true; - } - return false; - }; -} - -function MatchSubject(subject: Subject) { - return (rule: ACLRule) => { - // If no subject, matches anybody - if (!rule.subject) return true; - - if (rule.subject.startsWith("user:")) { - const ruleUser = rule.subject.split(":")[1]; - if (subject.user == ruleUser) return true; - } - - if (rule.subject.startsWith("group:")) { - const ruleGroup = rule.subject.split(":")[1]; - if (subject.groups.indexOf(ruleGroup) > -1) return true; - } - return false; - }; -} - -export class Authorizer implements IAuthorizer { - private logger: Winston; - private readonly configuration: ACLConfiguration; - - constructor(configuration: ACLConfiguration, logger_: Winston) { - this.logger = logger_; - this.configuration = configuration; - } - - private getMatchingRules(object: Object, subject: Subject): ACLRule[] { - const rules = this.configuration.rules; - if (!rules) return []; - return rules - .filter(MatchDomain(object.domain)) - .filter(MatchResource(object.resource)) - .filter(MatchSubject(subject)); - } - - private ruleToLevel(policy: string): Level { - if (policy == "bypass") { - return Level.BYPASS; - } else if (policy == "one_factor") { - return Level.ONE_FACTOR; - } else if (policy == "two_factor") { - return Level.TWO_FACTOR; - } - return Level.DENY; - } - - authorization(object: Object, subject: Subject): Level { - if (!this.configuration) return Level.BYPASS; - - const rules = this.getMatchingRules(object, subject); - - return (rules.length > 0) - ? this.ruleToLevel(rules[0].policy) // extract the policy of the first matching rule - : this.ruleToLevel(this.configuration.default_policy); // otherwise use the default policy - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authorization/AuthorizerStub.spec.ts b/themes/squares/server/src/lib/authorization/AuthorizerStub.spec.ts deleted file mode 100644 index 9bd6f4a85..000000000 --- a/themes/squares/server/src/lib/authorization/AuthorizerStub.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Sinon = require("sinon"); -import { IAuthorizer } from "./IAuthorizer"; -import { Level } from "./Level"; -import { Object } from "./Object"; -import { Subject } from "./Subject"; - -export class AuthorizerStub implements IAuthorizer { - authorizationMock: Sinon.SinonStub; - - constructor() { - this.authorizationMock = Sinon.stub(); - } - - authorization(object: Object, subject: Subject): Level { - return this.authorizationMock(object, subject); - } -} diff --git a/themes/squares/server/src/lib/authorization/IAuthorizer.ts b/themes/squares/server/src/lib/authorization/IAuthorizer.ts deleted file mode 100644 index fe7ba367a..000000000 --- a/themes/squares/server/src/lib/authorization/IAuthorizer.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Level } from "./Level"; -import { Subject } from "./Subject"; -import { Object } from "./Object"; - -export interface IAuthorizer { - authorization(object: Object, subject: Subject): Level; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authorization/Level.ts b/themes/squares/server/src/lib/authorization/Level.ts deleted file mode 100644 index d12802610..000000000 --- a/themes/squares/server/src/lib/authorization/Level.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum Level { - BYPASS = 0, - ONE_FACTOR = 1, - TWO_FACTOR = 2, - DENY = 3 -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authorization/MultipleDomainMatcher.ts b/themes/squares/server/src/lib/authorization/MultipleDomainMatcher.ts deleted file mode 100644 index 64c647a4a..000000000 --- a/themes/squares/server/src/lib/authorization/MultipleDomainMatcher.ts +++ /dev/null @@ -1,12 +0,0 @@ - -export class MultipleDomainMatcher { - static match(domain: string, pattern: string): boolean { - if (pattern.startsWith("*") && - domain.endsWith(pattern.substr(1))) { - return true; - } - else if (domain == pattern) { - return true; - } - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authorization/Object.ts b/themes/squares/server/src/lib/authorization/Object.ts deleted file mode 100644 index 5411b0d24..000000000 --- a/themes/squares/server/src/lib/authorization/Object.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface Object { - domain: string; - resource: string; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/authorization/Subject.ts b/themes/squares/server/src/lib/authorization/Subject.ts deleted file mode 100644 index 310d6b4c0..000000000 --- a/themes/squares/server/src/lib/authorization/Subject.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface Subject { - user: string; - groups: string[]; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/ConfigurationParser.spec.ts b/themes/squares/server/src/lib/configuration/ConfigurationParser.spec.ts deleted file mode 100644 index 60c0f6182..000000000 --- a/themes/squares/server/src/lib/configuration/ConfigurationParser.spec.ts +++ /dev/null @@ -1,171 +0,0 @@ -import * as Assert from "assert"; -import { Configuration } from "./schema/Configuration"; -import { ACLConfiguration } from "./schema/AclConfiguration"; -import { ConfigurationParser } from "./ConfigurationParser"; - -describe("configuration/ConfigurationParser", function () { - function buildYamlConfig(): Configuration { - const yaml_config: Configuration = { - port: 8080, - authentication_backend: { - ldap: { - url: "http://ldap", - base_dn: "dc=example,dc=com", - additional_users_dn: "ou=users", - additional_groups_dn: "ou=groups", - user: "user", - password: "pass" - }, - }, - session: { - domain: "example.com", - secret: "secret", - expiration: 40000 - }, - storage: { - local: { - path: "/mydirectory" - } - }, - regulation: { - max_retries: 3, - find_time: 5 * 60, - ban_time: 5 * 60 - }, - logs_level: "debug", - notifier: { - email: { - username: "user", - password: "password", - sender: "admin@example.com", - service: "gmail" - } - } - }; - return yaml_config; - } - - describe("port", function () { - it("should read the port from the yaml file", function () { - const yaml_config = buildYamlConfig(); - yaml_config.port = 7070; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.port, 7070); - }); - - it("should default the port to 8080 if not provided", function () { - const yaml_config = buildYamlConfig(); - delete yaml_config.port; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.port, 8080); - }); - }); - - describe("test session configuration", function() { - it("should get the session attributes", function () { - const yaml_config = buildYamlConfig(); - yaml_config.session = { - domain: "example.com", - secret: "secret", - expiration: 3600, - inactivity: 4000 - }; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.session.domain, "example.com"); - Assert.equal(config.session.secret, "secret"); - Assert.equal(config.session.expiration, 3600); - Assert.equal(config.session.inactivity, 4000); - }); - - it("should be ok not specifying inactivity", function () { - const yaml_config = buildYamlConfig(); - yaml_config.session = { - domain: "example.com", - secret: "secret", - expiration: 3600 - }; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.session.domain, "example.com"); - Assert.equal(config.session.secret, "secret"); - Assert.equal(config.session.expiration, 3600); - Assert.equal(config.session.inactivity, undefined); - }); - }); - - it("should get the log level", function () { - const yaml_config = buildYamlConfig(); - yaml_config.logs_level = "debug"; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.logs_level, "debug"); - }); - - it("should get the notifier config", function () { - const userConfig = buildYamlConfig(); - userConfig.notifier = { - email: { - username: "user", - password: "pass", - sender: "admin@example.com", - service: "gmail" - } - }; - const config = ConfigurationParser.parse(userConfig); - Assert.deepEqual(config.notifier, { - email: { - username: "user", - password: "pass", - sender: "admin@example.com", - service: "gmail" - } - }); - }); - - describe("access_control", function() { - it("should adapt access_control when it is already ok", function () { - const userConfig = buildYamlConfig(); - userConfig.access_control = { - default_policy: "deny", - rules: [{ - domain: "www.example.com", - policy: "two_factor", - subject: "user:user" - }, { - domain: "public.example.com", - policy: "two_factor" - }] - }; - const config = ConfigurationParser.parse(userConfig); - Assert.deepEqual(config.access_control, { - default_policy: "deny", - rules: [{ - domain: "www.example.com", - policy: "two_factor", - subject: "user:user" - }, { - domain: "public.example.com", - policy: "two_factor" - }] - } as ACLConfiguration); - }); - - - it("should adapt access_control when it is empty", function () { - const userConfig = buildYamlConfig(); - userConfig.access_control = {} as any; - const config = ConfigurationParser.parse(userConfig); - Assert.deepEqual(config.access_control, { - default_policy: "bypass", - rules: [] - }); - }); - }); - - describe("default_redirection_url", function() { - it("should parse default_redirection_url", function() { - const userConfig = buildYamlConfig(); - userConfig.default_redirection_url = "dummy_url"; - const config = ConfigurationParser.parse(userConfig); - Assert.deepEqual(config.default_redirection_url, "dummy_url"); - }); - }); -}); diff --git a/themes/squares/server/src/lib/configuration/ConfigurationParser.ts b/themes/squares/server/src/lib/configuration/ConfigurationParser.ts deleted file mode 100644 index d92d163c4..000000000 --- a/themes/squares/server/src/lib/configuration/ConfigurationParser.ts +++ /dev/null @@ -1,39 +0,0 @@ - -import * as ObjectPath from "object-path"; -import { Configuration, complete } from "./schema/Configuration"; -import Ajv = require("ajv"); -import Path = require("path"); -import Util = require("util"); - -export class ConfigurationParser { - private static parseTypes(configuration: Configuration): string[] { - const schema = require(Path.resolve(__dirname, "./Configuration.schema.json")); - const ajv = new Ajv({ - allErrors: true, - missingRefs: "fail" - }); - ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json")); - const valid = ajv.validate(schema, configuration); - if (!valid) - return ajv.errors.map( - (e: Ajv.ErrorObject) => { return ajv.errorsText([e]); }); - return []; - } - - static parse(configuration: Configuration): Configuration { - const validationErrors = this.parseTypes(configuration); - if (validationErrors.length > 0) { - validationErrors.forEach((e: string) => { console.log(e); }); - throw new Error("Malformed configuration (schema). Please double-check your configuration file."); - } - - const [newConfiguration, completionErrors] = complete(configuration); - - if (completionErrors.length > 0) { - completionErrors.forEach((e: string) => { console.log(e); }); - throw new Error("Malformed configuration (validator). Please double-check your configuration file."); - } - return newConfiguration; - } -} - diff --git a/themes/squares/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts b/themes/squares/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts deleted file mode 100644 index d4a3093ee..000000000 --- a/themes/squares/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { SessionConfigurationBuilder } from "./SessionConfigurationBuilder"; -import { Configuration } from "./schema/Configuration"; -import { GlobalDependencies } from "../../../types/Dependencies"; - -import ExpressSession = require("express-session"); -import ConnectRedis = require("connect-redis"); -import Sinon = require("sinon"); -import Assert = require("assert"); - -describe("configuration/SessionConfigurationBuilder", function () { - const configuration: Configuration = { - access_control: { - default_policy: "deny", - rules: [] - }, - totp: { - issuer: "authelia.com" - }, - authentication_backend: { - ldap: { - url: "ldap://ldap", - user: "user", - base_dn: "dc=example,dc=com", - password: "password", - additional_groups_dn: "ou=groups", - additional_users_dn: "ou=users", - group_name_attribute: "", - groups_filter: "", - mail_attribute: "", - users_filter: "" - }, - }, - logs_level: "debug", - notifier: { - filesystem: { - filename: "/test" - } - }, - port: 8080, - session: { - name: "authelia_session", - domain: "example.com", - expiration: 3600, - secret: "secret" - }, - regulation: { - max_retries: 3, - ban_time: 5 * 60, - find_time: 5 * 60 - }, - storage: { - local: { - in_memory: true - } - } - }; - - const deps: GlobalDependencies = { - ConnectRedis: Sinon.spy() as any, - ldapjs: Sinon.spy() as any, - nedb: Sinon.spy() as any, - session: Sinon.spy() as any, - speakeasy: Sinon.spy() as any, - u2f: Sinon.spy() as any, - winston: Sinon.spy() as any, - Redis: Sinon.spy() as any - }; - - it("should return session options without redis options", function () { - const options = SessionConfigurationBuilder.build(configuration, deps); - const expectedOptions = { - name: "authelia_session", - secret: "secret", - resave: false, - saveUninitialized: true, - cookie: { - secure: true, - httpOnly: true, - maxAge: 3600, - domain: "example.com" - } - }; - - Assert.deepEqual(expectedOptions, options); - }); - - it("should return session options with redis options", function () { - configuration.session["redis"] = { - host: "redis.example.com", - port: 6379 - }; - const RedisStoreMock = Sinon.spy(); - const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); - - deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; - deps.Redis = { - createClient: Sinon.mock().returns(redisClient) - } as any; - - const options = SessionConfigurationBuilder.build(configuration, deps); - - const expectedOptions: ExpressSession.SessionOptions = { - secret: "secret", - resave: false, - saveUninitialized: true, - name: "authelia_session", - cookie: { - secure: true, - httpOnly: true, - maxAge: 3600, - domain: "example.com" - }, - store: Sinon.match.object as any - }; - - Assert((deps.ConnectRedis as Sinon.SinonStub).calledWith(deps.session)); - Assert.equal(options.secret, expectedOptions.secret); - Assert.equal(options.resave, expectedOptions.resave); - Assert.equal(options.saveUninitialized, expectedOptions.saveUninitialized); - Assert.deepEqual(options.cookie, expectedOptions.cookie); - Assert(options.store != undefined); - }); - - it("should return session options with redis password", function () { - configuration.session["redis"] = { - host: "redis.example.com", - port: 6379, - password: "authelia_pass" - }; - const RedisStoreMock = Sinon.spy(); - const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); - const createClientStub = Sinon.stub(); - - deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; - deps.Redis = { - createClient: createClientStub - } as any; - - createClientStub.returns(redisClient); - - const options = SessionConfigurationBuilder.build(configuration, deps); - - Assert(createClientStub.calledWith({ - host: "redis.example.com", - port: 6379, - password: "authelia_pass" - })); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/SessionConfigurationBuilder.ts b/themes/squares/server/src/lib/configuration/SessionConfigurationBuilder.ts deleted file mode 100644 index 6ce643d9d..000000000 --- a/themes/squares/server/src/lib/configuration/SessionConfigurationBuilder.ts +++ /dev/null @@ -1,52 +0,0 @@ -import ExpressSession = require("express-session"); -import Redis = require("redis"); - -import { Configuration } from "./schema/Configuration"; -import { GlobalDependencies } from "../../../types/Dependencies"; -import { RedisStoreOptions } from "connect-redis"; - -export class SessionConfigurationBuilder { - - static build(configuration: Configuration, deps: GlobalDependencies): ExpressSession.SessionOptions { - const sessionOptions: ExpressSession.SessionOptions = { - name: configuration.session.name, - secret: configuration.session.secret, - resave: false, - saveUninitialized: true, - cookie: { - secure: true, - httpOnly: true, - maxAge: configuration.session.expiration, - domain: configuration.session.domain - }, - }; - - if (configuration.session.redis) { - let redisOptions; - const options: Redis.ClientOpts = { - host: configuration.session.redis.host, - port: configuration.session.redis.port - }; - - if (configuration.session.redis.password) { - options["password"] = configuration.session.redis.password; - } - const client = deps.Redis.createClient(options); - - client.on("error", function (err: Error) { - console.error("Redis error:", err); - }); - - redisOptions = { - client: client, - logErrors: true - }; - - if (redisOptions) { - const RedisStore = deps.ConnectRedis(deps.session); - sessionOptions.store = new RedisStore(redisOptions); - } - } - return sessionOptions; - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/AclConfiguration.spec.ts b/themes/squares/server/src/lib/configuration/schema/AclConfiguration.spec.ts deleted file mode 100644 index d1e2a03a2..000000000 --- a/themes/squares/server/src/lib/configuration/schema/AclConfiguration.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ACLConfiguration, complete } from "./AclConfiguration"; -import Assert = require("assert"); - -describe("configuration/schema/AclConfiguration", function() { - it("should complete ACLConfiguration", function() { - const configuration: ACLConfiguration = {}; - const [newConfiguration, errors] = complete(configuration); - - Assert.deepEqual(newConfiguration.default_policy, "bypass"); - Assert.deepEqual(newConfiguration.rules, []); - }); - - it("should return errors when subject is not good", function() { - const configuration: ACLConfiguration = { - default_policy: "deny", - rules: [{ - domain: "dev.example.com", - subject: "user:abc", - policy: "bypass" - }, { - domain: "dev.example.com", - subject: "user:def", - policy: "bypass" - }, { - domain: "dev.example.com", - subject: "badkey:abc", - policy: "bypass" - }] - }; - const [newConfiguration, errors] = complete(configuration); - - Assert.deepEqual(errors, ["Rule 2 has wrong subject. It should be starting with user: or group:."]); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/AclConfiguration.ts b/themes/squares/server/src/lib/configuration/schema/AclConfiguration.ts deleted file mode 100644 index 40401dd64..000000000 --- a/themes/squares/server/src/lib/configuration/schema/AclConfiguration.ts +++ /dev/null @@ -1,41 +0,0 @@ - -export type ACLPolicy = "deny" | "bypass" | "one_factor" | "two_factor"; - -export type ACLRule = { - domain: string; - resources?: string[]; - subject?: string; - policy: ACLPolicy; -}; - -export interface ACLConfiguration { - default_policy?: ACLPolicy; - rules?: ACLRule[]; -} - -export function complete(configuration: ACLConfiguration): [ACLConfiguration, string[]] { - const newConfiguration: ACLConfiguration = (configuration) - ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.default_policy) { - newConfiguration.default_policy = "bypass"; - } - - if (!newConfiguration.rules) { - newConfiguration.rules = []; - } - - if (newConfiguration.rules.length > 0) { - const errors: string[] = []; - newConfiguration.rules.forEach((r, idx) => { - if (r.subject && !r.subject.match(/^(user|group):[a-zA-Z0-9]+$/)) { - errors.push(`Rule ${idx} has wrong subject. It should be starting with user: or group:.`); - } - }); - if (errors.length > 0) { - return [newConfiguration, errors]; - } - } - - return [newConfiguration, []]; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts b/themes/squares/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts deleted file mode 100644 index 3ca86381c..000000000 --- a/themes/squares/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { AuthenticationBackendConfiguration, complete } from "./AuthenticationBackendConfiguration"; -import Assert = require("assert"); - -describe("configuration/schema/AuthenticationBackendConfiguration", function() { - it("should ensure there is at least one key", function() { - const configuration: AuthenticationBackendConfiguration = {} as any; - const [newConfiguration, error] = complete(configuration); - - Assert.equal(error, "Authentication backend must have one of the following keys:`ldap` or `file`"); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts b/themes/squares/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts deleted file mode 100644 index 7f77f894f..000000000 --- a/themes/squares/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { LdapConfiguration } from "./LdapConfiguration"; -import { FileUsersDatabaseConfiguration } from "./FileUsersDatabaseConfiguration"; - -export interface AuthenticationBackendConfiguration { - ldap?: LdapConfiguration; - file?: FileUsersDatabaseConfiguration; -} - -export function complete( - configuration: AuthenticationBackendConfiguration) - : [AuthenticationBackendConfiguration, string] { - - const newConfiguration: AuthenticationBackendConfiguration = (configuration) - ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (Object.keys(newConfiguration).length != 1) { - return [ - newConfiguration, - "Authentication backend must have one of the following keys:" + - "`ldap` or `file`" - ]; - } - - return [newConfiguration, undefined]; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/Configuration.ts b/themes/squares/server/src/lib/configuration/schema/Configuration.ts deleted file mode 100644 index 8d16a5fb2..000000000 --- a/themes/squares/server/src/lib/configuration/schema/Configuration.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ACLConfiguration, complete as AclConfigurationComplete } from "./AclConfiguration"; -import { AuthenticationBackendConfiguration, complete as AuthenticationBackendComplete } from "./AuthenticationBackendConfiguration"; -import { NotifierConfiguration, complete as NotifierConfigurationComplete } from "./NotifierConfiguration"; -import { RegulationConfiguration, complete as RegulationConfigurationComplete } from "./RegulationConfiguration"; -import { SessionConfiguration, complete as SessionConfigurationComplete } from "./SessionConfiguration"; -import { StorageConfiguration, complete as StorageConfigurationComplete } from "./StorageConfiguration"; -import { TotpConfiguration, complete as TotpConfigurationComplete } from "./TotpConfiguration"; - -export interface Configuration { - access_control?: ACLConfiguration; - authentication_backend: AuthenticationBackendConfiguration; - default_redirection_url?: string; - logs_level?: string; - notifier?: NotifierConfiguration; - port?: number; - regulation?: RegulationConfiguration; - session?: SessionConfiguration; - storage?: StorageConfiguration; - totp?: TotpConfiguration; -} - -export function complete( - configuration: Configuration): - [Configuration, string[]] { - - const newConfiguration: Configuration = JSON.parse( - JSON.stringify(configuration)); - const errors: string[] = []; - - const [acls, aclsErrors] = AclConfigurationComplete( - newConfiguration.access_control); - - newConfiguration.access_control = acls; - if (aclsErrors.length > 0) { - errors.concat(aclsErrors); - } - - const [backend, error] = - AuthenticationBackendComplete( - newConfiguration.authentication_backend); - - if (error) errors.push(error); - newConfiguration.authentication_backend = backend; - - if (!newConfiguration.logs_level) { - newConfiguration.logs_level = "info"; - } - - const [notifier, notifierError] = NotifierConfigurationComplete( - newConfiguration.notifier); - newConfiguration.notifier = notifier; - if (notifierError) errors.push(notifierError); - - if (!newConfiguration.port) { - newConfiguration.port = 8080; - } - - newConfiguration.regulation = RegulationConfigurationComplete( - newConfiguration.regulation); - newConfiguration.session = SessionConfigurationComplete( - newConfiguration.session); - newConfiguration.storage = StorageConfigurationComplete( - newConfiguration.storage); - newConfiguration.totp = TotpConfigurationComplete( - newConfiguration.totp); - - return [newConfiguration, errors]; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts b/themes/squares/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts deleted file mode 100644 index d19002ba9..000000000 --- a/themes/squares/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts +++ /dev/null @@ -1,4 +0,0 @@ - -export interface FileUsersDatabaseConfiguration { - path: string; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/LdapConfiguration.spec.ts b/themes/squares/server/src/lib/configuration/schema/LdapConfiguration.spec.ts deleted file mode 100644 index cc73d1085..000000000 --- a/themes/squares/server/src/lib/configuration/schema/LdapConfiguration.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Assert = require("assert"); -import { LdapConfiguration, complete } from "./LdapConfiguration"; - -describe("configuration/schema/AuthenticationMethodsConfiguration", function() { - it("should ensure at least one key is provided", function() { - const configuration: LdapConfiguration = { - url: "ldap.example.com", - base_dn: "dc=example,dc=com", - user: "admin", - password: "password" - }; - const newConfiguration = complete(configuration); - - Assert.deepEqual(newConfiguration, { - url: "ldap.example.com", - base_dn: "dc=example,dc=com", - user: "admin", - password: "password", - users_filter: "cn={0}", - group_name_attribute: "cn", - groups_filter: "member={dn}", - mail_attribute: "mail" - }); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/LdapConfiguration.ts b/themes/squares/server/src/lib/configuration/schema/LdapConfiguration.ts deleted file mode 100644 index 5dacb9390..000000000 --- a/themes/squares/server/src/lib/configuration/schema/LdapConfiguration.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Util = require("util"); - -export interface LdapConfiguration { - url: string; - base_dn: string; - - additional_users_dn?: string; - users_filter?: string; - - additional_groups_dn?: string; - groups_filter?: string; - - group_name_attribute?: string; - mail_attribute?: string; - - user: string; // admin username - password: string; // admin password -} - -export function complete(configuration: LdapConfiguration): LdapConfiguration { - const newConfiguration: LdapConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.users_filter) { - newConfiguration.users_filter = "cn={0}"; - } - - if (!newConfiguration.groups_filter) { - newConfiguration.groups_filter = "member={dn}"; - } - - if (!newConfiguration.group_name_attribute) { - newConfiguration.group_name_attribute = "cn"; - } - - if (!newConfiguration.mail_attribute) { - newConfiguration.mail_attribute = "mail"; - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts b/themes/squares/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts deleted file mode 100644 index 6c576e8e0..000000000 --- a/themes/squares/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Assert = require("assert"); -import { NotifierConfiguration, complete } from "./NotifierConfiguration"; - -describe("configuration/schema/NotifierConfiguration", function() { - it("should use a default notifier when none is provided", function() { - const configuration: NotifierConfiguration = {}; - const [newConfiguration, error] = complete(configuration); - - Assert.deepEqual(newConfiguration.filesystem, {filename: "/tmp/authelia/notification.txt"}); - }); - - it("should ensure correct key is provided", function() { - const configuration = { - abc: "badvalue" - }; - const [newConfiguration, error] = complete(configuration as any); - - Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"); - }); - - it("should ensure there is no more than one key", function() { - const configuration: NotifierConfiguration = { - smtp: { - host: "smtp.example.com", - port: 25, - secure: false, - sender: "test@example.com" - }, - email: { - username: "test", - password: "test", - sender: "test@example.com", - service: "gmail" - } - }; - const [newConfiguration, error] = complete(configuration); - - Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/NotifierConfiguration.ts b/themes/squares/server/src/lib/configuration/schema/NotifierConfiguration.ts deleted file mode 100644 index 7bcce15c2..000000000 --- a/themes/squares/server/src/lib/configuration/schema/NotifierConfiguration.ts +++ /dev/null @@ -1,45 +0,0 @@ - -export interface EmailNotifierConfiguration { - username: string; - password: string; - sender: string; - service: string; -} - -export interface SmtpNotifierConfiguration { - username?: string; - password?: string; - host: string; - port: number; - secure: boolean; - sender: string; -} - -export interface FileSystemNotifierConfiguration { - filename: string; -} - -export interface NotifierConfiguration { - email?: EmailNotifierConfiguration; - smtp?: SmtpNotifierConfiguration; - filesystem?: FileSystemNotifierConfiguration; -} - -export function complete(configuration: NotifierConfiguration): [NotifierConfiguration, string] { - const newConfiguration: NotifierConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (Object.keys(newConfiguration).length == 0) - newConfiguration.filesystem = { filename: "/tmp/authelia/notification.txt" }; - - const ERROR = "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"; - - if (Object.keys(newConfiguration).length != 1) - return [newConfiguration, ERROR]; - - const key = Object.keys(newConfiguration)[0]; - - if (key != "filesystem" && key != "smtp" && key != "email") - return [newConfiguration, ERROR]; - - return [newConfiguration, undefined]; -} diff --git a/themes/squares/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts b/themes/squares/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts deleted file mode 100644 index dce2caf4e..000000000 --- a/themes/squares/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Assert = require("assert"); -import { RegulationConfiguration, complete } from "./RegulationConfiguration"; - -describe("configuration/schema/RegulationConfiguration", function() { - it("should return default regulation configuration", function() { - const configuration: RegulationConfiguration = {}; - const newConfiguration = complete(configuration); - - Assert.equal(newConfiguration.ban_time, 300); - Assert.equal(newConfiguration.find_time, 120); - Assert.equal(newConfiguration.max_retries, 3); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/RegulationConfiguration.ts b/themes/squares/server/src/lib/configuration/schema/RegulationConfiguration.ts deleted file mode 100644 index 117463f43..000000000 --- a/themes/squares/server/src/lib/configuration/schema/RegulationConfiguration.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface RegulationConfiguration { - max_retries?: number; - find_time?: number; - ban_time?: number; -} - -export function complete(configuration: RegulationConfiguration): RegulationConfiguration { - const newConfiguration: RegulationConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.max_retries) { - newConfiguration.max_retries = 3; - } - - if (!newConfiguration.find_time) { - newConfiguration.find_time = 120; // seconds - } - - if (!newConfiguration.ban_time) { - newConfiguration.ban_time = 300; // seconds - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/SessionConfiguration.spec.ts b/themes/squares/server/src/lib/configuration/schema/SessionConfiguration.spec.ts deleted file mode 100644 index e54010837..000000000 --- a/themes/squares/server/src/lib/configuration/schema/SessionConfiguration.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Assert = require("assert"); -import { SessionConfiguration, complete } from "./SessionConfiguration"; - -describe("configuration/schema/SessionConfiguration", function() { - it("should return default regulation configuration", function() { - const configuration: SessionConfiguration = { - domain: "example.com", - secret: "unsecure_secret" - }; - const newConfiguration = complete(configuration); - - Assert.equal(newConfiguration.name, 'authelia_session'); - Assert.equal(newConfiguration.expiration, 3600000); - Assert.equal(newConfiguration.inactivity, undefined); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/SessionConfiguration.ts b/themes/squares/server/src/lib/configuration/schema/SessionConfiguration.ts deleted file mode 100644 index 2c88bb215..000000000 --- a/themes/squares/server/src/lib/configuration/schema/SessionConfiguration.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface SessionRedisOptions { - host: string; - port: number; - password?: string; -} - -export interface SessionConfiguration { - name?: string; - domain: string; - secret: string; - expiration?: number; - inactivity?: number; - redis?: SessionRedisOptions; -} - -export function complete(configuration: SessionConfiguration): SessionConfiguration { - const newConfiguration: SessionConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.name) { - newConfiguration.name = "authelia_session"; - } - - if (!newConfiguration.expiration) { - newConfiguration.expiration = 3600000; // 1 hour - } - - if (!newConfiguration.inactivity) { - newConfiguration.inactivity = undefined; // disabled - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/StorageConfiguration.spec.ts b/themes/squares/server/src/lib/configuration/schema/StorageConfiguration.spec.ts deleted file mode 100644 index 9d02a11b6..000000000 --- a/themes/squares/server/src/lib/configuration/schema/StorageConfiguration.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Assert = require("assert"); -import { StorageConfiguration, complete } from "./StorageConfiguration"; - -describe("configuration/schema/StorageConfiguration", function() { - it("should return default regulation configuration", function() { - const configuration: StorageConfiguration = {}; - const newConfiguration = complete(configuration); - - Assert.deepEqual(newConfiguration, { - local: { - in_memory: true - } - }); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/StorageConfiguration.ts b/themes/squares/server/src/lib/configuration/schema/StorageConfiguration.ts deleted file mode 100644 index 47e356ef4..000000000 --- a/themes/squares/server/src/lib/configuration/schema/StorageConfiguration.ts +++ /dev/null @@ -1,30 +0,0 @@ -export interface MongoStorageConfiguration { - url: string; - database: string; - auth?: { - username: string; - password: string; - }; -} - -export interface LocalStorageConfiguration { - path?: string; - in_memory?: boolean; -} - -export interface StorageConfiguration { - local?: LocalStorageConfiguration; - mongo?: MongoStorageConfiguration; -} - -export function complete(configuration: StorageConfiguration): StorageConfiguration { - const newConfiguration: StorageConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.local && !newConfiguration.mongo) { - newConfiguration.local = { - in_memory: true - }; - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/TotpConfiguration.ts b/themes/squares/server/src/lib/configuration/schema/TotpConfiguration.ts deleted file mode 100644 index 683135639..000000000 --- a/themes/squares/server/src/lib/configuration/schema/TotpConfiguration.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface TotpConfiguration { - issuer: string; -} - -export function complete(configuration: TotpConfiguration): TotpConfiguration { - const newConfiguration: TotpConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.issuer) { - newConfiguration.issuer = "authelia.com"; - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts b/themes/squares/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts deleted file mode 100644 index 8008b4833..000000000 --- a/themes/squares/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts +++ /dev/null @@ -1,9 +0,0 @@ - -export interface UserInfo { - username: string; - password_hash: string; - email: string; - groups?: string[]; -} - -export type UserDatabaseConfiguration = UserInfo[]; \ No newline at end of file diff --git a/themes/squares/server/src/lib/connectors/mongo/IMongoClient.d.ts b/themes/squares/server/src/lib/connectors/mongo/IMongoClient.d.ts deleted file mode 100644 index 36cb4b8bf..000000000 --- a/themes/squares/server/src/lib/connectors/mongo/IMongoClient.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import MongoDB = require("mongodb"); -import Bluebird = require("bluebird"); - -export interface IMongoClient { - collection(name: string): Bluebird -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/connectors/mongo/MongoClient.spec.ts b/themes/squares/server/src/lib/connectors/mongo/MongoClient.spec.ts deleted file mode 100644 index ca0c68593..000000000 --- a/themes/squares/server/src/lib/connectors/mongo/MongoClient.spec.ts +++ /dev/null @@ -1,119 +0,0 @@ -import Assert = require("assert"); -import Bluebird = require("bluebird"); -import MongoDB = require("mongodb"); -import Sinon = require("sinon"); - -import { MongoClient } from "./MongoClient"; -import { GlobalLoggerStub } from "../../logging/GlobalLoggerStub.spec"; -import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration"; - -describe("connectors/mongo/MongoClient", function () { - let MongoClientStub: any; - let mongoClientStub: any; - let mongoDatabaseStub: any; - let logger: GlobalLoggerStub = new GlobalLoggerStub(); - - const configuration: MongoStorageConfiguration = { - url: "mongo://url", - database: "databasename" - }; - - describe("connection", () => { - before(() => { - mongoClientStub = { - db: Sinon.stub() - }; - mongoDatabaseStub = { - on: Sinon.stub(), - collection: Sinon.stub() - } - MongoClientStub = Sinon.stub( - MongoDB.MongoClient, "connect"); - MongoClientStub.yields( - undefined, mongoClientStub); - mongoClientStub.db.returns( - mongoDatabaseStub); - }); - - after(() => { - MongoClientStub.restore(); - }); - - it("should use credentials from configuration", () => { - configuration.auth = { - username: "authelia", - password: "authelia_pass" - }; - - const client = new MongoClient(configuration, logger); - return client.collection("test") - .then(() => { - Assert(MongoClientStub.calledWith("mongo://url", { - auth: { - user: "authelia", - password: "authelia_pass" - } - })) - }); - }); - }); - - describe("collection", () => { - before(function() { - mongoClientStub = { - db: Sinon.stub() - }; - mongoDatabaseStub = { - on: Sinon.stub(), - collection: Sinon.stub() - } - }); - - describe("Connection to mongo is ok", function() { - before(function () { - MongoClientStub = Sinon.stub( - MongoDB.MongoClient, "connect"); - MongoClientStub.yields( - undefined, mongoClientStub); - mongoClientStub.db.returns( - mongoDatabaseStub); - }); - - after(function () { - MongoClientStub.restore(); - }); - - it("should create a collection", function () { - const COLLECTION_NAME = "mycollection"; - const client = new MongoClient(configuration, logger); - - mongoDatabaseStub.collection.returns("COL"); - return client.collection(COLLECTION_NAME) - .then((collection) => mongoDatabaseStub.collection.calledWith(COLLECTION_NAME)); - }); - }); - - describe("Connection to mongo is broken", function() { - before(function () { - MongoClientStub = Sinon.stub( - MongoDB.MongoClient, "connect"); - MongoClientStub.yields( - new Error("Failed connection"), undefined); - }); - - after(function () { - MongoClientStub.restore(); - }); - - it("should fail creating the collection", function() { - const COLLECTION_NAME = "mycollection"; - const client = new MongoClient(configuration, logger); - - mongoDatabaseStub.collection.returns("COL"); - return client.collection(COLLECTION_NAME) - .then((collection) => Bluebird.reject(new Error("should not be here."))) - .catch((err) => Bluebird.resolve()); - }); - }) - }); -}); diff --git a/themes/squares/server/src/lib/connectors/mongo/MongoClient.ts b/themes/squares/server/src/lib/connectors/mongo/MongoClient.ts deleted file mode 100644 index d15731e97..000000000 --- a/themes/squares/server/src/lib/connectors/mongo/MongoClient.ts +++ /dev/null @@ -1,76 +0,0 @@ - -import MongoDB = require("mongodb"); -import { IMongoClient } from "./IMongoClient"; -import Bluebird = require("bluebird"); -import { AUTHENTICATION_FAILED } from "../../../../../shared/UserMessages"; -import { IGlobalLogger } from "../../logging/IGlobalLogger"; -import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration"; - -export class MongoClient implements IMongoClient { - private configuration: MongoStorageConfiguration; - - private database: MongoDB.Db; - private client: MongoDB.MongoClient; - private logger: IGlobalLogger; - - constructor( - configuration: MongoStorageConfiguration, - logger: IGlobalLogger) { - - this.configuration = configuration; - this.logger = logger; - } - - connect(): Bluebird { - const that = this; - const options: MongoDB.MongoClientOptions = {}; - if (that.configuration.auth) { - options["auth"] = { - user: that.configuration.auth.username, - password: that.configuration.auth.password - }; - } - - return new Bluebird((resolve, reject) => { - MongoDB.MongoClient.connect( - this.configuration.url, - options, - function(err, client) { - if (err) { - reject(err); - return; - } - resolve(client); - }); - }) - .then(function (client: MongoDB.MongoClient) { - that.database = client.db(that.configuration.database); - that.database.on("close", () => { - that.logger.info("[MongoClient] Lost connection."); - }); - that.database.on("reconnect", () => { - that.logger.info("[MongoClient] Reconnected."); - }); - that.client = client; - }); - } - - close(): Bluebird { - if (this.client) { - this.client.close(); - this.database = undefined; - this.client = undefined; - } - return Bluebird.resolve(); - } - - collection(name: string): Bluebird { - if (!this.client) { - const that = this; - return this.connect() - .then(() => Bluebird.resolve(that.database.collection(name))); - } - - return Bluebird.resolve(this.database.collection(name)); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/connectors/mongo/MongoClientStub.spec.ts b/themes/squares/server/src/lib/connectors/mongo/MongoClientStub.spec.ts deleted file mode 100644 index 1cfd48e32..000000000 --- a/themes/squares/server/src/lib/connectors/mongo/MongoClientStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Sinon = require("sinon"); -import MongoDB = require("mongodb"); -import Bluebird = require("bluebird"); -import { IMongoClient } from "../../../../src/lib/connectors/mongo/IMongoClient"; - -export class MongoClientStub implements IMongoClient { - public collectionStub: Sinon.SinonStub; - - constructor() { - this.collectionStub = Sinon.stub(); - } - - collection(name: string): Bluebird { - return this.collectionStub(name); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/logging/GlobalLogger.ts b/themes/squares/server/src/lib/logging/GlobalLogger.ts deleted file mode 100644 index 4da7acf49..000000000 --- a/themes/squares/server/src/lib/logging/GlobalLogger.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { IGlobalLogger } from "./IGlobalLogger"; -import Util = require("util"); -import Express = require("express"); -import Winston = require("winston"); - -declare module "express" { - interface Request { - id: string; - } -} - -export class GlobalLogger implements IGlobalLogger { - private winston: typeof Winston; - constructor(winston: typeof Winston) { - this.winston = winston; - } - - private buildMessage(message: string, ...args: any[]): string { - return Util.format("date='%s' message='%s'", new Date(), - Util.format(message, ...args)); - } - - info(message: string, ...args: any[]): void { - this.winston.info(this.buildMessage(message, ...args)); - } - - debug(message: string, ...args: any[]): void { - this.winston.debug(this.buildMessage(message, ...args)); - } - - error(message: string, ...args: any[]): void { - this.winston.debug(this.buildMessage(message, ...args)); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/logging/GlobalLoggerStub.spec.ts b/themes/squares/server/src/lib/logging/GlobalLoggerStub.spec.ts deleted file mode 100644 index d4bb13710..000000000 --- a/themes/squares/server/src/lib/logging/GlobalLoggerStub.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Sinon = require("sinon"); -import { GlobalLogger } from "./GlobalLogger"; -import Winston = require("winston"); -import Express = require("express"); -import { IGlobalLogger } from "./IGlobalLogger"; - -export class GlobalLoggerStub implements IGlobalLogger { - infoStub: Sinon.SinonStub; - debugStub: Sinon.SinonStub; - errorStub: Sinon.SinonStub; - private globalLogger: IGlobalLogger; - - constructor(enableLogging?: boolean) { - this.infoStub = Sinon.stub(); - this.debugStub = Sinon.stub(); - this.errorStub = Sinon.stub(); - if (enableLogging) - this.globalLogger = new GlobalLogger(Winston); - } - - info(message: string, ...args: any[]): void { - if (this.globalLogger) - this.globalLogger.info(message, ...args); - this.infoStub(message, ...args); - } - - debug(message: string, ...args: any[]): void { - if (this.globalLogger) - this.globalLogger.info(message, ...args); - this.debugStub(message, ...args); - } - - error(message: string, ...args: any[]): void { - if (this.globalLogger) - this.globalLogger.info(message, ...args); - this.errorStub(message, ...args); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/logging/IGlobalLogger.ts b/themes/squares/server/src/lib/logging/IGlobalLogger.ts deleted file mode 100644 index 548515ec7..000000000 --- a/themes/squares/server/src/lib/logging/IGlobalLogger.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface IGlobalLogger { - info(message: string, ...args: any[]): void; - debug(message: string, ...args: any[]): void; - error(message: string, ...args: any[]): void; -} diff --git a/themes/squares/server/src/lib/logging/IRequestLogger.ts b/themes/squares/server/src/lib/logging/IRequestLogger.ts deleted file mode 100644 index 126a601fe..000000000 --- a/themes/squares/server/src/lib/logging/IRequestLogger.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Express = require("express"); - -export interface IRequestLogger { - info(req: Express.Request, message: string, ...args: any[]): void; - debug(req: Express.Request, message: string, ...args: any[]): void; - error(req: Express.Request, message: string, ...args: any[]): void; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/logging/RequestLogger.ts b/themes/squares/server/src/lib/logging/RequestLogger.ts deleted file mode 100644 index c45c66018..000000000 --- a/themes/squares/server/src/lib/logging/RequestLogger.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { IRequestLogger } from "./IRequestLogger"; -import Util = require("util"); -import Express = require("express"); -import Winston = require("winston"); - -declare module "express" { - interface Request { - id: string; - } -} - -export class RequestLogger implements IRequestLogger { - private winston: typeof Winston; - - constructor(winston: typeof Winston) { - this.winston = winston; - } - - private formatHeader(req: Express.Request) { - const clientIP = req.ip; // The IP of the original client going through the proxy chain. - return Util.format("date='%s' method='%s', path='%s' requestId='%s' sessionId='%s' ip='%s'", - new Date(), req.method, req.path, req.id, req.sessionID, clientIP); - } - - private formatBody(message: string) { - return Util.format("message='%s'", message); - } - - private formatMessage(req: Express.Request, message: string) { - return Util.format("%s %s", this.formatHeader(req), - this.formatBody(message)); - } - - info(req: Express.Request, message: string, ...args: any[]): void { - this.winston.info(this.formatMessage(req, message), ...args); - } - - debug(req: Express.Request, message: string, ...args: any[]): void { - this.winston.debug(this.formatMessage(req, message), ...args); - } - - error(req: Express.Request, message: string, ...args: any[]): void { - this.winston.error(this.formatMessage(req, message), ...args); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/logging/RequestLoggerStub.spec.ts b/themes/squares/server/src/lib/logging/RequestLoggerStub.spec.ts deleted file mode 100644 index b0e375210..000000000 --- a/themes/squares/server/src/lib/logging/RequestLoggerStub.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { IRequestLogger } from "./IRequestLogger"; -import Sinon = require("sinon"); -import { RequestLogger } from "./RequestLogger"; -import Winston = require("winston"); -import Express = require("express"); - -export class RequestLoggerStub implements IRequestLogger { - infoStub: Sinon.SinonStub; - debugStub: Sinon.SinonStub; - errorStub: Sinon.SinonStub; - private requestLogger: RequestLogger; - - constructor(enableLogging?: boolean) { - this.infoStub = Sinon.stub(); - this.debugStub = Sinon.stub(); - this.errorStub = Sinon.stub(); - if (enableLogging) - this.requestLogger = new RequestLogger(Winston); - } - - info(req: Express.Request, message: string, ...args: any[]): void { - if (this.requestLogger) - this.requestLogger.info(req, message, ...args); - this.infoStub(req, message, ...args); - } - - debug(req: Express.Request, message: string, ...args: any[]): void { - if (this.requestLogger) - this.requestLogger.info(req, message, ...args); - this.debugStub(req, message, ...args); - } - - error(req: Express.Request, message: string, ...args: any[]): void { - if (this.requestLogger) - this.requestLogger.info(req, message, ...args); - this.errorStub(req, message, ...args); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/AbstractEmailNotifier.ts b/themes/squares/server/src/lib/notifiers/AbstractEmailNotifier.ts deleted file mode 100644 index 198e4e5db..000000000 --- a/themes/squares/server/src/lib/notifiers/AbstractEmailNotifier.ts +++ /dev/null @@ -1,23 +0,0 @@ - -import { INotifier } from "../notifiers/INotifier"; -import { Identity } from "../../../types/Identity"; - -import Fs = require("fs"); -import Path = require("path"); -import Ejs = require("ejs"); -import BluebirdPromise = require("bluebird"); - -const email_template = Fs.readFileSync(Path.join(__dirname, "../../resources/email-template.ejs"), "UTF-8"); - -export abstract class AbstractEmailNotifier implements INotifier { - notify(to: string, subject: string, link: string): BluebirdPromise { - const d = { - url: link, - button_title: "Continue", - title: subject - }; - return this.sendEmail(to, subject, Ejs.render(email_template, d)); - } - - abstract sendEmail(to: string, subject: string, content: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/EmailNotifier.spec.ts b/themes/squares/server/src/lib/notifiers/EmailNotifier.spec.ts deleted file mode 100644 index 8211bbc02..000000000 --- a/themes/squares/server/src/lib/notifiers/EmailNotifier.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as sinon from "sinon"; -import * as Assert from "assert"; -import BluebirdPromise = require("bluebird"); - -import { MailSenderStub } from "./MailSenderStub.spec"; -import EmailNotifier = require("./EmailNotifier"); - - -describe("notifiers/EmailNotifier", function () { - it("should send an email to given user", function () { - const mailSender = new MailSenderStub(); - const options = { - username: "user_gmail", - password: "pass_gmail", - sender: "admin@example.com", - service: "gmail" - }; - - mailSender.sendStub.returns(BluebirdPromise.resolve()); - const sender = new EmailNotifier.EmailNotifier(options, mailSender); - const subject = "subject"; - const url = "http://test.com"; - - return sender.notify("user@example.com", subject, url) - .then(function () { - Assert.equal(mailSender.sendStub.getCall(0).args[0].to, "user@example.com"); - Assert.equal(mailSender.sendStub.getCall(0).args[0].subject, "subject"); - return BluebirdPromise.resolve(); - }); - }); - - it("should fail while sending an email", function () { - const mailSender = new MailSenderStub(); - const options = { - username: "user_gmail", - password: "pass_gmail", - sender: "admin@example.com", - service: "gmail" - }; - - mailSender.sendStub.returns(BluebirdPromise.reject(new Error("Failed to send mail"))); - const sender = new EmailNotifier.EmailNotifier(options, mailSender); - const subject = "subject"; - const url = "http://test.com"; - - return sender.notify("user@example.com", subject, url) - .then(function () { - return BluebirdPromise.reject(new Error()); - }, function() { - Assert.equal(mailSender.sendStub.getCall(0).args[0].from, "admin@example.com"); - return BluebirdPromise.resolve(); - }); - }); -}); diff --git a/themes/squares/server/src/lib/notifiers/EmailNotifier.ts b/themes/squares/server/src/lib/notifiers/EmailNotifier.ts deleted file mode 100644 index 4df7c861e..000000000 --- a/themes/squares/server/src/lib/notifiers/EmailNotifier.ts +++ /dev/null @@ -1,27 +0,0 @@ - -import * as BluebirdPromise from "bluebird"; - -import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; -import { EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; -import { IMailSender } from "./IMailSender"; - -export class EmailNotifier extends AbstractEmailNotifier { - private mailSender: IMailSender; - private sender: string; - - constructor(options: EmailNotifierConfiguration, mailSender: IMailSender) { - super(); - this.mailSender = mailSender; - this.sender = options.sender; - } - - sendEmail(to: string, subject: string, content: string) { - const mailOptions = { - from: this.sender, - to: to, - subject: subject, - html: content - }; - return this.mailSender.send(mailOptions); - } -} diff --git a/themes/squares/server/src/lib/notifiers/FileSystemNotifier.ts b/themes/squares/server/src/lib/notifiers/FileSystemNotifier.ts deleted file mode 100644 index 23f6242c4..000000000 --- a/themes/squares/server/src/lib/notifiers/FileSystemNotifier.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as BluebirdPromise from "bluebird"; -import * as util from "util"; -import * as Fs from "fs"; -import { INotifier } from "./INotifier"; -import { Identity } from "../../../types/Identity"; - -import { FileSystemNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; - -export class FileSystemNotifier implements INotifier { - private filename: string; - - constructor(options: FileSystemNotifierConfiguration) { - this.filename = options.filename; - } - - notify(to: string, subject: string, link: string): BluebirdPromise { - const content = util.format("Date: %s\nEmail: %s\nSubject: %s\nLink: %s", - new Date().toString(), to, subject, link); - const writeFilePromised: any = BluebirdPromise.promisify(Fs.writeFile); - return writeFilePromised(this.filename, content); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/IMailSender.ts b/themes/squares/server/src/lib/notifiers/IMailSender.ts deleted file mode 100644 index 34ac464a8..000000000 --- a/themes/squares/server/src/lib/notifiers/IMailSender.ts +++ /dev/null @@ -1,6 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Nodemailer = require("nodemailer"); - -export interface IMailSender { - send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/IMailSenderBuilder.ts b/themes/squares/server/src/lib/notifiers/IMailSenderBuilder.ts deleted file mode 100644 index 36d4dcdf7..000000000 --- a/themes/squares/server/src/lib/notifiers/IMailSenderBuilder.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IMailSender } from "./IMailSender"; -import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; - -export interface IMailSenderBuilder { - buildEmail(options: EmailNotifierConfiguration): IMailSender; - buildSmtp(options: SmtpNotifierConfiguration): IMailSender; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/INotifier.ts b/themes/squares/server/src/lib/notifiers/INotifier.ts deleted file mode 100644 index b9a6b138c..000000000 --- a/themes/squares/server/src/lib/notifiers/INotifier.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as BluebirdPromise from "bluebird"; - -export interface INotifier { - notify(to: string, subject: string, link: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/MailSender.ts b/themes/squares/server/src/lib/notifiers/MailSender.ts deleted file mode 100644 index 536a88e63..000000000 --- a/themes/squares/server/src/lib/notifiers/MailSender.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { IMailSender } from "./IMailSender"; -import Nodemailer = require("nodemailer"); -import NodemailerDirectTransport = require("nodemailer-direct-transport"); -import NodemailerSmtpTransport = require("nodemailer-smtp-transport"); -import BluebirdPromise = require("bluebird"); - -export class MailSender implements IMailSender { - private transporter: Nodemailer.Transporter; - - constructor(options: NodemailerDirectTransport.DirectOptions | - NodemailerSmtpTransport.SmtpOptions, nodemailer: typeof Nodemailer) { - this.transporter = nodemailer.createTransport(options); - } - - verify(): BluebirdPromise { - const that = this; - return new BluebirdPromise(function (resolve, reject) { - that.transporter.verify(function (error: Error, success: any) { - if (error) { - reject(new Error("Unable to connect to SMTP server. \ - Please check the service is running and your credentials are correct.")); - return; - } - resolve(); - }); - }); - } - - send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise { - const that = this; - return new BluebirdPromise(function (resolve, reject) { - that.transporter.sendMail(mailOptions, (error: Error, - data: Nodemailer.SentMessageInfo) => { - if (error) { - reject(new Error("Error while sending email: " + error.message)); - return; - } - resolve(); - }); - }); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/MailSenderBuilder.spec.ts b/themes/squares/server/src/lib/notifiers/MailSenderBuilder.spec.ts deleted file mode 100644 index 41e0db426..000000000 --- a/themes/squares/server/src/lib/notifiers/MailSenderBuilder.spec.ts +++ /dev/null @@ -1,67 +0,0 @@ - -import { MailSenderBuilder } from ".//MailSenderBuilder"; -import Nodemailer = require("nodemailer"); -import Sinon = require("sinon"); -import Assert = require("assert"); - -describe("notifiers/MailSenderBuilder", function() { - let createTransportStub: Sinon.SinonStub; - beforeEach(function() { - createTransportStub = Sinon.stub(Nodemailer, "createTransport"); - }); - - afterEach(function() { - createTransportStub.restore(); - }); - - it("should create a email mail sender", function() { - const mailSenderBuilder = new MailSenderBuilder(Nodemailer); - mailSenderBuilder.buildEmail({ - username: "user_gmail", - password: "pass_gmail", - sender: "admin@example.com", - service: "gmail" - }); - Assert.equal(createTransportStub.getCall(0).args[0].auth.user, "user_gmail"); - Assert.equal(createTransportStub.getCall(0).args[0].auth.pass, "pass_gmail"); - Assert.equal(createTransportStub.getCall(0).args[0].service, "gmail"); - }); - - describe("build smtp mail sender", function() { - it("should create a smtp mail sender with authenticated user", function() { - const mailSenderBuilder = new MailSenderBuilder(Nodemailer); - mailSenderBuilder.buildSmtp({ - host: "mail.example.com", - password: "password", - port: 25, - secure: true, - username: "user", - sender: "admin@example.com" - }); - Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { - host: "mail.example.com", - auth: { - pass: "password", - user: "user" - }, - port: 25, - secure: true, - }); - }); - - it("should create a smtp mail sender with anonymous user", function() { - const mailSenderBuilder = new MailSenderBuilder(Nodemailer); - mailSenderBuilder.buildSmtp({ - host: "mail.example.com", - port: 25, - secure: true, - sender: "admin@example.com" - }); - Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { - host: "mail.example.com", - port: 25, - secure: true, - }); - }); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/MailSenderBuilder.ts b/themes/squares/server/src/lib/notifiers/MailSenderBuilder.ts deleted file mode 100644 index 1d06be52f..000000000 --- a/themes/squares/server/src/lib/notifiers/MailSenderBuilder.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { IMailSender } from "./IMailSender"; -import { IMailSenderBuilder } from "./IMailSenderBuilder"; -import { MailSender } from "./MailSender"; -import Nodemailer = require("nodemailer"); -import NodemailerSmtpTransport = require("nodemailer-smtp-transport"); -import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; - -export class MailSenderBuilder implements IMailSenderBuilder { - private nodemailer: typeof Nodemailer; - - constructor(nodemailer: typeof Nodemailer) { - this.nodemailer = nodemailer; - } - - buildEmail(options: EmailNotifierConfiguration): IMailSender { - const emailOptions = { - service: options.service, - auth: { - user: options.username, - pass: options.password - } - }; - return new MailSender(emailOptions, this.nodemailer); - } - - buildSmtp(options: SmtpNotifierConfiguration): IMailSender { - const smtpOptions: NodemailerSmtpTransport.SmtpOptions = { - host: options.host, - port: options.port, - secure: options.secure, // upgrade later with STARTTLS - }; - - if (options.username && options.password) { - smtpOptions.auth = { - user: options.username, - pass: options.password - }; - } - - return new MailSender(smtpOptions, this.nodemailer); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts b/themes/squares/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts deleted file mode 100644 index 5b76f6e56..000000000 --- a/themes/squares/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { IMailSenderBuilder } from "../../../src/lib/notifiers/IMailSenderBuilder"; -import BluebirdPromise = require("bluebird"); -import Nodemailer = require("nodemailer"); -import Sinon = require("sinon"); -import { IMailSender } from "../../../src/lib/notifiers/IMailSender"; -import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../../../src/lib/configuration/schema/NotifierConfiguration"; - -export class MailSenderBuilderStub implements IMailSenderBuilder { - buildEmailStub: Sinon.SinonStub; - buildSmtpStub: Sinon.SinonStub; - - constructor() { - this.buildEmailStub = Sinon.stub(); - this.buildSmtpStub = Sinon.stub(); - } - - buildEmail(options: EmailNotifierConfiguration): IMailSender { - return this.buildEmailStub(options); - } - - buildSmtp(options: SmtpNotifierConfiguration): IMailSender { - return this.buildSmtpStub(options); - } - -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/MailSenderStub.spec.ts b/themes/squares/server/src/lib/notifiers/MailSenderStub.spec.ts deleted file mode 100644 index d57c458fc..000000000 --- a/themes/squares/server/src/lib/notifiers/MailSenderStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { IMailSender } from "../../../src/lib/notifiers/IMailSender"; -import BluebirdPromise = require("bluebird"); -import Nodemailer = require("nodemailer"); -import Sinon = require("sinon"); - -export class MailSenderStub implements IMailSender { - sendStub: Sinon.SinonStub; - - constructor() { - this.sendStub = Sinon.stub(); - } - - send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise { - return this.sendStub(mailOptions); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/NotifierFactory.spec.ts b/themes/squares/server/src/lib/notifiers/NotifierFactory.spec.ts deleted file mode 100644 index f15e7667f..000000000 --- a/themes/squares/server/src/lib/notifiers/NotifierFactory.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ - -import * as sinon from "sinon"; -import * as BluebirdPromise from "bluebird"; -import * as assert from "assert"; - -import { NotifierFactory } from "./NotifierFactory"; -import { EmailNotifier } from "./EmailNotifier"; -import { SmtpNotifier } from "./SmtpNotifier"; -import { MailSenderBuilderStub } from "./MailSenderBuilderStub.spec"; - - -describe("notifiers/NotifierFactory", function () { - let mailSenderBuilderStub: MailSenderBuilderStub; - it("should build a Email Notifier", function () { - const options = { - email: { - username: "abc", - password: "password", - sender: "admin@example.com", - service: "gmail" - } - }; - mailSenderBuilderStub = new MailSenderBuilderStub(); - assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof EmailNotifier); - }); - - it("should build a SMTP Notifier", function () { - const options = { - smtp: { - username: "user", - password: "pass", - secure: true, - host: "localhost", - port: 25, - sender: "admin@example.com" - } - }; - - mailSenderBuilderStub = new MailSenderBuilderStub(); - assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof SmtpNotifier); - }); -}); diff --git a/themes/squares/server/src/lib/notifiers/NotifierFactory.ts b/themes/squares/server/src/lib/notifiers/NotifierFactory.ts deleted file mode 100644 index a89155feb..000000000 --- a/themes/squares/server/src/lib/notifiers/NotifierFactory.ts +++ /dev/null @@ -1,33 +0,0 @@ - -import { NotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; -import Nodemailer = require("nodemailer"); -import { INotifier } from "./INotifier"; - -import { FileSystemNotifier } from "./FileSystemNotifier"; -import { EmailNotifier } from "./EmailNotifier"; -import { SmtpNotifier } from "./SmtpNotifier"; -import { IMailSender } from "./IMailSender"; -import { IMailSenderBuilder } from "./IMailSenderBuilder"; - -export class NotifierFactory { - static build(options: NotifierConfiguration, mailSenderBuilder: IMailSenderBuilder): INotifier { - if ("email" in options) { - const mailSender = mailSenderBuilder.buildEmail(options.email); - return new EmailNotifier(options.email, mailSender); - } - else if ("smtp" in options) { - const mailSender = mailSenderBuilder.buildSmtp(options.smtp); - return new SmtpNotifier(options.smtp, mailSender); - } - else if ("filesystem" in options) { - return new FileSystemNotifier(options.filesystem); - } - else { - throw new Error("No available notifier option detected."); - } - } -} - - - - diff --git a/themes/squares/server/src/lib/notifiers/NotifierStub.spec.ts b/themes/squares/server/src/lib/notifiers/NotifierStub.spec.ts deleted file mode 100644 index f99231b50..000000000 --- a/themes/squares/server/src/lib/notifiers/NotifierStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); - -import { INotifier } from "./INotifier"; - -export class NotifierStub implements INotifier { - notifyStub: Sinon.SinonStub; - - constructor() { - this.notifyStub = Sinon.stub(); - } - - notify(to: string, subject: string, link: string): BluebirdPromise { - return this.notifyStub(to, subject, link); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/notifiers/SmtpNotifier.ts b/themes/squares/server/src/lib/notifiers/SmtpNotifier.ts deleted file mode 100644 index f93a6d4a0..000000000 --- a/themes/squares/server/src/lib/notifiers/SmtpNotifier.ts +++ /dev/null @@ -1,30 +0,0 @@ - - -import * as BluebirdPromise from "bluebird"; - -import { IMailSender } from "./IMailSender"; -import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; -import { SmtpNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; - -export class SmtpNotifier extends AbstractEmailNotifier { - private mailSender: IMailSender; - private sender: string; - - constructor(options: SmtpNotifierConfiguration, - mailSender: IMailSender) { - super(); - this.mailSender = mailSender; - this.sender = options.sender; - } - - sendEmail(to: string, subject: string, content: string) { - const mailOptions = { - from: this.sender, - to: to, - subject: subject, - html: content - }; - const that = this; - return this.mailSender.send(mailOptions); - } -} diff --git a/themes/squares/server/src/lib/regulation/IRegulator.ts b/themes/squares/server/src/lib/regulation/IRegulator.ts deleted file mode 100644 index c49425b24..000000000 --- a/themes/squares/server/src/lib/regulation/IRegulator.ts +++ /dev/null @@ -1,6 +0,0 @@ -import BluebirdPromise = require("bluebird"); - -export interface IRegulator { - mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; - regulate(userId: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/regulation/Regulator.spec.ts b/themes/squares/server/src/lib/regulation/Regulator.spec.ts deleted file mode 100644 index f9c6e6086..000000000 --- a/themes/squares/server/src/lib/regulation/Regulator.spec.ts +++ /dev/null @@ -1,186 +0,0 @@ - -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import Assert = require("assert"); - -import { Regulator } from "./Regulator"; -import MockDate = require("mockdate"); -import exceptions = require("../Exceptions"); -import { UserDataStoreStub } from "../storage/UserDataStoreStub.spec"; - -describe("regulation/Regulator", function () { - const USER1 = "USER1"; - const USER2 = "USER2"; - let userDataStoreStub: UserDataStoreStub; - - beforeEach(function () { - userDataStoreStub = new UserDataStoreStub(); - const dataStore: { [userId: string]: { userId: string, date: Date, isAuthenticationSuccessful: boolean }[] } = { - [USER1]: [], - [USER2]: [] - }; - - userDataStoreStub.saveAuthenticationTraceStub.callsFake(function (userId, isAuthenticationSuccessful) { - dataStore[userId].unshift({ - userId: userId, - date: new Date(), - isAuthenticationSuccessful: isAuthenticationSuccessful, - }); - return BluebirdPromise.resolve(); - }); - - userDataStoreStub.retrieveLatestAuthenticationTracesStub.callsFake(function (userId, count) { - const ret = (dataStore[userId].length <= count) ? dataStore[userId] : dataStore[userId].slice(0, 3); - return BluebirdPromise.resolve(ret); - }); - }); - - afterEach(function () { - MockDate.reset(); - }); - - function markAuthenticationAt(regulator: Regulator, user: string, time: string, success: boolean) { - MockDate.set(time); - return regulator.mark(user, success); - } - - it("should mark 2 authentication and regulate (accept)", function () { - const regulator = new Regulator(userDataStoreStub, 3, 10, 10); - - return regulator.mark(USER1, false) - .then(function () { - return regulator.mark(USER1, true); - }) - .then(function () { - return regulator.regulate(USER1); - }); - }); - - it("should mark 3 authentications and regulate (reject)", function () { - const regulator = new Regulator(userDataStoreStub, 3, 10, 10); - - return regulator.mark(USER1, false) - .then(function () { - return regulator.mark(USER1, false); - }) - .then(function () { - return regulator.mark(USER1, false); - }) - .then(function () { - return regulator.regulate(USER1); - }) - .then(function () { return BluebirdPromise.reject(new Error("should not be here!")); }) - .catch(exceptions.AuthenticationRegulationError, function () { - return BluebirdPromise.resolve(); - }); - }); - - it("should mark 1 failed, 1 successful and 1 failed authentications within minimum time and regulate (accept)", function () { - const regulator = new Regulator(userDataStoreStub, 3, 60, 30); - - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", true); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:20", false); - }) - .then(function () { - return regulator.regulate(USER1); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:30", false); - }) - .then(function () { - return regulator.regulate(USER1); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:39", false); - }) - .then(function () { - return regulator.regulate(USER1); - }) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here!")); - }, - function () { - return BluebirdPromise.resolve(); - }); - }); - - it("should regulate user if number of failures is greater than 3 in allowed time lapse", function () { - function markAuthentications(regulator: Regulator, user: string) { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:45", false); - }) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:01:05", false); - }) - .then(function () { - return regulator.regulate(user); - }); - } - - const regulator1 = new Regulator(userDataStoreStub, 3, 60, 60); - const regulator2 = new Regulator(userDataStoreStub, 3, 2 * 60, 60); - - const p1 = markAuthentications(regulator1, USER1); - const p2 = markAuthentications(regulator2, USER2); - - return BluebirdPromise.join(p1, p2) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here...")); - }, function () { - Assert(p1.isFulfilled()); - Assert(p2.isRejected()); - }); - }); - - it("should user wait after regulation to authenticate again", function () { - function markAuthentications(regulator: Regulator, user: string) { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:10", false); - }) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:15", false); - }) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:25", false); - }) - .then(function () { - MockDate.set("1/2/2000 00:00:54"); - return regulator.regulate(user); - }) - .then(function () { - return BluebirdPromise.reject(new Error("should fail at this time")); - }, function () { - MockDate.set("1/2/2000 00:00:56"); - return regulator.regulate(user); - }); - } - - const regulator = new Regulator(userDataStoreStub, 4, 30, 30); - return markAuthentications(regulator, USER1); - }); - - it("should disable regulation when max_retries is set to 0", function () { - const maxRetries = 0; - const regulator = new Regulator(userDataStoreStub, maxRetries, 60, 30); - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", false); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:15", false); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:25", false); - }) - .then(function () { - MockDate.set("1/2/2000 00:00:26"); - return regulator.regulate(USER1); - }); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/regulation/Regulator.ts b/themes/squares/server/src/lib/regulation/Regulator.ts deleted file mode 100644 index 1037a6a17..000000000 --- a/themes/squares/server/src/lib/regulation/Regulator.ts +++ /dev/null @@ -1,55 +0,0 @@ - -import * as BluebirdPromise from "bluebird"; -import exceptions = require("../Exceptions"); -import { IUserDataStore } from "../storage/IUserDataStore"; -import { AuthenticationTraceDocument } from "../storage/AuthenticationTraceDocument"; -import { IRegulator } from "./IRegulator"; - -export class Regulator implements IRegulator { - private userDataStore: IUserDataStore; - private banTime: number; - private findTime: number; - private maxRetries: number; - - constructor(userDataStore: any, maxRetries: number, findTime: number, banTime: number) { - this.userDataStore = userDataStore; - this.banTime = banTime; - this.findTime = findTime; - this.maxRetries = maxRetries; - } - - // Mark authentication - mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { - return this.userDataStore.saveAuthenticationTrace(userId, isAuthenticationSuccessful); - } - - regulate(userId: string): BluebirdPromise { - const that = this; - - if (that.maxRetries <= 0) return BluebirdPromise.resolve(); - - return this.userDataStore.retrieveLatestAuthenticationTraces(userId, that.maxRetries) - .then((docs: AuthenticationTraceDocument[]) => { - // less than the max authorized number of authentication in time range, thus authorizing access - if (docs.length < that.maxRetries) return BluebirdPromise.resolve(); - - const numberOfFailedAuth = docs - .map(function (d: AuthenticationTraceDocument) { return d.isAuthenticationSuccessful == false ? 1 : 0; }) - .reduce(function (acc, v) { return acc + v; }, 0); - - if (numberOfFailedAuth < this.maxRetries) return BluebirdPromise.resolve(); - - const newestDocument = docs[0]; - const oldestDocument = docs[that.maxRetries - 1]; - - const authenticationsTimeRangeInSeconds = (newestDocument.date.getTime() - oldestDocument.date.getTime()) / 1000; - const tooManyAuthInTimelapse = (authenticationsTimeRangeInSeconds < this.findTime); - const stillInBannedTimeRange = (new Date(new Date().getTime() - this.banTime * 1000) < newestDocument.date); - - if (tooManyAuthInTimelapse && stillInBannedTimeRange) - throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes."); - - return BluebirdPromise.resolve(); - }); - } -} diff --git a/themes/squares/server/src/lib/regulation/RegulatorStub.spec.ts b/themes/squares/server/src/lib/regulation/RegulatorStub.spec.ts deleted file mode 100644 index ca8a00fb1..000000000 --- a/themes/squares/server/src/lib/regulation/RegulatorStub.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Bluebird = require("bluebird"); -import Sinon = require("sinon"); -import { IRegulator } from "./IRegulator"; - - -export class RegulatorStub implements IRegulator { - markStub: Sinon.SinonStub; - regulateStub: Sinon.SinonStub; - - constructor() { - this.markStub = Sinon.stub(); - this.regulateStub = Sinon.stub(); - } - - mark(userId: string, isAuthenticationSuccessful: boolean): Bluebird { - return this.markStub(userId, isAuthenticationSuccessful); - } - - regulate(userId: string): Bluebird { - return this.regulateStub(userId); - } -} diff --git a/themes/squares/server/src/lib/routes/error/401/get.spec.ts b/themes/squares/server/src/lib/routes/error/401/get.spec.ts deleted file mode 100644 index 9fdac9c3c..000000000 --- a/themes/squares/server/src/lib/routes/error/401/get.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import Sinon = require("sinon"); -import Express = require("express"); -import Assert = require("assert"); -import Get401 from "./get"; -import { ServerVariables } from "../../../ServerVariables"; -import { ServerVariablesMockBuilder, ServerVariablesMock } - from "../../../ServerVariablesMockBuilder.spec"; - -describe("routes/error/401/get", function () { - let vars: ServerVariables; - let mocks: ServerVariablesMock; - let req: any; - let res: any; - let renderSpy: Sinon.SinonSpy; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - vars = s.variables; - mocks = s.mocks; - - renderSpy = Sinon.spy(); - req = { - headers: {} - }; - res = { - render: renderSpy - }; - }); - - it("should set redirection url to the default redirection url", function () { - vars.config.default_redirection_url = "http://default-redirection"; - return Get401(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/401", { - redirection_url: "http://default-redirection" - })); - }); - }); - - it("should set redirection url to the referer", function () { - req.headers["referer"] = "http://redirection"; - return Get401(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/401", { - redirection_url: "http://redirection" - })); - }); - }); - - it("should render without redirecting the user", function () { - return Get401(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/401", { - redirection_url: undefined - })); - }); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/error/401/get.ts b/themes/squares/server/src/lib/routes/error/401/get.ts deleted file mode 100644 index ca4a3963d..000000000 --- a/themes/squares/server/src/lib/routes/error/401/get.ts +++ /dev/null @@ -1,15 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import redirector from "../redirector"; -import { ServerVariables } from "../../../ServerVariables"; - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response): BluebirdPromise { - const redirectionUrl = redirector(req, vars); - res.render("errors/401", { - redirection_url: redirectionUrl - }); - return BluebirdPromise.resolve(); - }; -} diff --git a/themes/squares/server/src/lib/routes/error/403/get.spec.ts b/themes/squares/server/src/lib/routes/error/403/get.spec.ts deleted file mode 100644 index 22eb84853..000000000 --- a/themes/squares/server/src/lib/routes/error/403/get.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import Sinon = require("sinon"); -import Express = require("express"); -import Assert = require("assert"); -import Get403 from "./get"; -import { ServerVariables } from "../../../ServerVariables"; -import { ServerVariablesMockBuilder, ServerVariablesMock } - from "../../../ServerVariablesMockBuilder.spec"; - -describe("routes/error/403/get", function () { - let vars: ServerVariables; - let mocks: ServerVariablesMock; - let req: any; - let res: any; - let renderSpy: Sinon.SinonSpy; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - vars = s.variables; - mocks = s.mocks; - - renderSpy = Sinon.spy(); - req = { - headers: {} - }; - res = { - render: renderSpy - }; - }); - - it("should set redirection url to the default redirection url", function () { - vars.config.default_redirection_url = "http://default-redirection"; - return Get403(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/403", { - redirection_url: "http://default-redirection" - })); - }); - }); - - it("should set redirection url to the referer", function () { - req.headers["referer"] = "http://redirection"; - return Get403(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/403", { - redirection_url: "http://redirection" - })); - }); - }); - - it("should render without redirecting the user", function () { - return Get403(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/403", { - redirection_url: undefined - })); - }); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/error/403/get.ts b/themes/squares/server/src/lib/routes/error/403/get.ts deleted file mode 100644 index 3ab0319e5..000000000 --- a/themes/squares/server/src/lib/routes/error/403/get.ts +++ /dev/null @@ -1,15 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import redirector from "../redirector"; -import { ServerVariables } from "../../../ServerVariables"; - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response): BluebirdPromise { - const redirectionUrl = redirector(req, vars); - res.render("errors/403", { - redirection_url: redirectionUrl - }); - return BluebirdPromise.resolve(); - }; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/error/404/get.spec.ts b/themes/squares/server/src/lib/routes/error/404/get.spec.ts deleted file mode 100644 index 73e4e6cef..000000000 --- a/themes/squares/server/src/lib/routes/error/404/get.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Sinon = require("sinon"); -import Express = require("express"); -import Assert = require("assert"); -import Get404 from "./get"; - -describe("routes/error/404/get", function () { - it("should render the page", function () { - const req = {} as Express.Request; - const res = { - render: Sinon.stub() - }; - - return Get404(req, res as any) - .then(function () { - Assert(res.render.calledOnce); - Assert(res.render.calledWith("errors/404")); - }); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/error/404/get.ts b/themes/squares/server/src/lib/routes/error/404/get.ts deleted file mode 100644 index 6693b6fc8..000000000 --- a/themes/squares/server/src/lib/routes/error/404/get.ts +++ /dev/null @@ -1,8 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import express = require("express"); - -export default function (req: express.Request, res: express.Response): BluebirdPromise { - res.render("errors/404"); - return BluebirdPromise.resolve(); -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/error/redirector.ts b/themes/squares/server/src/lib/routes/error/redirector.ts deleted file mode 100644 index b1a3ccc11..000000000 --- a/themes/squares/server/src/lib/routes/error/redirector.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Express = require("express"); -import { ServerVariables } from "../../ServerVariables"; - -export default function (req: Express.Request, vars: ServerVariables): string { - let redirectionUrl: string; - - if (req.headers && req.headers["referer"]) - redirectionUrl = "" + req.headers["referer"]; - else if (vars.config.default_redirection_url) - redirectionUrl = vars.config.default_redirection_url; - - return redirectionUrl; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/firstfactor/get.ts b/themes/squares/server/src/lib/routes/firstfactor/get.ts deleted file mode 100644 index d94f656c8..000000000 --- a/themes/squares/server/src/lib/routes/firstfactor/get.ts +++ /dev/null @@ -1,72 +0,0 @@ - -import express = require("express"); -import Endpoints = require("../../../../../shared/api"); -import BluebirdPromise = require("bluebird"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import Constants = require("../../../../../shared/constants"); -import Util = require("util"); -import { ServerVariables } from "../../ServerVariables"; -import { SafeRedirector } from "../../utils/SafeRedirection"; -import { Level } from "../../authentication/Level"; - -function getRedirectParam( - req: express.Request) { - return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : undefined; -} - -function redirectToSecondFactorPage( - req: express.Request, - res: express.Response) { - - const redirectUrl = getRedirectParam(req); - if (!redirectUrl) - res.redirect(Endpoints.SECOND_FACTOR_GET); - else - res.redirect( - Util.format("%s?%s=%s", - Endpoints.SECOND_FACTOR_GET, - Constants.REDIRECT_QUERY_PARAM, - redirectUrl)); -} - -function redirectToService( - req: express.Request, - res: express.Response, - redirector: SafeRedirector) { - const redirectUrl = getRedirectParam(req); - if (!redirectUrl) { - res.redirect(Endpoints.LOGGED_IN); - } else { - redirector.redirectOrElse(res, redirectUrl, Endpoints.LOGGED_IN); - } -} - -function renderFirstFactor( - res: express.Response) { - - res.render("firstfactor", { - first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST, - reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET - }); -} - -export default function ( - vars: ServerVariables) { - - const redirector = new SafeRedirector(vars.config.session.domain); - return function (req: express.Request, res: express.Response): BluebirdPromise { - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, vars.logger); - if (authSession.authentication_level == Level.ONE_FACTOR) { - redirectToSecondFactorPage(req, res); - } else if (authSession.authentication_level == Level.TWO_FACTOR) { - redirectToService(req, res, redirector); - } else { - renderFirstFactor(res); - } - resolve(); - }); - }; -} diff --git a/themes/squares/server/src/lib/routes/firstfactor/post.spec.ts b/themes/squares/server/src/lib/routes/firstfactor/post.spec.ts deleted file mode 100644 index e1d078cdd..000000000 --- a/themes/squares/server/src/lib/routes/firstfactor/post.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ - -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import Assert = require("assert"); -import FirstFactorPost = require("./post"); -import exceptions = require("../../Exceptions"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../types/AuthenticationSession"; -import Endpoints = require("../../../../../shared/api"); -import AuthenticationRegulatorMock = require("../../regulation/RegulatorStub.spec"); -import ExpressMock = require("../../stubs/express.spec"); -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../ServerVariables"; - -describe("routes/firstfactor/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let emails: string[]; - let groups: string[]; - let vars: ServerVariables; - let mocks: ServerVariablesMock; - let authSession: AuthenticationSession; - - beforeEach(function () { - emails = ["test_ok@example.com"]; - groups = ["group1", "group2" ]; - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - mocks.authorizer.authorizationMock.returns(true); - mocks.regulator.regulateStub.returns(BluebirdPromise.resolve()); - mocks.regulator.markStub.returns(BluebirdPromise.resolve()); - - req = { - originalUrl: "/api/firstfactor", - body: { - username: "username", - password: "password" - }, - query: { - redirect: "http://redirect.url" - }, - session: { - cookie: {} - }, - headers: { - host: "home.example.com" - } - }; - - res = ExpressMock.ResponseMock(); - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - }); - - it("should reply with 204 if success", function () { - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.resolve({ - emails: emails, - groups: groups - })); - return FirstFactorPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal("username", authSession.userid); - Assert(res.send.calledOnce); - }); - }); - - describe("keep me logged in", () => { - beforeEach(() => { - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.resolve({ - emails: emails, - groups: groups - })); - req.body.keepMeLoggedIn = "true"; - return FirstFactorPost.default(vars)(req as any, res as any); - }); - - it("should set keep_me_logged_in session variable to true", function () { - Assert.equal(authSession.keep_me_logged_in, true); - }); - - it("should set cookie maxAge to one year", function () { - Assert.equal(req.session.cookie.maxAge, 365 * 24 * 60 * 60 * 1000); - }); - }); - - it("should retrieve email from LDAP", function () { - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.resolve([{ mail: ["test@example.com"] }])); - return FirstFactorPost.default(vars)(req as any, res as any); - }); - - it("should set first email address as user session variable", function () { - const emails = ["test_ok@example.com"]; - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.resolve({ - emails: emails, - groups: groups - })); - - return FirstFactorPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal("test_ok@example.com", authSession.email); - }); - }); - - it("should return error message when LDAP authenticator throws", function () { - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.reject(new exceptions.LdapBindError("Bad credentials"))); - - return FirstFactorPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.equal(mocks.regulator.markStub.getCall(0).args[0], "username"); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - }); - }); - - it("should return error message when regulator rejects authentication", function () { - const err = new exceptions.AuthenticationRegulationError("Authentication regulation..."); - mocks.regulator.regulateStub.returns(BluebirdPromise.reject(err)); - return FirstFactorPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - }); - }); -}); - - diff --git a/themes/squares/server/src/lib/routes/firstfactor/post.ts b/themes/squares/server/src/lib/routes/firstfactor/post.ts deleted file mode 100644 index 565681d6a..000000000 --- a/themes/squares/server/src/lib/routes/firstfactor/post.ts +++ /dev/null @@ -1,101 +0,0 @@ - -import Exceptions = require("../../Exceptions"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import Endpoint = require("../../../../../shared/api"); -import ErrorReplies = require("../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import Constants = require("../../../../../shared/constants"); -import UserMessages = require("../../../../../shared/UserMessages"); -import { ServerVariables } from "../../ServerVariables"; -import { AuthenticationSession } from "../../../../types/AuthenticationSession"; -import { GroupsAndEmails } from "../../authentication/backends/GroupsAndEmails"; -import { Level as AuthenticationLevel } from "../../authentication/Level"; -import { Level as AuthorizationLevel } from "../../authorization/Level"; -import { URLDecomposer } from "../../utils/URLDecomposer"; - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response) - : BluebirdPromise { - const username: string = req.body.username; - const password: string = req.body.password; - const keepMeLoggedIn: boolean = req.body.keepMeLoggedIn && - req.body.keepMeLoggedIn === "true"; - let authSession: AuthenticationSession; - - if (keepMeLoggedIn) { - // Stay connected for 1 year. - vars.logger.debug(req, "User requested to stay logged in for one year."); - req.session.cookie.maxAge = 365 * 24 * 60 * 60 * 1000; - } - - return BluebirdPromise.resolve() - .then(function () { - if (!username || !password) { - return BluebirdPromise.reject(new Error("No username or password.")); - } - vars.logger.info(req, "Starting authentication of user \"%s\"", username); - authSession = AuthenticationSessionHandler.get(req, vars.logger); - return vars.regulator.regulate(username); - }) - .then(function () { - vars.logger.info(req, "No regulation applied."); - return vars.usersDatabase.checkUserPassword(username, password); - }) - .then(function (groupsAndEmails: GroupsAndEmails) { - vars.logger.info(req, - "LDAP binding successful. Retrieved information about user are %s", - JSON.stringify(groupsAndEmails)); - authSession.userid = username; - authSession.keep_me_logged_in = keepMeLoggedIn; - authSession.authentication_level = AuthenticationLevel.ONE_FACTOR; - const redirectUrl: string = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined" - // Fuck, don't know why it is a string! - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : ""; - - const emails: string[] = groupsAndEmails.emails; - const groups: string[] = groupsAndEmails.groups; - const decomposition = URLDecomposer.fromUrl(redirectUrl); - const authorizationLevel = (decomposition) - ? vars.authorizer.authorization( - {domain: decomposition.domain, resource: decomposition.path}, - {user: username, groups: groups}) - : AuthorizationLevel.TWO_FACTOR; - - if (emails.length > 0) - authSession.email = emails[0]; - authSession.groups = groups; - - vars.logger.debug(req, "Mark successful authentication to regulator."); - vars.regulator.mark(username, true); - - if (authorizationLevel <= AuthorizationLevel.ONE_FACTOR) { - let newRedirectionUrl: string = redirectUrl; - if (!newRedirectionUrl) - newRedirectionUrl = Endpoint.LOGGED_IN; - res.send({ - redirect: newRedirectionUrl - }); - vars.logger.debug(req, "Redirect to '%s'", redirectUrl); - } - else { - let newRedirectUrl = Endpoint.SECOND_FACTOR_GET; - if (redirectUrl) { - newRedirectUrl += "?" + Constants.REDIRECT_QUERY_PARAM + "=" - + redirectUrl; - } - vars.logger.debug(req, "Redirect to '%s'", newRedirectUrl); - res.send({ - redirect: newRedirectUrl - }); - } - return BluebirdPromise.resolve(); - }) - .catch(Exceptions.LdapBindError, function (err: Error) { - vars.regulator.mark(username, false); - return ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)(err); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)); - }; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/loggedin/get.ts b/themes/squares/server/src/lib/routes/loggedin/get.ts deleted file mode 100644 index 283a041b1..000000000 --- a/themes/squares/server/src/lib/routes/loggedin/get.ts +++ /dev/null @@ -1,23 +0,0 @@ -import Express = require("express"); -import Endpoints = require("../../../../../shared/api"); -import BluebirdPromise = require("bluebird"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { ServerVariables } from "../../ServerVariables"; -import ErrorReplies = require("../../ErrorReplies"); - -export default function (vars: ServerVariables) { - function handler(req: Express.Request, res: Express.Response): BluebirdPromise { - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, vars.logger); - res.render("already-logged-in", { - logout_endpoint: Endpoints.LOGOUT_GET, - username: authSession.userid, - redirection_url: vars.config.default_redirection_url - }); - resolve(); - }) - .catch(ErrorReplies.replyWithError401(req, res, vars.logger)); - } - - return handler; -} diff --git a/themes/squares/server/src/lib/routes/logout/get.ts b/themes/squares/server/src/lib/routes/logout/get.ts deleted file mode 100644 index 4d5112146..000000000 --- a/themes/squares/server/src/lib/routes/logout/get.ts +++ /dev/null @@ -1,20 +0,0 @@ - -import express = require("express"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import Constants = require("../../../../../shared/constants"); -import { ServerVariables } from "../../ServerVariables"; - -function getRedirectParam(req: express.Request) { - return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : undefined; -} - -export default function (vars: ServerVariables) { - return function(req: express.Request, res: express.Response) { - const redirect_param = getRedirectParam(req); - const redirect_url = redirect_param || "/"; - AuthenticationSessionHandler.reset(req); - res.redirect(redirect_url); - }; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/password-reset/constants.ts b/themes/squares/server/src/lib/routes/password-reset/constants.ts deleted file mode 100644 index 5c639e92a..000000000 --- a/themes/squares/server/src/lib/routes/password-reset/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export const CHALLENGE = "reset-password"; \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/password-reset/form/post.spec.ts b/themes/squares/server/src/lib/routes/password-reset/form/post.spec.ts deleted file mode 100644 index ed029c906..000000000 --- a/themes/squares/server/src/lib/routes/password-reset/form/post.spec.ts +++ /dev/null @@ -1,122 +0,0 @@ - -import PasswordResetFormPost = require("./post"); -import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; -import { UserDataStore } from "../../../storage/UserDataStore"; -import Sinon = require("sinon"); -import Assert = require("assert"); -import BluebirdPromise = require("bluebird"); -import ExpressMock = require("../../../stubs/express.spec"); -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../ServerVariables"; -import { Level } from "../../../authentication/Level"; - -describe("routes/password-reset/form/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let vars: ServerVariables; - let mocks: ServerVariablesMock; - let authSession: AuthenticationSession; - - beforeEach(function () { - req = { - originalUrl: "/api/password-reset", - body: { - userid: "user" - }, - session: {}, - headers: { - host: "localhost" - } - }; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - - mocks.config.authentication_backend.ldap = { - url: "ldap://ldapjs", - mail_attribute: "mail", - user: "user", - password: "password", - additional_users_dn: "ou=users", - additional_groups_dn: "ou=groups", - base_dn: "dc=example,dc=com", - users_filter: "user", - group_name_attribute: "cn", - groups_filter: "groups" - }; - - res = ExpressMock.ResponseMock(); - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - authSession.userid = "user"; - authSession.email = "user@example.com"; - authSession.authentication_level = Level.ONE_FACTOR; - }); - - describe("test reset password post", () => { - it("should update the password and reset auth_session for reauthentication", function () { - req.body = {}; - req.body.password = "new-password"; - - mocks.usersDatabase.updatePasswordStub.returns(BluebirdPromise.resolve()); - - authSession.identity_check = { - userid: "user", - challenge: "reset-password" - }; - return PasswordResetFormPost.default(vars)(req as any, res as any) - .then(function () { - return AuthenticationSessionHandler.get(req as any, vars.logger); - }).then(function (_authSession) { - Assert.equal(res.status.getCall(0).args[0], 204); - Assert.equal(_authSession.authentication_level, Level.NOT_AUTHENTICATED); - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if identity_challenge does not exist", function () { - authSession.identity_check = { - userid: "user", - challenge: undefined - }; - return PasswordResetFormPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "An error occurred during password reset. Your password has not been changed." - }); - }); - }); - - it("should fail when ldap fails", function () { - req.body = {}; - req.body.password = "new-password"; - - mocks.usersDatabase.updatePasswordStub - .returns(BluebirdPromise.reject("Internal error with LDAP")); - - authSession.identity_check = { - challenge: "reset-password", - userid: "user" - }; - return PasswordResetFormPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "An error occurred during password reset. Your password has not been changed." - }); - return BluebirdPromise.resolve(); - }); - }); - }); -}); diff --git a/themes/squares/server/src/lib/routes/password-reset/form/post.ts b/themes/squares/server/src/lib/routes/password-reset/form/post.ts deleted file mode 100644 index fccd7471b..000000000 --- a/themes/squares/server/src/lib/routes/password-reset/form/post.ts +++ /dev/null @@ -1,50 +0,0 @@ - -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import objectPath = require("object-path"); -import exceptions = require("../../../Exceptions"); -import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; -import ErrorReplies = require("../../../ErrorReplies"); -import UserMessages = require("../../../../../../shared/UserMessages"); -import { ServerVariables } from "../../../ServerVariables"; - -import Constants = require("./../constants"); - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const newPassword = objectPath.get(req, "body.password"); - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - if (!authSession.identity_check) { - reject(new Error("No identity check initiated")); - return; - } - - vars.logger.info(req, "User %s wants to reset his/her password.", - authSession.identity_check.userid); - vars.logger.debug(req, "Challenge %s", authSession.identity_check.challenge); - - if (authSession.identity_check.challenge != Constants.CHALLENGE) { - reject(new Error("Bad challenge.")); - return; - } - resolve(); - }) - .then(function () { - return vars.usersDatabase.updatePassword(authSession.identity_check.userid, newPassword); - }) - .then(function () { - vars.logger.info(req, "Password reset for user '%s'", - authSession.identity_check.userid); - AuthenticationSessionHandler.reset(req); - res.status(204); - res.send(); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.RESET_PASSWORD_FAILED)); - }; -} diff --git a/themes/squares/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts b/themes/squares/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts deleted file mode 100644 index ac6a41754..000000000 --- a/themes/squares/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts +++ /dev/null @@ -1,92 +0,0 @@ - -import PasswordResetHandler - from "./PasswordResetHandler"; -import { UserDataStore } from "../../../storage/UserDataStore"; -import Sinon = require("sinon"); -import winston = require("winston"); -import assert = require("assert"); -import BluebirdPromise = require("bluebird"); -import ExpressMock = require("../../../stubs/express.spec"); -import { ServerVariablesMock, ServerVariablesMockBuilder } - from "../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../ServerVariables"; - -describe("routes/password-reset/identity/PasswordResetHandler", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - req = { - originalUrl: "/non-api/xxx", - query: { - userid: "user" - }, - session: { - auth: { - userid: "user", - email: "user@example.com", - first_factor: true, - second_factor: false - } - }, - headers: { - host: "localhost" - } - }; - - const options = { - inMemoryOnly: true - }; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - mocks.userDataStore.saveU2FRegistrationStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.produceIdentityValidationTokenStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.consumeIdentityValidationTokenStub - .returns(BluebirdPromise.resolve({})); - res = ExpressMock.ResponseMock(); - }); - - describe("test reset password identity pre check", () => { - it("should fail when no userid is provided", function () { - req.query.userid = undefined; - const handler = new PasswordResetHandler(vars.logger, - vars.usersDatabase); - return handler.preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject("It should fail"); - }) - .catch(function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if ldap fail", function () { - mocks.usersDatabase.getEmailsStub - .returns(BluebirdPromise.reject("Internal error")); - new PasswordResetHandler(vars.logger, vars.usersDatabase) - .preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here")); - }, - function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should returns identity when ldap replies", function () { - mocks.usersDatabase.getEmailsStub - .returns(BluebirdPromise.resolve(["test@example.com"])); - return new PasswordResetHandler(vars.logger, vars.usersDatabase) - .preValidationInit(req as any); - }); - }); -}); diff --git a/themes/squares/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts b/themes/squares/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts deleted file mode 100644 index 42ae92cda..000000000 --- a/themes/squares/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts +++ /dev/null @@ -1,69 +0,0 @@ -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import objectPath = require("object-path"); - -import exceptions = require("../../../Exceptions"); -import { Identity } from "../../../../../types/Identity"; -import { IdentityValidable } from "../../../IdentityValidable"; -import { PRE_VALIDATION_TEMPLATE } from "../../../IdentityCheckPreValidationTemplate"; -import Constants = require("../constants"); -import { IRequestLogger } from "../../../logging/IRequestLogger"; -import { IUsersDatabase } from "../../../authentication/backends/IUsersDatabase"; - -export const TEMPLATE_NAME = "password-reset-form"; - -export default class PasswordResetHandler implements IdentityValidable { - private logger: IRequestLogger; - private usersDatabase: IUsersDatabase; - - constructor(logger: IRequestLogger, usersDatabase: IUsersDatabase) { - this.logger = logger; - this.usersDatabase = usersDatabase; - } - - challenge(): string { - return Constants.CHALLENGE; - } - - preValidationInit(req: express.Request): BluebirdPromise { - const that = this; - const userid: string = - objectPath.get(req, "query.userid"); - return BluebirdPromise.resolve() - .then(function () { - that.logger.debug(req, "User '%s' requested a password reset", userid); - if (!userid) - return BluebirdPromise.reject( - new exceptions.AccessDeniedError("No user id provided")); - - return that.usersDatabase.getEmails(userid); - }) - .then(function (emails: string[]) { - if (!emails && emails.length <= 0) throw new Error("No email found"); - const identity = { - email: emails[0], - userid: userid - }; - return BluebirdPromise.resolve(identity); - }) - .catch(function (err: Error) { - return BluebirdPromise.reject(new exceptions.IdentityError(err.message)); - }); - } - - preValidationResponse(req: express.Request, res: express.Response) { - res.render(PRE_VALIDATION_TEMPLATE); - } - - postValidationInit(req: express.Request) { - return BluebirdPromise.resolve(); - } - - postValidationResponse(req: express.Request, res: express.Response) { - res.render(TEMPLATE_NAME); - } - - mailSubject(): string { - return "Reset your password"; - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/password-reset/request/get.ts b/themes/squares/server/src/lib/routes/password-reset/request/get.ts deleted file mode 100644 index 8f3ae2b4b..000000000 --- a/themes/squares/server/src/lib/routes/password-reset/request/get.ts +++ /dev/null @@ -1,13 +0,0 @@ - -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import objectPath = require("object-path"); -import exceptions = require("../../../Exceptions"); - -import Constants = require("./../constants"); - -const TEMPLATE_NAME = "password-reset-request"; - -export default function (req: express.Request, res: express.Response) { - res.render(TEMPLATE_NAME); -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/secondfactor/get.spec.ts b/themes/squares/server/src/lib/routes/secondfactor/get.spec.ts deleted file mode 100644 index 6c77e1f69..000000000 --- a/themes/squares/server/src/lib/routes/secondfactor/get.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import SecondFactorGet from "./get"; -import { ServerVariablesMockBuilder, ServerVariablesMock } - from "../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../ServerVariables"; -import Sinon = require("sinon"); -import ExpressMock = require("../../stubs/express.spec"); -import Assert = require("assert"); -import Endpoints = require("../../../../../shared/api"); -import BluebirdPromise = require("bluebird"); - -describe("routes/secondfactor/get", function () { - let mocks: ServerVariablesMock; - let vars: ServerVariables; - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req = ExpressMock.RequestMock(); - res = ExpressMock.ResponseMock(); - - req.session = { - auth: { - userid: "user", - first_factor: true, - second_factor: false - } - }; - }); - - describe("test rendering", function () { - it("should render second factor page", function () { - req.session.auth.second_factor = false; - return SecondFactorGet(vars)(req as any, res as any) - .then(function () { - Assert(res.render.calledWith("secondfactor")); - return BluebirdPromise.resolve(); - }); - }); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/secondfactor/get.ts b/themes/squares/server/src/lib/routes/secondfactor/get.ts deleted file mode 100644 index 9f6deb4c6..000000000 --- a/themes/squares/server/src/lib/routes/secondfactor/get.ts +++ /dev/null @@ -1,28 +0,0 @@ - -import Express = require("express"); -import Endpoints = require("../../../../../shared/api"); -import BluebirdPromise = require("bluebird"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { ServerVariables } from "../../ServerVariables"; - -const TEMPLATE_NAME = "secondfactor"; - -export default function (vars: ServerVariables) { - function handler(req: Express.Request, res: Express.Response) - : BluebirdPromise { - - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, vars.logger); - - res.render(TEMPLATE_NAME, { - username: authSession.userid, - totp_identity_start_endpoint: - Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, - u2f_identity_start_endpoint: - Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET - }); - resolve(); - }); - } - return handler; -} diff --git a/themes/squares/server/src/lib/routes/secondfactor/redirect.spec.ts b/themes/squares/server/src/lib/routes/secondfactor/redirect.spec.ts deleted file mode 100644 index ea66e6dca..000000000 --- a/themes/squares/server/src/lib/routes/secondfactor/redirect.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import Redirect from "./redirect"; -import ExpressMock = require("../../stubs/express.spec"); -import { ServerVariablesMockBuilder, ServerVariablesMock } -from "../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../ServerVariables"; -import Assert = require("assert"); - -describe("routes/secondfactor/redirect", function() { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req = ExpressMock.RequestMock(); - res = ExpressMock.ResponseMock(); - }); - - it("should redirect to default_redirection_url", function() { - vars.config.default_redirection_url = "http://default_redirection_url"; - Redirect(vars)(req as any, res as any) - .then(function() { - Assert(res.json.calledWith({ - redirect: "http://default_redirection_url" - })); - }); - }); - - it("should redirect to /", function() { - Redirect(vars)(req as any, res as any) - .then(function() { - Assert(res.json.calledWith({ - redirect: "/" - })); - }); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/secondfactor/redirect.ts b/themes/squares/server/src/lib/routes/secondfactor/redirect.ts deleted file mode 100644 index 5d84d9ebf..000000000 --- a/themes/squares/server/src/lib/routes/secondfactor/redirect.ts +++ /dev/null @@ -1,30 +0,0 @@ - -import express = require("express"); -import objectPath = require("object-path"); -import Endpoints = require("../../../../../shared/api"); -import { ServerVariables } from "../../ServerVariables"; -import BluebirdPromise = require("bluebird"); -import ErrorReplies = require("../../ErrorReplies"); -import UserMessages = require("../../../../../shared/UserMessages"); -import { RedirectionMessage } from "../../../../../shared/RedirectionMessage"; -import Constants = require("../../../../../shared/constants"); - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response) - : BluebirdPromise { - - return new BluebirdPromise(function (resolve, reject) { - let redirectUrl: string = "/"; - if (vars.config.default_redirection_url) { - redirectUrl = vars.config.default_redirection_url; - } - vars.logger.debug(req, "Request redirection to \"%s\".", redirectUrl); - res.json({ - redirect: redirectUrl - } as RedirectionMessage); - return resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - }; -} diff --git a/themes/squares/server/src/lib/routes/secondfactor/totp/constants.ts b/themes/squares/server/src/lib/routes/secondfactor/totp/constants.ts deleted file mode 100644 index 7b5a1dcfe..000000000 --- a/themes/squares/server/src/lib/routes/secondfactor/totp/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ - -export const CHALLENGE = "totp-register"; -export const TEMPLATE_NAME = "totp-register"; - diff --git a/themes/squares/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts b/themes/squares/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts deleted file mode 100644 index 78b8ea3ea..000000000 --- a/themes/squares/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts +++ /dev/null @@ -1,116 +0,0 @@ -import Sinon = require("sinon"); -import RegistrationHandler from "./RegistrationHandler"; -import { Identity } from "../../../../../../types/Identity"; -import { UserDataStore } from "../../../../storage/UserDataStore"; -import BluebirdPromise = require("bluebird"); -import ExpressMock = require("../../../../stubs/express.spec"); -import { ServerVariablesMock, ServerVariablesMockBuilder } - from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; -import Assert = require("assert"); - -describe("routes/secondfactor/totp/identity/RegistrationHandler", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req = ExpressMock.RequestMock(); - req.session = { - auth: { - userid: "user", - email: "user@example.com", - first_factor: true, - second_factor: false, - identity_check: { - userid: "user", - challenge: "totp-register" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.produceIdentityValidationTokenStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.consumeIdentityValidationTokenStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.saveTOTPSecretStub - .returns(BluebirdPromise.resolve({})); - - res = ExpressMock.ResponseMock(); - }); - - describe("test totp registration pre validation", function () { - it("should fail if first_factor has not been passed", function () { - req.session.auth.first_factor = false; - return new RegistrationHandler(vars.logger, vars.userDataStore, - vars.totpHandler, vars.config.totp) - .preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject(new Error("It should fail")); - }) - .catch(function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if userid is missing", function (done) { - req.session.auth.first_factor = false; - req.session.auth.userid = undefined; - - new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler, - vars.config.totp) - .preValidationInit(req as any) - .catch(function (err: Error) { - done(); - }); - }); - - it("should fail if email is missing", function (done) { - req.session.auth.first_factor = false; - req.session.auth.email = undefined; - - new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler, - vars.config.totp) - .preValidationInit(req as any) - .catch(function (err: Error) { - done(); - }); - }); - - it("should succeed if first factor passed, userid and email are provided", - function () { - return new RegistrationHandler(vars.logger, vars.userDataStore, - vars.totpHandler, vars.config.totp) - .preValidationInit(req as any); - }); - }); - - describe("test totp registration post validation", function () { - it("should generate a secret using userId as label and issuer defined in config", function () { - vars.config.totp = { - issuer: "issuer" - }; - return new RegistrationHandler(vars.logger, vars.userDataStore, - vars.totpHandler, vars.config.totp) - .postValidationResponse(req as any, res as any) - .then(function() { - Assert(mocks.totpHandler.generateStub.calledWithExactly("user", "issuer")); - }); - }); - }); -}); diff --git a/themes/squares/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts b/themes/squares/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts deleted file mode 100644 index b39b6d045..000000000 --- a/themes/squares/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts +++ /dev/null @@ -1,112 +0,0 @@ - -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import objectPath = require("object-path"); - -import { Identity } from "../../../../../../types/Identity"; -import { IdentityValidable } from "../../../../IdentityValidable"; -import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; -import Constants = require("../constants"); -import Endpoints = require("../../../../../../../shared/api"); -import ErrorReplies = require("../../../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import FirstFactorValidator = require("../../../../FirstFactorValidator"); -import { IRequestLogger } from "../../../../logging/IRequestLogger"; -import { IUserDataStore } from "../../../../storage/IUserDataStore"; -import { ITotpHandler } from "../../../../authentication/totp/ITotpHandler"; -import { TOTPSecret } from "../../../../../../types/TOTPSecret"; -import { TotpConfiguration } from "../../../../configuration/schema/TotpConfiguration"; - - -export default class RegistrationHandler implements IdentityValidable { - private logger: IRequestLogger; - private userDataStore: IUserDataStore; - private totp: ITotpHandler; - private configuration: TotpConfiguration; - - constructor(logger: IRequestLogger, - userDataStore: IUserDataStore, - totp: ITotpHandler, configuration: TotpConfiguration) { - this.logger = logger; - this.userDataStore = userDataStore; - this.totp = totp; - this.configuration = configuration; - } - - challenge(): string { - return Constants.CHALLENGE; - } - - private retrieveIdentity(req: express.Request): BluebirdPromise { - const that = this; - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, that.logger); - const userid = authSession.userid; - const email = authSession.email; - - if (!(userid && email)) { - return reject(new Error("User ID or email is missing")); - } - - const identity = { - email: email, - userid: userid - }; - return resolve(identity); - }); - } - - preValidationInit(req: express.Request): BluebirdPromise { - const that = this; - return FirstFactorValidator.validate(req, this.logger) - .then(function () { - return that.retrieveIdentity(req); - }); - } - - preValidationResponse(req: express.Request, res: express.Response) { - res.render(PRE_VALIDATION_TEMPLATE); - } - - postValidationInit(req: express.Request) { - return FirstFactorValidator.validate(req, this.logger); - } - - postValidationResponse(req: express.Request, res: express.Response) - : BluebirdPromise { - const that = this; - let secret: TOTPSecret; - let userId: string; - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, that.logger); - userId = authSession.userid; - - if (authSession.identity_check.challenge != Constants.CHALLENGE - || !userId) - return reject(new Error("Bad challenge.")); - - resolve(); - }) - .then(function () { - secret = that.totp.generate(userId, - that.configuration.issuer); - that.logger.debug(req, "Save the TOTP secret in DB"); - return that.userDataStore.saveTOTPSecret(userId, secret); - }) - .then(function () { - AuthenticationSessionHandler.reset(req); - - res.render(Constants.TEMPLATE_NAME, { - base32_secret: secret.base32, - otpauth_url: secret.otpauth_url, - login_endpoint: Endpoints.FIRST_FACTOR_GET - }); - }) - .catch(ErrorReplies.replyWithError200(req, res, that.logger, UserMessages.OPERATION_FAILED)); - } - - mailSubject(): string { - return "Set up Authelia's one-time password"; - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts b/themes/squares/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts deleted file mode 100644 index 70a20d39d..000000000 --- a/themes/squares/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import Sinon = require("sinon"); -import Assert = require("assert"); -import Exceptions = require("../../../../Exceptions"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import SignPost = require("./post"); -import { ServerVariables } from "../../../../ServerVariables"; - -import ExpressMock = require("../../../../stubs/express.spec"); -import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; -import { Level } from "../../../../authentication/Level"; - -describe("routes/secondfactor/totp/sign/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let authSession: AuthenticationSession; - let vars: ServerVariables; - let mocks: ServerVariablesMock; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - vars = s.variables; - mocks = s.mocks; - const app_get = Sinon.stub(); - req = { - originalUrl: "/api/totp-register", - app: {}, - body: { - token: "abc" - }, - session: {}, - query: { - redirect: "http://redirect" - } - }; - res = ExpressMock.ResponseMock(); - - const doc = { - userid: "user", - secret: { - base32: "ABCDEF" - } - }; - mocks.userDataStore.retrieveTOTPSecretStub.returns(BluebirdPromise.resolve(doc)); - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - authSession.userid = "user"; - authSession.authentication_level = Level.ONE_FACTOR; - }); - - - it("should send status code 200 when totp is valid", function () { - mocks.totpHandler.validateStub.returns(true); - return SignPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(authSession.authentication_level, Level.TWO_FACTOR); - return BluebirdPromise.resolve(); - }); - }); - - it("should send error message when totp is not valid", function () { - mocks.totpHandler.validateStub.returns(false); - return SignPost.default(vars)(req as any, res as any) - .then(function () { - Assert.notEqual(authSession.authentication_level, Level.TWO_FACTOR); - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); -}); - diff --git a/themes/squares/server/src/lib/routes/secondfactor/totp/sign/post.ts b/themes/squares/server/src/lib/routes/secondfactor/totp/sign/post.ts deleted file mode 100644 index 34a276d12..000000000 --- a/themes/squares/server/src/lib/routes/secondfactor/totp/sign/post.ts +++ /dev/null @@ -1,42 +0,0 @@ -import Bluebird = require("bluebird"); -import Express = require("express"); - -import { TOTPSecretDocument } from "../../../../storage/TOTPSecretDocument"; -import Endpoints = require("../../../../../../../shared/api"); -import Redirect from "../../redirect"; -import ErrorReplies = require("../../../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { ServerVariables } from "../../../../ServerVariables"; -import { Level } from "../../../../authentication/Level"; - -const UNAUTHORIZED_MESSAGE = "Unauthorized access"; - -export default function (vars: ServerVariables) { - function handler(req: Express.Request, res: Express.Response): Bluebird { - let authSession: AuthenticationSession; - const token = req.body.token; - - return new Bluebird(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - vars.logger.info(req, "Initiate TOTP validation for user \"%s\".", authSession.userid); - resolve(); - }) - .then(function () { - return vars.userDataStore.retrieveTOTPSecret(authSession.userid); - }) - .then(function (doc: TOTPSecretDocument) { - if (!vars.totpHandler.validate(token, doc.secret.base32)) - return Bluebird.reject(new Error("Invalid TOTP token.")); - - vars.logger.debug(req, "TOTP validation succeeded."); - authSession.authentication_level = Level.TWO_FACTOR; - Redirect(vars)(req, res); - return Bluebird.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - return handler; -} diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts deleted file mode 100644 index 7f16c0ee8..000000000 --- a/themes/squares/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts +++ /dev/null @@ -1,11 +0,0 @@ - -import util = require("util"); -import express = require("express"); - -function extract_app_id(req: express.Request): string { - return util.format("https://%s", req.headers.host); -} - -export = { - extract_app_id: extract_app_id -}; \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts deleted file mode 100644 index a54bfbfe6..000000000 --- a/themes/squares/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts +++ /dev/null @@ -1,96 +0,0 @@ -import Sinon = require("sinon"); -import Assert = require("assert"); -import BluebirdPromise = require("bluebird"); - -import { Identity } from "../../../../../../types/Identity"; -import RegistrationHandler from "./RegistrationHandler"; -import ExpressMock = require("../../../../stubs/express.spec"); -import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; - -describe("routes/secondfactor/u2f/identity/RegistrationHandler", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req = ExpressMock.RequestMock(); - req.app = {}; - req.session = { - auth: { - userid: "user", - email: "user@example.com", - first_factor: true, - second_factor: false - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - - res = ExpressMock.ResponseMock(); - res.send = Sinon.spy(); - res.json = Sinon.spy(); - res.status = Sinon.spy(); - }); - - describe("test u2f registration check", test_registration_check); - - function test_registration_check() { - it("should fail if first_factor has not been passed", function () { - req.session.auth.first_factor = false; - return new RegistrationHandler(vars.logger).preValidationInit(req as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if userid is missing", function () { - req.session.auth.first_factor = false; - req.session.auth.userid = undefined; - - return new RegistrationHandler(vars.logger).preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here")); - }, - function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if email is missing", function () { - req.session.auth.first_factor = false; - req.session.auth.email = undefined; - - return new RegistrationHandler(vars.logger).preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here")); - }, - function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should succeed if first factor passed, userid and email are provided", function () { - req.session.auth.first_factor = true; - req.session.auth.email = "admin@example.com"; - req.session.auth.userid = "user"; - return new RegistrationHandler(vars.logger).preValidationInit(req as any); - }); - } -}); diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts deleted file mode 100644 index bc4713c77..000000000 --- a/themes/squares/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts +++ /dev/null @@ -1,73 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import objectPath = require("object-path"); - -import { IdentityValidable } from "../../../../IdentityValidable"; -import { Identity } from "../../../../../../types/Identity"; -import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; -import FirstFactorValidator = require("../../../../FirstFactorValidator"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { IRequestLogger } from "../../../../logging/IRequestLogger"; - -const CHALLENGE = "u2f-register"; -const MAIL_SUBJECT = "Register your security key with Authelia"; - -const POST_VALIDATION_TEMPLATE_NAME = "u2f-register"; - - -export default class RegistrationHandler implements IdentityValidable { - private logger: IRequestLogger; - - constructor(logger: IRequestLogger) { - this.logger = logger; - } - - challenge(): string { - return CHALLENGE; - } - - private retrieveIdentity(req: express.Request): BluebirdPromise { - const that = this; - return new BluebirdPromise(function(resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, that.logger); - const userid = authSession.userid; - const email = authSession.email; - - if (!(userid && email)) { - return reject(new Error("User ID or email is missing")); - } - - const identity = { - email: email, - userid: userid - }; - return resolve(identity); - }); - } - - preValidationInit(req: express.Request): BluebirdPromise { - const that = this; - return FirstFactorValidator.validate(req, this.logger) - .then(function () { - return that.retrieveIdentity(req); - }); - } - - preValidationResponse(req: express.Request, res: express.Response) { - res.render(PRE_VALIDATION_TEMPLATE); - } - - postValidationInit(req: express.Request) { - return FirstFactorValidator.validate(req, this.logger); - } - - postValidationResponse(req: express.Request, res: express.Response) { - res.render(POST_VALIDATION_TEMPLATE_NAME); - } - - mailSubject(): string { - return MAIL_SUBJECT; - } -} - diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts deleted file mode 100644 index de3347a21..000000000 --- a/themes/squares/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts +++ /dev/null @@ -1,146 +0,0 @@ - -import sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import assert = require("assert"); -import U2FRegisterPost = require("./post"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import ExpressMock = require("../../../../stubs/express.spec"); -import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; - - -describe("routes/secondfactor/u2f/register/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - let authSession: AuthenticationSession; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.originalUrl = "/api/xxxx"; - req.app = {}; - req.session = { - auth: { - userid: "user", - first_factor: true, - second_factor: false, - identity_check: { - challenge: "u2f-register", - userid: "user" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - }); - - describe("test registration", test_registration); - - - function test_registration() { - it("should save u2f meta and return status code 200", function () { - const expectedStatus = { - keyHandle: "keyHandle", - publicKey: "pbk", - certificate: "cert" - }; - mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve(expectedStatus)); - - authSession.register_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { - assert.equal("user", mocks.userDataStore.saveU2FRegistrationStub.getCall(0).args[0]); - assert.equal(authSession.identity_check, undefined); - }); - }); - - it("should return error message on finishRegistration error", function () { - mocks.u2f.checkRegistrationStub.returns({ errorCode: 500 }); - - authSession.register_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); - - it("should return error message when register_request is not provided", function () { - mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); - authSession.register_request = undefined; - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); - - it("should return error message when no auth request has been initiated", function () { - mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); - authSession.register_request = undefined; - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); - - it("should return error message when identity has not been verified", function () { - authSession.identity_check = undefined; - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); - } -}); - diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/register/post.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/register/post.ts deleted file mode 100644 index 7296ccbe5..000000000 --- a/themes/squares/server/src/lib/routes/secondfactor/u2f/register/post.ts +++ /dev/null @@ -1,64 +0,0 @@ - -import { UserDataStore } from "../../../../storage/UserDataStore"; -import objectPath = require("object-path"); -import u2f_common = require("../U2FCommon"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import U2f = require("u2f"); -import { U2FRegistration } from "../../../../../../types/U2FRegistration"; -import redirect from "../../redirect"; -import ErrorReplies = require("../../../../ErrorReplies"); -import { ServerVariables } from "../../../../ServerVariables"; -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; - - -export default function (vars: ServerVariables) { - function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const appid = u2f_common.extract_app_id(req); - const registrationResponse: U2f.RegistrationData = req.body; - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - const registrationRequest = authSession.register_request; - - if (!registrationRequest) { - return reject(new Error("No registration request")); - } - - if (!authSession.identity_check - || authSession.identity_check.challenge != "u2f-register") { - return reject(new Error("Bad challenge for registration request")); - } - - vars.logger.info(req, "Finishing registration"); - vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); - vars.logger.debug(req, "RegistrationResponse = %s", JSON.stringify(registrationResponse)); - - return resolve(vars.u2f.checkRegistration(registrationRequest, registrationResponse)); - }) - .then(function (u2fResult: U2f.RegistrationResult | U2f.Error): BluebirdPromise { - if (objectPath.has(u2fResult, "errorCode")) - return BluebirdPromise.reject(new Error("Error while registering.")); - - const registrationResult: U2f.RegistrationResult = u2fResult as U2f.RegistrationResult; - vars.logger.info(req, "Store registration and reply"); - vars.logger.debug(req, "RegistrationResult = %s", JSON.stringify(registrationResult)); - const registration: U2FRegistration = { - keyHandle: registrationResult.keyHandle, - publicKey: registrationResult.publicKey - }; - return vars.userDataStore.saveU2FRegistration(authSession.userid, appid, registration); - }) - .then(function () { - authSession.identity_check = undefined; - redirect(vars)(req, res); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - return handler; -} diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts deleted file mode 100644 index a207c9109..000000000 --- a/themes/squares/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ - -import sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import Assert = require("assert"); -import U2FRegisterRequestGet = require("./get"); -import ExpressMock = require("../../../../stubs/express.spec"); -import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; - -describe("routes/secondfactor/u2f/register_request/get", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.originalUrl = "/api/xxxx"; - req.app = {}; - req.session = { - auth: { - userid: "user", - first_factor: true, - second_factor: false, - identity_check: { - challenge: "u2f-register", - userid: "user" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - }); - - describe("test registration request", () => { - it("should send back the registration request and save it in the session", function () { - const expectedRequest = { - test: "abc" - }; - mocks.u2f.requestStub.returns(BluebirdPromise.resolve(expectedRequest)); - return U2FRegisterRequestGet.default(vars)(req as any, res as any) - .then(function () { - Assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); - }); - }); - - it("should return internal error on registration request", function () { - res.send = sinon.spy(); - const user_key_container = {}; - mocks.u2f.requestStub.returns(BluebirdPromise.reject("Internal error")); - return U2FRegisterRequestGet.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - }); - }); - - it("should return forbidden if identity has not been verified", function () { - req.session.auth.identity_check = undefined; - return U2FRegisterRequestGet.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(403, res.status.getCall(0).args[0]); - }); - }); - }); -}); - diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/register_request/get.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/register_request/get.ts deleted file mode 100644 index f611af933..000000000 --- a/themes/squares/server/src/lib/routes/secondfactor/u2f/register_request/get.ts +++ /dev/null @@ -1,43 +0,0 @@ - -import { UserDataStore } from "../../../../storage/UserDataStore"; - -import objectPath = require("object-path"); -import u2f_common = require("../U2FCommon"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import U2f = require("u2f"); -import ErrorReplies = require("../../../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { ServerVariables } from "../../../../ServerVariables"; - -export default function (vars: ServerVariables) { - function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const appid: string = u2f_common.extract_app_id(req); - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - if (!authSession.identity_check - || authSession.identity_check.challenge != "u2f-register") { - res.status(403); - res.send(); - return reject(new Error("Bad challenge.")); - } - - vars.logger.info(req, "Starting registration for appId '%s'", appid); - return resolve(vars.u2f.request(appid)); - }) - .then(function (registrationRequest: U2f.Request) { - vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); - authSession.register_request = registrationRequest; - res.json(registrationRequest); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - - return handler; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts deleted file mode 100644 index 9b137e66d..000000000 --- a/themes/squares/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts +++ /dev/null @@ -1,101 +0,0 @@ - -import sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import Assert = require("assert"); -import U2FSignPost = require("./post"); -import { ServerVariables } from "../../../../ServerVariables"; -import winston = require("winston"); - -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; -import ExpressMock = require("../../../../stubs/express.spec"); -import U2FMock = require("../../../../stubs/u2f.spec"); -import U2f = require("u2f"); -import { Level } from "../../../../authentication/Level"; - -describe("routes/secondfactor/u2f/sign/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.app = {}; - req.originalUrl = "/api/xxxx"; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req.session = { - auth: { - userid: "user", - authentication_level: Level.ONE_FACTOR, - identity_check: { - challenge: "u2f-register", - userid: "user" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const options = { - inMemoryOnly: true - }; - - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - }); - - it("should return status code 204", function () { - const expectedStatus = { - keyHandle: "keyHandle", - publicKey: "pbk", - certificate: "cert" - }; - mocks.u2f.checkSignatureStub.returns(expectedStatus); - - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ - registration: { - publicKey: "PUBKEY" - } - })); - - req.session.auth.sign_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - return U2FSignPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(req.session.auth.authentication_level, Level.TWO_FACTOR); - }); - }); - - it("should return unauthorized error on registration request internal error", function () { - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ - registration: { - publicKey: "PUBKEY" - } - })); - mocks.u2f.checkSignatureStub.returns({ errorCode: 500 }); - - req.session.auth.sign_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - return U2FSignPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], - { error: "Operation failed." }); - }); - }); -}); - diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/sign/post.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/sign/post.ts deleted file mode 100644 index 7ee711c2c..000000000 --- a/themes/squares/server/src/lib/routes/secondfactor/u2f/sign/post.ts +++ /dev/null @@ -1,57 +0,0 @@ - -import objectPath = require("object-path"); -import u2f_common = require("../U2FCommon"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import { UserDataStore } from "../../../../storage/UserDataStore"; -import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; -import { Winston } from "../../../../../../types/Dependencies"; -import U2f = require("u2f"); -import exceptions = require("../../../../Exceptions"); -import redirect from "../../redirect"; -import ErrorReplies = require("../../../../ErrorReplies"); -import { ServerVariables } from "../../../../ServerVariables"; -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import { Level } from "../../../../authentication/Level"; - -export default function (vars: ServerVariables) { - function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const appId = u2f_common.extract_app_id(req); - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - if (!authSession.sign_request) { - const err = new Error("No sign request"); - ErrorReplies.replyWithError401(req, res, vars.logger)(err); - return reject(err); - } - resolve(); - }) - .then(function () { - const userid = authSession.userid; - return vars.userDataStore.retrieveU2FRegistration(userid, appId); - }) - .then(function (doc: U2FRegistrationDocument): BluebirdPromise { - const signRequest = authSession.sign_request; - const signData: U2f.SignatureData = req.body; - vars.logger.info(req, "Finish authentication"); - return BluebirdPromise.resolve(vars.u2f.checkSignature(signRequest, signData, doc.registration.publicKey)); - }) - .then(function (result: U2f.SignatureResult | U2f.Error): BluebirdPromise { - if (objectPath.has(result, "errorCode")) - return BluebirdPromise.reject(new Error("Error while signing")); - vars.logger.info(req, "Successful authentication"); - authSession.authentication_level = Level.TWO_FACTOR; - redirect(vars)(req, res); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - - return handler; -} - diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts deleted file mode 100644 index dd52b27e0..000000000 --- a/themes/squares/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ - -import sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import assert = require("assert"); -import U2FSignRequestGet = require("./get"); -import ExpressMock = require("../../../../stubs/express.spec"); -import { Request } from "u2f"; -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; - -import { SignMessage } from "../../../../../../../shared/SignMessage"; - -describe("routes/secondfactor/u2f/sign_request/get", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.originalUrl = "/api/xxxx"; - req.app = {}; - req.session = { - auth: { - userid: "user", - first_factor: true, - second_factor: false, - identity_check: { - challenge: "u2f-register", - userid: "user" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - }); - - it("should send back the sign request and save it in the session", function () { - const expectedRequest: Request = { - version: "U2F_V2", - appId: 'app', - challenge: 'challenge!' - }; - mocks.u2f.requestStub.returns(expectedRequest); - mocks.userDataStore.retrieveU2FRegistrationStub - .returns(BluebirdPromise.resolve({ - registration: { - keyHandle: "KeyHandle" - } - })); - - return U2FSignRequestGet.default(vars)(req as any, res as any) - .then(() => { - assert.deepEqual(expectedRequest, req.session.auth.sign_request); - assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); - }); - }); -}); - diff --git a/themes/squares/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts b/themes/squares/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts deleted file mode 100644 index 9e93dde06..000000000 --- a/themes/squares/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts +++ /dev/null @@ -1,42 +0,0 @@ - -import u2f_common = require("../../../secondfactor/u2f/U2FCommon"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; -import exceptions = require("../../../../Exceptions"); -import ErrorReplies = require("../../../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { ServerVariables } from "../../../../ServerVariables"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; - -export default function (vars: ServerVariables) { - function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const appId = u2f_common.extract_app_id(req); - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - resolve(); - }) - .then(function () { - return vars.userDataStore.retrieveU2FRegistration(authSession.userid, appId); - }) - .then(function (doc: U2FRegistrationDocument): BluebirdPromise { - if (!doc) - return BluebirdPromise.reject(new exceptions.AccessDeniedError("No U2F registration document found.")); - - const appId: string = u2f_common.extract_app_id(req); - vars.logger.info(req, "Start authentication of app '%s'", appId); - vars.logger.debug(req, "AppId = %s, keyHandle = %s", appId, JSON.stringify(doc.registration.keyHandle)); - - const request = vars.u2f.request(appId, doc.registration.keyHandle); - authSession.sign_request = request; - res.json(request); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - return handler; -} diff --git a/themes/squares/server/src/lib/routes/verify/access_control.ts b/themes/squares/server/src/lib/routes/verify/access_control.ts deleted file mode 100644 index 136239aeb..000000000 --- a/themes/squares/server/src/lib/routes/verify/access_control.ts +++ /dev/null @@ -1,51 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import Util = require("util"); - -import Exceptions = require("../../Exceptions"); - -import { Level as AuthorizationLevel } from "../../authorization/Level"; -import { Level as AuthenticationLevel } from "../../authentication/Level"; -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { ServerVariables } from "../../ServerVariables"; - -function isAuthorized( - authorization: AuthorizationLevel, - authentication: AuthenticationLevel): boolean { - - if (authorization == AuthorizationLevel.BYPASS) { - return true; - } else if (authorization == AuthorizationLevel.ONE_FACTOR && - authentication >= AuthenticationLevel.ONE_FACTOR) { - return true; - } else if (authorization == AuthorizationLevel.TWO_FACTOR && - authentication >= AuthenticationLevel.TWO_FACTOR) { - return true; - } - return false; -} - -export default function ( - req: Express.Request, - vars: ServerVariables, - domain: string, resource: string, - user: string, groups: string[], - authenticationLevel: AuthenticationLevel) { - - return new BluebirdPromise(function (resolve, reject) { - const authorizationLevel = vars.authorizer - .authorization({domain, resource}, {user, groups}); - - if (!isAuthorized(authorizationLevel, authenticationLevel)) { - if (authorizationLevel == AuthorizationLevel.DENY) { - reject(new Exceptions.NotAuthorizedError( - Util.format("User %s is not authorized to access %s%s", user, domain, resource))); - return; - } - reject(new Exceptions.NotAuthenticatedError(Util.format( - "User '%s' is not sufficiently authorized to access %s%s.", user, domain, resource))); - return; - } - resolve(); - }); -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/verify/get.spec.ts b/themes/squares/server/src/lib/routes/verify/get.spec.ts deleted file mode 100644 index 67cf19fb6..000000000 --- a/themes/squares/server/src/lib/routes/verify/get.spec.ts +++ /dev/null @@ -1,320 +0,0 @@ - -import Assert = require("assert"); -import BluebirdPromise = require("bluebird"); -import Express = require("express"); -import Sinon = require("sinon"); -import winston = require("winston"); - -import VerifyGet = require("./get"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../types/AuthenticationSession"; -import ExpressMock = require("../../stubs/express.spec"); -import { ServerVariables } from "../../ServerVariables"; -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../ServerVariablesMockBuilder.spec"; -import { Level } from "../../authentication/Level"; -import { Level as AuthorizationLevel } from "../../authorization/Level"; - -describe("routes/verify/get", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - let authSession: AuthenticationSession; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - res = ExpressMock.ResponseMock(); - req.originalUrl = "/api/xxxx"; - req.query = { - redirect: "undefined" - }; - AuthenticationSessionHandler.reset(req as any); - req.headers["x-original-url"] = "https://secret.example.com/"; - const s = ServerVariablesMockBuilder.build(false); - mocks = s.mocks; - vars = s.variables; - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - }); - - describe("with session cookie", function () { - it("should be already authenticated", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser"); - Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); - Assert.equal(204, res.status.getCall(0).args[0]); - }); - }); - - function test_session(_authSession: AuthenticationSession, status_code: number) { - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert.equal(status_code, res.status.getCall(0).args[0]); - }); - } - - function test_non_authenticated_401(authSession: AuthenticationSession) { - return test_session(authSession, 401); - } - - function test_unauthorized_403(authSession: AuthenticationSession) { - return test_session(authSession, 403); - } - - function test_authorized(authSession: AuthenticationSession) { - return test_session(authSession, 204); - } - - describe("given user tries to access a 2-factor endpoint", function () { - before(function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - }); - - describe("given different cases of session", function () { - it("should not be authenticated when second factor is missing", function () { - return test_non_authenticated_401({ - keep_me_logged_in: false, - userid: "user", - authentication_level: Level.ONE_FACTOR, - email: undefined, - groups: [], - last_activity_datetime: new Date().getTime() - }); - }); - - it("should not be authenticated when userid is missing", function () { - return test_non_authenticated_401({ - keep_me_logged_in: false, - userid: undefined, - authentication_level: Level.TWO_FACTOR, - email: undefined, - groups: [], - last_activity_datetime: new Date().getTime() - }); - }); - - it("should not be authenticated when level is insufficient", function () { - return test_non_authenticated_401({ - keep_me_logged_in: false, - userid: "user", - authentication_level: Level.NOT_AUTHENTICATED, - email: undefined, - groups: [], - last_activity_datetime: new Date().getTime() - }); - }); - - it("should not be authenticated when session has not be initiated", function () { - return test_non_authenticated_401(undefined); - }); - - it("should not be authenticated when domain is not allowed for user", function () { - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - req.headers["x-original-url"] = "https://test.example.com/"; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.DENY); - - return test_unauthorized_403({ - keep_me_logged_in: false, - authentication_level: Level.TWO_FACTOR, - userid: "user", - groups: ["group1", "group2"], - email: undefined, - last_activity_datetime: new Date().getTime() - }); - }); - }); - }); - - describe("given user tries to access a single factor endpoint", function () { - beforeEach(function () { - req.headers["x-original-url"] = "https://redirect.url/"; - }); - - it("should be authenticated when first factor is validated", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); - authSession.authentication_level = Level.ONE_FACTOR; - authSession.userid = "user1"; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWith(204)); - Assert(res.send.calledOnce); - }); - }); - - it("should be rejected with 401 when not authenticated", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); - authSession.authentication_level = Level.NOT_AUTHENTICATED; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWith(401)); - }); - }); - }); - - describe("inactivity period", function () { - it("should update last inactivity period on requests on /api/verify", function () { - mocks.config.session.inactivity = 200000; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - const currentTime = new Date().getTime() - 1000; - AuthenticationSessionHandler.reset(req as any); - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - authSession.last_activity_datetime = currentTime; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - return AuthenticationSessionHandler.get(req as any, vars.logger); - }) - .then(function (authSession) { - Assert(authSession.last_activity_datetime > currentTime); - }); - }); - - it("should reset session when max inactivity period has been reached", function () { - mocks.config.session.inactivity = 1; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - const currentTime = new Date().getTime() - 1000; - AuthenticationSessionHandler.reset(req as any); - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - authSession.last_activity_datetime = currentTime; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - return AuthenticationSessionHandler.get(req as any, vars.logger); - }) - .then(function (authSession) { - Assert.equal(authSession.authentication_level, Level.NOT_AUTHENTICATED); - Assert.equal(authSession.userid, undefined); - }); - }); - }); - }); - - describe("response type 401 | 302", function() { - it("should return error code 401", function() { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( - "Invalid credentials")); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should redirect to provided redirection url", function() { - const REDIRECT_URL = "http://redirection_url.com"; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( - "Invalid credentials")); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - req.query["rd"] = REDIRECT_URL; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.redirect.calledWithExactly(REDIRECT_URL)); - }); - }); - }); - - describe("with basic auth", function () { - it("should authenticate correctly", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.returns({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "john"); - Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); - Assert.equal(204, res.status.getCall(0).args[0]); - }); - }); - - it("should fail when endpoint is protected by two factors", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.config.access_control.rules = [{ - domain: "secret.example.com", - policy: "two_factor" - }]; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when base64 token is not valid", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic i_m*not_a_base64*token"; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when base64 token has not format user:psswd", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzOmJhZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when bad user password is provided", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( - "Invalid credentials")); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when resource is restricted", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - }); -}); - diff --git a/themes/squares/server/src/lib/routes/verify/get.ts b/themes/squares/server/src/lib/routes/verify/get.ts deleted file mode 100644 index f73861696..000000000 --- a/themes/squares/server/src/lib/routes/verify/get.ts +++ /dev/null @@ -1,91 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Express = require("express"); -import Exceptions = require("../../Exceptions"); -import ErrorReplies = require("../../ErrorReplies"); -import { ServerVariables } from "../../ServerVariables"; -import GetWithSessionCookieMethod from "./get_session_cookie"; -import GetWithBasicAuthMethod from "./get_basic_auth"; -import Constants = require("../../../../../shared/constants"); -import ObjectPath = require("object-path"); - -import { AuthenticationSessionHandler } - from "../../AuthenticationSessionHandler"; -import { AuthenticationSession } - from "../../../../types/AuthenticationSession"; - -const REMOTE_USER = "Remote-User"; -const REMOTE_GROUPS = "Remote-Groups"; - - -function verifyWithSelectedMethod(req: Express.Request, res: Express.Response, - vars: ServerVariables, authSession: AuthenticationSession) - : () => BluebirdPromise<{ username: string, groups: string[] }> { - return function () { - const authorization: string = "" + req.headers["proxy-authorization"]; - if (authorization && authorization.startsWith("Basic ")) - return GetWithBasicAuthMethod(req, res, vars, authorization); - - return GetWithSessionCookieMethod(req, res, vars, authSession); - }; -} - -function setRedirectHeader(req: Express.Request, res: Express.Response) { - return function () { - const originalUrl = ObjectPath.get( - req, "headers.x-original-url"); - res.set("Redirect", originalUrl); - return BluebirdPromise.resolve(); - }; -} - -function setUserAndGroupsHeaders(res: Express.Response) { - return function (u: { username: string, groups: string[] }) { - res.setHeader(REMOTE_USER, u.username); - res.setHeader(REMOTE_GROUPS, u.groups.join(",")); - return BluebirdPromise.resolve(); - }; -} - -function replyWith200(res: Express.Response) { - return function () { - res.status(204); - res.send(); - }; -} - -function getRedirectParam(req: Express.Request) { - return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : undefined; -} - -export default function (vars: ServerVariables) { - return function (req: Express.Request, res: Express.Response) - : BluebirdPromise { - let authSession: AuthenticationSession; - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - resolve(); - }) - .then(setRedirectHeader(req, res)) - .then(verifyWithSelectedMethod(req, res, vars, authSession)) - .then(setUserAndGroupsHeaders(res)) - .then(replyWith200(res)) - // The user is authenticated but has restricted access -> 403 - .catch(Exceptions.NotAuthorizedError, - ErrorReplies.replyWithError403(req, res, vars.logger)) - .catch(Exceptions.NotAuthenticatedError, - ErrorReplies.replyWithError401(req, res, vars.logger)) - // The user is not yet authenticated -> 401 - .catch((err) => { - const redirectUrl = getRedirectParam(req); - if (redirectUrl) { - ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err); - } - else { - ErrorReplies.replyWithError401(req, res, vars.logger)(err); - } - }); - }; -} - diff --git a/themes/squares/server/src/lib/routes/verify/get_basic_auth.ts b/themes/squares/server/src/lib/routes/verify/get_basic_auth.ts deleted file mode 100644 index af23c76c9..000000000 --- a/themes/squares/server/src/lib/routes/verify/get_basic_auth.ts +++ /dev/null @@ -1,55 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import ObjectPath = require("object-path"); -import { ServerVariables } from "../../ServerVariables"; -import { AuthenticationSession } - from "../../../../types/AuthenticationSession"; -import AccessControl from "./access_control"; -import { URLDecomposer } from "../../utils/URLDecomposer"; -import { Level } from "../../authentication/Level"; - -export default function (req: Express.Request, res: Express.Response, - vars: ServerVariables, authorizationHeader: string) - : BluebirdPromise<{ username: string, groups: string[] }> { - let username: string; - const uri = ObjectPath.get(req, "headers.x-original-url"); - const urlDecomposition = URLDecomposer.fromUrl(uri); - - return BluebirdPromise.resolve() - .then(() => { - const base64Re = new RegExp("^Basic ((?:[A-Za-z0-9+/]{4})*" + - "(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)$"); - const isTokenValidBase64 = base64Re.test(authorizationHeader); - - if (!isTokenValidBase64) { - return BluebirdPromise.reject(new Error("No valid base64 token found in the header")); - } - - const tokenMatches = authorizationHeader.match(base64Re); - const base64Token = tokenMatches[1]; - const decodedToken = Buffer.from(base64Token, "base64").toString(); - const splittedToken = decodedToken.split(":"); - - if (splittedToken.length != 2) { - return BluebirdPromise.reject(new Error( - "The authorization token is invalid. Expecting 'userid:password'")); - } - - username = splittedToken[0]; - const password = splittedToken[1]; - return vars.usersDatabase.checkUserPassword(username, password); - }) - .then(function (groupsAndEmails) { - return AccessControl(req, vars, urlDecomposition.domain, urlDecomposition.path, - username, groupsAndEmails.groups, Level.ONE_FACTOR) - .then(() => BluebirdPromise.resolve({ - username: username, - groups: groupsAndEmails.groups - })); - }) - .catch(function (err: Error) { - return BluebirdPromise.reject( - new Error("Unable to authenticate the user with basic auth. Cause: " - + err.message)); - }); -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/routes/verify/get_session_cookie.ts b/themes/squares/server/src/lib/routes/verify/get_session_cookie.ts deleted file mode 100644 index 070344812..000000000 --- a/themes/squares/server/src/lib/routes/verify/get_session_cookie.ts +++ /dev/null @@ -1,78 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import Util = require("util"); -import ObjectPath = require("object-path"); - -import Exceptions = require("../../Exceptions"); -import { Configuration } from "../../configuration/schema/Configuration"; -import { ServerVariables } from "../../ServerVariables"; -import { IRequestLogger } from "../../logging/IRequestLogger"; -import { AuthenticationSession } - from "../../../../types/AuthenticationSession"; -import { AuthenticationSessionHandler } - from "../../AuthenticationSessionHandler"; -import AccessControl from "./access_control"; -import { URLDecomposer } from "../../utils/URLDecomposer"; - -function verify_inactivity(req: Express.Request, - authSession: AuthenticationSession, - configuration: Configuration, logger: IRequestLogger) - : BluebirdPromise { - - // If inactivity is not specified, then inactivity timeout does not apply - if (!configuration.session.inactivity || authSession.keep_me_logged_in) { - return BluebirdPromise.resolve(); - } - - const lastActivityTime = authSession.last_activity_datetime; - const currentTime = new Date().getTime(); - authSession.last_activity_datetime = currentTime; - - const inactivityPeriodMs = currentTime - lastActivityTime; - logger.debug(req, "Inactivity period was %s s and max period was %s.", - inactivityPeriodMs / 1000, configuration.session.inactivity / 1000); - if (inactivityPeriodMs < configuration.session.inactivity) { - return BluebirdPromise.resolve(); - } - - logger.debug(req, "Session has been reset after too long inactivity period."); - AuthenticationSessionHandler.reset(req); - return BluebirdPromise.reject(new Error("Inactivity period exceeded.")); -} - -export default function (req: Express.Request, res: Express.Response, - vars: ServerVariables, authSession: AuthenticationSession) - : BluebirdPromise<{ username: string, groups: string[] }> { - - return BluebirdPromise.resolve() - .then(() => { - const username = authSession.userid; - const groups = authSession.groups; - - if (!authSession.userid) { - return BluebirdPromise.reject(new Exceptions.AccessDeniedError( - "userid is missing")); - } - - const originalUrl = ObjectPath.get( - req, "headers.x-original-url"); - const originalUri = - ObjectPath.get(req, "headers.x-original-uri"); - - const d = URLDecomposer.fromUrl(originalUrl); - vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", d.domain, - d.path, username, groups.join(",")); - return AccessControl(req, vars, d.domain, d.path, username, groups, - authSession.authentication_level); - }) - .then(() => { - return verify_inactivity(req, authSession, - vars.config, vars.logger); - }) - .then(() => { - return BluebirdPromise.resolve({ - username: authSession.userid, - groups: authSession.groups - }); - }); -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/AuthenticationTraceDocument.d.ts b/themes/squares/server/src/lib/storage/AuthenticationTraceDocument.d.ts deleted file mode 100644 index 69818c055..000000000 --- a/themes/squares/server/src/lib/storage/AuthenticationTraceDocument.d.ts +++ /dev/null @@ -1,6 +0,0 @@ - -export interface AuthenticationTraceDocument { - userId: string; - date: Date; - isAuthenticationSuccessful: boolean; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/CollectionFactoryFactory.ts b/themes/squares/server/src/lib/storage/CollectionFactoryFactory.ts deleted file mode 100644 index 92b29abfa..000000000 --- a/themes/squares/server/src/lib/storage/CollectionFactoryFactory.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ICollectionFactory } from "./ICollectionFactory"; -import { NedbCollectionFactory } from "./nedb/NedbCollectionFactory"; -import { MongoCollectionFactory } from "./mongo/MongoCollectionFactory"; -import { IMongoClient } from "../connectors/mongo/IMongoClient"; - - -export class CollectionFactoryFactory { - static createNedb(options: Nedb.DataStoreOptions): ICollectionFactory { - return new NedbCollectionFactory(options); - } - - static createMongo(client: IMongoClient): ICollectionFactory { - return new MongoCollectionFactory(client); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/CollectionFactoryStub.spec.ts b/themes/squares/server/src/lib/storage/CollectionFactoryStub.spec.ts deleted file mode 100644 index 17f8bb021..000000000 --- a/themes/squares/server/src/lib/storage/CollectionFactoryStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Sinon = require("sinon"); -import { ICollection } from "./ICollection"; -import { ICollectionFactory } from "./ICollectionFactory"; - -export class CollectionFactoryStub implements ICollectionFactory { - buildStub: Sinon.SinonStub; - - constructor() { - this.buildStub = Sinon.stub(); - } - - build(collectionName: string): ICollection { - return this.buildStub(collectionName); - } -} diff --git a/themes/squares/server/src/lib/storage/CollectionStub.spec.ts b/themes/squares/server/src/lib/storage/CollectionStub.spec.ts deleted file mode 100644 index 42895d672..000000000 --- a/themes/squares/server/src/lib/storage/CollectionStub.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Sinon = require("sinon"); -import { ICollection } from "./ICollection"; - -export class CollectionStub implements ICollection { - findStub: Sinon.SinonStub; - findOneStub: Sinon.SinonStub; - updateStub: Sinon.SinonStub; - removeStub: Sinon.SinonStub; - insertStub: Sinon.SinonStub; - - constructor() { - this.findStub = Sinon.stub(); - this.findOneStub = Sinon.stub(); - this.updateStub = Sinon.stub(); - this.removeStub = Sinon.stub(); - this.insertStub = Sinon.stub(); - } - - find(filter: any, sortKeys: any, count: number): BluebirdPromise { - return this.findStub(filter, sortKeys, count); - } - - findOne(filter: any): BluebirdPromise { - return this.findOneStub(filter); - } - - update(filter: any, document: any, options: any): BluebirdPromise { - return this.updateStub(filter, document, options); - } - - remove(filter: any): BluebirdPromise { - return this.removeStub(filter); - } - - insert(document: any): BluebirdPromise { - return this.insertStub(document); - } -} diff --git a/themes/squares/server/src/lib/storage/ICollection.d.ts b/themes/squares/server/src/lib/storage/ICollection.d.ts deleted file mode 100644 index caa6c2a87..000000000 --- a/themes/squares/server/src/lib/storage/ICollection.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* istanbul ignore next */ -import BluebirdPromise = require("bluebird"); - -/* istanbul ignore next */ -export interface ICollection { - find(query: any, sortKeys: any, count: number): BluebirdPromise; - findOne(query: any): BluebirdPromise; - update(query: any, updateQuery: any, options?: any): BluebirdPromise; - remove(query: any): BluebirdPromise; - insert(document: any): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/ICollectionFactory.d.ts b/themes/squares/server/src/lib/storage/ICollectionFactory.d.ts deleted file mode 100644 index 39eb42c77..000000000 --- a/themes/squares/server/src/lib/storage/ICollectionFactory.d.ts +++ /dev/null @@ -1,6 +0,0 @@ - -import { ICollection } from "./ICollection"; - -export interface ICollectionFactory { - build(collectionName: string): ICollection; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/IUserDataStore.d.ts b/themes/squares/server/src/lib/storage/IUserDataStore.d.ts deleted file mode 100644 index 81df482aa..000000000 --- a/themes/squares/server/src/lib/storage/IUserDataStore.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import { TOTPSecretDocument } from "./TOTPSecretDocument"; -import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; -import { U2FRegistration } from "../../../types/U2FRegistration"; -import { TOTPSecret } from "../../../types/TOTPSecret"; -import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; -import { IdentityValidationDocument } from "./IdentityValidationDocument"; - -export interface IUserDataStore { - saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise; - retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise; - - saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; - retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise; - - produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise; - consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise; - - saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise; - retrieveTOTPSecret(userId: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/IdentityValidationDocument.d.ts b/themes/squares/server/src/lib/storage/IdentityValidationDocument.d.ts deleted file mode 100644 index e7fd7b3f4..000000000 --- a/themes/squares/server/src/lib/storage/IdentityValidationDocument.d.ts +++ /dev/null @@ -1,7 +0,0 @@ - -export interface IdentityValidationDocument { - userId: string; - token: string; - challenge: string; - maxDate: Date; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/TOTPSecretDocument.d.ts b/themes/squares/server/src/lib/storage/TOTPSecretDocument.d.ts deleted file mode 100644 index a6c0bf9e6..000000000 --- a/themes/squares/server/src/lib/storage/TOTPSecretDocument.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { TOTPSecret } from "../../../types/TOTPSecret"; - -export interface TOTPSecretDocument { - userid: string; - secret: TOTPSecret; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/U2FRegistrationDocument.d.ts b/themes/squares/server/src/lib/storage/U2FRegistrationDocument.d.ts deleted file mode 100644 index efec6cb1d..000000000 --- a/themes/squares/server/src/lib/storage/U2FRegistrationDocument.d.ts +++ /dev/null @@ -1,8 +0,0 @@ - -import { U2FRegistration } from "../../../types/U2FRegistration"; - -export interface U2FRegistrationDocument { - userId: string; - appId: string; - registration: U2FRegistration; -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/UserDataStore.spec.ts b/themes/squares/server/src/lib/storage/UserDataStore.spec.ts deleted file mode 100644 index 66fb85461..000000000 --- a/themes/squares/server/src/lib/storage/UserDataStore.spec.ts +++ /dev/null @@ -1,264 +0,0 @@ - -import * as Assert from "assert"; -import * as Sinon from "sinon"; -import * as MockDate from "mockdate"; -import BluebirdPromise = require("bluebird"); - -import { UserDataStore } from "./UserDataStore"; -import { TOTPSecret } from "../../../types/TOTPSecret"; -import { U2FRegistration } from "../../../types/U2FRegistration"; -import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; -import { CollectionStub } from "./CollectionStub.spec"; -import { CollectionFactoryStub } from "./CollectionFactoryStub.spec"; - -describe("storage/UserDataStore", function () { - let factory: CollectionFactoryStub; - let collection: CollectionStub; - let userId: string; - let appId: string; - let totpSecret: TOTPSecret; - let u2fRegistration: U2FRegistration; - - beforeEach(function () { - factory = new CollectionFactoryStub(); - collection = new CollectionStub(); - - userId = "user"; - appId = "https://myappId"; - - totpSecret = { - ascii: "abc", - base32: "ABCDKZLEFZGREJK", - otpauth_url: "totp://test", - google_auth_qr: "dummy", - hex: "dummy", - qr_code_ascii: "dummy", - qr_code_base32: "dummy", - qr_code_hex: "dummy" - }; - - u2fRegistration = { - keyHandle: "KEY_HANDLE", - publicKey: "publickey" - }; - }); - - it("should correctly creates collections", function () { - new UserDataStore(factory); - - Assert.equal(4, factory.buildStub.callCount); - Assert(factory.buildStub.calledWith("authentication_traces")); - Assert(factory.buildStub.calledWith("identity_validation_tokens")); - Assert(factory.buildStub.calledWith("u2f_registrations")); - Assert(factory.buildStub.calledWith("totp_secrets")); - }); - - describe("TOTP secrets collection", function () { - it("should save a totp secret", function () { - factory.buildStub.returns(collection); - collection.updateStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.saveTOTPSecret(userId, totpSecret) - .then(function (doc) { - Assert(collection.updateStub.calledOnce); - Assert(collection.updateStub.calledWith({ userId: userId }, { - userId: userId, - secret: totpSecret - }, { upsert: true })); - return BluebirdPromise.resolve(); - }); - }); - - it("should retrieve a totp secret", function () { - factory.buildStub.returns(collection); - collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.retrieveTOTPSecret(userId) - .then(function (doc) { - Assert(collection.findOneStub.calledOnce); - Assert(collection.findOneStub.calledWith({ userId: userId })); - return BluebirdPromise.resolve(); - }); - }); - }); - - describe("U2F secrets collection", function () { - it("should save a U2F secret", function () { - factory.buildStub.returns(collection); - collection.updateStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.saveU2FRegistration(userId, appId, u2fRegistration) - .then(function (doc) { - Assert(collection.updateStub.calledOnce); - Assert(collection.updateStub.calledWith({ - userId: userId, - appId: appId - }, { - userId: userId, - appId: appId, - registration: u2fRegistration - }, { upsert: true })); - return BluebirdPromise.resolve(); - }); - }); - - it("should retrieve a U2F secret", function () { - factory.buildStub.returns(collection); - collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.retrieveU2FRegistration(userId, appId) - .then(function (doc) { - Assert(collection.findOneStub.calledOnce); - Assert(collection.findOneStub.calledWith({ - userId: userId, - appId: appId - })); - return BluebirdPromise.resolve(); - }); - }); - }); - - - describe("Regulator traces collection", function () { - it("should save a trace", function () { - factory.buildStub.returns(collection); - collection.insertStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.saveAuthenticationTrace(userId, true) - .then(function (doc) { - Assert(collection.insertStub.calledOnce); - Assert(collection.insertStub.calledWith({ - userId: userId, - date: Sinon.match.date, - isAuthenticationSuccessful: true - })); - return BluebirdPromise.resolve(); - }); - }); - - function should_retrieve_latest_authentication_traces(count: number) { - factory.buildStub.returns(collection); - collection.findStub.withArgs().returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.retrieveLatestAuthenticationTraces(userId, count) - .then(function (doc: AuthenticationTraceDocument[]) { - Assert(collection.findStub.calledOnce); - Assert(collection.findStub.calledWith({ - userId: userId, - }, { date: -1 }, count)); - return BluebirdPromise.resolve(); - }); - } - - it("should retrieve 3 latest failed authentication traces", function () { - should_retrieve_latest_authentication_traces(3); - }); - }); - - - describe("Identity validation collection", function () { - it("should save a identity validation token", function () { - factory.buildStub.returns(collection); - collection.insertStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - const maxAge = 400; - const token = "TOKEN"; - const challenge = "CHALLENGE"; - - return dataStore.produceIdentityValidationToken(userId, token, challenge, maxAge) - .then(function (doc) { - Assert(collection.insertStub.calledOnce); - Assert(collection.insertStub.calledWith({ - userId: userId, - token: token, - challenge: challenge, - maxDate: Sinon.match.date - })); - return BluebirdPromise.resolve(); - }); - }); - - it("should consume an identity token successfully", function () { - factory.buildStub.returns(collection); - - MockDate.set(100); - - const token = "TOKEN"; - const challenge = "CHALLENGE"; - - collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ - userId: "USER", - token: token, - challenge: challenge, - maxDate: new Date() - })); - collection.removeStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - MockDate.set(80); - - return dataStore.consumeIdentityValidationToken(token, challenge) - .then(function (doc) { - MockDate.reset(); - Assert(collection.findOneStub.calledOnce); - Assert(collection.findOneStub.calledWith({ - token: token, - challenge: challenge - })); - - Assert(collection.removeStub.calledOnce); - Assert(collection.removeStub.calledWith({ - token: token, - challenge: challenge - })); - return BluebirdPromise.resolve(); - }); - }); - - it("should consume an expired identity token", function () { - factory.buildStub.returns(collection); - - MockDate.set(0); - - const token = "TOKEN"; - const challenge = "CHALLENGE"; - - collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ - userId: "USER", - token: token, - challenge: challenge, - maxDate: new Date() - })); - - const dataStore = new UserDataStore(factory); - - MockDate.set(80000); - - return dataStore.consumeIdentityValidationToken(token, challenge) - .then(function () { return BluebirdPromise.reject(new Error("should not be here")); }) - .catch(function () { - MockDate.reset(); - Assert(collection.findOneStub.calledOnce); - Assert(collection.findOneStub.calledWith({ - token: token, - challenge: challenge - })); - return BluebirdPromise.resolve(); - }); - }); - }); -}); diff --git a/themes/squares/server/src/lib/storage/UserDataStore.ts b/themes/squares/server/src/lib/storage/UserDataStore.ts deleted file mode 100644 index 27b0cddbd..000000000 --- a/themes/squares/server/src/lib/storage/UserDataStore.ts +++ /dev/null @@ -1,143 +0,0 @@ -import * as BluebirdPromise from "bluebird"; -import * as path from "path"; -import { IUserDataStore } from "./IUserDataStore"; -import { ICollection } from "./ICollection"; -import { ICollectionFactory } from "./ICollectionFactory"; -import { TOTPSecretDocument } from "./TOTPSecretDocument"; -import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; -import { U2FRegistration } from "../../../types/U2FRegistration"; -import { TOTPSecret } from "../../../types/TOTPSecret"; -import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; -import { IdentityValidationDocument } from "./IdentityValidationDocument"; - -// Constants - -const IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME = "identity_validation_tokens"; -const AUTHENTICATION_TRACES_COLLECTION_NAME = "authentication_traces"; - -const U2F_REGISTRATIONS_COLLECTION_NAME = "u2f_registrations"; -const TOTP_SECRETS_COLLECTION_NAME = "totp_secrets"; - - -export interface U2FRegistrationKey { - userId: string; - appId: string; -} - -// Source - -export class UserDataStore implements IUserDataStore { - private u2fSecretCollection: ICollection; - private identityCheckTokensCollection: ICollection; - private authenticationTracesCollection: ICollection; - private totpSecretCollection: ICollection; - - private collectionFactory: ICollectionFactory; - - constructor(collectionFactory: ICollectionFactory) { - this.collectionFactory = collectionFactory; - - this.u2fSecretCollection = this.collectionFactory.build(U2F_REGISTRATIONS_COLLECTION_NAME); - this.identityCheckTokensCollection = this.collectionFactory.build(IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME); - this.authenticationTracesCollection = this.collectionFactory.build(AUTHENTICATION_TRACES_COLLECTION_NAME); - this.totpSecretCollection = this.collectionFactory.build(TOTP_SECRETS_COLLECTION_NAME); - } - - saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { - const newDocument: U2FRegistrationDocument = { - userId: userId, - appId: appId, - registration: registration - }; - - const filter: U2FRegistrationKey = { - userId: userId, - appId: appId - }; - - return this.u2fSecretCollection.update(filter, newDocument, { upsert: true }); - } - - retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { - const filter: U2FRegistrationKey = { - userId: userId, - appId: appId - }; - return this.u2fSecretCollection.findOne(filter); - } - - saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { - const newDocument: AuthenticationTraceDocument = { - userId: userId, - date: new Date(), - isAuthenticationSuccessful: isAuthenticationSuccessful, - }; - - return this.authenticationTracesCollection.insert(newDocument); - } - - retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { - const q = { - userId: userId - }; - - return this.authenticationTracesCollection.find(q, { date: -1 }, count); - } - - produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { - const newDocument: IdentityValidationDocument = { - userId: userId, - token: token, - challenge: challenge, - maxDate: new Date(new Date().getTime() + maxAge) - }; - - return this.identityCheckTokensCollection.insert(newDocument); - } - - consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { - const that = this; - const filter = { - token: token, - challenge: challenge - }; - - let identityValidationDocument: IdentityValidationDocument; - - return this.identityCheckTokensCollection.findOne(filter) - .then(function (doc: IdentityValidationDocument) { - if (!doc) { - return BluebirdPromise.reject(new Error("Registration token does not exist")); - } - - identityValidationDocument = doc; - const current_date = new Date(); - if (current_date > doc.maxDate) - return BluebirdPromise.reject(new Error("Registration token is not valid anymore")); - - return that.identityCheckTokensCollection.remove(filter); - }) - .then(() => { - return BluebirdPromise.resolve(identityValidationDocument); - }); - } - - saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { - const doc = { - userId: userId, - secret: secret - }; - - const filter = { - userId: userId - }; - return this.totpSecretCollection.update(filter, doc, { upsert: true }); - } - - retrieveTOTPSecret(userId: string): BluebirdPromise { - const filter = { - userId: userId - }; - return this.totpSecretCollection.findOne(filter); - } -} diff --git a/themes/squares/server/src/lib/storage/UserDataStoreStub.spec.ts b/themes/squares/server/src/lib/storage/UserDataStoreStub.spec.ts deleted file mode 100644 index 5ea27a2de..000000000 --- a/themes/squares/server/src/lib/storage/UserDataStoreStub.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); - -import { TOTPSecretDocument } from "./TOTPSecretDocument"; -import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; -import { U2FRegistration } from "../../../types/U2FRegistration"; -import { TOTPSecret } from "../../../types/TOTPSecret"; -import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; -import { IdentityValidationDocument } from "./IdentityValidationDocument"; -import { IUserDataStore } from "./IUserDataStore"; - -export class UserDataStoreStub implements IUserDataStore { - saveU2FRegistrationStub: Sinon.SinonStub; - retrieveU2FRegistrationStub: Sinon.SinonStub; - saveAuthenticationTraceStub: Sinon.SinonStub; - retrieveLatestAuthenticationTracesStub: Sinon.SinonStub; - produceIdentityValidationTokenStub: Sinon.SinonStub; - consumeIdentityValidationTokenStub: Sinon.SinonStub; - saveTOTPSecretStub: Sinon.SinonStub; - retrieveTOTPSecretStub: Sinon.SinonStub; - - constructor() { - this.saveU2FRegistrationStub = Sinon.stub(); - this.retrieveU2FRegistrationStub = Sinon.stub(); - this.saveAuthenticationTraceStub = Sinon.stub(); - this.retrieveLatestAuthenticationTracesStub = Sinon.stub(); - this.produceIdentityValidationTokenStub = Sinon.stub(); - this.consumeIdentityValidationTokenStub = Sinon.stub(); - this.saveTOTPSecretStub = Sinon.stub(); - this.retrieveTOTPSecretStub = Sinon.stub(); - } - - saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { - return this.saveU2FRegistrationStub(userId, appId, registration); - } - - retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { - return this.retrieveU2FRegistrationStub(userId, appId); - } - - saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { - return this.saveAuthenticationTraceStub(userId, isAuthenticationSuccessful); - } - - retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { - return this.retrieveLatestAuthenticationTracesStub(userId, count); - } - - produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { - return this.produceIdentityValidationTokenStub(userId, token, challenge, maxAge); - } - - consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { - return this.consumeIdentityValidationTokenStub(token, challenge); - } - - saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { - return this.saveTOTPSecretStub(userId, secret); - } - - retrieveTOTPSecret(userId: string): BluebirdPromise { - return this.retrieveTOTPSecretStub(userId); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/mongo/MongoCollection.spec.ts b/themes/squares/server/src/lib/storage/mongo/MongoCollection.spec.ts deleted file mode 100644 index 74a773a1e..000000000 --- a/themes/squares/server/src/lib/storage/mongo/MongoCollection.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -import Assert = require("assert"); -import Sinon = require("sinon"); -import MongoDB = require("mongodb"); -import BluebirdPromise = require("bluebird"); -import { MongoClientStub } from "../../connectors/mongo/MongoClientStub.spec"; -import { MongoCollection } from "./MongoCollection"; - -describe("storage/mongo/MongoCollection", function () { - let mongoCollectionStub: any; - let mongoClientStub: MongoClientStub; - let findStub: Sinon.SinonStub; - let findOneStub: Sinon.SinonStub; - let insertOneStub: Sinon.SinonStub; - let updateStub: Sinon.SinonStub; - let removeStub: Sinon.SinonStub; - let countStub: Sinon.SinonStub; - const COLLECTION_NAME = "collection"; - - before(function () { - mongoClientStub = new MongoClientStub(); - mongoCollectionStub = Sinon.createStubInstance(require("mongodb").Collection as any); - findStub = mongoCollectionStub.find as Sinon.SinonStub; - findOneStub = mongoCollectionStub.findOne as Sinon.SinonStub; - insertOneStub = mongoCollectionStub.insertOne as Sinon.SinonStub; - updateStub = mongoCollectionStub.update as Sinon.SinonStub; - removeStub = mongoCollectionStub.remove as Sinon.SinonStub; - countStub = mongoCollectionStub.count as Sinon.SinonStub; - mongoClientStub.collectionStub.returns( - BluebirdPromise.resolve(mongoCollectionStub) - ); - }); - - describe("find", function () { - it("should find a document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - findStub.returns({ - sort: Sinon.stub().returns({ - limit: Sinon.stub().returns({ - toArray: Sinon.stub().returns(BluebirdPromise.resolve([])) - }) - }) - }); - - return collection.find({ key: "KEY" }) - .then(function () { - Assert(findStub.calledWith({ key: "KEY" })); - }); - }); - }); - - describe("findOne", function () { - it("should find one document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - findOneStub.returns(BluebirdPromise.resolve({})); - - return collection.findOne({ key: "KEY" }) - .then(function () { - Assert(findOneStub.calledWith({ key: "KEY" })); - }); - }); - }); - - describe("insert", function () { - it("should insert a document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - insertOneStub.returns(BluebirdPromise.resolve({})); - - return collection.insert({ key: "KEY" }) - .then(function () { - Assert(insertOneStub.calledWith({ key: "KEY" })); - }); - }); - }); - - describe("update", function () { - it("should update a document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - updateStub.returns(BluebirdPromise.resolve({})); - - return collection.update({ key: "KEY" }, { key: "KEY", value: 1 }) - .then(function () { - Assert(updateStub.calledWith({ key: "KEY" }, { key: "KEY", value: 1 })); - }); - }); - }); - - describe("remove", function () { - it("should remove a document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - removeStub.returns(BluebirdPromise.resolve({})); - - return collection.remove({ key: "KEY" }) - .then(function () { - Assert(removeStub.calledWith({ key: "KEY" })); - }); - }); - }); - - describe("count", function () { - it("should count documents in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - countStub.returns(BluebirdPromise.resolve({})); - - return collection.count({ key: "KEY" }) - .then(function () { - Assert(countStub.calledWith({ key: "KEY" })); - }); - }); - }); -}); diff --git a/themes/squares/server/src/lib/storage/mongo/MongoCollection.ts b/themes/squares/server/src/lib/storage/mongo/MongoCollection.ts deleted file mode 100644 index 9771389f1..000000000 --- a/themes/squares/server/src/lib/storage/mongo/MongoCollection.ts +++ /dev/null @@ -1,50 +0,0 @@ -import Bluebird = require("bluebird"); -import { ICollection } from "../ICollection"; -import MongoDB = require("mongodb"); -import { IMongoClient } from "../../connectors/mongo/IMongoClient"; - - -export class MongoCollection implements ICollection { - private mongoClient: IMongoClient; - private collectionName: string; - - constructor(collectionName: string, mongoClient: IMongoClient) { - this.collectionName = collectionName; - this.mongoClient = mongoClient; - } - - private collection(): Bluebird { - return this.mongoClient.collection(this.collectionName); - } - - find(query: any, sortKeys?: any, count?: number): Bluebird { - return this.collection() - .then((collection) => collection.find(query).sort(sortKeys).limit(count)) - .then((query) => query.toArray()); - } - - findOne(query: any): Bluebird { - return this.collection() - .then((collection) => collection.findOne(query)); - } - - update(query: any, updateQuery: any, options?: any): Bluebird { - return this.collection() - .then((collection) => collection.update(query, updateQuery, options)); - } - - remove(query: any): Bluebird { - return this.collection() - .then((collection) => collection.remove(query)); - } - - insert(document: any): Bluebird { - return this.collection() - .then((collection) => collection.insertOne(document)); - } - - count(query: any): Bluebird { - return this.collection() - .then((collection) => collection.count(query)); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts b/themes/squares/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts deleted file mode 100644 index bd959cacb..000000000 --- a/themes/squares/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Assert = require("assert"); -import Sinon = require("sinon"); -import { MongoClientStub } from "../../connectors/mongo/MongoClientStub.spec"; -import { MongoCollectionFactory } from "./MongoCollectionFactory"; - -describe("storage/mongo/MongoCollectionFactory", function () { - let mongoClient: MongoClientStub; - - before(function() { - mongoClient = new MongoClientStub(); - }); - - describe("create", function () { - it("should create a collection", function () { - const COLLECTION_NAME = "COLLECTION_NAME"; - - const factory = new MongoCollectionFactory(mongoClient); - Assert(factory.build(COLLECTION_NAME)); - }); - }); -}); diff --git a/themes/squares/server/src/lib/storage/mongo/MongoCollectionFactory.ts b/themes/squares/server/src/lib/storage/mongo/MongoCollectionFactory.ts deleted file mode 100644 index 14a8262c9..000000000 --- a/themes/squares/server/src/lib/storage/mongo/MongoCollectionFactory.ts +++ /dev/null @@ -1,19 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import { ICollection } from "../ICollection"; -import { ICollectionFactory } from "../ICollectionFactory"; -import { MongoCollection } from "./MongoCollection"; -import path = require("path"); -import MongoDB = require("mongodb"); -import { IMongoClient } from "../../connectors/mongo/IMongoClient"; - -export class MongoCollectionFactory implements ICollectionFactory { - private mongoClient: IMongoClient; - - constructor(mongoClient: IMongoClient) { - this.mongoClient = mongoClient; - } - - build(collectionName: string): ICollection { - return new MongoCollection(collectionName, this.mongoClient); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/nedb/NedbCollection.spec.ts b/themes/squares/server/src/lib/storage/nedb/NedbCollection.spec.ts deleted file mode 100644 index a69962b67..000000000 --- a/themes/squares/server/src/lib/storage/nedb/NedbCollection.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -import Sinon = require("sinon"); -import Assert = require("assert"); - -import { NedbCollection } from "./NedbCollection"; - -describe("storage/nedb/NedbCollection", function () { - describe("insert", function () { - it("should insert one entry", function () { - const nedbOptions = { - inMemoryOnly: true - }; - const collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou" }); - - return collection.count({}).then(function (count: number) { - Assert.equal(1, count); - }); - }); - - it("should insert three entries", function () { - const nedbOptions = { - inMemoryOnly: true - }; - const collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou" }); - collection.insert({ key: "hello" }); - collection.insert({ key: "hey" }); - - return collection.count({}).then(function (count: number) { - Assert.equal(3, count); - }); - }); - }); - - describe("find", function () { - let collection: NedbCollection; - before(function () { - const nedbOptions = { - inMemoryOnly: true - }; - collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou", value: 1 }); - collection.insert({ key: "hello" }); - collection.insert({ key: "hey" }); - collection.insert({ key: "coucou", value: 2 }); - }); - - it("should find one hello", function () { - return collection.find({ key: "hello" }, { key: 1 }) - .then(function (docs: { key: string }[]) { - Assert.equal(1, docs.length); - Assert(docs[0].key == "hello"); - }); - }); - - it("should find two coucou", function () { - return collection.find({ key: "coucou" }, { value: 1 }) - .then(function (docs: { value: number }[]) { - Assert.equal(2, docs.length); - }); - }); - }); - - describe("findOne", function () { - let collection: NedbCollection; - before(function () { - const nedbOptions = { - inMemoryOnly: true - }; - collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou", value: 1 }); - collection.insert({ key: "coucou", value: 1 }); - collection.insert({ key: "coucou", value: 1 }); - collection.insert({ key: "coucou", value: 1 }); - }); - - it("should find two coucou", function () { - const doc = { key: "coucou", value: 1 }; - return collection.count(doc) - .then(function (count: number) { - Assert.equal(4, count); - return collection.findOne(doc); - }); - }); - }); - - describe("update", function () { - let collection: NedbCollection; - before(function () { - const nedbOptions = { - inMemoryOnly: true - }; - collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou", value: 1 }); - }); - - it("should update the value", function () { - return collection.update({ key: "coucou" }, { key: "coucou", value: 2 }, { multi: true }) - .then(function () { - return collection.find({ key: "coucou" }); - }) - .then(function (docs: { key: string, value: number }[]) { - Assert.equal(1, docs.length); - Assert.equal(2, docs[0].value); - }); - }); - }); - - describe("update", function () { - let collection: NedbCollection; - before(function () { - const nedbOptions = { - inMemoryOnly: true - }; - collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou" }); - collection.insert({ key: "hello" }); - }); - - it("should update the value", function () { - return collection.remove({ key: "coucou" }) - .then(function () { - return collection.count({}); - }) - .then(function (count: number) { - Assert.equal(1, count); - }); - }); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/nedb/NedbCollection.ts b/themes/squares/server/src/lib/storage/nedb/NedbCollection.ts deleted file mode 100644 index 88a93ad05..000000000 --- a/themes/squares/server/src/lib/storage/nedb/NedbCollection.ts +++ /dev/null @@ -1,47 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import { ICollection } from "../ICollection"; -import Nedb = require("nedb"); - -declare module "nedb" { - export class NedbAsync extends Nedb { - constructor(pathOrOptions?: string | Nedb.DataStoreOptions); - updateAsync(query: any, updateQuery: any, options?: Nedb.UpdateOptions): BluebirdPromise; - findOneAsync(query: any): BluebirdPromise; - insertAsync(newDoc: T): BluebirdPromise; - removeAsync(query: any): BluebirdPromise; - countAsync(query: any): BluebirdPromise; - } -} - -export class NedbCollection implements ICollection { - private collection: Nedb.NedbAsync; - - constructor(options: Nedb.DataStoreOptions) { - this.collection = BluebirdPromise.promisifyAll(new Nedb(options)) as Nedb.NedbAsync; - } - - find(query: any, sortKeys?: any, count?: number): BluebirdPromise { - const q = this.collection.find(query).sort(sortKeys).limit(count); - return BluebirdPromise.promisify(q.exec, { context: q })(); - } - - findOne(query: any): BluebirdPromise { - return this.collection.findOneAsync(query); - } - - update(query: any, updateQuery: any, options?: any): BluebirdPromise { - return this.collection.updateAsync(query, updateQuery, options); - } - - remove(query: any): BluebirdPromise { - return this.collection.removeAsync(query); - } - - insert(document: any): BluebirdPromise { - return this.collection.insertAsync(document); - } - - count(query: any): BluebirdPromise { - return this.collection.countAsync(query); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts b/themes/squares/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts deleted file mode 100644 index da90c661f..000000000 --- a/themes/squares/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Sinon = require("sinon"); -import Assert = require("assert"); - -import { NedbCollectionFactory } from "./NedbCollectionFactory"; - -describe("storage/nedb/NedbCollectionFactory", function() { - it("should create a nedb collection", function() { - const nedbOptions = { - inMemoryOnly: true - }; - const factory = new NedbCollectionFactory(nedbOptions); - - const collection = factory.build("mycollection"); - Assert(collection); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/storage/nedb/NedbCollectionFactory.ts b/themes/squares/server/src/lib/storage/nedb/NedbCollectionFactory.ts deleted file mode 100644 index 49c4dc853..000000000 --- a/themes/squares/server/src/lib/storage/nedb/NedbCollectionFactory.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ICollection } from "../ICollection"; -import { ICollectionFactory } from "../ICollectionFactory"; -import { NedbCollection } from "./NedbCollection"; -import path = require("path"); -import Nedb = require("nedb"); - -export interface NedbOptions { - inMemoryOnly?: boolean; - directory?: string; -} - -export class NedbCollectionFactory implements ICollectionFactory { - private options: Nedb.DataStoreOptions; - - constructor(options: Nedb.DataStoreOptions) { - this.options = options; - } - - build(collectionName: string): ICollection { - const datastoreOptions: Nedb.DataStoreOptions = { - inMemoryOnly: this.options.inMemoryOnly || false, - autoload: true, - filename: (this.options.filename) ? path.resolve(this.options.filename, collectionName) : undefined - }; - - return new NedbCollection(datastoreOptions); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/stubs/express.spec.ts b/themes/squares/server/src/lib/stubs/express.spec.ts deleted file mode 100644 index 48f15d7e1..000000000 --- a/themes/squares/server/src/lib/stubs/express.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ - -import sinon = require("sinon"); -import express = require("express"); - -export interface RequestMock { - app?: any; - body?: any; - session?: any; - headers?: any; - get?: any; - query?: any; - originalUrl: string; -} - -export interface ResponseMock { - send: sinon.SinonStub | sinon.SinonSpy; - sendStatus: sinon.SinonStub; - sendFile: sinon.SinonStub; - sendfile: sinon.SinonStub; - status: sinon.SinonStub | sinon.SinonSpy; - json: sinon.SinonStub | sinon.SinonSpy; - links: sinon.SinonStub; - jsonp: sinon.SinonStub; - download: sinon.SinonStub; - contentType: sinon.SinonStub; - type: sinon.SinonStub; - format: sinon.SinonStub; - attachment: sinon.SinonStub; - set: sinon.SinonStub; - header: sinon.SinonStub; - headersSent: boolean; - get: sinon.SinonStub; - clearCookie: sinon.SinonStub; - cookie: sinon.SinonStub; - location: sinon.SinonStub; - redirect: sinon.SinonStub | sinon.SinonSpy; - render: sinon.SinonStub | sinon.SinonSpy; - locals: sinon.SinonStub; - charset: string; - vary: sinon.SinonStub; - app: any; - write: sinon.SinonStub; - writeContinue: sinon.SinonStub; - writeHead: sinon.SinonStub; - statusCode: number; - statusMessage: string; - setHeader: sinon.SinonStub; - setTimeout: sinon.SinonStub; - sendDate: boolean; - getHeader: sinon.SinonStub; -} - -export function RequestMock(): RequestMock { - return { - originalUrl: "/non-api/xxx", - app: { - get: sinon.stub() - }, - headers: { - "x-forwarded-for": "127.0.0.1" - }, - session: {} - }; -} -export function ResponseMock(): ResponseMock { - return { - send: sinon.stub(), - status: sinon.stub(), - json: sinon.stub(), - sendStatus: sinon.stub(), - links: sinon.stub(), - jsonp: sinon.stub(), - sendFile: sinon.stub(), - sendfile: sinon.stub(), - download: sinon.stub(), - contentType: sinon.stub(), - type: sinon.stub(), - format: sinon.stub(), - attachment: sinon.stub(), - set: sinon.stub(), - header: sinon.stub(), - headersSent: true, - get: sinon.stub(), - clearCookie: sinon.stub(), - cookie: sinon.stub(), - location: sinon.stub(), - redirect: sinon.stub(), - render: sinon.stub(), - locals: sinon.stub(), - charset: "utf-8", - vary: sinon.stub(), - app: sinon.stub(), - write: sinon.stub(), - writeContinue: sinon.stub(), - writeHead: sinon.stub(), - statusCode: 200, - statusMessage: "message", - setHeader: sinon.stub(), - setTimeout: sinon.stub(), - sendDate: true, - getHeader: sinon.stub() - }; -} diff --git a/themes/squares/server/src/lib/stubs/ldapjs.spec.ts b/themes/squares/server/src/lib/stubs/ldapjs.spec.ts deleted file mode 100644 index 045c0e11b..000000000 --- a/themes/squares/server/src/lib/stubs/ldapjs.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ - -import Sinon = require("sinon"); - -export class LdapjsMock { - createClientStub: sinon.SinonStub; - - constructor() { - this.createClientStub = Sinon.stub(); - } - - createClient(params: any) { - return this.createClientStub(params); - } -} - -export class LdapjsClientMock { - bindStub: sinon.SinonStub; - unbindStub: sinon.SinonStub; - searchStub: sinon.SinonStub; - modifyStub: sinon.SinonStub; - onStub: sinon.SinonStub; - - constructor() { - this.bindStub = Sinon.stub(); - this.unbindStub = Sinon.stub(); - this.searchStub = Sinon.stub(); - this.modifyStub = Sinon.stub(); - this.onStub = Sinon.stub(); - } - - bind() { - return this.bindStub(); - } - - unbind() { - return this.unbindStub(); - } - - search() { - return this.searchStub(); - } - - modify() { - return this.modifyStub(); - } - - on() { - return this.onStub(); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/stubs/speakeasy.spec.ts b/themes/squares/server/src/lib/stubs/speakeasy.spec.ts deleted file mode 100644 index 023614dcd..000000000 --- a/themes/squares/server/src/lib/stubs/speakeasy.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ - -import sinon = require("sinon"); - -export = { - totp: sinon.stub(), - generateSecret: sinon.stub() -}; diff --git a/themes/squares/server/src/lib/stubs/u2f.spec.ts b/themes/squares/server/src/lib/stubs/u2f.spec.ts deleted file mode 100644 index 234b28c11..000000000 --- a/themes/squares/server/src/lib/stubs/u2f.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ - -import sinon = require("sinon"); - -export interface U2FMock { - request: sinon.SinonStub; - checkSignature: sinon.SinonStub; - checkRegistration: sinon.SinonStub; -} - -export function U2FMock(): U2FMock { - return { - request: sinon.stub(), - checkSignature: sinon.stub(), - checkRegistration: sinon.stub() - }; -} diff --git a/themes/squares/server/src/lib/utils/HashGenerator.spec.ts b/themes/squares/server/src/lib/utils/HashGenerator.spec.ts deleted file mode 100644 index f19619a65..000000000 --- a/themes/squares/server/src/lib/utils/HashGenerator.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Assert = require("assert"); -import { HashGenerator } from "./HashGenerator"; - -describe("utils/HashGenerator", function () { - it("should compute correct ssha512 (password)", function () { - return HashGenerator.ssha512("password", 500000, "jgiCMRyGXzoqpxS3") - .then(function (hash: string) { - Assert.equal(hash, "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"); - }); - }); - - it("should compute correct ssha512 (test)", function () { - return HashGenerator.ssha512("test", 500000, "abcdefghijklmnop") - .then(function (hash: string) { - Assert.equal(hash, "{CRYPT}$6$rounds=500000$abcdefghijklmnop$sTlNGf0VO/HTQIOXemmaBbV28HUch/qhWOA1/4dsDj6CDQYhUgXbYSPL6gccAsWMr2zD5fFWwhKmPdG.yxphs."); - }); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/utils/HashGenerator.ts b/themes/squares/server/src/lib/utils/HashGenerator.ts deleted file mode 100644 index e67de32b7..000000000 --- a/themes/squares/server/src/lib/utils/HashGenerator.ts +++ /dev/null @@ -1,23 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import RandomString = require("randomstring"); -import Util = require("util"); -const crypt = require("crypt3"); - -export class HashGenerator { - static ssha512( - password: string, - rounds: number = 500000, - salt?: string): BluebirdPromise { - const saltSize = 16; - // $6 means SHA512 - const _salt = Util.format("$6$rounds=%d$%s", rounds, - (salt) ? salt : RandomString.generate(16)); - - const cryptAsync = BluebirdPromise.promisify(crypt); - - return cryptAsync(password, _salt) - .then(function (hash: string) { - return BluebirdPromise.resolve(Util.format("{CRYPT}%s", hash)); - }); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/utils/ObjectCloner.ts b/themes/squares/server/src/lib/utils/ObjectCloner.ts deleted file mode 100644 index 3e125d749..000000000 --- a/themes/squares/server/src/lib/utils/ObjectCloner.ts +++ /dev/null @@ -1,6 +0,0 @@ - -export class ObjectCloner { - static clone(obj: any): any { - return JSON.parse(JSON.stringify(obj)); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/utils/SafeRedirection.spec.ts b/themes/squares/server/src/lib/utils/SafeRedirection.spec.ts deleted file mode 100644 index 4126949fd..000000000 --- a/themes/squares/server/src/lib/utils/SafeRedirection.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import Assert = require("assert"); -import Sinon = require("sinon"); -import { SafeRedirector } from "./SafeRedirection"; - -describe("web_server/middlewares/SafeRedirection", () => { - describe("Url is in protected domain", () => { - before(() => { - this.redirector = new SafeRedirector("example.com"); - this.res = {redirect: Sinon.stub()}; - }); - - it("should redirect to provided url", () => { - this.redirector.redirectOrElse(this.res, - "https://mysubdomain.example.com:8080/abc", - "https://authelia.example.com"); - Assert(this.res.redirect.calledWith("https://mysubdomain.example.com:8080/abc")); - }); - - it("should redirect to default url when wrong domain", () => { - this.redirector.redirectOrElse(this.res, - "https://mysubdomain.domain.rtf:8080/abc", - "https://authelia.example.com"); - Assert(this.res.redirect.calledWith("https://authelia.example.com")); - }); - - it("should redirect to default url when not terminating by domain", () => { - this.redirector.redirectOrElse(this.res, - "https://mysubdomain.example.com.rtf:8080/abc", - "https://authelia.example.com"); - Assert(this.res.redirect.calledWith("https://authelia.example.com")); - }); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/utils/SafeRedirection.ts b/themes/squares/server/src/lib/utils/SafeRedirection.ts deleted file mode 100644 index 9e6a32e0c..000000000 --- a/themes/squares/server/src/lib/utils/SafeRedirection.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Express = require("express"); -import { DomainExtractor } from "../../../../shared/DomainExtractor"; -import { BelongToDomain } from "../../../../shared/BelongToDomain"; - - -export class SafeRedirector { - private domain: string; - - constructor(domain: string) { - this.domain = domain; - } - - redirectOrElse( - res: Express.Response, - url: string, - defaultUrl: string): void { - if (BelongToDomain(url, this.domain)) { - res.redirect(url); - } - res.redirect(defaultUrl); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/utils/URLDecomposer.spec.ts b/themes/squares/server/src/lib/utils/URLDecomposer.spec.ts deleted file mode 100644 index cbb038738..000000000 --- a/themes/squares/server/src/lib/utils/URLDecomposer.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { URLDecomposer } from "./URLDecomposer"; -import Assert = require("assert"); - -describe("utils/URLDecomposer", function () { - describe("test fromUrl", function () { - it("should return domain from https url", function () { - const d = URLDecomposer.fromUrl("https://www.example.com/test/abc"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/test/abc"); - }); - - it("should return domain from http url", function () { - const d = URLDecomposer.fromUrl("http://www.example.com/test/abc"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/test/abc"); - }); - - it("should return domain when url contains port", function () { - const d = URLDecomposer.fromUrl("https://www.example.com:8080/test/abc"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/test/abc"); - }); - - it("should return default path when no path provided", function () { - const d = URLDecomposer.fromUrl("https://www.example.com:8080"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/"); - }); - - it("should return default path when provided", function () { - const d = URLDecomposer.fromUrl("https://www.example.com:8080/"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/"); - }); - - it("should return undefined when does not match", function () { - const d = URLDecomposer.fromUrl("https:///abc/test"); - Assert.equal(d, undefined); - }); - - it("should return undefined when does not match", function () { - const d = URLDecomposer.fromUrl("https:///abc/test"); - Assert.equal(d, undefined); - }); - }); -}); \ No newline at end of file diff --git a/themes/squares/server/src/lib/utils/URLDecomposer.ts b/themes/squares/server/src/lib/utils/URLDecomposer.ts deleted file mode 100644 index 9bdf2e9d2..000000000 --- a/themes/squares/server/src/lib/utils/URLDecomposer.ts +++ /dev/null @@ -1,15 +0,0 @@ -export class URLDecomposer { - static fromUrl(url: string): {domain: string, path: string} { - if (!url) return; - const match = url.match(/https?:\/\/([a-z0-9_.-]+)(:[0-9]+)?(.*)/); - - if (!match) return; - - if (match[1] && !match[3]) { - return {domain: match[1], path: "/"}; - } else if (match[1] && match[3]) { - return {domain: match[1], path: match[3]}; - } - return; - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/web_server/Configurator.ts b/themes/squares/server/src/lib/web_server/Configurator.ts deleted file mode 100644 index 6e404874a..000000000 --- a/themes/squares/server/src/lib/web_server/Configurator.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Configuration } from "../configuration/schema/Configuration"; -import { GlobalDependencies } from "../../../types/Dependencies"; -import { SessionConfigurationBuilder } from - "../configuration/SessionConfigurationBuilder"; -import Path = require("path"); -import Express = require("express"); -import * as BodyParser from "body-parser"; -import { RestApi } from "./RestApi"; -import { WithHeadersLogged } from "./middlewares/WithHeadersLogged"; -import { ServerVariables } from "../ServerVariables"; -import Helmet = require("helmet"); - -const addRequestId = require("express-request-id")(); - -// Constants -const TRUST_PROXY = "trust proxy"; -const X_POWERED_BY = "x-powered-by"; -const VIEWS = "views"; -const VIEW_ENGINE = "view engine"; -const PUG = "pug"; - -export class Configurator { - static configure(config: Configuration, - app: Express.Application, - vars: ServerVariables, - deps: GlobalDependencies): void { - const viewsDirectory = Path.resolve(__dirname, "../../views"); - const publicHtmlDirectory = Path.resolve(__dirname, "../../public_html"); - - const expressSessionOptions = SessionConfigurationBuilder.build(config, deps); - - app.use(Express.static(publicHtmlDirectory)); - app.use(BodyParser.urlencoded({ extended: false })); - app.use(BodyParser.json()); - app.use(deps.session(expressSessionOptions)); - app.use(addRequestId); - app.use(WithHeadersLogged.middleware(vars.logger)); - app.disable(X_POWERED_BY); - app.enable(TRUST_PROXY); - app.use(Helmet()); - - app.set(VIEWS, viewsDirectory); - app.set(VIEW_ENGINE, PUG); - - RestApi.setup(app, vars); - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/web_server/RestApi.ts b/themes/squares/server/src/lib/web_server/RestApi.ts deleted file mode 100644 index 9144a15b9..000000000 --- a/themes/squares/server/src/lib/web_server/RestApi.ts +++ /dev/null @@ -1,125 +0,0 @@ -import Express = require("express"); - -import FirstFactorGet = require("../routes/firstfactor/get"); -import SecondFactorGet = require("../routes/secondfactor/get"); - -import FirstFactorPost = require("../routes/firstfactor/post"); -import LogoutGet = require("../routes/logout/get"); -import VerifyGet = require("../routes/verify/get"); -import TOTPSignGet = require("../routes/secondfactor/totp/sign/post"); - -import IdentityCheckMiddleware = require("../IdentityCheckMiddleware"); - -import TOTPRegistrationIdentityHandler from "../routes/secondfactor/totp/identity/RegistrationHandler"; -import U2FRegistrationIdentityHandler from "../routes/secondfactor/u2f/identity/RegistrationHandler"; -import ResetPasswordIdentityHandler from "../routes/password-reset/identity/PasswordResetHandler"; - -import U2FSignPost = require("../routes/secondfactor/u2f/sign/post"); -import U2FSignRequestGet = require("../routes/secondfactor/u2f/sign_request/get"); - -import U2FRegisterPost = require("../routes/secondfactor/u2f/register/post"); -import U2FRegisterRequestGet = require("../routes/secondfactor/u2f/register_request/get"); - -import ResetPasswordFormPost = require("../routes/password-reset/form/post"); -import ResetPasswordRequestPost = require("../routes/password-reset/request/get"); - -import Error401Get = require("../routes/error/401/get"); -import Error403Get = require("../routes/error/403/get"); -import Error404Get = require("../routes/error/404/get"); - -import LoggedIn = require("../routes/loggedin/get"); - -import { ServerVariables } from "../ServerVariables"; -import Endpoints = require("../../../../shared/api"); -import { RequireValidatedFirstFactor } from "./middlewares/RequireValidatedFirstFactor"; - -function setupTotp(app: Express.Application, vars: ServerVariables) { - app.post(Endpoints.SECOND_FACTOR_TOTP_POST, - RequireValidatedFirstFactor.middleware(vars.logger), - TOTPSignGet.default(vars)); - - app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, - RequireValidatedFirstFactor.middleware(vars.logger)); - - app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, - RequireValidatedFirstFactor.middleware(vars.logger)); - - IdentityCheckMiddleware.register(app, - Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, - Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, - new TOTPRegistrationIdentityHandler(vars.logger, - vars.userDataStore, vars.totpHandler, vars.config.totp), - vars); -} - -function setupU2f(app: Express.Application, vars: ServerVariables) { - app.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, - RequireValidatedFirstFactor.middleware(vars.logger), - U2FSignRequestGet.default(vars)); - - app.post(Endpoints.SECOND_FACTOR_U2F_SIGN_POST, - RequireValidatedFirstFactor.middleware(vars.logger), - U2FSignPost.default(vars)); - - app.get(Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, - RequireValidatedFirstFactor.middleware(vars.logger), - U2FRegisterRequestGet.default(vars)); - - app.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, - RequireValidatedFirstFactor.middleware(vars.logger), - U2FRegisterPost.default(vars)); - - app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, - RequireValidatedFirstFactor.middleware(vars.logger)); - - app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, - RequireValidatedFirstFactor.middleware(vars.logger)); - - IdentityCheckMiddleware.register(app, - Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, - Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, - new U2FRegistrationIdentityHandler(vars.logger), vars); -} - -function setupResetPassword(app: Express.Application, vars: ServerVariables) { - IdentityCheckMiddleware.register(app, - Endpoints.RESET_PASSWORD_IDENTITY_START_GET, - Endpoints.RESET_PASSWORD_IDENTITY_FINISH_GET, - new ResetPasswordIdentityHandler(vars.logger, vars.usersDatabase), - vars); - - app.get(Endpoints.RESET_PASSWORD_REQUEST_GET, - ResetPasswordRequestPost.default); - app.post(Endpoints.RESET_PASSWORD_FORM_POST, - ResetPasswordFormPost.default(vars)); -} - -function setupErrors(app: Express.Application, vars: ServerVariables) { - app.get(Endpoints.ERROR_401_GET, Error401Get.default(vars)); - app.get(Endpoints.ERROR_403_GET, Error403Get.default(vars)); - app.get(Endpoints.ERROR_404_GET, Error404Get.default); -} - -export class RestApi { - static setup(app: Express.Application, vars: ServerVariables): void { - app.get(Endpoints.FIRST_FACTOR_GET, FirstFactorGet.default(vars)); - - app.get(Endpoints.SECOND_FACTOR_GET, - RequireValidatedFirstFactor.middleware(vars.logger), - SecondFactorGet.default(vars)); - - app.get(Endpoints.LOGOUT_GET, LogoutGet.default(vars)); - - app.get(Endpoints.VERIFY_GET, VerifyGet.default(vars)); - app.post(Endpoints.FIRST_FACTOR_POST, FirstFactorPost.default(vars)); - - setupTotp(app, vars); - setupU2f(app, vars); - setupResetPassword(app, vars); - setupErrors(app, vars); - - app.get(Endpoints.LOGGED_IN, - RequireValidatedFirstFactor.middleware(vars.logger), - LoggedIn.default(vars)); - } -} diff --git a/themes/squares/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts b/themes/squares/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts deleted file mode 100644 index ecfd75765..000000000 --- a/themes/squares/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts +++ /dev/null @@ -1,27 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import ErrorReplies = require("../../ErrorReplies"); -import { IRequestLogger } from "../../logging/IRequestLogger"; -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import Exceptions = require("../../Exceptions"); -import { Level } from "../../authentication/Level"; - -export class RequireValidatedFirstFactor { - static middleware(logger: IRequestLogger) { - return function (req: Express.Request, res: Express.Response, - next: Express.NextFunction): BluebirdPromise { - - 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.")); - - next(); - resolve(); - }) - .catch(ErrorReplies.replyWithError401(req, res, logger)); - }; - } -} \ No newline at end of file diff --git a/themes/squares/server/src/lib/web_server/middlewares/WithHeadersLogged.ts b/themes/squares/server/src/lib/web_server/middlewares/WithHeadersLogged.ts deleted file mode 100644 index 139db1141..000000000 --- a/themes/squares/server/src/lib/web_server/middlewares/WithHeadersLogged.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Express = require("express"); -import { IRequestLogger } from "../../logging/IRequestLogger"; - -export class WithHeadersLogged { - static middleware(logger: IRequestLogger) { - return function (req: Express.Request, res: Express.Response, - next: Express.NextFunction): void { - logger.debug(req, "Headers = %s", JSON.stringify(req.headers)); - next(); - }; - } -} \ No newline at end of file diff --git a/themes/squares/server/src/resources/email-template.ejs b/themes/squares/server/src/resources/email-template.ejs old mode 100644 new mode 100755 diff --git a/themes/squares/server/src/views/already-logged-in.pug b/themes/squares/server/src/views/already-logged-in.pug old mode 100644 new mode 100755 diff --git a/themes/squares/server/src/views/errors/.directory b/themes/squares/server/src/views/errors/.directory old mode 100644 new mode 100755 diff --git a/themes/squares/server/src/views/errors/401.pug b/themes/squares/server/src/views/errors/401.pug old mode 100644 new mode 100755 diff --git a/themes/squares/server/src/views/errors/403.pug b/themes/squares/server/src/views/errors/403.pug old mode 100644 new mode 100755 diff --git a/themes/squares/server/src/views/errors/404.pug b/themes/squares/server/src/views/errors/404.pug old mode 100644 new mode 100755 diff --git a/themes/squares/server/src/views/firstfactor.pug b/themes/squares/server/src/views/firstfactor.pug old mode 100644 new mode 100755 diff --git a/themes/squares/server/src/views/layout/layout.pug b/themes/squares/server/src/views/layout/layout.pug old mode 100644 new mode 100755 diff --git a/themes/squares/server/src/views/need-identity-validation.pug b/themes/squares/server/src/views/need-identity-validation.pug old mode 100644 new mode 100755 diff --git a/themes/squares/server/src/views/password-reset-form.pug b/themes/squares/server/src/views/password-reset-form.pug old mode 100644 new mode 100755 diff --git a/themes/squares/server/src/views/password-reset-request.pug b/themes/squares/server/src/views/password-reset-request.pug old mode 100644 new mode 100755 diff --git a/themes/squares/server/src/views/secondfactor.pug b/themes/squares/server/src/views/secondfactor.pug old mode 100644 new mode 100755 diff --git a/themes/squares/server/src/views/totp-register.pug b/themes/squares/server/src/views/totp-register.pug old mode 100644 new mode 100755 diff --git a/themes/squares/server/src/views/u2f-register.pug b/themes/squares/server/src/views/u2f-register.pug old mode 100644 new mode 100755 diff --git a/themes/squares/server/test/requests.ts b/themes/squares/server/test/requests.ts deleted file mode 100644 index 93fa0de47..000000000 --- a/themes/squares/server/test/requests.ts +++ /dev/null @@ -1,94 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import request = require("request"); -import assert = require("assert"); -import express = require("express"); -import nodemailer = require("nodemailer"); -import Endpoints = require("../../shared/api"); - -declare module "request" { - export interface RequestAPI { - getAsync(uri: string, options?: RequiredUriUrl): BluebirdPromise; - getAsync(uri: string): BluebirdPromise; - getAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; - - postAsync(uri: string, options?: CoreOptions): BluebirdPromise; - postAsync(uri: string): BluebirdPromise; - postAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; - } -} - -const requestAsync: typeof request = BluebirdPromise.promisifyAll(request) as typeof request; - -export = function (port: number) { - const PORT = port; - const BASE_URL = "http://localhost:" + PORT; - - function execute_totp(jar: request.CookieJar, token: string) { - return requestAsync.postAsync({ - url: BASE_URL + Endpoints.SECOND_FACTOR_TOTP_POST, - jar: jar, - form: { - token: token - } - }); - } - - function execute_u2f_authentication(jar: request.CookieJar) { - return requestAsync.getAsync({ - url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, - jar: jar - }) - .then(function (res: request.RequestResponse) { - assert.equal(res.statusCode, 200); - return requestAsync.postAsync({ - url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_POST, - jar: jar, - form: { - } - }); - }); - } - - function execute_verification(jar: request.CookieJar) { - return requestAsync.getAsync({ url: BASE_URL + Endpoints.VERIFY_GET, jar: jar }); - } - - function execute_login(jar: request.CookieJar) { - return requestAsync.getAsync({ url: BASE_URL + Endpoints.FIRST_FACTOR_GET, jar: jar }); - } - - function execute_first_factor(jar: request.CookieJar) { - return requestAsync.postAsync({ - url: BASE_URL + Endpoints.FIRST_FACTOR_POST, - jar: jar, - form: { - username: "test_ok", - password: "password" - } - }); - } - - function execute_failing_first_factor(jar: request.CookieJar) { - return requestAsync.postAsync({ - url: BASE_URL + Endpoints.FIRST_FACTOR_POST, - jar: jar, - form: { - username: "test_nok", - password: "password" - } - }); - } - - return { - login: execute_login, - verify: execute_verification, - u2f_authentication: execute_u2f_authentication, - first_factor: execute_first_factor, - failing_first_factor: execute_failing_first_factor, - totp: execute_totp, - }; -}; - diff --git a/themes/squares/server/tsconfig.json b/themes/squares/server/tsconfig.json deleted file mode 100644 index ebe98c5ed..000000000 --- a/themes/squares/server/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "moduleResolution": "node", - "noImplicitAny": true, - "sourceMap": true, - "removeComments": true, - "outDir": "../dist", - "baseUrl": ".", - "paths": { - "*": [ - "./types/*", - "../shared/types/*" - ] - } - }, - "exclude": [ - "src/**/*.spec.ts" - ] -} diff --git a/themes/squares/server/tslint.json b/themes/squares/server/tslint.json deleted file mode 100644 index c2c1b7501..000000000 --- a/themes/squares/server/tslint.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "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 - } -} diff --git a/themes/squares/server/types/.directory b/themes/squares/server/types/.directory deleted file mode 100644 index 1e65000ea..000000000 --- a/themes/squares/server/types/.directory +++ /dev/null @@ -1,4 +0,0 @@ -[Dolphin] -Timestamp=2018,12,17,20,58,27 -Version=3 -ViewMode=1 diff --git a/themes/squares/server/types/AuthenticationSession.ts b/themes/squares/server/types/AuthenticationSession.ts deleted file mode 100644 index bbed0e715..000000000 --- a/themes/squares/server/types/AuthenticationSession.ts +++ /dev/null @@ -1,18 +0,0 @@ -import U2f = require("u2f"); -import { Level } from "../src/lib/authentication/Level"; - -export interface AuthenticationSession { - userid: string; - authentication_level: Level; - keep_me_logged_in: boolean; - last_activity_datetime: number; - identity_check?: { - challenge: string; - userid: string; - }; - register_request?: U2f.Request; - sign_request?: U2f.Request; - email: string; - groups: string[]; - redirect?: string; -} \ No newline at end of file diff --git a/themes/squares/server/types/Dependencies.ts b/themes/squares/server/types/Dependencies.ts deleted file mode 100644 index f20404dbc..000000000 --- a/themes/squares/server/types/Dependencies.ts +++ /dev/null @@ -1,29 +0,0 @@ -import winston = require("winston"); -import speakeasy = require("speakeasy"); -import nodemailer = require("nodemailer"); -import session = require("express-session"); -import nedb = require("nedb"); -import ldapjs = require("ldapjs"); -import u2f = require("u2f"); -import RedisSession = require("connect-redis"); -import Redis = require("redis"); - -export type Speakeasy = typeof speakeasy; -export type Winston = typeof winston; -export type Session = typeof session; -export type Nedb = typeof nedb; -export type Ldapjs = typeof ldapjs; -export type U2f = typeof u2f; -export type ConnectRedis = typeof RedisSession; -export type Redis = typeof Redis; - -export interface GlobalDependencies { - u2f: U2f; - ldapjs: Ldapjs; - session: Session; - Redis: Redis; - ConnectRedis: ConnectRedis; - winston: Winston; - speakeasy: Speakeasy; - nedb: Nedb; -} \ No newline at end of file diff --git a/themes/squares/server/types/Identity.ts b/themes/squares/server/types/Identity.ts deleted file mode 100644 index e985984e2..000000000 --- a/themes/squares/server/types/Identity.ts +++ /dev/null @@ -1,6 +0,0 @@ - - -export interface Identity { - userid: string; - email: string; -} \ No newline at end of file diff --git a/themes/squares/server/types/TOTPSecret.ts b/themes/squares/server/types/TOTPSecret.ts deleted file mode 100644 index d6775f2f0..000000000 --- a/themes/squares/server/types/TOTPSecret.ts +++ /dev/null @@ -1,11 +0,0 @@ - -export interface TOTPSecret { - ascii: string; - hex: string; - base32: string; - qr_code_ascii: string; - qr_code_hex: string; - qr_code_base32: string; - google_auth_qr: string; - otpauth_url: string; - } \ No newline at end of file diff --git a/themes/squares/server/types/U2FRegistration.ts b/themes/squares/server/types/U2FRegistration.ts deleted file mode 100644 index b6080af07..000000000 --- a/themes/squares/server/types/U2FRegistration.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface U2FRegistration { - keyHandle: string; - publicKey: string; -} \ No newline at end of file diff --git a/themes/squares/server/types/dovehash.d.ts b/themes/squares/server/types/dovehash.d.ts deleted file mode 100644 index c354609c0..000000000 --- a/themes/squares/server/types/dovehash.d.ts +++ /dev/null @@ -1,4 +0,0 @@ - -declare module "dovehash" { - function encode(algo: string, text: string): string; -} \ No newline at end of file diff --git a/themes/squares/server/types/speakeasy.d.ts b/themes/squares/server/types/speakeasy.d.ts deleted file mode 100644 index 6ea06948b..000000000 --- a/themes/squares/server/types/speakeasy.d.ts +++ /dev/null @@ -1,96 +0,0 @@ -declare module "speakeasy" { - export = speakeasy - - interface SharedOptions { - encoding?: string - algorithm?: string - } - - interface DigestOptions extends SharedOptions { - secret: string - counter: number - } - - interface HOTPOptions extends SharedOptions { - secret: string - counter: number - digest?: Buffer - digits?: number - } - - interface HOTPVerifyOptions extends SharedOptions { - secret: string - token: string - counter: number - digits?: number - window?: number - } - - interface TOTPOptions extends SharedOptions { - secret: string - time?: number - step?: number - epoch?: number - counter?: number - digits?: number - } - - interface TOTPVerifyOptions extends SharedOptions { - secret: string - token: string - time?: number - step?: number - epoch?: number - counter?: number - digits?: number - window?: number - } - - interface GenerateSecretOptions { - length?: number - symbols?: boolean - otpauth_url?: boolean - name?: string - issuer?: string - } - - interface GeneratedSecret { - ascii: string - hex: string - base32: string - qr_code_ascii: string - qr_code_hex: string - qr_code_base32: string - google_auth_qr: string - otpauth_url: string - } - - interface OTPAuthURLOptions extends SharedOptions { - secret: string - label: string - type?: string - counter?: number - issuer?: string - digits?: number - period?: number - } - - interface Speakeasy { - digest: (options: DigestOptions) => Buffer - hotp: { - (options: HOTPOptions): string, - verifyDelta: (options: HOTPVerifyOptions) => boolean, - verify: (options: HOTPVerifyOptions) => boolean, - } - totp: { - (options: TOTPOptions): string - verifyDelta: (options: TOTPVerifyOptions) => boolean, - verify: (options: TOTPVerifyOptions) => boolean, - } - generateSecret: (options?: GenerateSecretOptions) => GeneratedSecret - generateSecretASCII: (length?: number, symbols?: boolean) => string - otpauthURL: (options: OTPAuthURLOptions) => string - } - - const speakeasy: Speakeasy -} \ No newline at end of file diff --git a/themes/triangles/client/src/.directory b/themes/triangles/client/src/.directory old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/css/.directory b/themes/triangles/client/src/css/.directory old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/css/00-bootstrap.min.css b/themes/triangles/client/src/css/00-bootstrap.min.css old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/css/01-main.css b/themes/triangles/client/src/css/01-main.css old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/css/02-login.css b/themes/triangles/client/src/css/02-login.css old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/css/03-errors.css b/themes/triangles/client/src/css/03-errors.css old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/css/03-password-reset-form.css b/themes/triangles/client/src/css/03-password-reset-form.css old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/css/03-password-reset-request.css b/themes/triangles/client/src/css/03-password-reset-request.css old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/css/03-totp-register.css b/themes/triangles/client/src/css/03-totp-register.css old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/css/03-u2f-register.css b/themes/triangles/client/src/css/03-u2f-register.css old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/img/LargeTriangles.svg b/themes/triangles/client/src/img/LargeTriangles.svg old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/img/background.jpg b/themes/triangles/client/src/img/background.jpg old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/img/icon.png b/themes/triangles/client/src/img/icon.png old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/img/mail.png b/themes/triangles/client/src/img/mail.png old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/img/matrix_circle_128x128.png b/themes/triangles/client/src/img/matrix_circle_128x128.png old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/img/notifications/.directory b/themes/triangles/client/src/img/notifications/.directory old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/img/notifications/error.png b/themes/triangles/client/src/img/notifications/error.png old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/img/notifications/info.png b/themes/triangles/client/src/img/notifications/info.png old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/img/notifications/success.png b/themes/triangles/client/src/img/notifications/success.png old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/img/notifications/warning.png b/themes/triangles/client/src/img/notifications/warning.png old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/img/padlock.png b/themes/triangles/client/src/img/padlock.png old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/img/password_white.png b/themes/triangles/client/src/img/password_white.png old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/img/pendrive.png b/themes/triangles/client/src/img/pendrive.png old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/img/sharingan.png b/themes/triangles/client/src/img/sharingan.png old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/img/stores/.directory b/themes/triangles/client/src/img/stores/.directory old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/img/stores/applestore-badge.svg b/themes/triangles/client/src/img/stores/applestore-badge.svg old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/img/stores/googleplay-badge.svg b/themes/triangles/client/src/img/stores/googleplay-badge.svg old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/img/success.png b/themes/triangles/client/src/img/success.png old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/img/user.png b/themes/triangles/client/src/img/user.png old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/img/warning.png b/themes/triangles/client/src/img/warning.png old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/index.ts b/themes/triangles/client/src/index.ts deleted file mode 100644 index 802004a8f..000000000 --- a/themes/triangles/client/src/index.ts +++ /dev/null @@ -1,34 +0,0 @@ - -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 () { - (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, (global as any).u2f); - 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, (global as any).u2f); - 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); -})(); diff --git a/themes/triangles/client/src/lib/GetPromised.ts b/themes/triangles/client/src/lib/GetPromised.ts deleted file mode 100644 index 779139654..000000000 --- a/themes/triangles/client/src/lib/GetPromised.ts +++ /dev/null @@ -1,14 +0,0 @@ -import BluebirdPromise = require("bluebird"); - -export default function ($: JQueryStatic, url: string, data: Object, fn: any, - dataType: string): BluebirdPromise { - return new BluebirdPromise((resolve, reject) => { - $.get(url, {}, undefined, dataType) - .done((data: any) => { - resolve(data); - }) - .fail((xhr: JQueryXHR, textStatus: string) => { - reject(textStatus); - }); - }); -} \ No newline at end of file diff --git a/themes/triangles/client/src/lib/INotifier.ts b/themes/triangles/client/src/lib/INotifier.ts deleted file mode 100644 index df947538f..000000000 --- a/themes/triangles/client/src/lib/INotifier.ts +++ /dev/null @@ -1,14 +0,0 @@ - -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; -} \ No newline at end of file diff --git a/themes/triangles/client/src/lib/Notifier.ts b/themes/triangles/client/src/lib/Notifier.ts deleted file mode 100644 index c0252b9b9..000000000 --- a/themes/triangles/client/src/lib/Notifier.ts +++ /dev/null @@ -1,83 +0,0 @@ - - -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('status %s\ - %s', 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); - } -} \ No newline at end of file diff --git a/themes/triangles/client/src/lib/QueryParametersRetriever.ts b/themes/triangles/client/src/lib/QueryParametersRetriever.ts deleted file mode 100644 index a529adb6e..000000000 --- a/themes/triangles/client/src/lib/QueryParametersRetriever.ts +++ /dev/null @@ -1,12 +0,0 @@ - -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, " ")); - } -} \ No newline at end of file diff --git a/themes/triangles/client/src/lib/SafeRedirect.ts b/themes/triangles/client/src/lib/SafeRedirect.ts deleted file mode 100644 index 7e7684b8a..000000000 --- a/themes/triangles/client/src/lib/SafeRedirect.ts +++ /dev/null @@ -1,10 +0,0 @@ -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(); -} \ No newline at end of file diff --git a/themes/triangles/client/src/lib/firstfactor/FirstFactorValidator.ts b/themes/triangles/client/src/lib/firstfactor/FirstFactorValidator.ts deleted file mode 100644 index eaa496fdd..000000000 --- a/themes/triangles/client/src/lib/firstfactor/FirstFactorValidator.ts +++ /dev/null @@ -1,46 +0,0 @@ - -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 { - return new BluebirdPromise(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)); - }); - }); -} diff --git a/themes/triangles/client/src/lib/firstfactor/UISelectors.ts b/themes/triangles/client/src/lib/firstfactor/UISelectors.ts deleted file mode 100644 index 0e971b3c3..000000000 --- a/themes/triangles/client/src/lib/firstfactor/UISelectors.ts +++ /dev/null @@ -1,5 +0,0 @@ - -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"; diff --git a/themes/triangles/client/src/lib/firstfactor/index.ts b/themes/triangles/client/src/lib/firstfactor/index.ts deleted file mode 100644 index 24affee2d..000000000 --- a/themes/triangles/client/src/lib/firstfactor/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -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); - }); -} - diff --git a/themes/triangles/client/src/lib/reset-password/constants.ts b/themes/triangles/client/src/lib/reset-password/constants.ts deleted file mode 100644 index d48d4e67d..000000000 --- a/themes/triangles/client/src/lib/reset-password/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export const FORM_SELECTOR = ".form-signin"; \ No newline at end of file diff --git a/themes/triangles/client/src/lib/reset-password/reset-password-form.ts b/themes/triangles/client/src/lib/reset-password/reset-password-form.ts deleted file mode 100644 index b94279cde..000000000 --- a/themes/triangles/client/src/lib/reset-password/reset-password-form.ts +++ /dev/null @@ -1,57 +0,0 @@ -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); - }); -} diff --git a/themes/triangles/client/src/lib/reset-password/reset-password-request.ts b/themes/triangles/client/src/lib/reset-password/reset-password-request.ts deleted file mode 100644 index 846226d75..000000000 --- a/themes/triangles/client/src/lib/reset-password/reset-password-request.ts +++ /dev/null @@ -1,56 +0,0 @@ - -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); - }); -} - diff --git a/themes/triangles/client/src/lib/secondfactor/TOTPValidator.ts b/themes/triangles/client/src/lib/secondfactor/TOTPValidator.ts deleted file mode 100644 index 5394139a6..000000000 --- a/themes/triangles/client/src/lib/secondfactor/TOTPValidator.ts +++ /dev/null @@ -1,28 +0,0 @@ - -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 { - return new BluebirdPromise(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)); - }); - }); -} \ No newline at end of file diff --git a/themes/triangles/client/src/lib/secondfactor/U2FValidator.ts b/themes/triangles/client/src/lib/secondfactor/U2FValidator.ts deleted file mode 100644 index 5812922f7..000000000 --- a/themes/triangles/client/src/lib/secondfactor/U2FValidator.ts +++ /dev/null @@ -1,42 +0,0 @@ -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 { - return new BluebirdPromise(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 { - 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, $); - }); -} diff --git a/themes/triangles/client/src/lib/secondfactor/constants.ts b/themes/triangles/client/src/lib/secondfactor/constants.ts deleted file mode 100644 index 50bba7571..000000000 --- a/themes/triangles/client/src/lib/secondfactor/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ - -export const TOTP_FORM_SELECTOR = ".form-signin.totp"; -export const TOTP_TOKEN_SELECTOR = ".form-signin #token"; diff --git a/themes/triangles/client/src/lib/secondfactor/index.ts b/themes/triangles/client/src/lib/secondfactor/index.ts deleted file mode 100644 index 279723dce..000000000 --- a/themes/triangles/client/src/lib/secondfactor/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -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); - }); -} \ No newline at end of file diff --git a/themes/triangles/client/src/lib/totp-register/totp-register.ts b/themes/triangles/client/src/lib/totp-register/totp-register.ts deleted file mode 100644 index 6a9aa7ee0..000000000 --- a/themes/triangles/client/src/lib/totp-register/totp-register.ts +++ /dev/null @@ -1,11 +0,0 @@ - -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); -} diff --git a/themes/triangles/client/src/lib/totp-register/ui-selector.ts b/themes/triangles/client/src/lib/totp-register/ui-selector.ts deleted file mode 100644 index 9d43fabea..000000000 --- a/themes/triangles/client/src/lib/totp-register/ui-selector.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export const QRCODE_ID_SELECTOR = "#qrcode"; \ No newline at end of file diff --git a/themes/triangles/client/src/lib/u2f-register/u2f-register.ts b/themes/triangles/client/src/lib/u2f-register/u2f-register.ts deleted file mode 100644 index abf40ee05..000000000 --- a/themes/triangles/client/src/lib/u2f-register/u2f-register.ts +++ /dev/null @@ -1,56 +0,0 @@ - -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 { - return new BluebirdPromise(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 { - 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); - }); - }); -} diff --git a/themes/triangles/client/src/thirdparties/qrcode.min.js b/themes/triangles/client/src/thirdparties/qrcode.min.js old mode 100644 new mode 100755 diff --git a/themes/triangles/client/src/thirdparties/u2f-api.js b/themes/triangles/client/src/thirdparties/u2f-api.js old mode 100644 new mode 100755 diff --git a/themes/triangles/client/test/Notifier.test.ts b/themes/triangles/client/test/Notifier.test.ts deleted file mode 100644 index 70bfea146..000000000 --- a/themes/triangles/client/test/Notifier.test.ts +++ /dev/null @@ -1,71 +0,0 @@ - -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"); - }); -}); \ No newline at end of file diff --git a/themes/triangles/client/test/firstfactor/FirstFactorValidator.test.ts b/themes/triangles/client/test/firstfactor/FirstFactorValidator.test.ts deleted file mode 100644 index ac8353278..000000000 --- a/themes/triangles/client/test/firstfactor/FirstFactorValidator.test.ts +++ /dev/null @@ -1,44 +0,0 @@ - -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", "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", "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."); - }); - }); -}); \ No newline at end of file diff --git a/themes/triangles/client/test/mocks/NotifierStub.ts b/themes/triangles/client/test/mocks/NotifierStub.ts deleted file mode 100644 index 9c268d66d..000000000 --- a/themes/triangles/client/test/mocks/NotifierStub.ts +++ /dev/null @@ -1,33 +0,0 @@ - -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(); - } -} \ No newline at end of file diff --git a/themes/triangles/client/test/mocks/jquery.ts b/themes/triangles/client/test/mocks/jquery.ts deleted file mode 100644 index 273f90861..000000000 --- a/themes/triangles/client/test/mocks/jquery.ts +++ /dev/null @@ -1,59 +0,0 @@ - -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() - }; -} diff --git a/themes/triangles/client/test/mocks/u2f-api.ts b/themes/triangles/client/test/mocks/u2f-api.ts deleted file mode 100644 index d123f6a95..000000000 --- a/themes/triangles/client/test/mocks/u2f-api.ts +++ /dev/null @@ -1,14 +0,0 @@ - -import sinon = require("sinon"); - -export interface U2FApiMock { - sign: sinon.SinonStub; - register: sinon.SinonStub; -} - -export function U2FApiMock(): U2FApiMock { - return { - sign: sinon.stub(), - register: sinon.stub() - }; -} \ No newline at end of file diff --git a/themes/triangles/client/test/secondfactor/TOTPValidator.test.ts b/themes/triangles/client/test/secondfactor/TOTPValidator.test.ts deleted file mode 100644 index 5dd6f15c3..000000000 --- a/themes/triangles/client/test/secondfactor/TOTPValidator.test.ts +++ /dev/null @@ -1,37 +0,0 @@ - -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(); - }); - }); -}); \ No newline at end of file diff --git a/themes/triangles/client/test/totp-register/totp-register.test.ts b/themes/triangles/client/test/totp-register/totp-register.test.ts deleted file mode 100644 index 86fc455a1..000000000 --- a/themes/triangles/client/test/totp-register/totp-register.test.ts +++ /dev/null @@ -1,31 +0,0 @@ - -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); - }); -}); \ No newline at end of file diff --git a/themes/triangles/client/tsconfig.json b/themes/triangles/client/tsconfig.json deleted file mode 100644 index 0bb4d62ff..000000000 --- a/themes/triangles/client/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "moduleResolution": "node", - "noImplicitAny": true, - "sourceMap": true, - "removeComments": true, - "outDir": "../dist", - "baseUrl": ".", - "paths": { - "*": [ - "./types/*", - "../shared/types/*" - ] - } - }, - "include": [ - "src/**/*" - ], - "exclude": [ - "test/**/*" - ] -} diff --git a/themes/triangles/client/tslint.json b/themes/triangles/client/tslint.json deleted file mode 100644 index c2c1b7501..000000000 --- a/themes/triangles/client/tslint.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "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 - } -} diff --git a/themes/triangles/server/.directory b/themes/triangles/server/.directory old mode 100644 new mode 100755 diff --git a/themes/triangles/server/src/index.ts b/themes/triangles/server/src/index.ts deleted file mode 100755 index fcbf4d022..000000000 --- a/themes/triangles/server/src/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -#! /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 "); - 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); diff --git a/themes/triangles/server/src/lib/.directory b/themes/triangles/server/src/lib/.directory deleted file mode 100644 index 006b379ad..000000000 --- a/themes/triangles/server/src/lib/.directory +++ /dev/null @@ -1,4 +0,0 @@ -[Dolphin] -Timestamp=2018,12,17,20,59,13 -Version=3 -ViewMode=1 diff --git a/themes/triangles/server/src/lib/AuthenticationSessionHandler.ts b/themes/triangles/server/src/lib/AuthenticationSessionHandler.ts deleted file mode 100644 index 57361bf8a..000000000 --- a/themes/triangles/server/src/lib/AuthenticationSessionHandler.ts +++ /dev/null @@ -1,45 +0,0 @@ - - -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; - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/ErrorReplies.ts b/themes/triangles/server/src/lib/ErrorReplies.ts deleted file mode 100644 index f1c5f4fd1..000000000 --- a/themes/triangles/server/src/lib/ErrorReplies.ts +++ /dev/null @@ -1,49 +0,0 @@ -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 }); -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/Exceptions.ts b/themes/triangles/server/src/lib/Exceptions.ts deleted file mode 100644 index 83fa4eb6b..000000000 --- a/themes/triangles/server/src/lib/Exceptions.ts +++ /dev/null @@ -1,88 +0,0 @@ - -export class LdapSearchError extends Error { - constructor(message?: string) { - super(message); - this.name = "LdapSearchError"; - (Object).setPrototypeOf(this, LdapSearchError.prototype); - } -} - -export class LdapBindError extends Error { - constructor(message?: string) { - super(message); - this.name = "LdapBindError"; - (Object).setPrototypeOf(this, LdapBindError.prototype); - } -} - -export class LdapError extends Error { - constructor(message?: string) { - super(message); - this.name = "LdapError"; - (Object).setPrototypeOf(this, LdapError.prototype); - } -} - -export class IdentityError extends Error { - constructor(message?: string) { - super(message); - this.name = "IdentityError"; - (Object).setPrototypeOf(this, IdentityError.prototype); - } -} - -export class AccessDeniedError extends Error { - constructor(message?: string) { - super(message); - this.name = "AccessDeniedError"; - (Object).setPrototypeOf(this, AccessDeniedError.prototype); - } -} - -export class AuthenticationRegulationError extends Error { - constructor(message?: string) { - super(message); - this.name = "AuthenticationRegulationError"; - (Object).setPrototypeOf(this, AuthenticationRegulationError.prototype); - } -} - -export class InvalidTOTPError extends Error { - constructor(message?: string) { - super(message); - this.name = "InvalidTOTPError"; - (Object).setPrototypeOf(this, InvalidTOTPError.prototype); - } -} - -export class NotAuthenticatedError extends Error { - constructor(message?: string) { - super(message); - this.name = "NotAuthenticatedError"; - (Object).setPrototypeOf(this, NotAuthenticatedError.prototype); - } -} - -export class NotAuthorizedError extends Error { - constructor(message?: string) { - super(message); - this.name = "NotAuthanticatedError"; - (Object).setPrototypeOf(this, NotAuthorizedError.prototype); - } -} - -export class FirstFactorValidationError extends Error { - constructor(message?: string) { - super(message); - this.name = "FirstFactorValidationError"; - (Object).setPrototypeOf(this, FirstFactorValidationError.prototype); - } -} - -export class SecondFactorValidationError extends Error { - constructor(message?: string) { - super(message); - this.name = "SecondFactorValidationError"; - (Object).setPrototypeOf(this, FirstFactorValidationError.prototype); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/FirstFactorValidator.ts b/themes/triangles/server/src/lib/FirstFactorValidator.ts deleted file mode 100644 index 231060002..000000000 --- a/themes/triangles/server/src/lib/FirstFactorValidator.ts +++ /dev/null @@ -1,20 +0,0 @@ - -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 { - 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(); - }); -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/IdentityCheckMiddleware.spec.ts b/themes/triangles/server/src/lib/IdentityCheckMiddleware.spec.ts deleted file mode 100644 index 842ed6bcb..000000000 --- a/themes/triangles/server/src/lib/IdentityCheckMiddleware.spec.ts +++ /dev/null @@ -1,176 +0,0 @@ - -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")); - }); - }); - }); -}); diff --git a/themes/triangles/server/src/lib/IdentityCheckMiddleware.ts b/themes/triangles/server/src/lib/IdentityCheckMiddleware.ts deleted file mode 100644 index e72ea4db1..000000000 --- a/themes/triangles/server/src/lib/IdentityCheckMiddleware.ts +++ /dev/null @@ -1,138 +0,0 @@ -import objectPath = require("object-path"); -import randomstring = require("randomstring"); -import BluebirdPromise = require("bluebird"); -import util = require("util"); -import Exceptions = require("./Exceptions"); -import fs = require("fs"); -import ejs = require("ejs"); -import { 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 { - - 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 { - 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 { - 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 { - - let authSession: AuthenticationSession; - const identityToken = objectPath.get( - 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 { - 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)); - }; -} diff --git a/themes/triangles/server/src/lib/IdentityCheckPreValidationTemplate.ts b/themes/triangles/server/src/lib/IdentityCheckPreValidationTemplate.ts deleted file mode 100644 index 0161ce406..000000000 --- a/themes/triangles/server/src/lib/IdentityCheckPreValidationTemplate.ts +++ /dev/null @@ -1,3 +0,0 @@ - - -export const PRE_VALIDATION_TEMPLATE = "need-identity-validation"; \ No newline at end of file diff --git a/themes/triangles/server/src/lib/IdentityValidable.ts b/themes/triangles/server/src/lib/IdentityValidable.ts deleted file mode 100644 index 075580c9e..000000000 --- a/themes/triangles/server/src/lib/IdentityValidable.ts +++ /dev/null @@ -1,19 +0,0 @@ -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; - postValidationInit(req: Express.Request): Bluebird; - - // 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; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/IdentityValidableStub.spec.ts b/themes/triangles/server/src/lib/IdentityValidableStub.spec.ts deleted file mode 100644 index 20a977140..000000000 --- a/themes/triangles/server/src/lib/IdentityValidableStub.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ - -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 { - return this.preValidationInitStub(req); - } - - postValidationInit(req: Express.Request): Bluebird { - 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(); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/Server.spec.ts b/themes/triangles/server/src/lib/Server.spec.ts deleted file mode 100644 index 365163254..000000000 --- a/themes/triangles/server/src/lib/Server.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ - -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(); - }); - }); -}); diff --git a/themes/triangles/server/src/lib/Server.ts b/themes/triangles/server/src/lib/Server.ts deleted file mode 100644 index 4090f6294..000000000 --- a/themes/triangles/server/src/lib/Server.ts +++ /dev/null @@ -1,93 +0,0 @@ -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 { - 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((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 { - 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(); - } -} - diff --git a/themes/triangles/server/src/lib/ServerVariables.ts b/themes/triangles/server/src/lib/ServerVariables.ts deleted file mode 100644 index cd3dd6dc8..000000000 --- a/themes/triangles/server/src/lib/ServerVariables.ts +++ /dev/null @@ -1,21 +0,0 @@ -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; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/ServerVariablesInitializer.ts b/themes/triangles/server/src/lib/ServerVariablesInitializer.ts deleted file mode 100644 index df79238cc..000000000 --- a/themes/triangles/server/src/lib/ServerVariablesInitializer.ts +++ /dev/null @@ -1,116 +0,0 @@ - -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 { - 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 { - - 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); - }); - } -} diff --git a/themes/triangles/server/src/lib/ServerVariablesMockBuilder.spec.ts b/themes/triangles/server/src/lib/ServerVariablesMockBuilder.spec.ts deleted file mode 100644 index 7874702a0..000000000 --- a/themes/triangles/server/src/lib/ServerVariablesMockBuilder.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -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 - }; - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/Level.ts b/themes/triangles/server/src/lib/authentication/Level.ts deleted file mode 100644 index 57b6a2346..000000000 --- a/themes/triangles/server/src/lib/authentication/Level.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum Level { - NOT_AUTHENTICATED = 0, - ONE_FACTOR = 1, - TWO_FACTOR = 2 -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/GroupsAndEmails.ts b/themes/triangles/server/src/lib/authentication/backends/GroupsAndEmails.ts deleted file mode 100644 index 3434ba66f..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/GroupsAndEmails.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface GroupsAndEmails { - groups: string[]; - emails: string[]; -} diff --git a/themes/triangles/server/src/lib/authentication/backends/IUsersDatabase.ts b/themes/triangles/server/src/lib/authentication/backends/IUsersDatabase.ts deleted file mode 100644 index d7fa13b7a..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/IUsersDatabase.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Bluebird = require("bluebird"); - -import { GroupsAndEmails } from "./GroupsAndEmails"; - -export interface IUsersDatabase { - checkUserPassword(username: string, password: string): Bluebird; - getEmails(username: string): Bluebird; - getGroups(username: string): Bluebird; - updatePassword(username: string, newPassword: string): Bluebird; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts b/themes/triangles/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts deleted file mode 100644 index 19341a5dd..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -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 { - return this.checkUserPasswordStub(username, password); - } - - getEmails(username: string): Bluebird { - return this.getEmailsStub(username); - } - - getGroups(username: string): Bluebird { - return this.getGroupsStub(username); - } - - updatePassword(username: string, newPassword: string): Bluebird { - return this.updatePasswordStub(username, newPassword); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts b/themes/triangles/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts deleted file mode 100644 index a258a78f7..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts +++ /dev/null @@ -1,224 +0,0 @@ -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()); - }); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/file/FileUsersDatabase.ts b/themes/triangles/server/src/lib/authentication/backends/file/FileUsersDatabase.ts deleted file mode 100644 index d34dde21e..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/file/FileUsersDatabase.ts +++ /dev/null @@ -1,182 +0,0 @@ -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 { - return new Bluebird((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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - return this.readDatabase() - .then((database) => { - return this.checkUserExists(database, username) - .then(() => this.retrieveEmails(database, username)); - }); - } - - getGroups(username: string): Bluebird { - return this.readDatabase() - .then((database) => { - return this.checkUserExists(database, username) - .then(() => this.retrieveGroups(database, username)); - }); - } - - updatePassword(username: string, newPassword: string): Bluebird { - return this.readDatabase() - .then((database) => { - return this.checkUserExists(database, username) - .then(() => this.replacePassword(database, username, newPassword)); - }); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/file/ReadWriteQueue.ts b/themes/triangles/server/src/lib/authentication/backends/file/ReadWriteQueue.ts deleted file mode 100644 index 957ddaec5..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/file/ReadWriteQueue.ts +++ /dev/null @@ -1,60 +0,0 @@ -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(); - } - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/ISession.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/ISession.ts deleted file mode 100644 index da2c74433..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/ldap/ISession.ts +++ /dev/null @@ -1,12 +0,0 @@ - -import BluebirdPromise = require("bluebird"); - -export interface ISession { - open(): BluebirdPromise; - close(): BluebirdPromise; - - searchUserDn(username: string): BluebirdPromise; - searchEmails(username: string): BluebirdPromise; - searchGroups(username: string): BluebirdPromise; - modifyPassword(username: string, newPassword: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/ISessionFactory.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/ISessionFactory.ts deleted file mode 100644 index 014d1eea5..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/ldap/ISessionFactory.ts +++ /dev/null @@ -1,6 +0,0 @@ - -import { ISession } from "./ISession"; - -export interface ISessionFactory { - create(userDN: string, password: string): ISession; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts deleted file mode 100644 index f4a6e6306..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts +++ /dev/null @@ -1,386 +0,0 @@ -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); - }) - }); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts deleted file mode 100644 index edda62ec6..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts +++ /dev/null @@ -1,107 +0,0 @@ -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 = (session: ISession) => Bluebird; - -export class LdapUsersDatabase implements IUsersDatabase { - private sessionFactory: ISessionFactory; - private configuration: LdapConfiguration; - - constructor( - sessionFactory: ISessionFactory, - configuration: LdapConfiguration) { - this.sessionFactory = sessionFactory; - this.configuration = configuration; - } - - private withSession( - username: string, - password: string, - cb: SessionCallback): Bluebird { - const session = this.sessionFactory.create(username, password); - return session.open() - .then(() => cb(session)) - .finally(() => session.close()); - } - - checkUserPassword(username: string, password: string): Bluebird { - const that = this; - function verifyUserPassword(userDN: string) { - return that.withSession( - 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 { - 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 { - 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 { - 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)); - }); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts deleted file mode 100644 index 9dedfcb7c..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -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"); - }); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/SafeSession.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/SafeSession.ts deleted file mode 100644 index 572209066..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/ldap/SafeSession.ts +++ /dev/null @@ -1,62 +0,0 @@ -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 { - return this.sesion.open(); - } - - close(): BluebirdPromise { - return this.sesion.close(); - } - - searchGroups(username: string): BluebirdPromise { - 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 { - 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 { - 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 { - try { - const sanitizedUsername = Sanitizer.sanitize(username); - return this.sesion.modifyPassword(sanitizedUsername, newPassword); - } - catch (e) { - return BluebirdPromise.reject(new Error(SPECIAL_CHAR_USED_MESSAGE)); - } - } -} diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts deleted file mode 100644 index 9dd33fed4..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -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 { 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); - }); -}); diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/Sanitizer.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/Sanitizer.ts deleted file mode 100644 index be74132ad..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/ldap/Sanitizer.ts +++ /dev/null @@ -1,25 +0,0 @@ - -// 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; - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/Session.spec.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/Session.spec.ts deleted file mode 100644 index d55f6a805..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/ldap/Session.spec.ts +++ /dev/null @@ -1,127 +0,0 @@ - -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"]); - }); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/Session.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/Session.ts deleted file mode 100644 index e0284b3c4..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/ldap/Session.ts +++ /dev/null @@ -1,156 +0,0 @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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(); - }); - } -} diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/SessionFactory.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/SessionFactory.ts deleted file mode 100644 index 0b6c4bff3..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/ldap/SessionFactory.ts +++ /dev/null @@ -1,37 +0,0 @@ -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 - ) - ); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts deleted file mode 100644 index face3930d..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts deleted file mode 100644 index 5faf2ba18..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -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 { - return this.openStub(); - } - - close(): Bluebird { - return this.closeStub(); - } - - searchUserDn(username: string): Bluebird { - return this.searchUserDnStub(username); - } - - searchEmails(username: string): Bluebird { - return this.searchEmailsStub(username); - } - - searchGroups(username: string): Bluebird { - return this.searchGroupsStub(username); - } - - modifyPassword(username: string, newPassword: string): Bluebird { - return this.modifyPasswordStub(username, newPassword); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/connector/Connector.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/connector/Connector.ts deleted file mode 100644 index 2542ea7f9..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/ldap/connector/Connector.ts +++ /dev/null @@ -1,69 +0,0 @@ -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; - unbindAsync(): Bluebird; - searchAsync(base: string, query: LdapJs.SearchOptions): Bluebird; - modifyAsync(userdn: string, change: LdapJs.Change): Bluebird; -} - -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 { - return this.client.bindAsync(username, password); - } - - unbindAsync(): Bluebird { - return this.client.unbindAsync(); - } - - searchAsync(base: string, query: any): Bluebird { - const that = this; - return this.client.searchAsync(base, query) - .then(function (res: EventEmitter) { - const doc: SearchEntry[] = []; - return new Bluebird((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 { - return this.client.modifyAsync(dn, changeRequest); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts deleted file mode 100644 index 61fef07a4..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts +++ /dev/null @@ -1,18 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts deleted file mode 100644 index d11fa6386..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -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(); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts deleted file mode 100644 index 0b78225bf..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -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 { - return this.bindAsyncStub(username, password); - } - - unbindAsync(): BluebirdPromise { - return this.unbindAsyncStub(); - } - - searchAsync(base: string, query: any): BluebirdPromise { - return this.searchAsyncStub(base, query); - } - - modifyAsync(dn: string, changeRequest: any): BluebirdPromise { - return this.modifyAsyncStub(dn, changeRequest); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/connector/IConnector.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/connector/IConnector.ts deleted file mode 100644 index 1e63ab193..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/ldap/connector/IConnector.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Bluebird = require("bluebird"); -import EventEmitter = require("events"); - -export interface IConnector { - bindAsync(username: string, password: string): Bluebird; - unbindAsync(): Bluebird; - searchAsync(base: string, query: any): Bluebird; - modifyAsync(dn: string, changeRequest: any): Bluebird; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts b/themes/triangles/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts deleted file mode 100644 index f9ed65eff..000000000 --- a/themes/triangles/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { IConnector } from "./IConnector"; - -export interface IConnectorFactory { - create(): IConnector; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/totp/ITotpHandler.ts b/themes/triangles/server/src/lib/authentication/totp/ITotpHandler.ts deleted file mode 100644 index d600d31e4..000000000 --- a/themes/triangles/server/src/lib/authentication/totp/ITotpHandler.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { TOTPSecret } from "../../../../types/TOTPSecret"; - -export interface ITotpHandler { - generate(label: string, issuer: string): TOTPSecret; - validate(token: string, secret: string): boolean; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/totp/TotpHandler.spec.ts b/themes/triangles/server/src/lib/authentication/totp/TotpHandler.spec.ts deleted file mode 100644 index 67cffa638..000000000 --- a/themes/triangles/server/src/lib/authentication/totp/TotpHandler.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ - -import { TotpHandler } from "./TotpHandler"; -import Sinon = require("sinon"); -import Speakeasy = require("speakeasy"); -import Assert = require("assert"); - -describe("authentication/totp/TotpHandler", function() { - let totpValidator: TotpHandler; - let validateStub: Sinon.SinonStub; - - beforeEach(() => { - validateStub = Sinon.stub(Speakeasy.totp, "verify"); - totpValidator = new TotpHandler(Speakeasy); - }); - - afterEach(function() { - validateStub.restore(); - }); - - it("should validate the TOTP token", function() { - const totp_secret = "NBD2ZV64R9UV1O7K"; - const token = "token"; - validateStub.withArgs({ - secret: totp_secret, - token: token, - encoding: "base32", - window: 1 - }).returns(true); - Assert(totpValidator.validate(token, totp_secret)); - }); - - it("should not validate a wrong TOTP token", function() { - const totp_secret = "NBD2ZV64R9UV1O7K"; - const token = "wrong token"; - validateStub.returns(false); - Assert(!totpValidator.validate(token, totp_secret)); - }); -}); - diff --git a/themes/triangles/server/src/lib/authentication/totp/TotpHandler.ts b/themes/triangles/server/src/lib/authentication/totp/TotpHandler.ts deleted file mode 100644 index dfab502a7..000000000 --- a/themes/triangles/server/src/lib/authentication/totp/TotpHandler.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ITotpHandler } from "./ITotpHandler"; -import { TOTPSecret } from "../../../../types/TOTPSecret"; -import Speakeasy = require("speakeasy"); - -const TOTP_ENCODING = "base32"; -const WINDOW: number = 1; - -export class TotpHandler implements ITotpHandler { - private speakeasy: typeof Speakeasy; - - constructor(speakeasy: typeof Speakeasy) { - this.speakeasy = speakeasy; - } - - generate(label: string, issuer: string): TOTPSecret { - const secret = this.speakeasy.generateSecret({ - otpauth_url: false - }) as TOTPSecret; - - secret.otpauth_url = this.speakeasy.otpauthURL({ - secret: secret.ascii, - label: label, - issuer: issuer - }); - return secret; - } - - validate(token: string, secret: string): boolean { - return this.speakeasy.totp.verify({ - secret: secret, - encoding: TOTP_ENCODING, - token: token, - window: WINDOW - }); - } -} diff --git a/themes/triangles/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts b/themes/triangles/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts deleted file mode 100644 index ea93330d9..000000000 --- a/themes/triangles/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import { ITotpHandler } from "./ITotpHandler"; -import { TOTPSecret } from "../../../../types/TOTPSecret"; - -export class TotpHandlerStub implements ITotpHandler { - generateStub: Sinon.SinonStub; - validateStub: Sinon.SinonStub; - - constructor() { - this.generateStub = Sinon.stub(); - this.validateStub = Sinon.stub(); - } - - generate(label: string, issuer: string): TOTPSecret { - return this.generateStub(label, issuer); - } - - validate(token: string, secret: string): boolean { - return this.validateStub(token, secret); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/u2f/IU2fHandler.ts b/themes/triangles/server/src/lib/authentication/u2f/IU2fHandler.ts deleted file mode 100644 index b9b7d6f26..000000000 --- a/themes/triangles/server/src/lib/authentication/u2f/IU2fHandler.ts +++ /dev/null @@ -1,9 +0,0 @@ -import U2f = require("u2f"); - -export interface IU2fHandler { - request(appId: string, keyHandle?: string): U2f.Request; - checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) - : U2f.RegistrationResult | U2f.Error; - checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) - : U2f.SignatureResult | U2f.Error; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authentication/u2f/U2fHandler.ts b/themes/triangles/server/src/lib/authentication/u2f/U2fHandler.ts deleted file mode 100644 index bf3891e5b..000000000 --- a/themes/triangles/server/src/lib/authentication/u2f/U2fHandler.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { IU2fHandler } from "./IU2fHandler"; -import U2f = require("u2f"); - -export class U2fHandler implements IU2fHandler { - private u2f: typeof U2f; - - constructor(u2f: typeof U2f) { - this.u2f = u2f; - } - - request(appId: string, keyHandle?: string): U2f.Request { - return this.u2f.request(appId, keyHandle); - } - - checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) - : U2f.RegistrationResult | U2f.Error { - return this.u2f.checkRegistration(registrationRequest, registrationResponse); - } - - checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) - : U2f.SignatureResult | U2f.Error { - return this.u2f.checkSignature(signatureRequest, signatureResponse, publicKey); - } -} diff --git a/themes/triangles/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts b/themes/triangles/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts deleted file mode 100644 index 135d7eb06..000000000 --- a/themes/triangles/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import U2f = require("u2f"); -import { IU2fHandler } from "./IU2fHandler"; - - -export class U2fHandlerStub implements IU2fHandler { - requestStub: Sinon.SinonStub; - checkRegistrationStub: Sinon.SinonStub; - checkSignatureStub: Sinon.SinonStub; - - constructor() { - this.requestStub = Sinon.stub(); - this.checkRegistrationStub = Sinon.stub(); - this.checkSignatureStub = Sinon.stub(); - } - - request(appId: string, keyHandle?: string): U2f.Request { - return this.requestStub(appId, keyHandle); - } - - checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) - : U2f.RegistrationResult | U2f.Error { - return this.checkRegistrationStub(registrationRequest, registrationResponse); - } - - checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) - : U2f.SignatureResult | U2f.Error { - return this.checkSignatureStub(signatureRequest, signatureResponse, publicKey); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authorization/Authorizer.spec.ts b/themes/triangles/server/src/lib/authorization/Authorizer.spec.ts deleted file mode 100644 index 586814042..000000000 --- a/themes/triangles/server/src/lib/authorization/Authorizer.spec.ts +++ /dev/null @@ -1,372 +0,0 @@ - -import Assert = require("assert"); -import winston = require("winston"); -import { Authorizer } from "./Authorizer"; -import { ACLConfiguration, ACLRule } from "../configuration/schema/AclConfiguration"; -import { Level } from "./Level"; - -describe("authorization/Authorizer", function () { - let authorizer: Authorizer; - let configuration: ACLConfiguration; - - describe("configuration is null", function() { - it("should allow access to anything, anywhere for anybody", function() { - configuration = undefined; - authorizer = new Authorizer(configuration, winston); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1", "group2"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "admin.example.com", resource: "/"}, {user: "user3", groups: ["group3"]}), Level.BYPASS); - }); - }); - - describe("configuration is not null", function () { - beforeEach(function () { - configuration = { - default_policy: "deny", - rules: [] - }; - authorizer = new Authorizer(configuration, winston); - }); - - describe("check access control with default policy to deny", function () { - beforeEach(function () { - configuration.default_policy = "deny"; - }); - - it("should deny access when no rule is provided", function () { - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should control access when multiple domain matcher is provided", function () { - configuration.rules = [{ - domain: "*.mail.example.com", - policy: "two_factor", - subject: "user:user1", - resources: [".*"] - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should allow access to all resources when resources is not provided", function () { - configuration.rules = [{ - domain: "*.mail.example.com", - policy: "two_factor", - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - describe("check user rules", function () { - it("should allow access when user has a matching allowing rule", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: [".*"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should deny to other users", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: [".*"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user2", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); - }); - - it("should allow user access only to specific resources", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: ["/private/.*", "^/begin", "/end$"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/middle/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/begin"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/not/begin"}, {user: "user1", groups: ["group1"]}), Level.DENY); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end/x"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should allow access to multiple domains", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: [".*"], - subject: "user:user1" - }, { - domain: "home1.example.com", - policy: "one_factor", - resources: [".*"], - subject: "user:user1" - }, { - domain: "home2.example.com", - policy: "deny", - resources: [".*"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home1.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); - Assert.equal(authorizer.authorization({domain: "home2.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home3.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); - }); - - it("should apply rules in order", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "one_factor", - resources: ["/my/private/resource"], - subject: "user:user1" - }, { - domain: "home.example.com", - policy: "deny", - resources: ["^/my/private/.*"], - subject: "user:user1" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/my/.*"], - subject: "user:user1" - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/poney"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/duck"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/resource"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); - }); - }); - - describe("check group rules", function () { - it("should allow access when user is in group having a matching allowing rule", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: ["^/$"], - subject: "group:group1" - }, { - domain: "home.example.com", - policy: "one_factor", - resources: ["^/test$"], - subject: "group:group2" - }, { - domain: "home.example.com", - policy: "deny", - resources: ["^/private$"], - subject: "group:group2" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.ONE_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); - }); - }); - }); - - describe("check any rules", function () { - it("should control access when any rules are defined", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "bypass", - resources: ["^/public$"] - }, { - domain: "home.example.com", - policy: "deny", - resources: ["^/private$"] - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, - {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, - {user: "user4", groups: ["group5"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, - {user: "user4", groups: ["group5"]}), Level.DENY); - }); - }); - - describe("check access control with default policy to allow", function () { - beforeEach(function () { - configuration.default_policy = "bypass"; - }); - - it("should allow access to anything when no rule is provided", function () { - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - }); - - it("should deny access to one resource when defined", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "deny", - resources: ["/test"], - subject: "user:user1" - }]; - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); - }); - }); - - describe("check access control with complete use case", function () { - beforeEach(function () { - configuration.default_policy = "deny"; - }); - - it("should control access of multiple user (real use case)", function () { - // Let say we have three users: admin, john, harry. - // admin is in groups ["admins"] - // john is in groups ["dev", "admin-private"] - // harry is in groups ["dev"] - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: ["^/public$", "^/$"] - }, { - domain: "home.example.com", - policy: "two_factor", - resources: [".*"], - subject: "group:admins" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/private/?.*"], - subject: "group:admin-private" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/private/john$"], - subject: "user:john" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/private/harry"], - subject: "user:harry" - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "harry", groups: ["dev"]}), Level.DENY); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); - }); - - it("should allow when allowed at group level and denied at user level", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "deny", - resources: ["^/dev/bob$"], - subject: "user:john" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"], - subject: "group:dev" - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); - }); - - it("should allow access when allowed at 'any' level and denied at user level", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "deny", - resources: ["^/dev/bob$"], - subject: "user:john" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"] - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); - }); - - it("should allow access when allowed at 'any' level and denied at group level", function () { - configuration.rules = [{ - domain: "home.example.com", - policy: "deny", - resources: ["^/dev/bob$"], - subject: "group:dev" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"] - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); - }); - - it("should respect rules precedence", function () { - // the priority from least to most is 'default_policy', 'all', 'group', 'user' - // and the first rules in each category as a lower priority than the latest. - // You can think of it that way: they override themselves inside each category. - configuration.rules = [{ - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"], - subject: "user:john" - }, { - domain: "home.example.com", - policy: "deny", - resources: ["^/dev/bob$"], - subject: "group:dev" - }, { - domain: "home.example.com", - policy: "two_factor", - resources: ["^/dev/?.*$"] - }]; - - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); - }); - }); - }); -}); diff --git a/themes/triangles/server/src/lib/authorization/Authorizer.ts b/themes/triangles/server/src/lib/authorization/Authorizer.ts deleted file mode 100644 index 889b7ec20..000000000 --- a/themes/triangles/server/src/lib/authorization/Authorizer.ts +++ /dev/null @@ -1,85 +0,0 @@ - -import { ACLConfiguration, ACLPolicy, ACLRule } from "../configuration/schema/AclConfiguration"; -import { IAuthorizer } from "./IAuthorizer"; -import { Winston } from "../../../types/Dependencies"; -import { MultipleDomainMatcher } from "./MultipleDomainMatcher"; -import { Level } from "./Level"; -import { Object } from "./Object"; -import { Subject } from "./Subject"; - -function MatchDomain(actualDomain: string) { - return function (rule: ACLRule): boolean { - return MultipleDomainMatcher.match(actualDomain, rule.domain); - }; -} - -function MatchResource(actualResource: string) { - return function (rule: ACLRule): boolean { - // If resources key is not provided, the rule applies to all resources. - if (!rule.resources) return true; - - for (let i = 0; i < rule.resources.length; ++i) { - const regexp = new RegExp(rule.resources[i]); - if (regexp.test(actualResource)) return true; - } - return false; - }; -} - -function MatchSubject(subject: Subject) { - return (rule: ACLRule) => { - // If no subject, matches anybody - if (!rule.subject) return true; - - if (rule.subject.startsWith("user:")) { - const ruleUser = rule.subject.split(":")[1]; - if (subject.user == ruleUser) return true; - } - - if (rule.subject.startsWith("group:")) { - const ruleGroup = rule.subject.split(":")[1]; - if (subject.groups.indexOf(ruleGroup) > -1) return true; - } - return false; - }; -} - -export class Authorizer implements IAuthorizer { - private logger: Winston; - private readonly configuration: ACLConfiguration; - - constructor(configuration: ACLConfiguration, logger_: Winston) { - this.logger = logger_; - this.configuration = configuration; - } - - private getMatchingRules(object: Object, subject: Subject): ACLRule[] { - const rules = this.configuration.rules; - if (!rules) return []; - return rules - .filter(MatchDomain(object.domain)) - .filter(MatchResource(object.resource)) - .filter(MatchSubject(subject)); - } - - private ruleToLevel(policy: string): Level { - if (policy == "bypass") { - return Level.BYPASS; - } else if (policy == "one_factor") { - return Level.ONE_FACTOR; - } else if (policy == "two_factor") { - return Level.TWO_FACTOR; - } - return Level.DENY; - } - - authorization(object: Object, subject: Subject): Level { - if (!this.configuration) return Level.BYPASS; - - const rules = this.getMatchingRules(object, subject); - - return (rules.length > 0) - ? this.ruleToLevel(rules[0].policy) // extract the policy of the first matching rule - : this.ruleToLevel(this.configuration.default_policy); // otherwise use the default policy - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authorization/AuthorizerStub.spec.ts b/themes/triangles/server/src/lib/authorization/AuthorizerStub.spec.ts deleted file mode 100644 index 9bd6f4a85..000000000 --- a/themes/triangles/server/src/lib/authorization/AuthorizerStub.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Sinon = require("sinon"); -import { IAuthorizer } from "./IAuthorizer"; -import { Level } from "./Level"; -import { Object } from "./Object"; -import { Subject } from "./Subject"; - -export class AuthorizerStub implements IAuthorizer { - authorizationMock: Sinon.SinonStub; - - constructor() { - this.authorizationMock = Sinon.stub(); - } - - authorization(object: Object, subject: Subject): Level { - return this.authorizationMock(object, subject); - } -} diff --git a/themes/triangles/server/src/lib/authorization/IAuthorizer.ts b/themes/triangles/server/src/lib/authorization/IAuthorizer.ts deleted file mode 100644 index fe7ba367a..000000000 --- a/themes/triangles/server/src/lib/authorization/IAuthorizer.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Level } from "./Level"; -import { Subject } from "./Subject"; -import { Object } from "./Object"; - -export interface IAuthorizer { - authorization(object: Object, subject: Subject): Level; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authorization/Level.ts b/themes/triangles/server/src/lib/authorization/Level.ts deleted file mode 100644 index d12802610..000000000 --- a/themes/triangles/server/src/lib/authorization/Level.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum Level { - BYPASS = 0, - ONE_FACTOR = 1, - TWO_FACTOR = 2, - DENY = 3 -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authorization/MultipleDomainMatcher.ts b/themes/triangles/server/src/lib/authorization/MultipleDomainMatcher.ts deleted file mode 100644 index 64c647a4a..000000000 --- a/themes/triangles/server/src/lib/authorization/MultipleDomainMatcher.ts +++ /dev/null @@ -1,12 +0,0 @@ - -export class MultipleDomainMatcher { - static match(domain: string, pattern: string): boolean { - if (pattern.startsWith("*") && - domain.endsWith(pattern.substr(1))) { - return true; - } - else if (domain == pattern) { - return true; - } - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authorization/Object.ts b/themes/triangles/server/src/lib/authorization/Object.ts deleted file mode 100644 index 5411b0d24..000000000 --- a/themes/triangles/server/src/lib/authorization/Object.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface Object { - domain: string; - resource: string; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/authorization/Subject.ts b/themes/triangles/server/src/lib/authorization/Subject.ts deleted file mode 100644 index 310d6b4c0..000000000 --- a/themes/triangles/server/src/lib/authorization/Subject.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface Subject { - user: string; - groups: string[]; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/ConfigurationParser.spec.ts b/themes/triangles/server/src/lib/configuration/ConfigurationParser.spec.ts deleted file mode 100644 index 60c0f6182..000000000 --- a/themes/triangles/server/src/lib/configuration/ConfigurationParser.spec.ts +++ /dev/null @@ -1,171 +0,0 @@ -import * as Assert from "assert"; -import { Configuration } from "./schema/Configuration"; -import { ACLConfiguration } from "./schema/AclConfiguration"; -import { ConfigurationParser } from "./ConfigurationParser"; - -describe("configuration/ConfigurationParser", function () { - function buildYamlConfig(): Configuration { - const yaml_config: Configuration = { - port: 8080, - authentication_backend: { - ldap: { - url: "http://ldap", - base_dn: "dc=example,dc=com", - additional_users_dn: "ou=users", - additional_groups_dn: "ou=groups", - user: "user", - password: "pass" - }, - }, - session: { - domain: "example.com", - secret: "secret", - expiration: 40000 - }, - storage: { - local: { - path: "/mydirectory" - } - }, - regulation: { - max_retries: 3, - find_time: 5 * 60, - ban_time: 5 * 60 - }, - logs_level: "debug", - notifier: { - email: { - username: "user", - password: "password", - sender: "admin@example.com", - service: "gmail" - } - } - }; - return yaml_config; - } - - describe("port", function () { - it("should read the port from the yaml file", function () { - const yaml_config = buildYamlConfig(); - yaml_config.port = 7070; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.port, 7070); - }); - - it("should default the port to 8080 if not provided", function () { - const yaml_config = buildYamlConfig(); - delete yaml_config.port; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.port, 8080); - }); - }); - - describe("test session configuration", function() { - it("should get the session attributes", function () { - const yaml_config = buildYamlConfig(); - yaml_config.session = { - domain: "example.com", - secret: "secret", - expiration: 3600, - inactivity: 4000 - }; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.session.domain, "example.com"); - Assert.equal(config.session.secret, "secret"); - Assert.equal(config.session.expiration, 3600); - Assert.equal(config.session.inactivity, 4000); - }); - - it("should be ok not specifying inactivity", function () { - const yaml_config = buildYamlConfig(); - yaml_config.session = { - domain: "example.com", - secret: "secret", - expiration: 3600 - }; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.session.domain, "example.com"); - Assert.equal(config.session.secret, "secret"); - Assert.equal(config.session.expiration, 3600); - Assert.equal(config.session.inactivity, undefined); - }); - }); - - it("should get the log level", function () { - const yaml_config = buildYamlConfig(); - yaml_config.logs_level = "debug"; - const config = ConfigurationParser.parse(yaml_config); - Assert.equal(config.logs_level, "debug"); - }); - - it("should get the notifier config", function () { - const userConfig = buildYamlConfig(); - userConfig.notifier = { - email: { - username: "user", - password: "pass", - sender: "admin@example.com", - service: "gmail" - } - }; - const config = ConfigurationParser.parse(userConfig); - Assert.deepEqual(config.notifier, { - email: { - username: "user", - password: "pass", - sender: "admin@example.com", - service: "gmail" - } - }); - }); - - describe("access_control", function() { - it("should adapt access_control when it is already ok", function () { - const userConfig = buildYamlConfig(); - userConfig.access_control = { - default_policy: "deny", - rules: [{ - domain: "www.example.com", - policy: "two_factor", - subject: "user:user" - }, { - domain: "public.example.com", - policy: "two_factor" - }] - }; - const config = ConfigurationParser.parse(userConfig); - Assert.deepEqual(config.access_control, { - default_policy: "deny", - rules: [{ - domain: "www.example.com", - policy: "two_factor", - subject: "user:user" - }, { - domain: "public.example.com", - policy: "two_factor" - }] - } as ACLConfiguration); - }); - - - it("should adapt access_control when it is empty", function () { - const userConfig = buildYamlConfig(); - userConfig.access_control = {} as any; - const config = ConfigurationParser.parse(userConfig); - Assert.deepEqual(config.access_control, { - default_policy: "bypass", - rules: [] - }); - }); - }); - - describe("default_redirection_url", function() { - it("should parse default_redirection_url", function() { - const userConfig = buildYamlConfig(); - userConfig.default_redirection_url = "dummy_url"; - const config = ConfigurationParser.parse(userConfig); - Assert.deepEqual(config.default_redirection_url, "dummy_url"); - }); - }); -}); diff --git a/themes/triangles/server/src/lib/configuration/ConfigurationParser.ts b/themes/triangles/server/src/lib/configuration/ConfigurationParser.ts deleted file mode 100644 index d92d163c4..000000000 --- a/themes/triangles/server/src/lib/configuration/ConfigurationParser.ts +++ /dev/null @@ -1,39 +0,0 @@ - -import * as ObjectPath from "object-path"; -import { Configuration, complete } from "./schema/Configuration"; -import Ajv = require("ajv"); -import Path = require("path"); -import Util = require("util"); - -export class ConfigurationParser { - private static parseTypes(configuration: Configuration): string[] { - const schema = require(Path.resolve(__dirname, "./Configuration.schema.json")); - const ajv = new Ajv({ - allErrors: true, - missingRefs: "fail" - }); - ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json")); - const valid = ajv.validate(schema, configuration); - if (!valid) - return ajv.errors.map( - (e: Ajv.ErrorObject) => { return ajv.errorsText([e]); }); - return []; - } - - static parse(configuration: Configuration): Configuration { - const validationErrors = this.parseTypes(configuration); - if (validationErrors.length > 0) { - validationErrors.forEach((e: string) => { console.log(e); }); - throw new Error("Malformed configuration (schema). Please double-check your configuration file."); - } - - const [newConfiguration, completionErrors] = complete(configuration); - - if (completionErrors.length > 0) { - completionErrors.forEach((e: string) => { console.log(e); }); - throw new Error("Malformed configuration (validator). Please double-check your configuration file."); - } - return newConfiguration; - } -} - diff --git a/themes/triangles/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts b/themes/triangles/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts deleted file mode 100644 index d4a3093ee..000000000 --- a/themes/triangles/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { SessionConfigurationBuilder } from "./SessionConfigurationBuilder"; -import { Configuration } from "./schema/Configuration"; -import { GlobalDependencies } from "../../../types/Dependencies"; - -import ExpressSession = require("express-session"); -import ConnectRedis = require("connect-redis"); -import Sinon = require("sinon"); -import Assert = require("assert"); - -describe("configuration/SessionConfigurationBuilder", function () { - const configuration: Configuration = { - access_control: { - default_policy: "deny", - rules: [] - }, - totp: { - issuer: "authelia.com" - }, - authentication_backend: { - ldap: { - url: "ldap://ldap", - user: "user", - base_dn: "dc=example,dc=com", - password: "password", - additional_groups_dn: "ou=groups", - additional_users_dn: "ou=users", - group_name_attribute: "", - groups_filter: "", - mail_attribute: "", - users_filter: "" - }, - }, - logs_level: "debug", - notifier: { - filesystem: { - filename: "/test" - } - }, - port: 8080, - session: { - name: "authelia_session", - domain: "example.com", - expiration: 3600, - secret: "secret" - }, - regulation: { - max_retries: 3, - ban_time: 5 * 60, - find_time: 5 * 60 - }, - storage: { - local: { - in_memory: true - } - } - }; - - const deps: GlobalDependencies = { - ConnectRedis: Sinon.spy() as any, - ldapjs: Sinon.spy() as any, - nedb: Sinon.spy() as any, - session: Sinon.spy() as any, - speakeasy: Sinon.spy() as any, - u2f: Sinon.spy() as any, - winston: Sinon.spy() as any, - Redis: Sinon.spy() as any - }; - - it("should return session options without redis options", function () { - const options = SessionConfigurationBuilder.build(configuration, deps); - const expectedOptions = { - name: "authelia_session", - secret: "secret", - resave: false, - saveUninitialized: true, - cookie: { - secure: true, - httpOnly: true, - maxAge: 3600, - domain: "example.com" - } - }; - - Assert.deepEqual(expectedOptions, options); - }); - - it("should return session options with redis options", function () { - configuration.session["redis"] = { - host: "redis.example.com", - port: 6379 - }; - const RedisStoreMock = Sinon.spy(); - const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); - - deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; - deps.Redis = { - createClient: Sinon.mock().returns(redisClient) - } as any; - - const options = SessionConfigurationBuilder.build(configuration, deps); - - const expectedOptions: ExpressSession.SessionOptions = { - secret: "secret", - resave: false, - saveUninitialized: true, - name: "authelia_session", - cookie: { - secure: true, - httpOnly: true, - maxAge: 3600, - domain: "example.com" - }, - store: Sinon.match.object as any - }; - - Assert((deps.ConnectRedis as Sinon.SinonStub).calledWith(deps.session)); - Assert.equal(options.secret, expectedOptions.secret); - Assert.equal(options.resave, expectedOptions.resave); - Assert.equal(options.saveUninitialized, expectedOptions.saveUninitialized); - Assert.deepEqual(options.cookie, expectedOptions.cookie); - Assert(options.store != undefined); - }); - - it("should return session options with redis password", function () { - configuration.session["redis"] = { - host: "redis.example.com", - port: 6379, - password: "authelia_pass" - }; - const RedisStoreMock = Sinon.spy(); - const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); - const createClientStub = Sinon.stub(); - - deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; - deps.Redis = { - createClient: createClientStub - } as any; - - createClientStub.returns(redisClient); - - const options = SessionConfigurationBuilder.build(configuration, deps); - - Assert(createClientStub.calledWith({ - host: "redis.example.com", - port: 6379, - password: "authelia_pass" - })); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/SessionConfigurationBuilder.ts b/themes/triangles/server/src/lib/configuration/SessionConfigurationBuilder.ts deleted file mode 100644 index 6ce643d9d..000000000 --- a/themes/triangles/server/src/lib/configuration/SessionConfigurationBuilder.ts +++ /dev/null @@ -1,52 +0,0 @@ -import ExpressSession = require("express-session"); -import Redis = require("redis"); - -import { Configuration } from "./schema/Configuration"; -import { GlobalDependencies } from "../../../types/Dependencies"; -import { RedisStoreOptions } from "connect-redis"; - -export class SessionConfigurationBuilder { - - static build(configuration: Configuration, deps: GlobalDependencies): ExpressSession.SessionOptions { - const sessionOptions: ExpressSession.SessionOptions = { - name: configuration.session.name, - secret: configuration.session.secret, - resave: false, - saveUninitialized: true, - cookie: { - secure: true, - httpOnly: true, - maxAge: configuration.session.expiration, - domain: configuration.session.domain - }, - }; - - if (configuration.session.redis) { - let redisOptions; - const options: Redis.ClientOpts = { - host: configuration.session.redis.host, - port: configuration.session.redis.port - }; - - if (configuration.session.redis.password) { - options["password"] = configuration.session.redis.password; - } - const client = deps.Redis.createClient(options); - - client.on("error", function (err: Error) { - console.error("Redis error:", err); - }); - - redisOptions = { - client: client, - logErrors: true - }; - - if (redisOptions) { - const RedisStore = deps.ConnectRedis(deps.session); - sessionOptions.store = new RedisStore(redisOptions); - } - } - return sessionOptions; - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/AclConfiguration.spec.ts b/themes/triangles/server/src/lib/configuration/schema/AclConfiguration.spec.ts deleted file mode 100644 index d1e2a03a2..000000000 --- a/themes/triangles/server/src/lib/configuration/schema/AclConfiguration.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ACLConfiguration, complete } from "./AclConfiguration"; -import Assert = require("assert"); - -describe("configuration/schema/AclConfiguration", function() { - it("should complete ACLConfiguration", function() { - const configuration: ACLConfiguration = {}; - const [newConfiguration, errors] = complete(configuration); - - Assert.deepEqual(newConfiguration.default_policy, "bypass"); - Assert.deepEqual(newConfiguration.rules, []); - }); - - it("should return errors when subject is not good", function() { - const configuration: ACLConfiguration = { - default_policy: "deny", - rules: [{ - domain: "dev.example.com", - subject: "user:abc", - policy: "bypass" - }, { - domain: "dev.example.com", - subject: "user:def", - policy: "bypass" - }, { - domain: "dev.example.com", - subject: "badkey:abc", - policy: "bypass" - }] - }; - const [newConfiguration, errors] = complete(configuration); - - Assert.deepEqual(errors, ["Rule 2 has wrong subject. It should be starting with user: or group:."]); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/AclConfiguration.ts b/themes/triangles/server/src/lib/configuration/schema/AclConfiguration.ts deleted file mode 100644 index 40401dd64..000000000 --- a/themes/triangles/server/src/lib/configuration/schema/AclConfiguration.ts +++ /dev/null @@ -1,41 +0,0 @@ - -export type ACLPolicy = "deny" | "bypass" | "one_factor" | "two_factor"; - -export type ACLRule = { - domain: string; - resources?: string[]; - subject?: string; - policy: ACLPolicy; -}; - -export interface ACLConfiguration { - default_policy?: ACLPolicy; - rules?: ACLRule[]; -} - -export function complete(configuration: ACLConfiguration): [ACLConfiguration, string[]] { - const newConfiguration: ACLConfiguration = (configuration) - ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.default_policy) { - newConfiguration.default_policy = "bypass"; - } - - if (!newConfiguration.rules) { - newConfiguration.rules = []; - } - - if (newConfiguration.rules.length > 0) { - const errors: string[] = []; - newConfiguration.rules.forEach((r, idx) => { - if (r.subject && !r.subject.match(/^(user|group):[a-zA-Z0-9]+$/)) { - errors.push(`Rule ${idx} has wrong subject. It should be starting with user: or group:.`); - } - }); - if (errors.length > 0) { - return [newConfiguration, errors]; - } - } - - return [newConfiguration, []]; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts b/themes/triangles/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts deleted file mode 100644 index 3ca86381c..000000000 --- a/themes/triangles/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { AuthenticationBackendConfiguration, complete } from "./AuthenticationBackendConfiguration"; -import Assert = require("assert"); - -describe("configuration/schema/AuthenticationBackendConfiguration", function() { - it("should ensure there is at least one key", function() { - const configuration: AuthenticationBackendConfiguration = {} as any; - const [newConfiguration, error] = complete(configuration); - - Assert.equal(error, "Authentication backend must have one of the following keys:`ldap` or `file`"); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts b/themes/triangles/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts deleted file mode 100644 index 7f77f894f..000000000 --- a/themes/triangles/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { LdapConfiguration } from "./LdapConfiguration"; -import { FileUsersDatabaseConfiguration } from "./FileUsersDatabaseConfiguration"; - -export interface AuthenticationBackendConfiguration { - ldap?: LdapConfiguration; - file?: FileUsersDatabaseConfiguration; -} - -export function complete( - configuration: AuthenticationBackendConfiguration) - : [AuthenticationBackendConfiguration, string] { - - const newConfiguration: AuthenticationBackendConfiguration = (configuration) - ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (Object.keys(newConfiguration).length != 1) { - return [ - newConfiguration, - "Authentication backend must have one of the following keys:" + - "`ldap` or `file`" - ]; - } - - return [newConfiguration, undefined]; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/Configuration.ts b/themes/triangles/server/src/lib/configuration/schema/Configuration.ts deleted file mode 100644 index 8d16a5fb2..000000000 --- a/themes/triangles/server/src/lib/configuration/schema/Configuration.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ACLConfiguration, complete as AclConfigurationComplete } from "./AclConfiguration"; -import { AuthenticationBackendConfiguration, complete as AuthenticationBackendComplete } from "./AuthenticationBackendConfiguration"; -import { NotifierConfiguration, complete as NotifierConfigurationComplete } from "./NotifierConfiguration"; -import { RegulationConfiguration, complete as RegulationConfigurationComplete } from "./RegulationConfiguration"; -import { SessionConfiguration, complete as SessionConfigurationComplete } from "./SessionConfiguration"; -import { StorageConfiguration, complete as StorageConfigurationComplete } from "./StorageConfiguration"; -import { TotpConfiguration, complete as TotpConfigurationComplete } from "./TotpConfiguration"; - -export interface Configuration { - access_control?: ACLConfiguration; - authentication_backend: AuthenticationBackendConfiguration; - default_redirection_url?: string; - logs_level?: string; - notifier?: NotifierConfiguration; - port?: number; - regulation?: RegulationConfiguration; - session?: SessionConfiguration; - storage?: StorageConfiguration; - totp?: TotpConfiguration; -} - -export function complete( - configuration: Configuration): - [Configuration, string[]] { - - const newConfiguration: Configuration = JSON.parse( - JSON.stringify(configuration)); - const errors: string[] = []; - - const [acls, aclsErrors] = AclConfigurationComplete( - newConfiguration.access_control); - - newConfiguration.access_control = acls; - if (aclsErrors.length > 0) { - errors.concat(aclsErrors); - } - - const [backend, error] = - AuthenticationBackendComplete( - newConfiguration.authentication_backend); - - if (error) errors.push(error); - newConfiguration.authentication_backend = backend; - - if (!newConfiguration.logs_level) { - newConfiguration.logs_level = "info"; - } - - const [notifier, notifierError] = NotifierConfigurationComplete( - newConfiguration.notifier); - newConfiguration.notifier = notifier; - if (notifierError) errors.push(notifierError); - - if (!newConfiguration.port) { - newConfiguration.port = 8080; - } - - newConfiguration.regulation = RegulationConfigurationComplete( - newConfiguration.regulation); - newConfiguration.session = SessionConfigurationComplete( - newConfiguration.session); - newConfiguration.storage = StorageConfigurationComplete( - newConfiguration.storage); - newConfiguration.totp = TotpConfigurationComplete( - newConfiguration.totp); - - return [newConfiguration, errors]; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts b/themes/triangles/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts deleted file mode 100644 index d19002ba9..000000000 --- a/themes/triangles/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts +++ /dev/null @@ -1,4 +0,0 @@ - -export interface FileUsersDatabaseConfiguration { - path: string; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/LdapConfiguration.spec.ts b/themes/triangles/server/src/lib/configuration/schema/LdapConfiguration.spec.ts deleted file mode 100644 index cc73d1085..000000000 --- a/themes/triangles/server/src/lib/configuration/schema/LdapConfiguration.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Assert = require("assert"); -import { LdapConfiguration, complete } from "./LdapConfiguration"; - -describe("configuration/schema/AuthenticationMethodsConfiguration", function() { - it("should ensure at least one key is provided", function() { - const configuration: LdapConfiguration = { - url: "ldap.example.com", - base_dn: "dc=example,dc=com", - user: "admin", - password: "password" - }; - const newConfiguration = complete(configuration); - - Assert.deepEqual(newConfiguration, { - url: "ldap.example.com", - base_dn: "dc=example,dc=com", - user: "admin", - password: "password", - users_filter: "cn={0}", - group_name_attribute: "cn", - groups_filter: "member={dn}", - mail_attribute: "mail" - }); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/LdapConfiguration.ts b/themes/triangles/server/src/lib/configuration/schema/LdapConfiguration.ts deleted file mode 100644 index 5dacb9390..000000000 --- a/themes/triangles/server/src/lib/configuration/schema/LdapConfiguration.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Util = require("util"); - -export interface LdapConfiguration { - url: string; - base_dn: string; - - additional_users_dn?: string; - users_filter?: string; - - additional_groups_dn?: string; - groups_filter?: string; - - group_name_attribute?: string; - mail_attribute?: string; - - user: string; // admin username - password: string; // admin password -} - -export function complete(configuration: LdapConfiguration): LdapConfiguration { - const newConfiguration: LdapConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.users_filter) { - newConfiguration.users_filter = "cn={0}"; - } - - if (!newConfiguration.groups_filter) { - newConfiguration.groups_filter = "member={dn}"; - } - - if (!newConfiguration.group_name_attribute) { - newConfiguration.group_name_attribute = "cn"; - } - - if (!newConfiguration.mail_attribute) { - newConfiguration.mail_attribute = "mail"; - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts b/themes/triangles/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts deleted file mode 100644 index 6c576e8e0..000000000 --- a/themes/triangles/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Assert = require("assert"); -import { NotifierConfiguration, complete } from "./NotifierConfiguration"; - -describe("configuration/schema/NotifierConfiguration", function() { - it("should use a default notifier when none is provided", function() { - const configuration: NotifierConfiguration = {}; - const [newConfiguration, error] = complete(configuration); - - Assert.deepEqual(newConfiguration.filesystem, {filename: "/tmp/authelia/notification.txt"}); - }); - - it("should ensure correct key is provided", function() { - const configuration = { - abc: "badvalue" - }; - const [newConfiguration, error] = complete(configuration as any); - - Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"); - }); - - it("should ensure there is no more than one key", function() { - const configuration: NotifierConfiguration = { - smtp: { - host: "smtp.example.com", - port: 25, - secure: false, - sender: "test@example.com" - }, - email: { - username: "test", - password: "test", - sender: "test@example.com", - service: "gmail" - } - }; - const [newConfiguration, error] = complete(configuration); - - Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/NotifierConfiguration.ts b/themes/triangles/server/src/lib/configuration/schema/NotifierConfiguration.ts deleted file mode 100644 index 7bcce15c2..000000000 --- a/themes/triangles/server/src/lib/configuration/schema/NotifierConfiguration.ts +++ /dev/null @@ -1,45 +0,0 @@ - -export interface EmailNotifierConfiguration { - username: string; - password: string; - sender: string; - service: string; -} - -export interface SmtpNotifierConfiguration { - username?: string; - password?: string; - host: string; - port: number; - secure: boolean; - sender: string; -} - -export interface FileSystemNotifierConfiguration { - filename: string; -} - -export interface NotifierConfiguration { - email?: EmailNotifierConfiguration; - smtp?: SmtpNotifierConfiguration; - filesystem?: FileSystemNotifierConfiguration; -} - -export function complete(configuration: NotifierConfiguration): [NotifierConfiguration, string] { - const newConfiguration: NotifierConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (Object.keys(newConfiguration).length == 0) - newConfiguration.filesystem = { filename: "/tmp/authelia/notification.txt" }; - - const ERROR = "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'"; - - if (Object.keys(newConfiguration).length != 1) - return [newConfiguration, ERROR]; - - const key = Object.keys(newConfiguration)[0]; - - if (key != "filesystem" && key != "smtp" && key != "email") - return [newConfiguration, ERROR]; - - return [newConfiguration, undefined]; -} diff --git a/themes/triangles/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts b/themes/triangles/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts deleted file mode 100644 index dce2caf4e..000000000 --- a/themes/triangles/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Assert = require("assert"); -import { RegulationConfiguration, complete } from "./RegulationConfiguration"; - -describe("configuration/schema/RegulationConfiguration", function() { - it("should return default regulation configuration", function() { - const configuration: RegulationConfiguration = {}; - const newConfiguration = complete(configuration); - - Assert.equal(newConfiguration.ban_time, 300); - Assert.equal(newConfiguration.find_time, 120); - Assert.equal(newConfiguration.max_retries, 3); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/RegulationConfiguration.ts b/themes/triangles/server/src/lib/configuration/schema/RegulationConfiguration.ts deleted file mode 100644 index 117463f43..000000000 --- a/themes/triangles/server/src/lib/configuration/schema/RegulationConfiguration.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface RegulationConfiguration { - max_retries?: number; - find_time?: number; - ban_time?: number; -} - -export function complete(configuration: RegulationConfiguration): RegulationConfiguration { - const newConfiguration: RegulationConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.max_retries) { - newConfiguration.max_retries = 3; - } - - if (!newConfiguration.find_time) { - newConfiguration.find_time = 120; // seconds - } - - if (!newConfiguration.ban_time) { - newConfiguration.ban_time = 300; // seconds - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/SessionConfiguration.spec.ts b/themes/triangles/server/src/lib/configuration/schema/SessionConfiguration.spec.ts deleted file mode 100644 index e54010837..000000000 --- a/themes/triangles/server/src/lib/configuration/schema/SessionConfiguration.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Assert = require("assert"); -import { SessionConfiguration, complete } from "./SessionConfiguration"; - -describe("configuration/schema/SessionConfiguration", function() { - it("should return default regulation configuration", function() { - const configuration: SessionConfiguration = { - domain: "example.com", - secret: "unsecure_secret" - }; - const newConfiguration = complete(configuration); - - Assert.equal(newConfiguration.name, 'authelia_session'); - Assert.equal(newConfiguration.expiration, 3600000); - Assert.equal(newConfiguration.inactivity, undefined); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/SessionConfiguration.ts b/themes/triangles/server/src/lib/configuration/schema/SessionConfiguration.ts deleted file mode 100644 index 2c88bb215..000000000 --- a/themes/triangles/server/src/lib/configuration/schema/SessionConfiguration.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface SessionRedisOptions { - host: string; - port: number; - password?: string; -} - -export interface SessionConfiguration { - name?: string; - domain: string; - secret: string; - expiration?: number; - inactivity?: number; - redis?: SessionRedisOptions; -} - -export function complete(configuration: SessionConfiguration): SessionConfiguration { - const newConfiguration: SessionConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.name) { - newConfiguration.name = "authelia_session"; - } - - if (!newConfiguration.expiration) { - newConfiguration.expiration = 3600000; // 1 hour - } - - if (!newConfiguration.inactivity) { - newConfiguration.inactivity = undefined; // disabled - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/StorageConfiguration.spec.ts b/themes/triangles/server/src/lib/configuration/schema/StorageConfiguration.spec.ts deleted file mode 100644 index 9d02a11b6..000000000 --- a/themes/triangles/server/src/lib/configuration/schema/StorageConfiguration.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Assert = require("assert"); -import { StorageConfiguration, complete } from "./StorageConfiguration"; - -describe("configuration/schema/StorageConfiguration", function() { - it("should return default regulation configuration", function() { - const configuration: StorageConfiguration = {}; - const newConfiguration = complete(configuration); - - Assert.deepEqual(newConfiguration, { - local: { - in_memory: true - } - }); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/StorageConfiguration.ts b/themes/triangles/server/src/lib/configuration/schema/StorageConfiguration.ts deleted file mode 100644 index 47e356ef4..000000000 --- a/themes/triangles/server/src/lib/configuration/schema/StorageConfiguration.ts +++ /dev/null @@ -1,30 +0,0 @@ -export interface MongoStorageConfiguration { - url: string; - database: string; - auth?: { - username: string; - password: string; - }; -} - -export interface LocalStorageConfiguration { - path?: string; - in_memory?: boolean; -} - -export interface StorageConfiguration { - local?: LocalStorageConfiguration; - mongo?: MongoStorageConfiguration; -} - -export function complete(configuration: StorageConfiguration): StorageConfiguration { - const newConfiguration: StorageConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.local && !newConfiguration.mongo) { - newConfiguration.local = { - in_memory: true - }; - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/TotpConfiguration.ts b/themes/triangles/server/src/lib/configuration/schema/TotpConfiguration.ts deleted file mode 100644 index 683135639..000000000 --- a/themes/triangles/server/src/lib/configuration/schema/TotpConfiguration.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface TotpConfiguration { - issuer: string; -} - -export function complete(configuration: TotpConfiguration): TotpConfiguration { - const newConfiguration: TotpConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; - - if (!newConfiguration.issuer) { - newConfiguration.issuer = "authelia.com"; - } - - return newConfiguration; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts b/themes/triangles/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts deleted file mode 100644 index 8008b4833..000000000 --- a/themes/triangles/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts +++ /dev/null @@ -1,9 +0,0 @@ - -export interface UserInfo { - username: string; - password_hash: string; - email: string; - groups?: string[]; -} - -export type UserDatabaseConfiguration = UserInfo[]; \ No newline at end of file diff --git a/themes/triangles/server/src/lib/connectors/mongo/IMongoClient.d.ts b/themes/triangles/server/src/lib/connectors/mongo/IMongoClient.d.ts deleted file mode 100644 index 36cb4b8bf..000000000 --- a/themes/triangles/server/src/lib/connectors/mongo/IMongoClient.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import MongoDB = require("mongodb"); -import Bluebird = require("bluebird"); - -export interface IMongoClient { - collection(name: string): Bluebird -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/connectors/mongo/MongoClient.spec.ts b/themes/triangles/server/src/lib/connectors/mongo/MongoClient.spec.ts deleted file mode 100644 index ca0c68593..000000000 --- a/themes/triangles/server/src/lib/connectors/mongo/MongoClient.spec.ts +++ /dev/null @@ -1,119 +0,0 @@ -import Assert = require("assert"); -import Bluebird = require("bluebird"); -import MongoDB = require("mongodb"); -import Sinon = require("sinon"); - -import { MongoClient } from "./MongoClient"; -import { GlobalLoggerStub } from "../../logging/GlobalLoggerStub.spec"; -import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration"; - -describe("connectors/mongo/MongoClient", function () { - let MongoClientStub: any; - let mongoClientStub: any; - let mongoDatabaseStub: any; - let logger: GlobalLoggerStub = new GlobalLoggerStub(); - - const configuration: MongoStorageConfiguration = { - url: "mongo://url", - database: "databasename" - }; - - describe("connection", () => { - before(() => { - mongoClientStub = { - db: Sinon.stub() - }; - mongoDatabaseStub = { - on: Sinon.stub(), - collection: Sinon.stub() - } - MongoClientStub = Sinon.stub( - MongoDB.MongoClient, "connect"); - MongoClientStub.yields( - undefined, mongoClientStub); - mongoClientStub.db.returns( - mongoDatabaseStub); - }); - - after(() => { - MongoClientStub.restore(); - }); - - it("should use credentials from configuration", () => { - configuration.auth = { - username: "authelia", - password: "authelia_pass" - }; - - const client = new MongoClient(configuration, logger); - return client.collection("test") - .then(() => { - Assert(MongoClientStub.calledWith("mongo://url", { - auth: { - user: "authelia", - password: "authelia_pass" - } - })) - }); - }); - }); - - describe("collection", () => { - before(function() { - mongoClientStub = { - db: Sinon.stub() - }; - mongoDatabaseStub = { - on: Sinon.stub(), - collection: Sinon.stub() - } - }); - - describe("Connection to mongo is ok", function() { - before(function () { - MongoClientStub = Sinon.stub( - MongoDB.MongoClient, "connect"); - MongoClientStub.yields( - undefined, mongoClientStub); - mongoClientStub.db.returns( - mongoDatabaseStub); - }); - - after(function () { - MongoClientStub.restore(); - }); - - it("should create a collection", function () { - const COLLECTION_NAME = "mycollection"; - const client = new MongoClient(configuration, logger); - - mongoDatabaseStub.collection.returns("COL"); - return client.collection(COLLECTION_NAME) - .then((collection) => mongoDatabaseStub.collection.calledWith(COLLECTION_NAME)); - }); - }); - - describe("Connection to mongo is broken", function() { - before(function () { - MongoClientStub = Sinon.stub( - MongoDB.MongoClient, "connect"); - MongoClientStub.yields( - new Error("Failed connection"), undefined); - }); - - after(function () { - MongoClientStub.restore(); - }); - - it("should fail creating the collection", function() { - const COLLECTION_NAME = "mycollection"; - const client = new MongoClient(configuration, logger); - - mongoDatabaseStub.collection.returns("COL"); - return client.collection(COLLECTION_NAME) - .then((collection) => Bluebird.reject(new Error("should not be here."))) - .catch((err) => Bluebird.resolve()); - }); - }) - }); -}); diff --git a/themes/triangles/server/src/lib/connectors/mongo/MongoClient.ts b/themes/triangles/server/src/lib/connectors/mongo/MongoClient.ts deleted file mode 100644 index d15731e97..000000000 --- a/themes/triangles/server/src/lib/connectors/mongo/MongoClient.ts +++ /dev/null @@ -1,76 +0,0 @@ - -import MongoDB = require("mongodb"); -import { IMongoClient } from "./IMongoClient"; -import Bluebird = require("bluebird"); -import { AUTHENTICATION_FAILED } from "../../../../../shared/UserMessages"; -import { IGlobalLogger } from "../../logging/IGlobalLogger"; -import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration"; - -export class MongoClient implements IMongoClient { - private configuration: MongoStorageConfiguration; - - private database: MongoDB.Db; - private client: MongoDB.MongoClient; - private logger: IGlobalLogger; - - constructor( - configuration: MongoStorageConfiguration, - logger: IGlobalLogger) { - - this.configuration = configuration; - this.logger = logger; - } - - connect(): Bluebird { - const that = this; - const options: MongoDB.MongoClientOptions = {}; - if (that.configuration.auth) { - options["auth"] = { - user: that.configuration.auth.username, - password: that.configuration.auth.password - }; - } - - return new Bluebird((resolve, reject) => { - MongoDB.MongoClient.connect( - this.configuration.url, - options, - function(err, client) { - if (err) { - reject(err); - return; - } - resolve(client); - }); - }) - .then(function (client: MongoDB.MongoClient) { - that.database = client.db(that.configuration.database); - that.database.on("close", () => { - that.logger.info("[MongoClient] Lost connection."); - }); - that.database.on("reconnect", () => { - that.logger.info("[MongoClient] Reconnected."); - }); - that.client = client; - }); - } - - close(): Bluebird { - if (this.client) { - this.client.close(); - this.database = undefined; - this.client = undefined; - } - return Bluebird.resolve(); - } - - collection(name: string): Bluebird { - if (!this.client) { - const that = this; - return this.connect() - .then(() => Bluebird.resolve(that.database.collection(name))); - } - - return Bluebird.resolve(this.database.collection(name)); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/connectors/mongo/MongoClientStub.spec.ts b/themes/triangles/server/src/lib/connectors/mongo/MongoClientStub.spec.ts deleted file mode 100644 index 1cfd48e32..000000000 --- a/themes/triangles/server/src/lib/connectors/mongo/MongoClientStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Sinon = require("sinon"); -import MongoDB = require("mongodb"); -import Bluebird = require("bluebird"); -import { IMongoClient } from "../../../../src/lib/connectors/mongo/IMongoClient"; - -export class MongoClientStub implements IMongoClient { - public collectionStub: Sinon.SinonStub; - - constructor() { - this.collectionStub = Sinon.stub(); - } - - collection(name: string): Bluebird { - return this.collectionStub(name); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/logging/GlobalLogger.ts b/themes/triangles/server/src/lib/logging/GlobalLogger.ts deleted file mode 100644 index 4da7acf49..000000000 --- a/themes/triangles/server/src/lib/logging/GlobalLogger.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { IGlobalLogger } from "./IGlobalLogger"; -import Util = require("util"); -import Express = require("express"); -import Winston = require("winston"); - -declare module "express" { - interface Request { - id: string; - } -} - -export class GlobalLogger implements IGlobalLogger { - private winston: typeof Winston; - constructor(winston: typeof Winston) { - this.winston = winston; - } - - private buildMessage(message: string, ...args: any[]): string { - return Util.format("date='%s' message='%s'", new Date(), - Util.format(message, ...args)); - } - - info(message: string, ...args: any[]): void { - this.winston.info(this.buildMessage(message, ...args)); - } - - debug(message: string, ...args: any[]): void { - this.winston.debug(this.buildMessage(message, ...args)); - } - - error(message: string, ...args: any[]): void { - this.winston.debug(this.buildMessage(message, ...args)); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/logging/GlobalLoggerStub.spec.ts b/themes/triangles/server/src/lib/logging/GlobalLoggerStub.spec.ts deleted file mode 100644 index d4bb13710..000000000 --- a/themes/triangles/server/src/lib/logging/GlobalLoggerStub.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Sinon = require("sinon"); -import { GlobalLogger } from "./GlobalLogger"; -import Winston = require("winston"); -import Express = require("express"); -import { IGlobalLogger } from "./IGlobalLogger"; - -export class GlobalLoggerStub implements IGlobalLogger { - infoStub: Sinon.SinonStub; - debugStub: Sinon.SinonStub; - errorStub: Sinon.SinonStub; - private globalLogger: IGlobalLogger; - - constructor(enableLogging?: boolean) { - this.infoStub = Sinon.stub(); - this.debugStub = Sinon.stub(); - this.errorStub = Sinon.stub(); - if (enableLogging) - this.globalLogger = new GlobalLogger(Winston); - } - - info(message: string, ...args: any[]): void { - if (this.globalLogger) - this.globalLogger.info(message, ...args); - this.infoStub(message, ...args); - } - - debug(message: string, ...args: any[]): void { - if (this.globalLogger) - this.globalLogger.info(message, ...args); - this.debugStub(message, ...args); - } - - error(message: string, ...args: any[]): void { - if (this.globalLogger) - this.globalLogger.info(message, ...args); - this.errorStub(message, ...args); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/logging/IGlobalLogger.ts b/themes/triangles/server/src/lib/logging/IGlobalLogger.ts deleted file mode 100644 index 548515ec7..000000000 --- a/themes/triangles/server/src/lib/logging/IGlobalLogger.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface IGlobalLogger { - info(message: string, ...args: any[]): void; - debug(message: string, ...args: any[]): void; - error(message: string, ...args: any[]): void; -} diff --git a/themes/triangles/server/src/lib/logging/IRequestLogger.ts b/themes/triangles/server/src/lib/logging/IRequestLogger.ts deleted file mode 100644 index 126a601fe..000000000 --- a/themes/triangles/server/src/lib/logging/IRequestLogger.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Express = require("express"); - -export interface IRequestLogger { - info(req: Express.Request, message: string, ...args: any[]): void; - debug(req: Express.Request, message: string, ...args: any[]): void; - error(req: Express.Request, message: string, ...args: any[]): void; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/logging/RequestLogger.ts b/themes/triangles/server/src/lib/logging/RequestLogger.ts deleted file mode 100644 index c45c66018..000000000 --- a/themes/triangles/server/src/lib/logging/RequestLogger.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { IRequestLogger } from "./IRequestLogger"; -import Util = require("util"); -import Express = require("express"); -import Winston = require("winston"); - -declare module "express" { - interface Request { - id: string; - } -} - -export class RequestLogger implements IRequestLogger { - private winston: typeof Winston; - - constructor(winston: typeof Winston) { - this.winston = winston; - } - - private formatHeader(req: Express.Request) { - const clientIP = req.ip; // The IP of the original client going through the proxy chain. - return Util.format("date='%s' method='%s', path='%s' requestId='%s' sessionId='%s' ip='%s'", - new Date(), req.method, req.path, req.id, req.sessionID, clientIP); - } - - private formatBody(message: string) { - return Util.format("message='%s'", message); - } - - private formatMessage(req: Express.Request, message: string) { - return Util.format("%s %s", this.formatHeader(req), - this.formatBody(message)); - } - - info(req: Express.Request, message: string, ...args: any[]): void { - this.winston.info(this.formatMessage(req, message), ...args); - } - - debug(req: Express.Request, message: string, ...args: any[]): void { - this.winston.debug(this.formatMessage(req, message), ...args); - } - - error(req: Express.Request, message: string, ...args: any[]): void { - this.winston.error(this.formatMessage(req, message), ...args); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/logging/RequestLoggerStub.spec.ts b/themes/triangles/server/src/lib/logging/RequestLoggerStub.spec.ts deleted file mode 100644 index b0e375210..000000000 --- a/themes/triangles/server/src/lib/logging/RequestLoggerStub.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { IRequestLogger } from "./IRequestLogger"; -import Sinon = require("sinon"); -import { RequestLogger } from "./RequestLogger"; -import Winston = require("winston"); -import Express = require("express"); - -export class RequestLoggerStub implements IRequestLogger { - infoStub: Sinon.SinonStub; - debugStub: Sinon.SinonStub; - errorStub: Sinon.SinonStub; - private requestLogger: RequestLogger; - - constructor(enableLogging?: boolean) { - this.infoStub = Sinon.stub(); - this.debugStub = Sinon.stub(); - this.errorStub = Sinon.stub(); - if (enableLogging) - this.requestLogger = new RequestLogger(Winston); - } - - info(req: Express.Request, message: string, ...args: any[]): void { - if (this.requestLogger) - this.requestLogger.info(req, message, ...args); - this.infoStub(req, message, ...args); - } - - debug(req: Express.Request, message: string, ...args: any[]): void { - if (this.requestLogger) - this.requestLogger.info(req, message, ...args); - this.debugStub(req, message, ...args); - } - - error(req: Express.Request, message: string, ...args: any[]): void { - if (this.requestLogger) - this.requestLogger.info(req, message, ...args); - this.errorStub(req, message, ...args); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/AbstractEmailNotifier.ts b/themes/triangles/server/src/lib/notifiers/AbstractEmailNotifier.ts deleted file mode 100644 index 198e4e5db..000000000 --- a/themes/triangles/server/src/lib/notifiers/AbstractEmailNotifier.ts +++ /dev/null @@ -1,23 +0,0 @@ - -import { INotifier } from "../notifiers/INotifier"; -import { Identity } from "../../../types/Identity"; - -import Fs = require("fs"); -import Path = require("path"); -import Ejs = require("ejs"); -import BluebirdPromise = require("bluebird"); - -const email_template = Fs.readFileSync(Path.join(__dirname, "../../resources/email-template.ejs"), "UTF-8"); - -export abstract class AbstractEmailNotifier implements INotifier { - notify(to: string, subject: string, link: string): BluebirdPromise { - const d = { - url: link, - button_title: "Continue", - title: subject - }; - return this.sendEmail(to, subject, Ejs.render(email_template, d)); - } - - abstract sendEmail(to: string, subject: string, content: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/EmailNotifier.spec.ts b/themes/triangles/server/src/lib/notifiers/EmailNotifier.spec.ts deleted file mode 100644 index 8211bbc02..000000000 --- a/themes/triangles/server/src/lib/notifiers/EmailNotifier.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as sinon from "sinon"; -import * as Assert from "assert"; -import BluebirdPromise = require("bluebird"); - -import { MailSenderStub } from "./MailSenderStub.spec"; -import EmailNotifier = require("./EmailNotifier"); - - -describe("notifiers/EmailNotifier", function () { - it("should send an email to given user", function () { - const mailSender = new MailSenderStub(); - const options = { - username: "user_gmail", - password: "pass_gmail", - sender: "admin@example.com", - service: "gmail" - }; - - mailSender.sendStub.returns(BluebirdPromise.resolve()); - const sender = new EmailNotifier.EmailNotifier(options, mailSender); - const subject = "subject"; - const url = "http://test.com"; - - return sender.notify("user@example.com", subject, url) - .then(function () { - Assert.equal(mailSender.sendStub.getCall(0).args[0].to, "user@example.com"); - Assert.equal(mailSender.sendStub.getCall(0).args[0].subject, "subject"); - return BluebirdPromise.resolve(); - }); - }); - - it("should fail while sending an email", function () { - const mailSender = new MailSenderStub(); - const options = { - username: "user_gmail", - password: "pass_gmail", - sender: "admin@example.com", - service: "gmail" - }; - - mailSender.sendStub.returns(BluebirdPromise.reject(new Error("Failed to send mail"))); - const sender = new EmailNotifier.EmailNotifier(options, mailSender); - const subject = "subject"; - const url = "http://test.com"; - - return sender.notify("user@example.com", subject, url) - .then(function () { - return BluebirdPromise.reject(new Error()); - }, function() { - Assert.equal(mailSender.sendStub.getCall(0).args[0].from, "admin@example.com"); - return BluebirdPromise.resolve(); - }); - }); -}); diff --git a/themes/triangles/server/src/lib/notifiers/EmailNotifier.ts b/themes/triangles/server/src/lib/notifiers/EmailNotifier.ts deleted file mode 100644 index 4df7c861e..000000000 --- a/themes/triangles/server/src/lib/notifiers/EmailNotifier.ts +++ /dev/null @@ -1,27 +0,0 @@ - -import * as BluebirdPromise from "bluebird"; - -import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; -import { EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; -import { IMailSender } from "./IMailSender"; - -export class EmailNotifier extends AbstractEmailNotifier { - private mailSender: IMailSender; - private sender: string; - - constructor(options: EmailNotifierConfiguration, mailSender: IMailSender) { - super(); - this.mailSender = mailSender; - this.sender = options.sender; - } - - sendEmail(to: string, subject: string, content: string) { - const mailOptions = { - from: this.sender, - to: to, - subject: subject, - html: content - }; - return this.mailSender.send(mailOptions); - } -} diff --git a/themes/triangles/server/src/lib/notifiers/FileSystemNotifier.ts b/themes/triangles/server/src/lib/notifiers/FileSystemNotifier.ts deleted file mode 100644 index 23f6242c4..000000000 --- a/themes/triangles/server/src/lib/notifiers/FileSystemNotifier.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as BluebirdPromise from "bluebird"; -import * as util from "util"; -import * as Fs from "fs"; -import { INotifier } from "./INotifier"; -import { Identity } from "../../../types/Identity"; - -import { FileSystemNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; - -export class FileSystemNotifier implements INotifier { - private filename: string; - - constructor(options: FileSystemNotifierConfiguration) { - this.filename = options.filename; - } - - notify(to: string, subject: string, link: string): BluebirdPromise { - const content = util.format("Date: %s\nEmail: %s\nSubject: %s\nLink: %s", - new Date().toString(), to, subject, link); - const writeFilePromised: any = BluebirdPromise.promisify(Fs.writeFile); - return writeFilePromised(this.filename, content); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/IMailSender.ts b/themes/triangles/server/src/lib/notifiers/IMailSender.ts deleted file mode 100644 index 34ac464a8..000000000 --- a/themes/triangles/server/src/lib/notifiers/IMailSender.ts +++ /dev/null @@ -1,6 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Nodemailer = require("nodemailer"); - -export interface IMailSender { - send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/IMailSenderBuilder.ts b/themes/triangles/server/src/lib/notifiers/IMailSenderBuilder.ts deleted file mode 100644 index 36d4dcdf7..000000000 --- a/themes/triangles/server/src/lib/notifiers/IMailSenderBuilder.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IMailSender } from "./IMailSender"; -import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; - -export interface IMailSenderBuilder { - buildEmail(options: EmailNotifierConfiguration): IMailSender; - buildSmtp(options: SmtpNotifierConfiguration): IMailSender; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/INotifier.ts b/themes/triangles/server/src/lib/notifiers/INotifier.ts deleted file mode 100644 index b9a6b138c..000000000 --- a/themes/triangles/server/src/lib/notifiers/INotifier.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as BluebirdPromise from "bluebird"; - -export interface INotifier { - notify(to: string, subject: string, link: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/MailSender.ts b/themes/triangles/server/src/lib/notifiers/MailSender.ts deleted file mode 100644 index 536a88e63..000000000 --- a/themes/triangles/server/src/lib/notifiers/MailSender.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { IMailSender } from "./IMailSender"; -import Nodemailer = require("nodemailer"); -import NodemailerDirectTransport = require("nodemailer-direct-transport"); -import NodemailerSmtpTransport = require("nodemailer-smtp-transport"); -import BluebirdPromise = require("bluebird"); - -export class MailSender implements IMailSender { - private transporter: Nodemailer.Transporter; - - constructor(options: NodemailerDirectTransport.DirectOptions | - NodemailerSmtpTransport.SmtpOptions, nodemailer: typeof Nodemailer) { - this.transporter = nodemailer.createTransport(options); - } - - verify(): BluebirdPromise { - const that = this; - return new BluebirdPromise(function (resolve, reject) { - that.transporter.verify(function (error: Error, success: any) { - if (error) { - reject(new Error("Unable to connect to SMTP server. \ - Please check the service is running and your credentials are correct.")); - return; - } - resolve(); - }); - }); - } - - send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise { - const that = this; - return new BluebirdPromise(function (resolve, reject) { - that.transporter.sendMail(mailOptions, (error: Error, - data: Nodemailer.SentMessageInfo) => { - if (error) { - reject(new Error("Error while sending email: " + error.message)); - return; - } - resolve(); - }); - }); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/MailSenderBuilder.spec.ts b/themes/triangles/server/src/lib/notifiers/MailSenderBuilder.spec.ts deleted file mode 100644 index 41e0db426..000000000 --- a/themes/triangles/server/src/lib/notifiers/MailSenderBuilder.spec.ts +++ /dev/null @@ -1,67 +0,0 @@ - -import { MailSenderBuilder } from ".//MailSenderBuilder"; -import Nodemailer = require("nodemailer"); -import Sinon = require("sinon"); -import Assert = require("assert"); - -describe("notifiers/MailSenderBuilder", function() { - let createTransportStub: Sinon.SinonStub; - beforeEach(function() { - createTransportStub = Sinon.stub(Nodemailer, "createTransport"); - }); - - afterEach(function() { - createTransportStub.restore(); - }); - - it("should create a email mail sender", function() { - const mailSenderBuilder = new MailSenderBuilder(Nodemailer); - mailSenderBuilder.buildEmail({ - username: "user_gmail", - password: "pass_gmail", - sender: "admin@example.com", - service: "gmail" - }); - Assert.equal(createTransportStub.getCall(0).args[0].auth.user, "user_gmail"); - Assert.equal(createTransportStub.getCall(0).args[0].auth.pass, "pass_gmail"); - Assert.equal(createTransportStub.getCall(0).args[0].service, "gmail"); - }); - - describe("build smtp mail sender", function() { - it("should create a smtp mail sender with authenticated user", function() { - const mailSenderBuilder = new MailSenderBuilder(Nodemailer); - mailSenderBuilder.buildSmtp({ - host: "mail.example.com", - password: "password", - port: 25, - secure: true, - username: "user", - sender: "admin@example.com" - }); - Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { - host: "mail.example.com", - auth: { - pass: "password", - user: "user" - }, - port: 25, - secure: true, - }); - }); - - it("should create a smtp mail sender with anonymous user", function() { - const mailSenderBuilder = new MailSenderBuilder(Nodemailer); - mailSenderBuilder.buildSmtp({ - host: "mail.example.com", - port: 25, - secure: true, - sender: "admin@example.com" - }); - Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { - host: "mail.example.com", - port: 25, - secure: true, - }); - }); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/MailSenderBuilder.ts b/themes/triangles/server/src/lib/notifiers/MailSenderBuilder.ts deleted file mode 100644 index 1d06be52f..000000000 --- a/themes/triangles/server/src/lib/notifiers/MailSenderBuilder.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { IMailSender } from "./IMailSender"; -import { IMailSenderBuilder } from "./IMailSenderBuilder"; -import { MailSender } from "./MailSender"; -import Nodemailer = require("nodemailer"); -import NodemailerSmtpTransport = require("nodemailer-smtp-transport"); -import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; - -export class MailSenderBuilder implements IMailSenderBuilder { - private nodemailer: typeof Nodemailer; - - constructor(nodemailer: typeof Nodemailer) { - this.nodemailer = nodemailer; - } - - buildEmail(options: EmailNotifierConfiguration): IMailSender { - const emailOptions = { - service: options.service, - auth: { - user: options.username, - pass: options.password - } - }; - return new MailSender(emailOptions, this.nodemailer); - } - - buildSmtp(options: SmtpNotifierConfiguration): IMailSender { - const smtpOptions: NodemailerSmtpTransport.SmtpOptions = { - host: options.host, - port: options.port, - secure: options.secure, // upgrade later with STARTTLS - }; - - if (options.username && options.password) { - smtpOptions.auth = { - user: options.username, - pass: options.password - }; - } - - return new MailSender(smtpOptions, this.nodemailer); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts b/themes/triangles/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts deleted file mode 100644 index 5b76f6e56..000000000 --- a/themes/triangles/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { IMailSenderBuilder } from "../../../src/lib/notifiers/IMailSenderBuilder"; -import BluebirdPromise = require("bluebird"); -import Nodemailer = require("nodemailer"); -import Sinon = require("sinon"); -import { IMailSender } from "../../../src/lib/notifiers/IMailSender"; -import { SmtpNotifierConfiguration, EmailNotifierConfiguration } from "../../../src/lib/configuration/schema/NotifierConfiguration"; - -export class MailSenderBuilderStub implements IMailSenderBuilder { - buildEmailStub: Sinon.SinonStub; - buildSmtpStub: Sinon.SinonStub; - - constructor() { - this.buildEmailStub = Sinon.stub(); - this.buildSmtpStub = Sinon.stub(); - } - - buildEmail(options: EmailNotifierConfiguration): IMailSender { - return this.buildEmailStub(options); - } - - buildSmtp(options: SmtpNotifierConfiguration): IMailSender { - return this.buildSmtpStub(options); - } - -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/MailSenderStub.spec.ts b/themes/triangles/server/src/lib/notifiers/MailSenderStub.spec.ts deleted file mode 100644 index d57c458fc..000000000 --- a/themes/triangles/server/src/lib/notifiers/MailSenderStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { IMailSender } from "../../../src/lib/notifiers/IMailSender"; -import BluebirdPromise = require("bluebird"); -import Nodemailer = require("nodemailer"); -import Sinon = require("sinon"); - -export class MailSenderStub implements IMailSender { - sendStub: Sinon.SinonStub; - - constructor() { - this.sendStub = Sinon.stub(); - } - - send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise { - return this.sendStub(mailOptions); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/NotifierFactory.spec.ts b/themes/triangles/server/src/lib/notifiers/NotifierFactory.spec.ts deleted file mode 100644 index f15e7667f..000000000 --- a/themes/triangles/server/src/lib/notifiers/NotifierFactory.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ - -import * as sinon from "sinon"; -import * as BluebirdPromise from "bluebird"; -import * as assert from "assert"; - -import { NotifierFactory } from "./NotifierFactory"; -import { EmailNotifier } from "./EmailNotifier"; -import { SmtpNotifier } from "./SmtpNotifier"; -import { MailSenderBuilderStub } from "./MailSenderBuilderStub.spec"; - - -describe("notifiers/NotifierFactory", function () { - let mailSenderBuilderStub: MailSenderBuilderStub; - it("should build a Email Notifier", function () { - const options = { - email: { - username: "abc", - password: "password", - sender: "admin@example.com", - service: "gmail" - } - }; - mailSenderBuilderStub = new MailSenderBuilderStub(); - assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof EmailNotifier); - }); - - it("should build a SMTP Notifier", function () { - const options = { - smtp: { - username: "user", - password: "pass", - secure: true, - host: "localhost", - port: 25, - sender: "admin@example.com" - } - }; - - mailSenderBuilderStub = new MailSenderBuilderStub(); - assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof SmtpNotifier); - }); -}); diff --git a/themes/triangles/server/src/lib/notifiers/NotifierFactory.ts b/themes/triangles/server/src/lib/notifiers/NotifierFactory.ts deleted file mode 100644 index a89155feb..000000000 --- a/themes/triangles/server/src/lib/notifiers/NotifierFactory.ts +++ /dev/null @@ -1,33 +0,0 @@ - -import { NotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; -import Nodemailer = require("nodemailer"); -import { INotifier } from "./INotifier"; - -import { FileSystemNotifier } from "./FileSystemNotifier"; -import { EmailNotifier } from "./EmailNotifier"; -import { SmtpNotifier } from "./SmtpNotifier"; -import { IMailSender } from "./IMailSender"; -import { IMailSenderBuilder } from "./IMailSenderBuilder"; - -export class NotifierFactory { - static build(options: NotifierConfiguration, mailSenderBuilder: IMailSenderBuilder): INotifier { - if ("email" in options) { - const mailSender = mailSenderBuilder.buildEmail(options.email); - return new EmailNotifier(options.email, mailSender); - } - else if ("smtp" in options) { - const mailSender = mailSenderBuilder.buildSmtp(options.smtp); - return new SmtpNotifier(options.smtp, mailSender); - } - else if ("filesystem" in options) { - return new FileSystemNotifier(options.filesystem); - } - else { - throw new Error("No available notifier option detected."); - } - } -} - - - - diff --git a/themes/triangles/server/src/lib/notifiers/NotifierStub.spec.ts b/themes/triangles/server/src/lib/notifiers/NotifierStub.spec.ts deleted file mode 100644 index f99231b50..000000000 --- a/themes/triangles/server/src/lib/notifiers/NotifierStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); - -import { INotifier } from "./INotifier"; - -export class NotifierStub implements INotifier { - notifyStub: Sinon.SinonStub; - - constructor() { - this.notifyStub = Sinon.stub(); - } - - notify(to: string, subject: string, link: string): BluebirdPromise { - return this.notifyStub(to, subject, link); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/notifiers/SmtpNotifier.ts b/themes/triangles/server/src/lib/notifiers/SmtpNotifier.ts deleted file mode 100644 index f93a6d4a0..000000000 --- a/themes/triangles/server/src/lib/notifiers/SmtpNotifier.ts +++ /dev/null @@ -1,30 +0,0 @@ - - -import * as BluebirdPromise from "bluebird"; - -import { IMailSender } from "./IMailSender"; -import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; -import { SmtpNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; - -export class SmtpNotifier extends AbstractEmailNotifier { - private mailSender: IMailSender; - private sender: string; - - constructor(options: SmtpNotifierConfiguration, - mailSender: IMailSender) { - super(); - this.mailSender = mailSender; - this.sender = options.sender; - } - - sendEmail(to: string, subject: string, content: string) { - const mailOptions = { - from: this.sender, - to: to, - subject: subject, - html: content - }; - const that = this; - return this.mailSender.send(mailOptions); - } -} diff --git a/themes/triangles/server/src/lib/regulation/IRegulator.ts b/themes/triangles/server/src/lib/regulation/IRegulator.ts deleted file mode 100644 index c49425b24..000000000 --- a/themes/triangles/server/src/lib/regulation/IRegulator.ts +++ /dev/null @@ -1,6 +0,0 @@ -import BluebirdPromise = require("bluebird"); - -export interface IRegulator { - mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; - regulate(userId: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/regulation/Regulator.spec.ts b/themes/triangles/server/src/lib/regulation/Regulator.spec.ts deleted file mode 100644 index f9c6e6086..000000000 --- a/themes/triangles/server/src/lib/regulation/Regulator.spec.ts +++ /dev/null @@ -1,186 +0,0 @@ - -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import Assert = require("assert"); - -import { Regulator } from "./Regulator"; -import MockDate = require("mockdate"); -import exceptions = require("../Exceptions"); -import { UserDataStoreStub } from "../storage/UserDataStoreStub.spec"; - -describe("regulation/Regulator", function () { - const USER1 = "USER1"; - const USER2 = "USER2"; - let userDataStoreStub: UserDataStoreStub; - - beforeEach(function () { - userDataStoreStub = new UserDataStoreStub(); - const dataStore: { [userId: string]: { userId: string, date: Date, isAuthenticationSuccessful: boolean }[] } = { - [USER1]: [], - [USER2]: [] - }; - - userDataStoreStub.saveAuthenticationTraceStub.callsFake(function (userId, isAuthenticationSuccessful) { - dataStore[userId].unshift({ - userId: userId, - date: new Date(), - isAuthenticationSuccessful: isAuthenticationSuccessful, - }); - return BluebirdPromise.resolve(); - }); - - userDataStoreStub.retrieveLatestAuthenticationTracesStub.callsFake(function (userId, count) { - const ret = (dataStore[userId].length <= count) ? dataStore[userId] : dataStore[userId].slice(0, 3); - return BluebirdPromise.resolve(ret); - }); - }); - - afterEach(function () { - MockDate.reset(); - }); - - function markAuthenticationAt(regulator: Regulator, user: string, time: string, success: boolean) { - MockDate.set(time); - return regulator.mark(user, success); - } - - it("should mark 2 authentication and regulate (accept)", function () { - const regulator = new Regulator(userDataStoreStub, 3, 10, 10); - - return regulator.mark(USER1, false) - .then(function () { - return regulator.mark(USER1, true); - }) - .then(function () { - return regulator.regulate(USER1); - }); - }); - - it("should mark 3 authentications and regulate (reject)", function () { - const regulator = new Regulator(userDataStoreStub, 3, 10, 10); - - return regulator.mark(USER1, false) - .then(function () { - return regulator.mark(USER1, false); - }) - .then(function () { - return regulator.mark(USER1, false); - }) - .then(function () { - return regulator.regulate(USER1); - }) - .then(function () { return BluebirdPromise.reject(new Error("should not be here!")); }) - .catch(exceptions.AuthenticationRegulationError, function () { - return BluebirdPromise.resolve(); - }); - }); - - it("should mark 1 failed, 1 successful and 1 failed authentications within minimum time and regulate (accept)", function () { - const regulator = new Regulator(userDataStoreStub, 3, 60, 30); - - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", true); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:20", false); - }) - .then(function () { - return regulator.regulate(USER1); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:30", false); - }) - .then(function () { - return regulator.regulate(USER1); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:39", false); - }) - .then(function () { - return regulator.regulate(USER1); - }) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here!")); - }, - function () { - return BluebirdPromise.resolve(); - }); - }); - - it("should regulate user if number of failures is greater than 3 in allowed time lapse", function () { - function markAuthentications(regulator: Regulator, user: string) { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:45", false); - }) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:01:05", false); - }) - .then(function () { - return regulator.regulate(user); - }); - } - - const regulator1 = new Regulator(userDataStoreStub, 3, 60, 60); - const regulator2 = new Regulator(userDataStoreStub, 3, 2 * 60, 60); - - const p1 = markAuthentications(regulator1, USER1); - const p2 = markAuthentications(regulator2, USER2); - - return BluebirdPromise.join(p1, p2) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here...")); - }, function () { - Assert(p1.isFulfilled()); - Assert(p2.isRejected()); - }); - }); - - it("should user wait after regulation to authenticate again", function () { - function markAuthentications(regulator: Regulator, user: string) { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:10", false); - }) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:15", false); - }) - .then(function () { - return markAuthenticationAt(regulator, user, "1/2/2000 00:00:25", false); - }) - .then(function () { - MockDate.set("1/2/2000 00:00:54"); - return regulator.regulate(user); - }) - .then(function () { - return BluebirdPromise.reject(new Error("should fail at this time")); - }, function () { - MockDate.set("1/2/2000 00:00:56"); - return regulator.regulate(user); - }); - } - - const regulator = new Regulator(userDataStoreStub, 4, 30, 30); - return markAuthentications(regulator, USER1); - }); - - it("should disable regulation when max_retries is set to 0", function () { - const maxRetries = 0; - const regulator = new Regulator(userDataStoreStub, maxRetries, 60, 30); - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", false); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:15", false); - }) - .then(function () { - return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:25", false); - }) - .then(function () { - MockDate.set("1/2/2000 00:00:26"); - return regulator.regulate(USER1); - }); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/regulation/Regulator.ts b/themes/triangles/server/src/lib/regulation/Regulator.ts deleted file mode 100644 index 1037a6a17..000000000 --- a/themes/triangles/server/src/lib/regulation/Regulator.ts +++ /dev/null @@ -1,55 +0,0 @@ - -import * as BluebirdPromise from "bluebird"; -import exceptions = require("../Exceptions"); -import { IUserDataStore } from "../storage/IUserDataStore"; -import { AuthenticationTraceDocument } from "../storage/AuthenticationTraceDocument"; -import { IRegulator } from "./IRegulator"; - -export class Regulator implements IRegulator { - private userDataStore: IUserDataStore; - private banTime: number; - private findTime: number; - private maxRetries: number; - - constructor(userDataStore: any, maxRetries: number, findTime: number, banTime: number) { - this.userDataStore = userDataStore; - this.banTime = banTime; - this.findTime = findTime; - this.maxRetries = maxRetries; - } - - // Mark authentication - mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { - return this.userDataStore.saveAuthenticationTrace(userId, isAuthenticationSuccessful); - } - - regulate(userId: string): BluebirdPromise { - const that = this; - - if (that.maxRetries <= 0) return BluebirdPromise.resolve(); - - return this.userDataStore.retrieveLatestAuthenticationTraces(userId, that.maxRetries) - .then((docs: AuthenticationTraceDocument[]) => { - // less than the max authorized number of authentication in time range, thus authorizing access - if (docs.length < that.maxRetries) return BluebirdPromise.resolve(); - - const numberOfFailedAuth = docs - .map(function (d: AuthenticationTraceDocument) { return d.isAuthenticationSuccessful == false ? 1 : 0; }) - .reduce(function (acc, v) { return acc + v; }, 0); - - if (numberOfFailedAuth < this.maxRetries) return BluebirdPromise.resolve(); - - const newestDocument = docs[0]; - const oldestDocument = docs[that.maxRetries - 1]; - - const authenticationsTimeRangeInSeconds = (newestDocument.date.getTime() - oldestDocument.date.getTime()) / 1000; - const tooManyAuthInTimelapse = (authenticationsTimeRangeInSeconds < this.findTime); - const stillInBannedTimeRange = (new Date(new Date().getTime() - this.banTime * 1000) < newestDocument.date); - - if (tooManyAuthInTimelapse && stillInBannedTimeRange) - throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes."); - - return BluebirdPromise.resolve(); - }); - } -} diff --git a/themes/triangles/server/src/lib/regulation/RegulatorStub.spec.ts b/themes/triangles/server/src/lib/regulation/RegulatorStub.spec.ts deleted file mode 100644 index ca8a00fb1..000000000 --- a/themes/triangles/server/src/lib/regulation/RegulatorStub.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Bluebird = require("bluebird"); -import Sinon = require("sinon"); -import { IRegulator } from "./IRegulator"; - - -export class RegulatorStub implements IRegulator { - markStub: Sinon.SinonStub; - regulateStub: Sinon.SinonStub; - - constructor() { - this.markStub = Sinon.stub(); - this.regulateStub = Sinon.stub(); - } - - mark(userId: string, isAuthenticationSuccessful: boolean): Bluebird { - return this.markStub(userId, isAuthenticationSuccessful); - } - - regulate(userId: string): Bluebird { - return this.regulateStub(userId); - } -} diff --git a/themes/triangles/server/src/lib/routes/error/401/get.spec.ts b/themes/triangles/server/src/lib/routes/error/401/get.spec.ts deleted file mode 100644 index 9fdac9c3c..000000000 --- a/themes/triangles/server/src/lib/routes/error/401/get.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import Sinon = require("sinon"); -import Express = require("express"); -import Assert = require("assert"); -import Get401 from "./get"; -import { ServerVariables } from "../../../ServerVariables"; -import { ServerVariablesMockBuilder, ServerVariablesMock } - from "../../../ServerVariablesMockBuilder.spec"; - -describe("routes/error/401/get", function () { - let vars: ServerVariables; - let mocks: ServerVariablesMock; - let req: any; - let res: any; - let renderSpy: Sinon.SinonSpy; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - vars = s.variables; - mocks = s.mocks; - - renderSpy = Sinon.spy(); - req = { - headers: {} - }; - res = { - render: renderSpy - }; - }); - - it("should set redirection url to the default redirection url", function () { - vars.config.default_redirection_url = "http://default-redirection"; - return Get401(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/401", { - redirection_url: "http://default-redirection" - })); - }); - }); - - it("should set redirection url to the referer", function () { - req.headers["referer"] = "http://redirection"; - return Get401(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/401", { - redirection_url: "http://redirection" - })); - }); - }); - - it("should render without redirecting the user", function () { - return Get401(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/401", { - redirection_url: undefined - })); - }); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/error/401/get.ts b/themes/triangles/server/src/lib/routes/error/401/get.ts deleted file mode 100644 index ca4a3963d..000000000 --- a/themes/triangles/server/src/lib/routes/error/401/get.ts +++ /dev/null @@ -1,15 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import redirector from "../redirector"; -import { ServerVariables } from "../../../ServerVariables"; - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response): BluebirdPromise { - const redirectionUrl = redirector(req, vars); - res.render("errors/401", { - redirection_url: redirectionUrl - }); - return BluebirdPromise.resolve(); - }; -} diff --git a/themes/triangles/server/src/lib/routes/error/403/get.spec.ts b/themes/triangles/server/src/lib/routes/error/403/get.spec.ts deleted file mode 100644 index 22eb84853..000000000 --- a/themes/triangles/server/src/lib/routes/error/403/get.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import Sinon = require("sinon"); -import Express = require("express"); -import Assert = require("assert"); -import Get403 from "./get"; -import { ServerVariables } from "../../../ServerVariables"; -import { ServerVariablesMockBuilder, ServerVariablesMock } - from "../../../ServerVariablesMockBuilder.spec"; - -describe("routes/error/403/get", function () { - let vars: ServerVariables; - let mocks: ServerVariablesMock; - let req: any; - let res: any; - let renderSpy: Sinon.SinonSpy; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - vars = s.variables; - mocks = s.mocks; - - renderSpy = Sinon.spy(); - req = { - headers: {} - }; - res = { - render: renderSpy - }; - }); - - it("should set redirection url to the default redirection url", function () { - vars.config.default_redirection_url = "http://default-redirection"; - return Get403(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/403", { - redirection_url: "http://default-redirection" - })); - }); - }); - - it("should set redirection url to the referer", function () { - req.headers["referer"] = "http://redirection"; - return Get403(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/403", { - redirection_url: "http://redirection" - })); - }); - }); - - it("should render without redirecting the user", function () { - return Get403(vars)(req, res as any) - .then(function () { - Assert(renderSpy.calledOnce); - Assert(renderSpy.calledWithExactly("errors/403", { - redirection_url: undefined - })); - }); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/error/403/get.ts b/themes/triangles/server/src/lib/routes/error/403/get.ts deleted file mode 100644 index 3ab0319e5..000000000 --- a/themes/triangles/server/src/lib/routes/error/403/get.ts +++ /dev/null @@ -1,15 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import redirector from "../redirector"; -import { ServerVariables } from "../../../ServerVariables"; - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response): BluebirdPromise { - const redirectionUrl = redirector(req, vars); - res.render("errors/403", { - redirection_url: redirectionUrl - }); - return BluebirdPromise.resolve(); - }; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/error/404/get.spec.ts b/themes/triangles/server/src/lib/routes/error/404/get.spec.ts deleted file mode 100644 index 73e4e6cef..000000000 --- a/themes/triangles/server/src/lib/routes/error/404/get.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Sinon = require("sinon"); -import Express = require("express"); -import Assert = require("assert"); -import Get404 from "./get"; - -describe("routes/error/404/get", function () { - it("should render the page", function () { - const req = {} as Express.Request; - const res = { - render: Sinon.stub() - }; - - return Get404(req, res as any) - .then(function () { - Assert(res.render.calledOnce); - Assert(res.render.calledWith("errors/404")); - }); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/error/404/get.ts b/themes/triangles/server/src/lib/routes/error/404/get.ts deleted file mode 100644 index 6693b6fc8..000000000 --- a/themes/triangles/server/src/lib/routes/error/404/get.ts +++ /dev/null @@ -1,8 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import express = require("express"); - -export default function (req: express.Request, res: express.Response): BluebirdPromise { - res.render("errors/404"); - return BluebirdPromise.resolve(); -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/error/redirector.ts b/themes/triangles/server/src/lib/routes/error/redirector.ts deleted file mode 100644 index b1a3ccc11..000000000 --- a/themes/triangles/server/src/lib/routes/error/redirector.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Express = require("express"); -import { ServerVariables } from "../../ServerVariables"; - -export default function (req: Express.Request, vars: ServerVariables): string { - let redirectionUrl: string; - - if (req.headers && req.headers["referer"]) - redirectionUrl = "" + req.headers["referer"]; - else if (vars.config.default_redirection_url) - redirectionUrl = vars.config.default_redirection_url; - - return redirectionUrl; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/firstfactor/get.ts b/themes/triangles/server/src/lib/routes/firstfactor/get.ts deleted file mode 100644 index d94f656c8..000000000 --- a/themes/triangles/server/src/lib/routes/firstfactor/get.ts +++ /dev/null @@ -1,72 +0,0 @@ - -import express = require("express"); -import Endpoints = require("../../../../../shared/api"); -import BluebirdPromise = require("bluebird"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import Constants = require("../../../../../shared/constants"); -import Util = require("util"); -import { ServerVariables } from "../../ServerVariables"; -import { SafeRedirector } from "../../utils/SafeRedirection"; -import { Level } from "../../authentication/Level"; - -function getRedirectParam( - req: express.Request) { - return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : undefined; -} - -function redirectToSecondFactorPage( - req: express.Request, - res: express.Response) { - - const redirectUrl = getRedirectParam(req); - if (!redirectUrl) - res.redirect(Endpoints.SECOND_FACTOR_GET); - else - res.redirect( - Util.format("%s?%s=%s", - Endpoints.SECOND_FACTOR_GET, - Constants.REDIRECT_QUERY_PARAM, - redirectUrl)); -} - -function redirectToService( - req: express.Request, - res: express.Response, - redirector: SafeRedirector) { - const redirectUrl = getRedirectParam(req); - if (!redirectUrl) { - res.redirect(Endpoints.LOGGED_IN); - } else { - redirector.redirectOrElse(res, redirectUrl, Endpoints.LOGGED_IN); - } -} - -function renderFirstFactor( - res: express.Response) { - - res.render("firstfactor", { - first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST, - reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET - }); -} - -export default function ( - vars: ServerVariables) { - - const redirector = new SafeRedirector(vars.config.session.domain); - return function (req: express.Request, res: express.Response): BluebirdPromise { - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, vars.logger); - if (authSession.authentication_level == Level.ONE_FACTOR) { - redirectToSecondFactorPage(req, res); - } else if (authSession.authentication_level == Level.TWO_FACTOR) { - redirectToService(req, res, redirector); - } else { - renderFirstFactor(res); - } - resolve(); - }); - }; -} diff --git a/themes/triangles/server/src/lib/routes/firstfactor/post.spec.ts b/themes/triangles/server/src/lib/routes/firstfactor/post.spec.ts deleted file mode 100644 index e1d078cdd..000000000 --- a/themes/triangles/server/src/lib/routes/firstfactor/post.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ - -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import Assert = require("assert"); -import FirstFactorPost = require("./post"); -import exceptions = require("../../Exceptions"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../types/AuthenticationSession"; -import Endpoints = require("../../../../../shared/api"); -import AuthenticationRegulatorMock = require("../../regulation/RegulatorStub.spec"); -import ExpressMock = require("../../stubs/express.spec"); -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../ServerVariables"; - -describe("routes/firstfactor/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let emails: string[]; - let groups: string[]; - let vars: ServerVariables; - let mocks: ServerVariablesMock; - let authSession: AuthenticationSession; - - beforeEach(function () { - emails = ["test_ok@example.com"]; - groups = ["group1", "group2" ]; - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - mocks.authorizer.authorizationMock.returns(true); - mocks.regulator.regulateStub.returns(BluebirdPromise.resolve()); - mocks.regulator.markStub.returns(BluebirdPromise.resolve()); - - req = { - originalUrl: "/api/firstfactor", - body: { - username: "username", - password: "password" - }, - query: { - redirect: "http://redirect.url" - }, - session: { - cookie: {} - }, - headers: { - host: "home.example.com" - } - }; - - res = ExpressMock.ResponseMock(); - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - }); - - it("should reply with 204 if success", function () { - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.resolve({ - emails: emails, - groups: groups - })); - return FirstFactorPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal("username", authSession.userid); - Assert(res.send.calledOnce); - }); - }); - - describe("keep me logged in", () => { - beforeEach(() => { - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.resolve({ - emails: emails, - groups: groups - })); - req.body.keepMeLoggedIn = "true"; - return FirstFactorPost.default(vars)(req as any, res as any); - }); - - it("should set keep_me_logged_in session variable to true", function () { - Assert.equal(authSession.keep_me_logged_in, true); - }); - - it("should set cookie maxAge to one year", function () { - Assert.equal(req.session.cookie.maxAge, 365 * 24 * 60 * 60 * 1000); - }); - }); - - it("should retrieve email from LDAP", function () { - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.resolve([{ mail: ["test@example.com"] }])); - return FirstFactorPost.default(vars)(req as any, res as any); - }); - - it("should set first email address as user session variable", function () { - const emails = ["test_ok@example.com"]; - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.resolve({ - emails: emails, - groups: groups - })); - - return FirstFactorPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal("test_ok@example.com", authSession.email); - }); - }); - - it("should return error message when LDAP authenticator throws", function () { - mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") - .returns(BluebirdPromise.reject(new exceptions.LdapBindError("Bad credentials"))); - - return FirstFactorPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.equal(mocks.regulator.markStub.getCall(0).args[0], "username"); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - }); - }); - - it("should return error message when regulator rejects authentication", function () { - const err = new exceptions.AuthenticationRegulationError("Authentication regulation..."); - mocks.regulator.regulateStub.returns(BluebirdPromise.reject(err)); - return FirstFactorPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - }); - }); -}); - - diff --git a/themes/triangles/server/src/lib/routes/firstfactor/post.ts b/themes/triangles/server/src/lib/routes/firstfactor/post.ts deleted file mode 100644 index 565681d6a..000000000 --- a/themes/triangles/server/src/lib/routes/firstfactor/post.ts +++ /dev/null @@ -1,101 +0,0 @@ - -import Exceptions = require("../../Exceptions"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import Endpoint = require("../../../../../shared/api"); -import ErrorReplies = require("../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import Constants = require("../../../../../shared/constants"); -import UserMessages = require("../../../../../shared/UserMessages"); -import { ServerVariables } from "../../ServerVariables"; -import { AuthenticationSession } from "../../../../types/AuthenticationSession"; -import { GroupsAndEmails } from "../../authentication/backends/GroupsAndEmails"; -import { Level as AuthenticationLevel } from "../../authentication/Level"; -import { Level as AuthorizationLevel } from "../../authorization/Level"; -import { URLDecomposer } from "../../utils/URLDecomposer"; - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response) - : BluebirdPromise { - const username: string = req.body.username; - const password: string = req.body.password; - const keepMeLoggedIn: boolean = req.body.keepMeLoggedIn && - req.body.keepMeLoggedIn === "true"; - let authSession: AuthenticationSession; - - if (keepMeLoggedIn) { - // Stay connected for 1 year. - vars.logger.debug(req, "User requested to stay logged in for one year."); - req.session.cookie.maxAge = 365 * 24 * 60 * 60 * 1000; - } - - return BluebirdPromise.resolve() - .then(function () { - if (!username || !password) { - return BluebirdPromise.reject(new Error("No username or password.")); - } - vars.logger.info(req, "Starting authentication of user \"%s\"", username); - authSession = AuthenticationSessionHandler.get(req, vars.logger); - return vars.regulator.regulate(username); - }) - .then(function () { - vars.logger.info(req, "No regulation applied."); - return vars.usersDatabase.checkUserPassword(username, password); - }) - .then(function (groupsAndEmails: GroupsAndEmails) { - vars.logger.info(req, - "LDAP binding successful. Retrieved information about user are %s", - JSON.stringify(groupsAndEmails)); - authSession.userid = username; - authSession.keep_me_logged_in = keepMeLoggedIn; - authSession.authentication_level = AuthenticationLevel.ONE_FACTOR; - const redirectUrl: string = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined" - // Fuck, don't know why it is a string! - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : ""; - - const emails: string[] = groupsAndEmails.emails; - const groups: string[] = groupsAndEmails.groups; - const decomposition = URLDecomposer.fromUrl(redirectUrl); - const authorizationLevel = (decomposition) - ? vars.authorizer.authorization( - {domain: decomposition.domain, resource: decomposition.path}, - {user: username, groups: groups}) - : AuthorizationLevel.TWO_FACTOR; - - if (emails.length > 0) - authSession.email = emails[0]; - authSession.groups = groups; - - vars.logger.debug(req, "Mark successful authentication to regulator."); - vars.regulator.mark(username, true); - - if (authorizationLevel <= AuthorizationLevel.ONE_FACTOR) { - let newRedirectionUrl: string = redirectUrl; - if (!newRedirectionUrl) - newRedirectionUrl = Endpoint.LOGGED_IN; - res.send({ - redirect: newRedirectionUrl - }); - vars.logger.debug(req, "Redirect to '%s'", redirectUrl); - } - else { - let newRedirectUrl = Endpoint.SECOND_FACTOR_GET; - if (redirectUrl) { - newRedirectUrl += "?" + Constants.REDIRECT_QUERY_PARAM + "=" - + redirectUrl; - } - vars.logger.debug(req, "Redirect to '%s'", newRedirectUrl); - res.send({ - redirect: newRedirectUrl - }); - } - return BluebirdPromise.resolve(); - }) - .catch(Exceptions.LdapBindError, function (err: Error) { - vars.regulator.mark(username, false); - return ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)(err); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)); - }; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/loggedin/get.ts b/themes/triangles/server/src/lib/routes/loggedin/get.ts deleted file mode 100644 index 283a041b1..000000000 --- a/themes/triangles/server/src/lib/routes/loggedin/get.ts +++ /dev/null @@ -1,23 +0,0 @@ -import Express = require("express"); -import Endpoints = require("../../../../../shared/api"); -import BluebirdPromise = require("bluebird"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { ServerVariables } from "../../ServerVariables"; -import ErrorReplies = require("../../ErrorReplies"); - -export default function (vars: ServerVariables) { - function handler(req: Express.Request, res: Express.Response): BluebirdPromise { - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, vars.logger); - res.render("already-logged-in", { - logout_endpoint: Endpoints.LOGOUT_GET, - username: authSession.userid, - redirection_url: vars.config.default_redirection_url - }); - resolve(); - }) - .catch(ErrorReplies.replyWithError401(req, res, vars.logger)); - } - - return handler; -} diff --git a/themes/triangles/server/src/lib/routes/logout/get.ts b/themes/triangles/server/src/lib/routes/logout/get.ts deleted file mode 100644 index 4d5112146..000000000 --- a/themes/triangles/server/src/lib/routes/logout/get.ts +++ /dev/null @@ -1,20 +0,0 @@ - -import express = require("express"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import Constants = require("../../../../../shared/constants"); -import { ServerVariables } from "../../ServerVariables"; - -function getRedirectParam(req: express.Request) { - return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : undefined; -} - -export default function (vars: ServerVariables) { - return function(req: express.Request, res: express.Response) { - const redirect_param = getRedirectParam(req); - const redirect_url = redirect_param || "/"; - AuthenticationSessionHandler.reset(req); - res.redirect(redirect_url); - }; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/password-reset/constants.ts b/themes/triangles/server/src/lib/routes/password-reset/constants.ts deleted file mode 100644 index 5c639e92a..000000000 --- a/themes/triangles/server/src/lib/routes/password-reset/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export const CHALLENGE = "reset-password"; \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/password-reset/form/post.spec.ts b/themes/triangles/server/src/lib/routes/password-reset/form/post.spec.ts deleted file mode 100644 index ed029c906..000000000 --- a/themes/triangles/server/src/lib/routes/password-reset/form/post.spec.ts +++ /dev/null @@ -1,122 +0,0 @@ - -import PasswordResetFormPost = require("./post"); -import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; -import { UserDataStore } from "../../../storage/UserDataStore"; -import Sinon = require("sinon"); -import Assert = require("assert"); -import BluebirdPromise = require("bluebird"); -import ExpressMock = require("../../../stubs/express.spec"); -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../ServerVariables"; -import { Level } from "../../../authentication/Level"; - -describe("routes/password-reset/form/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let vars: ServerVariables; - let mocks: ServerVariablesMock; - let authSession: AuthenticationSession; - - beforeEach(function () { - req = { - originalUrl: "/api/password-reset", - body: { - userid: "user" - }, - session: {}, - headers: { - host: "localhost" - } - }; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - - mocks.config.authentication_backend.ldap = { - url: "ldap://ldapjs", - mail_attribute: "mail", - user: "user", - password: "password", - additional_users_dn: "ou=users", - additional_groups_dn: "ou=groups", - base_dn: "dc=example,dc=com", - users_filter: "user", - group_name_attribute: "cn", - groups_filter: "groups" - }; - - res = ExpressMock.ResponseMock(); - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - authSession.userid = "user"; - authSession.email = "user@example.com"; - authSession.authentication_level = Level.ONE_FACTOR; - }); - - describe("test reset password post", () => { - it("should update the password and reset auth_session for reauthentication", function () { - req.body = {}; - req.body.password = "new-password"; - - mocks.usersDatabase.updatePasswordStub.returns(BluebirdPromise.resolve()); - - authSession.identity_check = { - userid: "user", - challenge: "reset-password" - }; - return PasswordResetFormPost.default(vars)(req as any, res as any) - .then(function () { - return AuthenticationSessionHandler.get(req as any, vars.logger); - }).then(function (_authSession) { - Assert.equal(res.status.getCall(0).args[0], 204); - Assert.equal(_authSession.authentication_level, Level.NOT_AUTHENTICATED); - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if identity_challenge does not exist", function () { - authSession.identity_check = { - userid: "user", - challenge: undefined - }; - return PasswordResetFormPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "An error occurred during password reset. Your password has not been changed." - }); - }); - }); - - it("should fail when ldap fails", function () { - req.body = {}; - req.body.password = "new-password"; - - mocks.usersDatabase.updatePasswordStub - .returns(BluebirdPromise.reject("Internal error with LDAP")); - - authSession.identity_check = { - challenge: "reset-password", - userid: "user" - }; - return PasswordResetFormPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "An error occurred during password reset. Your password has not been changed." - }); - return BluebirdPromise.resolve(); - }); - }); - }); -}); diff --git a/themes/triangles/server/src/lib/routes/password-reset/form/post.ts b/themes/triangles/server/src/lib/routes/password-reset/form/post.ts deleted file mode 100644 index fccd7471b..000000000 --- a/themes/triangles/server/src/lib/routes/password-reset/form/post.ts +++ /dev/null @@ -1,50 +0,0 @@ - -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import objectPath = require("object-path"); -import exceptions = require("../../../Exceptions"); -import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; -import ErrorReplies = require("../../../ErrorReplies"); -import UserMessages = require("../../../../../../shared/UserMessages"); -import { ServerVariables } from "../../../ServerVariables"; - -import Constants = require("./../constants"); - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const newPassword = objectPath.get(req, "body.password"); - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - if (!authSession.identity_check) { - reject(new Error("No identity check initiated")); - return; - } - - vars.logger.info(req, "User %s wants to reset his/her password.", - authSession.identity_check.userid); - vars.logger.debug(req, "Challenge %s", authSession.identity_check.challenge); - - if (authSession.identity_check.challenge != Constants.CHALLENGE) { - reject(new Error("Bad challenge.")); - return; - } - resolve(); - }) - .then(function () { - return vars.usersDatabase.updatePassword(authSession.identity_check.userid, newPassword); - }) - .then(function () { - vars.logger.info(req, "Password reset for user '%s'", - authSession.identity_check.userid); - AuthenticationSessionHandler.reset(req); - res.status(204); - res.send(); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.RESET_PASSWORD_FAILED)); - }; -} diff --git a/themes/triangles/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts b/themes/triangles/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts deleted file mode 100644 index ac6a41754..000000000 --- a/themes/triangles/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts +++ /dev/null @@ -1,92 +0,0 @@ - -import PasswordResetHandler - from "./PasswordResetHandler"; -import { UserDataStore } from "../../../storage/UserDataStore"; -import Sinon = require("sinon"); -import winston = require("winston"); -import assert = require("assert"); -import BluebirdPromise = require("bluebird"); -import ExpressMock = require("../../../stubs/express.spec"); -import { ServerVariablesMock, ServerVariablesMockBuilder } - from "../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../ServerVariables"; - -describe("routes/password-reset/identity/PasswordResetHandler", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - req = { - originalUrl: "/non-api/xxx", - query: { - userid: "user" - }, - session: { - auth: { - userid: "user", - email: "user@example.com", - first_factor: true, - second_factor: false - } - }, - headers: { - host: "localhost" - } - }; - - const options = { - inMemoryOnly: true - }; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - mocks.userDataStore.saveU2FRegistrationStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.produceIdentityValidationTokenStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.consumeIdentityValidationTokenStub - .returns(BluebirdPromise.resolve({})); - res = ExpressMock.ResponseMock(); - }); - - describe("test reset password identity pre check", () => { - it("should fail when no userid is provided", function () { - req.query.userid = undefined; - const handler = new PasswordResetHandler(vars.logger, - vars.usersDatabase); - return handler.preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject("It should fail"); - }) - .catch(function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if ldap fail", function () { - mocks.usersDatabase.getEmailsStub - .returns(BluebirdPromise.reject("Internal error")); - new PasswordResetHandler(vars.logger, vars.usersDatabase) - .preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here")); - }, - function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should returns identity when ldap replies", function () { - mocks.usersDatabase.getEmailsStub - .returns(BluebirdPromise.resolve(["test@example.com"])); - return new PasswordResetHandler(vars.logger, vars.usersDatabase) - .preValidationInit(req as any); - }); - }); -}); diff --git a/themes/triangles/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts b/themes/triangles/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts deleted file mode 100644 index 42ae92cda..000000000 --- a/themes/triangles/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts +++ /dev/null @@ -1,69 +0,0 @@ -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import objectPath = require("object-path"); - -import exceptions = require("../../../Exceptions"); -import { Identity } from "../../../../../types/Identity"; -import { IdentityValidable } from "../../../IdentityValidable"; -import { PRE_VALIDATION_TEMPLATE } from "../../../IdentityCheckPreValidationTemplate"; -import Constants = require("../constants"); -import { IRequestLogger } from "../../../logging/IRequestLogger"; -import { IUsersDatabase } from "../../../authentication/backends/IUsersDatabase"; - -export const TEMPLATE_NAME = "password-reset-form"; - -export default class PasswordResetHandler implements IdentityValidable { - private logger: IRequestLogger; - private usersDatabase: IUsersDatabase; - - constructor(logger: IRequestLogger, usersDatabase: IUsersDatabase) { - this.logger = logger; - this.usersDatabase = usersDatabase; - } - - challenge(): string { - return Constants.CHALLENGE; - } - - preValidationInit(req: express.Request): BluebirdPromise { - const that = this; - const userid: string = - objectPath.get(req, "query.userid"); - return BluebirdPromise.resolve() - .then(function () { - that.logger.debug(req, "User '%s' requested a password reset", userid); - if (!userid) - return BluebirdPromise.reject( - new exceptions.AccessDeniedError("No user id provided")); - - return that.usersDatabase.getEmails(userid); - }) - .then(function (emails: string[]) { - if (!emails && emails.length <= 0) throw new Error("No email found"); - const identity = { - email: emails[0], - userid: userid - }; - return BluebirdPromise.resolve(identity); - }) - .catch(function (err: Error) { - return BluebirdPromise.reject(new exceptions.IdentityError(err.message)); - }); - } - - preValidationResponse(req: express.Request, res: express.Response) { - res.render(PRE_VALIDATION_TEMPLATE); - } - - postValidationInit(req: express.Request) { - return BluebirdPromise.resolve(); - } - - postValidationResponse(req: express.Request, res: express.Response) { - res.render(TEMPLATE_NAME); - } - - mailSubject(): string { - return "Reset your password"; - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/password-reset/request/get.ts b/themes/triangles/server/src/lib/routes/password-reset/request/get.ts deleted file mode 100644 index 8f3ae2b4b..000000000 --- a/themes/triangles/server/src/lib/routes/password-reset/request/get.ts +++ /dev/null @@ -1,13 +0,0 @@ - -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import objectPath = require("object-path"); -import exceptions = require("../../../Exceptions"); - -import Constants = require("./../constants"); - -const TEMPLATE_NAME = "password-reset-request"; - -export default function (req: express.Request, res: express.Response) { - res.render(TEMPLATE_NAME); -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/secondfactor/get.spec.ts b/themes/triangles/server/src/lib/routes/secondfactor/get.spec.ts deleted file mode 100644 index 6c77e1f69..000000000 --- a/themes/triangles/server/src/lib/routes/secondfactor/get.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import SecondFactorGet from "./get"; -import { ServerVariablesMockBuilder, ServerVariablesMock } - from "../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../ServerVariables"; -import Sinon = require("sinon"); -import ExpressMock = require("../../stubs/express.spec"); -import Assert = require("assert"); -import Endpoints = require("../../../../../shared/api"); -import BluebirdPromise = require("bluebird"); - -describe("routes/secondfactor/get", function () { - let mocks: ServerVariablesMock; - let vars: ServerVariables; - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req = ExpressMock.RequestMock(); - res = ExpressMock.ResponseMock(); - - req.session = { - auth: { - userid: "user", - first_factor: true, - second_factor: false - } - }; - }); - - describe("test rendering", function () { - it("should render second factor page", function () { - req.session.auth.second_factor = false; - return SecondFactorGet(vars)(req as any, res as any) - .then(function () { - Assert(res.render.calledWith("secondfactor")); - return BluebirdPromise.resolve(); - }); - }); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/secondfactor/get.ts b/themes/triangles/server/src/lib/routes/secondfactor/get.ts deleted file mode 100644 index 9f6deb4c6..000000000 --- a/themes/triangles/server/src/lib/routes/secondfactor/get.ts +++ /dev/null @@ -1,28 +0,0 @@ - -import Express = require("express"); -import Endpoints = require("../../../../../shared/api"); -import BluebirdPromise = require("bluebird"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { ServerVariables } from "../../ServerVariables"; - -const TEMPLATE_NAME = "secondfactor"; - -export default function (vars: ServerVariables) { - function handler(req: Express.Request, res: Express.Response) - : BluebirdPromise { - - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, vars.logger); - - res.render(TEMPLATE_NAME, { - username: authSession.userid, - totp_identity_start_endpoint: - Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, - u2f_identity_start_endpoint: - Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET - }); - resolve(); - }); - } - return handler; -} diff --git a/themes/triangles/server/src/lib/routes/secondfactor/redirect.spec.ts b/themes/triangles/server/src/lib/routes/secondfactor/redirect.spec.ts deleted file mode 100644 index ea66e6dca..000000000 --- a/themes/triangles/server/src/lib/routes/secondfactor/redirect.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import Redirect from "./redirect"; -import ExpressMock = require("../../stubs/express.spec"); -import { ServerVariablesMockBuilder, ServerVariablesMock } -from "../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../ServerVariables"; -import Assert = require("assert"); - -describe("routes/secondfactor/redirect", function() { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req = ExpressMock.RequestMock(); - res = ExpressMock.ResponseMock(); - }); - - it("should redirect to default_redirection_url", function() { - vars.config.default_redirection_url = "http://default_redirection_url"; - Redirect(vars)(req as any, res as any) - .then(function() { - Assert(res.json.calledWith({ - redirect: "http://default_redirection_url" - })); - }); - }); - - it("should redirect to /", function() { - Redirect(vars)(req as any, res as any) - .then(function() { - Assert(res.json.calledWith({ - redirect: "/" - })); - }); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/secondfactor/redirect.ts b/themes/triangles/server/src/lib/routes/secondfactor/redirect.ts deleted file mode 100644 index 5d84d9ebf..000000000 --- a/themes/triangles/server/src/lib/routes/secondfactor/redirect.ts +++ /dev/null @@ -1,30 +0,0 @@ - -import express = require("express"); -import objectPath = require("object-path"); -import Endpoints = require("../../../../../shared/api"); -import { ServerVariables } from "../../ServerVariables"; -import BluebirdPromise = require("bluebird"); -import ErrorReplies = require("../../ErrorReplies"); -import UserMessages = require("../../../../../shared/UserMessages"); -import { RedirectionMessage } from "../../../../../shared/RedirectionMessage"; -import Constants = require("../../../../../shared/constants"); - -export default function (vars: ServerVariables) { - return function (req: express.Request, res: express.Response) - : BluebirdPromise { - - return new BluebirdPromise(function (resolve, reject) { - let redirectUrl: string = "/"; - if (vars.config.default_redirection_url) { - redirectUrl = vars.config.default_redirection_url; - } - vars.logger.debug(req, "Request redirection to \"%s\".", redirectUrl); - res.json({ - redirect: redirectUrl - } as RedirectionMessage); - return resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - }; -} diff --git a/themes/triangles/server/src/lib/routes/secondfactor/totp/constants.ts b/themes/triangles/server/src/lib/routes/secondfactor/totp/constants.ts deleted file mode 100644 index 7b5a1dcfe..000000000 --- a/themes/triangles/server/src/lib/routes/secondfactor/totp/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ - -export const CHALLENGE = "totp-register"; -export const TEMPLATE_NAME = "totp-register"; - diff --git a/themes/triangles/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts b/themes/triangles/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts deleted file mode 100644 index 78b8ea3ea..000000000 --- a/themes/triangles/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts +++ /dev/null @@ -1,116 +0,0 @@ -import Sinon = require("sinon"); -import RegistrationHandler from "./RegistrationHandler"; -import { Identity } from "../../../../../../types/Identity"; -import { UserDataStore } from "../../../../storage/UserDataStore"; -import BluebirdPromise = require("bluebird"); -import ExpressMock = require("../../../../stubs/express.spec"); -import { ServerVariablesMock, ServerVariablesMockBuilder } - from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; -import Assert = require("assert"); - -describe("routes/secondfactor/totp/identity/RegistrationHandler", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req = ExpressMock.RequestMock(); - req.session = { - auth: { - userid: "user", - email: "user@example.com", - first_factor: true, - second_factor: false, - identity_check: { - userid: "user", - challenge: "totp-register" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.produceIdentityValidationTokenStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.consumeIdentityValidationTokenStub - .returns(BluebirdPromise.resolve({})); - mocks.userDataStore.saveTOTPSecretStub - .returns(BluebirdPromise.resolve({})); - - res = ExpressMock.ResponseMock(); - }); - - describe("test totp registration pre validation", function () { - it("should fail if first_factor has not been passed", function () { - req.session.auth.first_factor = false; - return new RegistrationHandler(vars.logger, vars.userDataStore, - vars.totpHandler, vars.config.totp) - .preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject(new Error("It should fail")); - }) - .catch(function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if userid is missing", function (done) { - req.session.auth.first_factor = false; - req.session.auth.userid = undefined; - - new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler, - vars.config.totp) - .preValidationInit(req as any) - .catch(function (err: Error) { - done(); - }); - }); - - it("should fail if email is missing", function (done) { - req.session.auth.first_factor = false; - req.session.auth.email = undefined; - - new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler, - vars.config.totp) - .preValidationInit(req as any) - .catch(function (err: Error) { - done(); - }); - }); - - it("should succeed if first factor passed, userid and email are provided", - function () { - return new RegistrationHandler(vars.logger, vars.userDataStore, - vars.totpHandler, vars.config.totp) - .preValidationInit(req as any); - }); - }); - - describe("test totp registration post validation", function () { - it("should generate a secret using userId as label and issuer defined in config", function () { - vars.config.totp = { - issuer: "issuer" - }; - return new RegistrationHandler(vars.logger, vars.userDataStore, - vars.totpHandler, vars.config.totp) - .postValidationResponse(req as any, res as any) - .then(function() { - Assert(mocks.totpHandler.generateStub.calledWithExactly("user", "issuer")); - }); - }); - }); -}); diff --git a/themes/triangles/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts b/themes/triangles/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts deleted file mode 100644 index b39b6d045..000000000 --- a/themes/triangles/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts +++ /dev/null @@ -1,112 +0,0 @@ - -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import objectPath = require("object-path"); - -import { Identity } from "../../../../../../types/Identity"; -import { IdentityValidable } from "../../../../IdentityValidable"; -import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; -import Constants = require("../constants"); -import Endpoints = require("../../../../../../../shared/api"); -import ErrorReplies = require("../../../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import FirstFactorValidator = require("../../../../FirstFactorValidator"); -import { IRequestLogger } from "../../../../logging/IRequestLogger"; -import { IUserDataStore } from "../../../../storage/IUserDataStore"; -import { ITotpHandler } from "../../../../authentication/totp/ITotpHandler"; -import { TOTPSecret } from "../../../../../../types/TOTPSecret"; -import { TotpConfiguration } from "../../../../configuration/schema/TotpConfiguration"; - - -export default class RegistrationHandler implements IdentityValidable { - private logger: IRequestLogger; - private userDataStore: IUserDataStore; - private totp: ITotpHandler; - private configuration: TotpConfiguration; - - constructor(logger: IRequestLogger, - userDataStore: IUserDataStore, - totp: ITotpHandler, configuration: TotpConfiguration) { - this.logger = logger; - this.userDataStore = userDataStore; - this.totp = totp; - this.configuration = configuration; - } - - challenge(): string { - return Constants.CHALLENGE; - } - - private retrieveIdentity(req: express.Request): BluebirdPromise { - const that = this; - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, that.logger); - const userid = authSession.userid; - const email = authSession.email; - - if (!(userid && email)) { - return reject(new Error("User ID or email is missing")); - } - - const identity = { - email: email, - userid: userid - }; - return resolve(identity); - }); - } - - preValidationInit(req: express.Request): BluebirdPromise { - const that = this; - return FirstFactorValidator.validate(req, this.logger) - .then(function () { - return that.retrieveIdentity(req); - }); - } - - preValidationResponse(req: express.Request, res: express.Response) { - res.render(PRE_VALIDATION_TEMPLATE); - } - - postValidationInit(req: express.Request) { - return FirstFactorValidator.validate(req, this.logger); - } - - postValidationResponse(req: express.Request, res: express.Response) - : BluebirdPromise { - const that = this; - let secret: TOTPSecret; - let userId: string; - return new BluebirdPromise(function (resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, that.logger); - userId = authSession.userid; - - if (authSession.identity_check.challenge != Constants.CHALLENGE - || !userId) - return reject(new Error("Bad challenge.")); - - resolve(); - }) - .then(function () { - secret = that.totp.generate(userId, - that.configuration.issuer); - that.logger.debug(req, "Save the TOTP secret in DB"); - return that.userDataStore.saveTOTPSecret(userId, secret); - }) - .then(function () { - AuthenticationSessionHandler.reset(req); - - res.render(Constants.TEMPLATE_NAME, { - base32_secret: secret.base32, - otpauth_url: secret.otpauth_url, - login_endpoint: Endpoints.FIRST_FACTOR_GET - }); - }) - .catch(ErrorReplies.replyWithError200(req, res, that.logger, UserMessages.OPERATION_FAILED)); - } - - mailSubject(): string { - return "Set up Authelia's one-time password"; - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts b/themes/triangles/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts deleted file mode 100644 index 70a20d39d..000000000 --- a/themes/triangles/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import Sinon = require("sinon"); -import Assert = require("assert"); -import Exceptions = require("../../../../Exceptions"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import SignPost = require("./post"); -import { ServerVariables } from "../../../../ServerVariables"; - -import ExpressMock = require("../../../../stubs/express.spec"); -import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; -import { Level } from "../../../../authentication/Level"; - -describe("routes/secondfactor/totp/sign/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let authSession: AuthenticationSession; - let vars: ServerVariables; - let mocks: ServerVariablesMock; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - vars = s.variables; - mocks = s.mocks; - const app_get = Sinon.stub(); - req = { - originalUrl: "/api/totp-register", - app: {}, - body: { - token: "abc" - }, - session: {}, - query: { - redirect: "http://redirect" - } - }; - res = ExpressMock.ResponseMock(); - - const doc = { - userid: "user", - secret: { - base32: "ABCDEF" - } - }; - mocks.userDataStore.retrieveTOTPSecretStub.returns(BluebirdPromise.resolve(doc)); - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - authSession.userid = "user"; - authSession.authentication_level = Level.ONE_FACTOR; - }); - - - it("should send status code 200 when totp is valid", function () { - mocks.totpHandler.validateStub.returns(true); - return SignPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(authSession.authentication_level, Level.TWO_FACTOR); - return BluebirdPromise.resolve(); - }); - }); - - it("should send error message when totp is not valid", function () { - mocks.totpHandler.validateStub.returns(false); - return SignPost.default(vars)(req as any, res as any) - .then(function () { - Assert.notEqual(authSession.authentication_level, Level.TWO_FACTOR); - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); -}); - diff --git a/themes/triangles/server/src/lib/routes/secondfactor/totp/sign/post.ts b/themes/triangles/server/src/lib/routes/secondfactor/totp/sign/post.ts deleted file mode 100644 index 34a276d12..000000000 --- a/themes/triangles/server/src/lib/routes/secondfactor/totp/sign/post.ts +++ /dev/null @@ -1,42 +0,0 @@ -import Bluebird = require("bluebird"); -import Express = require("express"); - -import { TOTPSecretDocument } from "../../../../storage/TOTPSecretDocument"; -import Endpoints = require("../../../../../../../shared/api"); -import Redirect from "../../redirect"; -import ErrorReplies = require("../../../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { ServerVariables } from "../../../../ServerVariables"; -import { Level } from "../../../../authentication/Level"; - -const UNAUTHORIZED_MESSAGE = "Unauthorized access"; - -export default function (vars: ServerVariables) { - function handler(req: Express.Request, res: Express.Response): Bluebird { - let authSession: AuthenticationSession; - const token = req.body.token; - - return new Bluebird(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - vars.logger.info(req, "Initiate TOTP validation for user \"%s\".", authSession.userid); - resolve(); - }) - .then(function () { - return vars.userDataStore.retrieveTOTPSecret(authSession.userid); - }) - .then(function (doc: TOTPSecretDocument) { - if (!vars.totpHandler.validate(token, doc.secret.base32)) - return Bluebird.reject(new Error("Invalid TOTP token.")); - - vars.logger.debug(req, "TOTP validation succeeded."); - authSession.authentication_level = Level.TWO_FACTOR; - Redirect(vars)(req, res); - return Bluebird.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - return handler; -} diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts deleted file mode 100644 index 7f16c0ee8..000000000 --- a/themes/triangles/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts +++ /dev/null @@ -1,11 +0,0 @@ - -import util = require("util"); -import express = require("express"); - -function extract_app_id(req: express.Request): string { - return util.format("https://%s", req.headers.host); -} - -export = { - extract_app_id: extract_app_id -}; \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts deleted file mode 100644 index a54bfbfe6..000000000 --- a/themes/triangles/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts +++ /dev/null @@ -1,96 +0,0 @@ -import Sinon = require("sinon"); -import Assert = require("assert"); -import BluebirdPromise = require("bluebird"); - -import { Identity } from "../../../../../../types/Identity"; -import RegistrationHandler from "./RegistrationHandler"; -import ExpressMock = require("../../../../stubs/express.spec"); -import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; - -describe("routes/secondfactor/u2f/identity/RegistrationHandler", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req = ExpressMock.RequestMock(); - req.app = {}; - req.session = { - auth: { - userid: "user", - email: "user@example.com", - first_factor: true, - second_factor: false - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - - res = ExpressMock.ResponseMock(); - res.send = Sinon.spy(); - res.json = Sinon.spy(); - res.status = Sinon.spy(); - }); - - describe("test u2f registration check", test_registration_check); - - function test_registration_check() { - it("should fail if first_factor has not been passed", function () { - req.session.auth.first_factor = false; - return new RegistrationHandler(vars.logger).preValidationInit(req as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if userid is missing", function () { - req.session.auth.first_factor = false; - req.session.auth.userid = undefined; - - return new RegistrationHandler(vars.logger).preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here")); - }, - function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should fail if email is missing", function () { - req.session.auth.first_factor = false; - req.session.auth.email = undefined; - - return new RegistrationHandler(vars.logger).preValidationInit(req as any) - .then(function () { - return BluebirdPromise.reject(new Error("should not be here")); - }, - function (err: Error) { - return BluebirdPromise.resolve(); - }); - }); - - it("should succeed if first factor passed, userid and email are provided", function () { - req.session.auth.first_factor = true; - req.session.auth.email = "admin@example.com"; - req.session.auth.userid = "user"; - return new RegistrationHandler(vars.logger).preValidationInit(req as any); - }); - } -}); diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts deleted file mode 100644 index bc4713c77..000000000 --- a/themes/triangles/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts +++ /dev/null @@ -1,73 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import objectPath = require("object-path"); - -import { IdentityValidable } from "../../../../IdentityValidable"; -import { Identity } from "../../../../../../types/Identity"; -import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; -import FirstFactorValidator = require("../../../../FirstFactorValidator"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { IRequestLogger } from "../../../../logging/IRequestLogger"; - -const CHALLENGE = "u2f-register"; -const MAIL_SUBJECT = "Register your security key with Authelia"; - -const POST_VALIDATION_TEMPLATE_NAME = "u2f-register"; - - -export default class RegistrationHandler implements IdentityValidable { - private logger: IRequestLogger; - - constructor(logger: IRequestLogger) { - this.logger = logger; - } - - challenge(): string { - return CHALLENGE; - } - - private retrieveIdentity(req: express.Request): BluebirdPromise { - const that = this; - return new BluebirdPromise(function(resolve, reject) { - const authSession = AuthenticationSessionHandler.get(req, that.logger); - const userid = authSession.userid; - const email = authSession.email; - - if (!(userid && email)) { - return reject(new Error("User ID or email is missing")); - } - - const identity = { - email: email, - userid: userid - }; - return resolve(identity); - }); - } - - preValidationInit(req: express.Request): BluebirdPromise { - const that = this; - return FirstFactorValidator.validate(req, this.logger) - .then(function () { - return that.retrieveIdentity(req); - }); - } - - preValidationResponse(req: express.Request, res: express.Response) { - res.render(PRE_VALIDATION_TEMPLATE); - } - - postValidationInit(req: express.Request) { - return FirstFactorValidator.validate(req, this.logger); - } - - postValidationResponse(req: express.Request, res: express.Response) { - res.render(POST_VALIDATION_TEMPLATE_NAME); - } - - mailSubject(): string { - return MAIL_SUBJECT; - } -} - diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts deleted file mode 100644 index de3347a21..000000000 --- a/themes/triangles/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts +++ /dev/null @@ -1,146 +0,0 @@ - -import sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import assert = require("assert"); -import U2FRegisterPost = require("./post"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import ExpressMock = require("../../../../stubs/express.spec"); -import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; - - -describe("routes/secondfactor/u2f/register/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - let authSession: AuthenticationSession; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.originalUrl = "/api/xxxx"; - req.app = {}; - req.session = { - auth: { - userid: "user", - first_factor: true, - second_factor: false, - identity_check: { - challenge: "u2f-register", - userid: "user" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - }); - - describe("test registration", test_registration); - - - function test_registration() { - it("should save u2f meta and return status code 200", function () { - const expectedStatus = { - keyHandle: "keyHandle", - publicKey: "pbk", - certificate: "cert" - }; - mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve(expectedStatus)); - - authSession.register_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { - assert.equal("user", mocks.userDataStore.saveU2FRegistrationStub.getCall(0).args[0]); - assert.equal(authSession.identity_check, undefined); - }); - }); - - it("should return error message on finishRegistration error", function () { - mocks.u2f.checkRegistrationStub.returns({ errorCode: 500 }); - - authSession.register_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); - - it("should return error message when register_request is not provided", function () { - mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); - authSession.register_request = undefined; - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); - - it("should return error message when no auth request has been initiated", function () { - mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); - authSession.register_request = undefined; - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); - - it("should return error message when identity has not been verified", function () { - authSession.identity_check = undefined; - return U2FRegisterPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - return BluebirdPromise.resolve(); - }); - }); - } -}); - diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/register/post.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/register/post.ts deleted file mode 100644 index 7296ccbe5..000000000 --- a/themes/triangles/server/src/lib/routes/secondfactor/u2f/register/post.ts +++ /dev/null @@ -1,64 +0,0 @@ - -import { UserDataStore } from "../../../../storage/UserDataStore"; -import objectPath = require("object-path"); -import u2f_common = require("../U2FCommon"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import U2f = require("u2f"); -import { U2FRegistration } from "../../../../../../types/U2FRegistration"; -import redirect from "../../redirect"; -import ErrorReplies = require("../../../../ErrorReplies"); -import { ServerVariables } from "../../../../ServerVariables"; -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; - - -export default function (vars: ServerVariables) { - function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const appid = u2f_common.extract_app_id(req); - const registrationResponse: U2f.RegistrationData = req.body; - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - const registrationRequest = authSession.register_request; - - if (!registrationRequest) { - return reject(new Error("No registration request")); - } - - if (!authSession.identity_check - || authSession.identity_check.challenge != "u2f-register") { - return reject(new Error("Bad challenge for registration request")); - } - - vars.logger.info(req, "Finishing registration"); - vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); - vars.logger.debug(req, "RegistrationResponse = %s", JSON.stringify(registrationResponse)); - - return resolve(vars.u2f.checkRegistration(registrationRequest, registrationResponse)); - }) - .then(function (u2fResult: U2f.RegistrationResult | U2f.Error): BluebirdPromise { - if (objectPath.has(u2fResult, "errorCode")) - return BluebirdPromise.reject(new Error("Error while registering.")); - - const registrationResult: U2f.RegistrationResult = u2fResult as U2f.RegistrationResult; - vars.logger.info(req, "Store registration and reply"); - vars.logger.debug(req, "RegistrationResult = %s", JSON.stringify(registrationResult)); - const registration: U2FRegistration = { - keyHandle: registrationResult.keyHandle, - publicKey: registrationResult.publicKey - }; - return vars.userDataStore.saveU2FRegistration(authSession.userid, appid, registration); - }) - .then(function () { - authSession.identity_check = undefined; - redirect(vars)(req, res); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - return handler; -} diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts deleted file mode 100644 index a207c9109..000000000 --- a/themes/triangles/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ - -import sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import Assert = require("assert"); -import U2FRegisterRequestGet = require("./get"); -import ExpressMock = require("../../../../stubs/express.spec"); -import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec"; -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; - -describe("routes/secondfactor/u2f/register_request/get", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.originalUrl = "/api/xxxx"; - req.app = {}; - req.session = { - auth: { - userid: "user", - first_factor: true, - second_factor: false, - identity_check: { - challenge: "u2f-register", - userid: "user" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - const options = { - inMemoryOnly: true - }; - - mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); - - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - }); - - describe("test registration request", () => { - it("should send back the registration request and save it in the session", function () { - const expectedRequest = { - test: "abc" - }; - mocks.u2f.requestStub.returns(BluebirdPromise.resolve(expectedRequest)); - return U2FRegisterRequestGet.default(vars)(req as any, res as any) - .then(function () { - Assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); - }); - }); - - it("should return internal error on registration request", function () { - res.send = sinon.spy(); - const user_key_container = {}; - mocks.u2f.requestStub.returns(BluebirdPromise.reject("Internal error")); - return U2FRegisterRequestGet.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], { - error: "Operation failed." - }); - }); - }); - - it("should return forbidden if identity has not been verified", function () { - req.session.auth.identity_check = undefined; - return U2FRegisterRequestGet.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(403, res.status.getCall(0).args[0]); - }); - }); - }); -}); - diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/register_request/get.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/register_request/get.ts deleted file mode 100644 index f611af933..000000000 --- a/themes/triangles/server/src/lib/routes/secondfactor/u2f/register_request/get.ts +++ /dev/null @@ -1,43 +0,0 @@ - -import { UserDataStore } from "../../../../storage/UserDataStore"; - -import objectPath = require("object-path"); -import u2f_common = require("../U2FCommon"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import U2f = require("u2f"); -import ErrorReplies = require("../../../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { ServerVariables } from "../../../../ServerVariables"; - -export default function (vars: ServerVariables) { - function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const appid: string = u2f_common.extract_app_id(req); - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - if (!authSession.identity_check - || authSession.identity_check.challenge != "u2f-register") { - res.status(403); - res.send(); - return reject(new Error("Bad challenge.")); - } - - vars.logger.info(req, "Starting registration for appId '%s'", appid); - return resolve(vars.u2f.request(appid)); - }) - .then(function (registrationRequest: U2f.Request) { - vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); - authSession.register_request = registrationRequest; - res.json(registrationRequest); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - - return handler; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts deleted file mode 100644 index 9b137e66d..000000000 --- a/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts +++ /dev/null @@ -1,101 +0,0 @@ - -import sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import Assert = require("assert"); -import U2FSignPost = require("./post"); -import { ServerVariables } from "../../../../ServerVariables"; -import winston = require("winston"); - -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec"; -import ExpressMock = require("../../../../stubs/express.spec"); -import U2FMock = require("../../../../stubs/u2f.spec"); -import U2f = require("u2f"); -import { Level } from "../../../../authentication/Level"; - -describe("routes/secondfactor/u2f/sign/post", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.app = {}; - req.originalUrl = "/api/xxxx"; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - req.session = { - auth: { - userid: "user", - authentication_level: Level.ONE_FACTOR, - identity_check: { - challenge: "u2f-register", - userid: "user" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const options = { - inMemoryOnly: true - }; - - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - }); - - it("should return status code 204", function () { - const expectedStatus = { - keyHandle: "keyHandle", - publicKey: "pbk", - certificate: "cert" - }; - mocks.u2f.checkSignatureStub.returns(expectedStatus); - - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ - registration: { - publicKey: "PUBKEY" - } - })); - - req.session.auth.sign_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - return U2FSignPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(req.session.auth.authentication_level, Level.TWO_FACTOR); - }); - }); - - it("should return unauthorized error on registration request internal error", function () { - mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ - registration: { - publicKey: "PUBKEY" - } - })); - mocks.u2f.checkSignatureStub.returns({ errorCode: 500 }); - - req.session.auth.sign_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - return U2FSignPost.default(vars)(req as any, res as any) - .then(function () { - Assert.equal(res.status.getCall(0).args[0], 200); - Assert.deepEqual(res.send.getCall(0).args[0], - { error: "Operation failed." }); - }); - }); -}); - diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign/post.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign/post.ts deleted file mode 100644 index 7ee711c2c..000000000 --- a/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign/post.ts +++ /dev/null @@ -1,57 +0,0 @@ - -import objectPath = require("object-path"); -import u2f_common = require("../U2FCommon"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import { UserDataStore } from "../../../../storage/UserDataStore"; -import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; -import { Winston } from "../../../../../../types/Dependencies"; -import U2f = require("u2f"); -import exceptions = require("../../../../Exceptions"); -import redirect from "../../redirect"; -import ErrorReplies = require("../../../../ErrorReplies"); -import { ServerVariables } from "../../../../ServerVariables"; -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; -import { Level } from "../../../../authentication/Level"; - -export default function (vars: ServerVariables) { - function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const appId = u2f_common.extract_app_id(req); - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - if (!authSession.sign_request) { - const err = new Error("No sign request"); - ErrorReplies.replyWithError401(req, res, vars.logger)(err); - return reject(err); - } - resolve(); - }) - .then(function () { - const userid = authSession.userid; - return vars.userDataStore.retrieveU2FRegistration(userid, appId); - }) - .then(function (doc: U2FRegistrationDocument): BluebirdPromise { - const signRequest = authSession.sign_request; - const signData: U2f.SignatureData = req.body; - vars.logger.info(req, "Finish authentication"); - return BluebirdPromise.resolve(vars.u2f.checkSignature(signRequest, signData, doc.registration.publicKey)); - }) - .then(function (result: U2f.SignatureResult | U2f.Error): BluebirdPromise { - if (objectPath.has(result, "errorCode")) - return BluebirdPromise.reject(new Error("Error while signing")); - vars.logger.info(req, "Successful authentication"); - authSession.authentication_level = Level.TWO_FACTOR; - redirect(vars)(req, res); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - - return handler; -} - diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts deleted file mode 100644 index dd52b27e0..000000000 --- a/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ - -import sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); -import assert = require("assert"); -import U2FSignRequestGet = require("./get"); -import ExpressMock = require("../../../../stubs/express.spec"); -import { Request } from "u2f"; -import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec"; -import { ServerVariables } from "../../../../ServerVariables"; - -import { SignMessage } from "../../../../../../../shared/SignMessage"; - -describe("routes/secondfactor/u2f/sign_request/get", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.originalUrl = "/api/xxxx"; - req.app = {}; - req.session = { - auth: { - userid: "user", - first_factor: true, - second_factor: false, - identity_check: { - challenge: "u2f-register", - userid: "user" - } - } - }; - req.headers = {}; - req.headers.host = "localhost"; - - const s = ServerVariablesMockBuilder.build(); - mocks = s.mocks; - vars = s.variables; - - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - }); - - it("should send back the sign request and save it in the session", function () { - const expectedRequest: Request = { - version: "U2F_V2", - appId: 'app', - challenge: 'challenge!' - }; - mocks.u2f.requestStub.returns(expectedRequest); - mocks.userDataStore.retrieveU2FRegistrationStub - .returns(BluebirdPromise.resolve({ - registration: { - keyHandle: "KeyHandle" - } - })); - - return U2FSignRequestGet.default(vars)(req as any, res as any) - .then(() => { - assert.deepEqual(expectedRequest, req.session.auth.sign_request); - assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); - }); - }); -}); - diff --git a/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts b/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts deleted file mode 100644 index 9e93dde06..000000000 --- a/themes/triangles/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts +++ /dev/null @@ -1,42 +0,0 @@ - -import u2f_common = require("../../../secondfactor/u2f/U2FCommon"); -import BluebirdPromise = require("bluebird"); -import express = require("express"); -import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; -import exceptions = require("../../../../Exceptions"); -import ErrorReplies = require("../../../../ErrorReplies"); -import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; -import UserMessages = require("../../../../../../../shared/UserMessages"); -import { ServerVariables } from "../../../../ServerVariables"; -import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; - -export default function (vars: ServerVariables) { - function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession; - const appId = u2f_common.extract_app_id(req); - - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - resolve(); - }) - .then(function () { - return vars.userDataStore.retrieveU2FRegistration(authSession.userid, appId); - }) - .then(function (doc: U2FRegistrationDocument): BluebirdPromise { - if (!doc) - return BluebirdPromise.reject(new exceptions.AccessDeniedError("No U2F registration document found.")); - - const appId: string = u2f_common.extract_app_id(req); - vars.logger.info(req, "Start authentication of app '%s'", appId); - vars.logger.debug(req, "AppId = %s, keyHandle = %s", appId, JSON.stringify(doc.registration.keyHandle)); - - const request = vars.u2f.request(appId, doc.registration.keyHandle); - authSession.sign_request = request; - res.json(request); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, vars.logger, - UserMessages.OPERATION_FAILED)); - } - return handler; -} diff --git a/themes/triangles/server/src/lib/routes/verify/access_control.ts b/themes/triangles/server/src/lib/routes/verify/access_control.ts deleted file mode 100644 index 136239aeb..000000000 --- a/themes/triangles/server/src/lib/routes/verify/access_control.ts +++ /dev/null @@ -1,51 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import Util = require("util"); - -import Exceptions = require("../../Exceptions"); - -import { Level as AuthorizationLevel } from "../../authorization/Level"; -import { Level as AuthenticationLevel } from "../../authentication/Level"; -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { ServerVariables } from "../../ServerVariables"; - -function isAuthorized( - authorization: AuthorizationLevel, - authentication: AuthenticationLevel): boolean { - - if (authorization == AuthorizationLevel.BYPASS) { - return true; - } else if (authorization == AuthorizationLevel.ONE_FACTOR && - authentication >= AuthenticationLevel.ONE_FACTOR) { - return true; - } else if (authorization == AuthorizationLevel.TWO_FACTOR && - authentication >= AuthenticationLevel.TWO_FACTOR) { - return true; - } - return false; -} - -export default function ( - req: Express.Request, - vars: ServerVariables, - domain: string, resource: string, - user: string, groups: string[], - authenticationLevel: AuthenticationLevel) { - - return new BluebirdPromise(function (resolve, reject) { - const authorizationLevel = vars.authorizer - .authorization({domain, resource}, {user, groups}); - - if (!isAuthorized(authorizationLevel, authenticationLevel)) { - if (authorizationLevel == AuthorizationLevel.DENY) { - reject(new Exceptions.NotAuthorizedError( - Util.format("User %s is not authorized to access %s%s", user, domain, resource))); - return; - } - reject(new Exceptions.NotAuthenticatedError(Util.format( - "User '%s' is not sufficiently authorized to access %s%s.", user, domain, resource))); - return; - } - resolve(); - }); -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/verify/get.spec.ts b/themes/triangles/server/src/lib/routes/verify/get.spec.ts deleted file mode 100644 index 67cf19fb6..000000000 --- a/themes/triangles/server/src/lib/routes/verify/get.spec.ts +++ /dev/null @@ -1,320 +0,0 @@ - -import Assert = require("assert"); -import BluebirdPromise = require("bluebird"); -import Express = require("express"); -import Sinon = require("sinon"); -import winston = require("winston"); - -import VerifyGet = require("./get"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../types/AuthenticationSession"; -import ExpressMock = require("../../stubs/express.spec"); -import { ServerVariables } from "../../ServerVariables"; -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../ServerVariablesMockBuilder.spec"; -import { Level } from "../../authentication/Level"; -import { Level as AuthorizationLevel } from "../../authorization/Level"; - -describe("routes/verify/get", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - let authSession: AuthenticationSession; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - res = ExpressMock.ResponseMock(); - req.originalUrl = "/api/xxxx"; - req.query = { - redirect: "undefined" - }; - AuthenticationSessionHandler.reset(req as any); - req.headers["x-original-url"] = "https://secret.example.com/"; - const s = ServerVariablesMockBuilder.build(false); - mocks = s.mocks; - vars = s.variables; - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - }); - - describe("with session cookie", function () { - it("should be already authenticated", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser"); - Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); - Assert.equal(204, res.status.getCall(0).args[0]); - }); - }); - - function test_session(_authSession: AuthenticationSession, status_code: number) { - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert.equal(status_code, res.status.getCall(0).args[0]); - }); - } - - function test_non_authenticated_401(authSession: AuthenticationSession) { - return test_session(authSession, 401); - } - - function test_unauthorized_403(authSession: AuthenticationSession) { - return test_session(authSession, 403); - } - - function test_authorized(authSession: AuthenticationSession) { - return test_session(authSession, 204); - } - - describe("given user tries to access a 2-factor endpoint", function () { - before(function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - }); - - describe("given different cases of session", function () { - it("should not be authenticated when second factor is missing", function () { - return test_non_authenticated_401({ - keep_me_logged_in: false, - userid: "user", - authentication_level: Level.ONE_FACTOR, - email: undefined, - groups: [], - last_activity_datetime: new Date().getTime() - }); - }); - - it("should not be authenticated when userid is missing", function () { - return test_non_authenticated_401({ - keep_me_logged_in: false, - userid: undefined, - authentication_level: Level.TWO_FACTOR, - email: undefined, - groups: [], - last_activity_datetime: new Date().getTime() - }); - }); - - it("should not be authenticated when level is insufficient", function () { - return test_non_authenticated_401({ - keep_me_logged_in: false, - userid: "user", - authentication_level: Level.NOT_AUTHENTICATED, - email: undefined, - groups: [], - last_activity_datetime: new Date().getTime() - }); - }); - - it("should not be authenticated when session has not be initiated", function () { - return test_non_authenticated_401(undefined); - }); - - it("should not be authenticated when domain is not allowed for user", function () { - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - req.headers["x-original-url"] = "https://test.example.com/"; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.DENY); - - return test_unauthorized_403({ - keep_me_logged_in: false, - authentication_level: Level.TWO_FACTOR, - userid: "user", - groups: ["group1", "group2"], - email: undefined, - last_activity_datetime: new Date().getTime() - }); - }); - }); - }); - - describe("given user tries to access a single factor endpoint", function () { - beforeEach(function () { - req.headers["x-original-url"] = "https://redirect.url/"; - }); - - it("should be authenticated when first factor is validated", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); - authSession.authentication_level = Level.ONE_FACTOR; - authSession.userid = "user1"; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWith(204)); - Assert(res.send.calledOnce); - }); - }); - - it("should be rejected with 401 when not authenticated", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); - authSession.authentication_level = Level.NOT_AUTHENTICATED; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWith(401)); - }); - }); - }); - - describe("inactivity period", function () { - it("should update last inactivity period on requests on /api/verify", function () { - mocks.config.session.inactivity = 200000; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - const currentTime = new Date().getTime() - 1000; - AuthenticationSessionHandler.reset(req as any); - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - authSession.last_activity_datetime = currentTime; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - return AuthenticationSessionHandler.get(req as any, vars.logger); - }) - .then(function (authSession) { - Assert(authSession.last_activity_datetime > currentTime); - }); - }); - - it("should reset session when max inactivity period has been reached", function () { - mocks.config.session.inactivity = 1; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - const currentTime = new Date().getTime() - 1000; - AuthenticationSessionHandler.reset(req as any); - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - authSession.last_activity_datetime = currentTime; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - return AuthenticationSessionHandler.get(req as any, vars.logger); - }) - .then(function (authSession) { - Assert.equal(authSession.authentication_level, Level.NOT_AUTHENTICATED); - Assert.equal(authSession.userid, undefined); - }); - }); - }); - }); - - describe("response type 401 | 302", function() { - it("should return error code 401", function() { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( - "Invalid credentials")); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should redirect to provided redirection url", function() { - const REDIRECT_URL = "http://redirection_url.com"; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( - "Invalid credentials")); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - req.query["rd"] = REDIRECT_URL; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.redirect.calledWithExactly(REDIRECT_URL)); - }); - }); - }); - - describe("with basic auth", function () { - it("should authenticate correctly", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.returns({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "john"); - Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); - Assert.equal(204, res.status.getCall(0).args[0]); - }); - }); - - it("should fail when endpoint is protected by two factors", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.config.access_control.rules = [{ - domain: "secret.example.com", - policy: "two_factor" - }]; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when base64 token is not valid", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic i_m*not_a_base64*token"; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when base64 token has not format user:psswd", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzOmJhZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when bad user password is provided", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( - "Invalid credentials")); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when resource is restricted", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - }); -}); - diff --git a/themes/triangles/server/src/lib/routes/verify/get.ts b/themes/triangles/server/src/lib/routes/verify/get.ts deleted file mode 100644 index f73861696..000000000 --- a/themes/triangles/server/src/lib/routes/verify/get.ts +++ /dev/null @@ -1,91 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Express = require("express"); -import Exceptions = require("../../Exceptions"); -import ErrorReplies = require("../../ErrorReplies"); -import { ServerVariables } from "../../ServerVariables"; -import GetWithSessionCookieMethod from "./get_session_cookie"; -import GetWithBasicAuthMethod from "./get_basic_auth"; -import Constants = require("../../../../../shared/constants"); -import ObjectPath = require("object-path"); - -import { AuthenticationSessionHandler } - from "../../AuthenticationSessionHandler"; -import { AuthenticationSession } - from "../../../../types/AuthenticationSession"; - -const REMOTE_USER = "Remote-User"; -const REMOTE_GROUPS = "Remote-Groups"; - - -function verifyWithSelectedMethod(req: Express.Request, res: Express.Response, - vars: ServerVariables, authSession: AuthenticationSession) - : () => BluebirdPromise<{ username: string, groups: string[] }> { - return function () { - const authorization: string = "" + req.headers["proxy-authorization"]; - if (authorization && authorization.startsWith("Basic ")) - return GetWithBasicAuthMethod(req, res, vars, authorization); - - return GetWithSessionCookieMethod(req, res, vars, authSession); - }; -} - -function setRedirectHeader(req: Express.Request, res: Express.Response) { - return function () { - const originalUrl = ObjectPath.get( - req, "headers.x-original-url"); - res.set("Redirect", originalUrl); - return BluebirdPromise.resolve(); - }; -} - -function setUserAndGroupsHeaders(res: Express.Response) { - return function (u: { username: string, groups: string[] }) { - res.setHeader(REMOTE_USER, u.username); - res.setHeader(REMOTE_GROUPS, u.groups.join(",")); - return BluebirdPromise.resolve(); - }; -} - -function replyWith200(res: Express.Response) { - return function () { - res.status(204); - res.send(); - }; -} - -function getRedirectParam(req: Express.Request) { - return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : undefined; -} - -export default function (vars: ServerVariables) { - return function (req: Express.Request, res: Express.Response) - : BluebirdPromise { - let authSession: AuthenticationSession; - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - resolve(); - }) - .then(setRedirectHeader(req, res)) - .then(verifyWithSelectedMethod(req, res, vars, authSession)) - .then(setUserAndGroupsHeaders(res)) - .then(replyWith200(res)) - // The user is authenticated but has restricted access -> 403 - .catch(Exceptions.NotAuthorizedError, - ErrorReplies.replyWithError403(req, res, vars.logger)) - .catch(Exceptions.NotAuthenticatedError, - ErrorReplies.replyWithError401(req, res, vars.logger)) - // The user is not yet authenticated -> 401 - .catch((err) => { - const redirectUrl = getRedirectParam(req); - if (redirectUrl) { - ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err); - } - else { - ErrorReplies.replyWithError401(req, res, vars.logger)(err); - } - }); - }; -} - diff --git a/themes/triangles/server/src/lib/routes/verify/get_basic_auth.ts b/themes/triangles/server/src/lib/routes/verify/get_basic_auth.ts deleted file mode 100644 index af23c76c9..000000000 --- a/themes/triangles/server/src/lib/routes/verify/get_basic_auth.ts +++ /dev/null @@ -1,55 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import ObjectPath = require("object-path"); -import { ServerVariables } from "../../ServerVariables"; -import { AuthenticationSession } - from "../../../../types/AuthenticationSession"; -import AccessControl from "./access_control"; -import { URLDecomposer } from "../../utils/URLDecomposer"; -import { Level } from "../../authentication/Level"; - -export default function (req: Express.Request, res: Express.Response, - vars: ServerVariables, authorizationHeader: string) - : BluebirdPromise<{ username: string, groups: string[] }> { - let username: string; - const uri = ObjectPath.get(req, "headers.x-original-url"); - const urlDecomposition = URLDecomposer.fromUrl(uri); - - return BluebirdPromise.resolve() - .then(() => { - const base64Re = new RegExp("^Basic ((?:[A-Za-z0-9+/]{4})*" + - "(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)$"); - const isTokenValidBase64 = base64Re.test(authorizationHeader); - - if (!isTokenValidBase64) { - return BluebirdPromise.reject(new Error("No valid base64 token found in the header")); - } - - const tokenMatches = authorizationHeader.match(base64Re); - const base64Token = tokenMatches[1]; - const decodedToken = Buffer.from(base64Token, "base64").toString(); - const splittedToken = decodedToken.split(":"); - - if (splittedToken.length != 2) { - return BluebirdPromise.reject(new Error( - "The authorization token is invalid. Expecting 'userid:password'")); - } - - username = splittedToken[0]; - const password = splittedToken[1]; - return vars.usersDatabase.checkUserPassword(username, password); - }) - .then(function (groupsAndEmails) { - return AccessControl(req, vars, urlDecomposition.domain, urlDecomposition.path, - username, groupsAndEmails.groups, Level.ONE_FACTOR) - .then(() => BluebirdPromise.resolve({ - username: username, - groups: groupsAndEmails.groups - })); - }) - .catch(function (err: Error) { - return BluebirdPromise.reject( - new Error("Unable to authenticate the user with basic auth. Cause: " - + err.message)); - }); -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/routes/verify/get_session_cookie.ts b/themes/triangles/server/src/lib/routes/verify/get_session_cookie.ts deleted file mode 100644 index 070344812..000000000 --- a/themes/triangles/server/src/lib/routes/verify/get_session_cookie.ts +++ /dev/null @@ -1,78 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import Util = require("util"); -import ObjectPath = require("object-path"); - -import Exceptions = require("../../Exceptions"); -import { Configuration } from "../../configuration/schema/Configuration"; -import { ServerVariables } from "../../ServerVariables"; -import { IRequestLogger } from "../../logging/IRequestLogger"; -import { AuthenticationSession } - from "../../../../types/AuthenticationSession"; -import { AuthenticationSessionHandler } - from "../../AuthenticationSessionHandler"; -import AccessControl from "./access_control"; -import { URLDecomposer } from "../../utils/URLDecomposer"; - -function verify_inactivity(req: Express.Request, - authSession: AuthenticationSession, - configuration: Configuration, logger: IRequestLogger) - : BluebirdPromise { - - // If inactivity is not specified, then inactivity timeout does not apply - if (!configuration.session.inactivity || authSession.keep_me_logged_in) { - return BluebirdPromise.resolve(); - } - - const lastActivityTime = authSession.last_activity_datetime; - const currentTime = new Date().getTime(); - authSession.last_activity_datetime = currentTime; - - const inactivityPeriodMs = currentTime - lastActivityTime; - logger.debug(req, "Inactivity period was %s s and max period was %s.", - inactivityPeriodMs / 1000, configuration.session.inactivity / 1000); - if (inactivityPeriodMs < configuration.session.inactivity) { - return BluebirdPromise.resolve(); - } - - logger.debug(req, "Session has been reset after too long inactivity period."); - AuthenticationSessionHandler.reset(req); - return BluebirdPromise.reject(new Error("Inactivity period exceeded.")); -} - -export default function (req: Express.Request, res: Express.Response, - vars: ServerVariables, authSession: AuthenticationSession) - : BluebirdPromise<{ username: string, groups: string[] }> { - - return BluebirdPromise.resolve() - .then(() => { - const username = authSession.userid; - const groups = authSession.groups; - - if (!authSession.userid) { - return BluebirdPromise.reject(new Exceptions.AccessDeniedError( - "userid is missing")); - } - - const originalUrl = ObjectPath.get( - req, "headers.x-original-url"); - const originalUri = - ObjectPath.get(req, "headers.x-original-uri"); - - const d = URLDecomposer.fromUrl(originalUrl); - vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", d.domain, - d.path, username, groups.join(",")); - return AccessControl(req, vars, d.domain, d.path, username, groups, - authSession.authentication_level); - }) - .then(() => { - return verify_inactivity(req, authSession, - vars.config, vars.logger); - }) - .then(() => { - return BluebirdPromise.resolve({ - username: authSession.userid, - groups: authSession.groups - }); - }); -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/AuthenticationTraceDocument.d.ts b/themes/triangles/server/src/lib/storage/AuthenticationTraceDocument.d.ts deleted file mode 100644 index 69818c055..000000000 --- a/themes/triangles/server/src/lib/storage/AuthenticationTraceDocument.d.ts +++ /dev/null @@ -1,6 +0,0 @@ - -export interface AuthenticationTraceDocument { - userId: string; - date: Date; - isAuthenticationSuccessful: boolean; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/CollectionFactoryFactory.ts b/themes/triangles/server/src/lib/storage/CollectionFactoryFactory.ts deleted file mode 100644 index 92b29abfa..000000000 --- a/themes/triangles/server/src/lib/storage/CollectionFactoryFactory.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ICollectionFactory } from "./ICollectionFactory"; -import { NedbCollectionFactory } from "./nedb/NedbCollectionFactory"; -import { MongoCollectionFactory } from "./mongo/MongoCollectionFactory"; -import { IMongoClient } from "../connectors/mongo/IMongoClient"; - - -export class CollectionFactoryFactory { - static createNedb(options: Nedb.DataStoreOptions): ICollectionFactory { - return new NedbCollectionFactory(options); - } - - static createMongo(client: IMongoClient): ICollectionFactory { - return new MongoCollectionFactory(client); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/CollectionFactoryStub.spec.ts b/themes/triangles/server/src/lib/storage/CollectionFactoryStub.spec.ts deleted file mode 100644 index 17f8bb021..000000000 --- a/themes/triangles/server/src/lib/storage/CollectionFactoryStub.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Sinon = require("sinon"); -import { ICollection } from "./ICollection"; -import { ICollectionFactory } from "./ICollectionFactory"; - -export class CollectionFactoryStub implements ICollectionFactory { - buildStub: Sinon.SinonStub; - - constructor() { - this.buildStub = Sinon.stub(); - } - - build(collectionName: string): ICollection { - return this.buildStub(collectionName); - } -} diff --git a/themes/triangles/server/src/lib/storage/CollectionStub.spec.ts b/themes/triangles/server/src/lib/storage/CollectionStub.spec.ts deleted file mode 100644 index 42895d672..000000000 --- a/themes/triangles/server/src/lib/storage/CollectionStub.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Sinon = require("sinon"); -import { ICollection } from "./ICollection"; - -export class CollectionStub implements ICollection { - findStub: Sinon.SinonStub; - findOneStub: Sinon.SinonStub; - updateStub: Sinon.SinonStub; - removeStub: Sinon.SinonStub; - insertStub: Sinon.SinonStub; - - constructor() { - this.findStub = Sinon.stub(); - this.findOneStub = Sinon.stub(); - this.updateStub = Sinon.stub(); - this.removeStub = Sinon.stub(); - this.insertStub = Sinon.stub(); - } - - find(filter: any, sortKeys: any, count: number): BluebirdPromise { - return this.findStub(filter, sortKeys, count); - } - - findOne(filter: any): BluebirdPromise { - return this.findOneStub(filter); - } - - update(filter: any, document: any, options: any): BluebirdPromise { - return this.updateStub(filter, document, options); - } - - remove(filter: any): BluebirdPromise { - return this.removeStub(filter); - } - - insert(document: any): BluebirdPromise { - return this.insertStub(document); - } -} diff --git a/themes/triangles/server/src/lib/storage/ICollection.d.ts b/themes/triangles/server/src/lib/storage/ICollection.d.ts deleted file mode 100644 index caa6c2a87..000000000 --- a/themes/triangles/server/src/lib/storage/ICollection.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* istanbul ignore next */ -import BluebirdPromise = require("bluebird"); - -/* istanbul ignore next */ -export interface ICollection { - find(query: any, sortKeys: any, count: number): BluebirdPromise; - findOne(query: any): BluebirdPromise; - update(query: any, updateQuery: any, options?: any): BluebirdPromise; - remove(query: any): BluebirdPromise; - insert(document: any): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/ICollectionFactory.d.ts b/themes/triangles/server/src/lib/storage/ICollectionFactory.d.ts deleted file mode 100644 index 39eb42c77..000000000 --- a/themes/triangles/server/src/lib/storage/ICollectionFactory.d.ts +++ /dev/null @@ -1,6 +0,0 @@ - -import { ICollection } from "./ICollection"; - -export interface ICollectionFactory { - build(collectionName: string): ICollection; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/IUserDataStore.d.ts b/themes/triangles/server/src/lib/storage/IUserDataStore.d.ts deleted file mode 100644 index 81df482aa..000000000 --- a/themes/triangles/server/src/lib/storage/IUserDataStore.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import { TOTPSecretDocument } from "./TOTPSecretDocument"; -import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; -import { U2FRegistration } from "../../../types/U2FRegistration"; -import { TOTPSecret } from "../../../types/TOTPSecret"; -import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; -import { IdentityValidationDocument } from "./IdentityValidationDocument"; - -export interface IUserDataStore { - saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise; - retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise; - - saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; - retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise; - - produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise; - consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise; - - saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise; - retrieveTOTPSecret(userId: string): BluebirdPromise; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/IdentityValidationDocument.d.ts b/themes/triangles/server/src/lib/storage/IdentityValidationDocument.d.ts deleted file mode 100644 index e7fd7b3f4..000000000 --- a/themes/triangles/server/src/lib/storage/IdentityValidationDocument.d.ts +++ /dev/null @@ -1,7 +0,0 @@ - -export interface IdentityValidationDocument { - userId: string; - token: string; - challenge: string; - maxDate: Date; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/TOTPSecretDocument.d.ts b/themes/triangles/server/src/lib/storage/TOTPSecretDocument.d.ts deleted file mode 100644 index a6c0bf9e6..000000000 --- a/themes/triangles/server/src/lib/storage/TOTPSecretDocument.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { TOTPSecret } from "../../../types/TOTPSecret"; - -export interface TOTPSecretDocument { - userid: string; - secret: TOTPSecret; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/U2FRegistrationDocument.d.ts b/themes/triangles/server/src/lib/storage/U2FRegistrationDocument.d.ts deleted file mode 100644 index efec6cb1d..000000000 --- a/themes/triangles/server/src/lib/storage/U2FRegistrationDocument.d.ts +++ /dev/null @@ -1,8 +0,0 @@ - -import { U2FRegistration } from "../../../types/U2FRegistration"; - -export interface U2FRegistrationDocument { - userId: string; - appId: string; - registration: U2FRegistration; -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/UserDataStore.spec.ts b/themes/triangles/server/src/lib/storage/UserDataStore.spec.ts deleted file mode 100644 index 66fb85461..000000000 --- a/themes/triangles/server/src/lib/storage/UserDataStore.spec.ts +++ /dev/null @@ -1,264 +0,0 @@ - -import * as Assert from "assert"; -import * as Sinon from "sinon"; -import * as MockDate from "mockdate"; -import BluebirdPromise = require("bluebird"); - -import { UserDataStore } from "./UserDataStore"; -import { TOTPSecret } from "../../../types/TOTPSecret"; -import { U2FRegistration } from "../../../types/U2FRegistration"; -import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; -import { CollectionStub } from "./CollectionStub.spec"; -import { CollectionFactoryStub } from "./CollectionFactoryStub.spec"; - -describe("storage/UserDataStore", function () { - let factory: CollectionFactoryStub; - let collection: CollectionStub; - let userId: string; - let appId: string; - let totpSecret: TOTPSecret; - let u2fRegistration: U2FRegistration; - - beforeEach(function () { - factory = new CollectionFactoryStub(); - collection = new CollectionStub(); - - userId = "user"; - appId = "https://myappId"; - - totpSecret = { - ascii: "abc", - base32: "ABCDKZLEFZGREJK", - otpauth_url: "totp://test", - google_auth_qr: "dummy", - hex: "dummy", - qr_code_ascii: "dummy", - qr_code_base32: "dummy", - qr_code_hex: "dummy" - }; - - u2fRegistration = { - keyHandle: "KEY_HANDLE", - publicKey: "publickey" - }; - }); - - it("should correctly creates collections", function () { - new UserDataStore(factory); - - Assert.equal(4, factory.buildStub.callCount); - Assert(factory.buildStub.calledWith("authentication_traces")); - Assert(factory.buildStub.calledWith("identity_validation_tokens")); - Assert(factory.buildStub.calledWith("u2f_registrations")); - Assert(factory.buildStub.calledWith("totp_secrets")); - }); - - describe("TOTP secrets collection", function () { - it("should save a totp secret", function () { - factory.buildStub.returns(collection); - collection.updateStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.saveTOTPSecret(userId, totpSecret) - .then(function (doc) { - Assert(collection.updateStub.calledOnce); - Assert(collection.updateStub.calledWith({ userId: userId }, { - userId: userId, - secret: totpSecret - }, { upsert: true })); - return BluebirdPromise.resolve(); - }); - }); - - it("should retrieve a totp secret", function () { - factory.buildStub.returns(collection); - collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.retrieveTOTPSecret(userId) - .then(function (doc) { - Assert(collection.findOneStub.calledOnce); - Assert(collection.findOneStub.calledWith({ userId: userId })); - return BluebirdPromise.resolve(); - }); - }); - }); - - describe("U2F secrets collection", function () { - it("should save a U2F secret", function () { - factory.buildStub.returns(collection); - collection.updateStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.saveU2FRegistration(userId, appId, u2fRegistration) - .then(function (doc) { - Assert(collection.updateStub.calledOnce); - Assert(collection.updateStub.calledWith({ - userId: userId, - appId: appId - }, { - userId: userId, - appId: appId, - registration: u2fRegistration - }, { upsert: true })); - return BluebirdPromise.resolve(); - }); - }); - - it("should retrieve a U2F secret", function () { - factory.buildStub.returns(collection); - collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.retrieveU2FRegistration(userId, appId) - .then(function (doc) { - Assert(collection.findOneStub.calledOnce); - Assert(collection.findOneStub.calledWith({ - userId: userId, - appId: appId - })); - return BluebirdPromise.resolve(); - }); - }); - }); - - - describe("Regulator traces collection", function () { - it("should save a trace", function () { - factory.buildStub.returns(collection); - collection.insertStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.saveAuthenticationTrace(userId, true) - .then(function (doc) { - Assert(collection.insertStub.calledOnce); - Assert(collection.insertStub.calledWith({ - userId: userId, - date: Sinon.match.date, - isAuthenticationSuccessful: true - })); - return BluebirdPromise.resolve(); - }); - }); - - function should_retrieve_latest_authentication_traces(count: number) { - factory.buildStub.returns(collection); - collection.findStub.withArgs().returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - return dataStore.retrieveLatestAuthenticationTraces(userId, count) - .then(function (doc: AuthenticationTraceDocument[]) { - Assert(collection.findStub.calledOnce); - Assert(collection.findStub.calledWith({ - userId: userId, - }, { date: -1 }, count)); - return BluebirdPromise.resolve(); - }); - } - - it("should retrieve 3 latest failed authentication traces", function () { - should_retrieve_latest_authentication_traces(3); - }); - }); - - - describe("Identity validation collection", function () { - it("should save a identity validation token", function () { - factory.buildStub.returns(collection); - collection.insertStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - const maxAge = 400; - const token = "TOKEN"; - const challenge = "CHALLENGE"; - - return dataStore.produceIdentityValidationToken(userId, token, challenge, maxAge) - .then(function (doc) { - Assert(collection.insertStub.calledOnce); - Assert(collection.insertStub.calledWith({ - userId: userId, - token: token, - challenge: challenge, - maxDate: Sinon.match.date - })); - return BluebirdPromise.resolve(); - }); - }); - - it("should consume an identity token successfully", function () { - factory.buildStub.returns(collection); - - MockDate.set(100); - - const token = "TOKEN"; - const challenge = "CHALLENGE"; - - collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ - userId: "USER", - token: token, - challenge: challenge, - maxDate: new Date() - })); - collection.removeStub.returns(BluebirdPromise.resolve()); - - const dataStore = new UserDataStore(factory); - - MockDate.set(80); - - return dataStore.consumeIdentityValidationToken(token, challenge) - .then(function (doc) { - MockDate.reset(); - Assert(collection.findOneStub.calledOnce); - Assert(collection.findOneStub.calledWith({ - token: token, - challenge: challenge - })); - - Assert(collection.removeStub.calledOnce); - Assert(collection.removeStub.calledWith({ - token: token, - challenge: challenge - })); - return BluebirdPromise.resolve(); - }); - }); - - it("should consume an expired identity token", function () { - factory.buildStub.returns(collection); - - MockDate.set(0); - - const token = "TOKEN"; - const challenge = "CHALLENGE"; - - collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ - userId: "USER", - token: token, - challenge: challenge, - maxDate: new Date() - })); - - const dataStore = new UserDataStore(factory); - - MockDate.set(80000); - - return dataStore.consumeIdentityValidationToken(token, challenge) - .then(function () { return BluebirdPromise.reject(new Error("should not be here")); }) - .catch(function () { - MockDate.reset(); - Assert(collection.findOneStub.calledOnce); - Assert(collection.findOneStub.calledWith({ - token: token, - challenge: challenge - })); - return BluebirdPromise.resolve(); - }); - }); - }); -}); diff --git a/themes/triangles/server/src/lib/storage/UserDataStore.ts b/themes/triangles/server/src/lib/storage/UserDataStore.ts deleted file mode 100644 index 27b0cddbd..000000000 --- a/themes/triangles/server/src/lib/storage/UserDataStore.ts +++ /dev/null @@ -1,143 +0,0 @@ -import * as BluebirdPromise from "bluebird"; -import * as path from "path"; -import { IUserDataStore } from "./IUserDataStore"; -import { ICollection } from "./ICollection"; -import { ICollectionFactory } from "./ICollectionFactory"; -import { TOTPSecretDocument } from "./TOTPSecretDocument"; -import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; -import { U2FRegistration } from "../../../types/U2FRegistration"; -import { TOTPSecret } from "../../../types/TOTPSecret"; -import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; -import { IdentityValidationDocument } from "./IdentityValidationDocument"; - -// Constants - -const IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME = "identity_validation_tokens"; -const AUTHENTICATION_TRACES_COLLECTION_NAME = "authentication_traces"; - -const U2F_REGISTRATIONS_COLLECTION_NAME = "u2f_registrations"; -const TOTP_SECRETS_COLLECTION_NAME = "totp_secrets"; - - -export interface U2FRegistrationKey { - userId: string; - appId: string; -} - -// Source - -export class UserDataStore implements IUserDataStore { - private u2fSecretCollection: ICollection; - private identityCheckTokensCollection: ICollection; - private authenticationTracesCollection: ICollection; - private totpSecretCollection: ICollection; - - private collectionFactory: ICollectionFactory; - - constructor(collectionFactory: ICollectionFactory) { - this.collectionFactory = collectionFactory; - - this.u2fSecretCollection = this.collectionFactory.build(U2F_REGISTRATIONS_COLLECTION_NAME); - this.identityCheckTokensCollection = this.collectionFactory.build(IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME); - this.authenticationTracesCollection = this.collectionFactory.build(AUTHENTICATION_TRACES_COLLECTION_NAME); - this.totpSecretCollection = this.collectionFactory.build(TOTP_SECRETS_COLLECTION_NAME); - } - - saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { - const newDocument: U2FRegistrationDocument = { - userId: userId, - appId: appId, - registration: registration - }; - - const filter: U2FRegistrationKey = { - userId: userId, - appId: appId - }; - - return this.u2fSecretCollection.update(filter, newDocument, { upsert: true }); - } - - retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { - const filter: U2FRegistrationKey = { - userId: userId, - appId: appId - }; - return this.u2fSecretCollection.findOne(filter); - } - - saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { - const newDocument: AuthenticationTraceDocument = { - userId: userId, - date: new Date(), - isAuthenticationSuccessful: isAuthenticationSuccessful, - }; - - return this.authenticationTracesCollection.insert(newDocument); - } - - retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { - const q = { - userId: userId - }; - - return this.authenticationTracesCollection.find(q, { date: -1 }, count); - } - - produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { - const newDocument: IdentityValidationDocument = { - userId: userId, - token: token, - challenge: challenge, - maxDate: new Date(new Date().getTime() + maxAge) - }; - - return this.identityCheckTokensCollection.insert(newDocument); - } - - consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { - const that = this; - const filter = { - token: token, - challenge: challenge - }; - - let identityValidationDocument: IdentityValidationDocument; - - return this.identityCheckTokensCollection.findOne(filter) - .then(function (doc: IdentityValidationDocument) { - if (!doc) { - return BluebirdPromise.reject(new Error("Registration token does not exist")); - } - - identityValidationDocument = doc; - const current_date = new Date(); - if (current_date > doc.maxDate) - return BluebirdPromise.reject(new Error("Registration token is not valid anymore")); - - return that.identityCheckTokensCollection.remove(filter); - }) - .then(() => { - return BluebirdPromise.resolve(identityValidationDocument); - }); - } - - saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { - const doc = { - userId: userId, - secret: secret - }; - - const filter = { - userId: userId - }; - return this.totpSecretCollection.update(filter, doc, { upsert: true }); - } - - retrieveTOTPSecret(userId: string): BluebirdPromise { - const filter = { - userId: userId - }; - return this.totpSecretCollection.findOne(filter); - } -} diff --git a/themes/triangles/server/src/lib/storage/UserDataStoreStub.spec.ts b/themes/triangles/server/src/lib/storage/UserDataStoreStub.spec.ts deleted file mode 100644 index 5ea27a2de..000000000 --- a/themes/triangles/server/src/lib/storage/UserDataStoreStub.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import Sinon = require("sinon"); -import BluebirdPromise = require("bluebird"); - -import { TOTPSecretDocument } from "./TOTPSecretDocument"; -import { U2FRegistrationDocument } from "./U2FRegistrationDocument"; -import { U2FRegistration } from "../../../types/U2FRegistration"; -import { TOTPSecret } from "../../../types/TOTPSecret"; -import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument"; -import { IdentityValidationDocument } from "./IdentityValidationDocument"; -import { IUserDataStore } from "./IUserDataStore"; - -export class UserDataStoreStub implements IUserDataStore { - saveU2FRegistrationStub: Sinon.SinonStub; - retrieveU2FRegistrationStub: Sinon.SinonStub; - saveAuthenticationTraceStub: Sinon.SinonStub; - retrieveLatestAuthenticationTracesStub: Sinon.SinonStub; - produceIdentityValidationTokenStub: Sinon.SinonStub; - consumeIdentityValidationTokenStub: Sinon.SinonStub; - saveTOTPSecretStub: Sinon.SinonStub; - retrieveTOTPSecretStub: Sinon.SinonStub; - - constructor() { - this.saveU2FRegistrationStub = Sinon.stub(); - this.retrieveU2FRegistrationStub = Sinon.stub(); - this.saveAuthenticationTraceStub = Sinon.stub(); - this.retrieveLatestAuthenticationTracesStub = Sinon.stub(); - this.produceIdentityValidationTokenStub = Sinon.stub(); - this.consumeIdentityValidationTokenStub = Sinon.stub(); - this.saveTOTPSecretStub = Sinon.stub(); - this.retrieveTOTPSecretStub = Sinon.stub(); - } - - saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { - return this.saveU2FRegistrationStub(userId, appId, registration); - } - - retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { - return this.retrieveU2FRegistrationStub(userId, appId); - } - - saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { - return this.saveAuthenticationTraceStub(userId, isAuthenticationSuccessful); - } - - retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { - return this.retrieveLatestAuthenticationTracesStub(userId, count); - } - - produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { - return this.produceIdentityValidationTokenStub(userId, token, challenge, maxAge); - } - - consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { - return this.consumeIdentityValidationTokenStub(token, challenge); - } - - saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { - return this.saveTOTPSecretStub(userId, secret); - } - - retrieveTOTPSecret(userId: string): BluebirdPromise { - return this.retrieveTOTPSecretStub(userId); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/mongo/MongoCollection.spec.ts b/themes/triangles/server/src/lib/storage/mongo/MongoCollection.spec.ts deleted file mode 100644 index 74a773a1e..000000000 --- a/themes/triangles/server/src/lib/storage/mongo/MongoCollection.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -import Assert = require("assert"); -import Sinon = require("sinon"); -import MongoDB = require("mongodb"); -import BluebirdPromise = require("bluebird"); -import { MongoClientStub } from "../../connectors/mongo/MongoClientStub.spec"; -import { MongoCollection } from "./MongoCollection"; - -describe("storage/mongo/MongoCollection", function () { - let mongoCollectionStub: any; - let mongoClientStub: MongoClientStub; - let findStub: Sinon.SinonStub; - let findOneStub: Sinon.SinonStub; - let insertOneStub: Sinon.SinonStub; - let updateStub: Sinon.SinonStub; - let removeStub: Sinon.SinonStub; - let countStub: Sinon.SinonStub; - const COLLECTION_NAME = "collection"; - - before(function () { - mongoClientStub = new MongoClientStub(); - mongoCollectionStub = Sinon.createStubInstance(require("mongodb").Collection as any); - findStub = mongoCollectionStub.find as Sinon.SinonStub; - findOneStub = mongoCollectionStub.findOne as Sinon.SinonStub; - insertOneStub = mongoCollectionStub.insertOne as Sinon.SinonStub; - updateStub = mongoCollectionStub.update as Sinon.SinonStub; - removeStub = mongoCollectionStub.remove as Sinon.SinonStub; - countStub = mongoCollectionStub.count as Sinon.SinonStub; - mongoClientStub.collectionStub.returns( - BluebirdPromise.resolve(mongoCollectionStub) - ); - }); - - describe("find", function () { - it("should find a document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - findStub.returns({ - sort: Sinon.stub().returns({ - limit: Sinon.stub().returns({ - toArray: Sinon.stub().returns(BluebirdPromise.resolve([])) - }) - }) - }); - - return collection.find({ key: "KEY" }) - .then(function () { - Assert(findStub.calledWith({ key: "KEY" })); - }); - }); - }); - - describe("findOne", function () { - it("should find one document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - findOneStub.returns(BluebirdPromise.resolve({})); - - return collection.findOne({ key: "KEY" }) - .then(function () { - Assert(findOneStub.calledWith({ key: "KEY" })); - }); - }); - }); - - describe("insert", function () { - it("should insert a document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - insertOneStub.returns(BluebirdPromise.resolve({})); - - return collection.insert({ key: "KEY" }) - .then(function () { - Assert(insertOneStub.calledWith({ key: "KEY" })); - }); - }); - }); - - describe("update", function () { - it("should update a document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - updateStub.returns(BluebirdPromise.resolve({})); - - return collection.update({ key: "KEY" }, { key: "KEY", value: 1 }) - .then(function () { - Assert(updateStub.calledWith({ key: "KEY" }, { key: "KEY", value: 1 })); - }); - }); - }); - - describe("remove", function () { - it("should remove a document in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - removeStub.returns(BluebirdPromise.resolve({})); - - return collection.remove({ key: "KEY" }) - .then(function () { - Assert(removeStub.calledWith({ key: "KEY" })); - }); - }); - }); - - describe("count", function () { - it("should count documents in the collection", function () { - const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - countStub.returns(BluebirdPromise.resolve({})); - - return collection.count({ key: "KEY" }) - .then(function () { - Assert(countStub.calledWith({ key: "KEY" })); - }); - }); - }); -}); diff --git a/themes/triangles/server/src/lib/storage/mongo/MongoCollection.ts b/themes/triangles/server/src/lib/storage/mongo/MongoCollection.ts deleted file mode 100644 index 9771389f1..000000000 --- a/themes/triangles/server/src/lib/storage/mongo/MongoCollection.ts +++ /dev/null @@ -1,50 +0,0 @@ -import Bluebird = require("bluebird"); -import { ICollection } from "../ICollection"; -import MongoDB = require("mongodb"); -import { IMongoClient } from "../../connectors/mongo/IMongoClient"; - - -export class MongoCollection implements ICollection { - private mongoClient: IMongoClient; - private collectionName: string; - - constructor(collectionName: string, mongoClient: IMongoClient) { - this.collectionName = collectionName; - this.mongoClient = mongoClient; - } - - private collection(): Bluebird { - return this.mongoClient.collection(this.collectionName); - } - - find(query: any, sortKeys?: any, count?: number): Bluebird { - return this.collection() - .then((collection) => collection.find(query).sort(sortKeys).limit(count)) - .then((query) => query.toArray()); - } - - findOne(query: any): Bluebird { - return this.collection() - .then((collection) => collection.findOne(query)); - } - - update(query: any, updateQuery: any, options?: any): Bluebird { - return this.collection() - .then((collection) => collection.update(query, updateQuery, options)); - } - - remove(query: any): Bluebird { - return this.collection() - .then((collection) => collection.remove(query)); - } - - insert(document: any): Bluebird { - return this.collection() - .then((collection) => collection.insertOne(document)); - } - - count(query: any): Bluebird { - return this.collection() - .then((collection) => collection.count(query)); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts b/themes/triangles/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts deleted file mode 100644 index bd959cacb..000000000 --- a/themes/triangles/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Assert = require("assert"); -import Sinon = require("sinon"); -import { MongoClientStub } from "../../connectors/mongo/MongoClientStub.spec"; -import { MongoCollectionFactory } from "./MongoCollectionFactory"; - -describe("storage/mongo/MongoCollectionFactory", function () { - let mongoClient: MongoClientStub; - - before(function() { - mongoClient = new MongoClientStub(); - }); - - describe("create", function () { - it("should create a collection", function () { - const COLLECTION_NAME = "COLLECTION_NAME"; - - const factory = new MongoCollectionFactory(mongoClient); - Assert(factory.build(COLLECTION_NAME)); - }); - }); -}); diff --git a/themes/triangles/server/src/lib/storage/mongo/MongoCollectionFactory.ts b/themes/triangles/server/src/lib/storage/mongo/MongoCollectionFactory.ts deleted file mode 100644 index 14a8262c9..000000000 --- a/themes/triangles/server/src/lib/storage/mongo/MongoCollectionFactory.ts +++ /dev/null @@ -1,19 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import { ICollection } from "../ICollection"; -import { ICollectionFactory } from "../ICollectionFactory"; -import { MongoCollection } from "./MongoCollection"; -import path = require("path"); -import MongoDB = require("mongodb"); -import { IMongoClient } from "../../connectors/mongo/IMongoClient"; - -export class MongoCollectionFactory implements ICollectionFactory { - private mongoClient: IMongoClient; - - constructor(mongoClient: IMongoClient) { - this.mongoClient = mongoClient; - } - - build(collectionName: string): ICollection { - return new MongoCollection(collectionName, this.mongoClient); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/nedb/NedbCollection.spec.ts b/themes/triangles/server/src/lib/storage/nedb/NedbCollection.spec.ts deleted file mode 100644 index a69962b67..000000000 --- a/themes/triangles/server/src/lib/storage/nedb/NedbCollection.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -import Sinon = require("sinon"); -import Assert = require("assert"); - -import { NedbCollection } from "./NedbCollection"; - -describe("storage/nedb/NedbCollection", function () { - describe("insert", function () { - it("should insert one entry", function () { - const nedbOptions = { - inMemoryOnly: true - }; - const collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou" }); - - return collection.count({}).then(function (count: number) { - Assert.equal(1, count); - }); - }); - - it("should insert three entries", function () { - const nedbOptions = { - inMemoryOnly: true - }; - const collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou" }); - collection.insert({ key: "hello" }); - collection.insert({ key: "hey" }); - - return collection.count({}).then(function (count: number) { - Assert.equal(3, count); - }); - }); - }); - - describe("find", function () { - let collection: NedbCollection; - before(function () { - const nedbOptions = { - inMemoryOnly: true - }; - collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou", value: 1 }); - collection.insert({ key: "hello" }); - collection.insert({ key: "hey" }); - collection.insert({ key: "coucou", value: 2 }); - }); - - it("should find one hello", function () { - return collection.find({ key: "hello" }, { key: 1 }) - .then(function (docs: { key: string }[]) { - Assert.equal(1, docs.length); - Assert(docs[0].key == "hello"); - }); - }); - - it("should find two coucou", function () { - return collection.find({ key: "coucou" }, { value: 1 }) - .then(function (docs: { value: number }[]) { - Assert.equal(2, docs.length); - }); - }); - }); - - describe("findOne", function () { - let collection: NedbCollection; - before(function () { - const nedbOptions = { - inMemoryOnly: true - }; - collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou", value: 1 }); - collection.insert({ key: "coucou", value: 1 }); - collection.insert({ key: "coucou", value: 1 }); - collection.insert({ key: "coucou", value: 1 }); - }); - - it("should find two coucou", function () { - const doc = { key: "coucou", value: 1 }; - return collection.count(doc) - .then(function (count: number) { - Assert.equal(4, count); - return collection.findOne(doc); - }); - }); - }); - - describe("update", function () { - let collection: NedbCollection; - before(function () { - const nedbOptions = { - inMemoryOnly: true - }; - collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou", value: 1 }); - }); - - it("should update the value", function () { - return collection.update({ key: "coucou" }, { key: "coucou", value: 2 }, { multi: true }) - .then(function () { - return collection.find({ key: "coucou" }); - }) - .then(function (docs: { key: string, value: number }[]) { - Assert.equal(1, docs.length); - Assert.equal(2, docs[0].value); - }); - }); - }); - - describe("update", function () { - let collection: NedbCollection; - before(function () { - const nedbOptions = { - inMemoryOnly: true - }; - collection = new NedbCollection(nedbOptions); - - collection.insert({ key: "coucou" }); - collection.insert({ key: "hello" }); - }); - - it("should update the value", function () { - return collection.remove({ key: "coucou" }) - .then(function () { - return collection.count({}); - }) - .then(function (count: number) { - Assert.equal(1, count); - }); - }); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/nedb/NedbCollection.ts b/themes/triangles/server/src/lib/storage/nedb/NedbCollection.ts deleted file mode 100644 index 88a93ad05..000000000 --- a/themes/triangles/server/src/lib/storage/nedb/NedbCollection.ts +++ /dev/null @@ -1,47 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import { ICollection } from "../ICollection"; -import Nedb = require("nedb"); - -declare module "nedb" { - export class NedbAsync extends Nedb { - constructor(pathOrOptions?: string | Nedb.DataStoreOptions); - updateAsync(query: any, updateQuery: any, options?: Nedb.UpdateOptions): BluebirdPromise; - findOneAsync(query: any): BluebirdPromise; - insertAsync(newDoc: T): BluebirdPromise; - removeAsync(query: any): BluebirdPromise; - countAsync(query: any): BluebirdPromise; - } -} - -export class NedbCollection implements ICollection { - private collection: Nedb.NedbAsync; - - constructor(options: Nedb.DataStoreOptions) { - this.collection = BluebirdPromise.promisifyAll(new Nedb(options)) as Nedb.NedbAsync; - } - - find(query: any, sortKeys?: any, count?: number): BluebirdPromise { - const q = this.collection.find(query).sort(sortKeys).limit(count); - return BluebirdPromise.promisify(q.exec, { context: q })(); - } - - findOne(query: any): BluebirdPromise { - return this.collection.findOneAsync(query); - } - - update(query: any, updateQuery: any, options?: any): BluebirdPromise { - return this.collection.updateAsync(query, updateQuery, options); - } - - remove(query: any): BluebirdPromise { - return this.collection.removeAsync(query); - } - - insert(document: any): BluebirdPromise { - return this.collection.insertAsync(document); - } - - count(query: any): BluebirdPromise { - return this.collection.countAsync(query); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts b/themes/triangles/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts deleted file mode 100644 index da90c661f..000000000 --- a/themes/triangles/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Sinon = require("sinon"); -import Assert = require("assert"); - -import { NedbCollectionFactory } from "./NedbCollectionFactory"; - -describe("storage/nedb/NedbCollectionFactory", function() { - it("should create a nedb collection", function() { - const nedbOptions = { - inMemoryOnly: true - }; - const factory = new NedbCollectionFactory(nedbOptions); - - const collection = factory.build("mycollection"); - Assert(collection); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/storage/nedb/NedbCollectionFactory.ts b/themes/triangles/server/src/lib/storage/nedb/NedbCollectionFactory.ts deleted file mode 100644 index 49c4dc853..000000000 --- a/themes/triangles/server/src/lib/storage/nedb/NedbCollectionFactory.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ICollection } from "../ICollection"; -import { ICollectionFactory } from "../ICollectionFactory"; -import { NedbCollection } from "./NedbCollection"; -import path = require("path"); -import Nedb = require("nedb"); - -export interface NedbOptions { - inMemoryOnly?: boolean; - directory?: string; -} - -export class NedbCollectionFactory implements ICollectionFactory { - private options: Nedb.DataStoreOptions; - - constructor(options: Nedb.DataStoreOptions) { - this.options = options; - } - - build(collectionName: string): ICollection { - const datastoreOptions: Nedb.DataStoreOptions = { - inMemoryOnly: this.options.inMemoryOnly || false, - autoload: true, - filename: (this.options.filename) ? path.resolve(this.options.filename, collectionName) : undefined - }; - - return new NedbCollection(datastoreOptions); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/stubs/express.spec.ts b/themes/triangles/server/src/lib/stubs/express.spec.ts deleted file mode 100644 index 48f15d7e1..000000000 --- a/themes/triangles/server/src/lib/stubs/express.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ - -import sinon = require("sinon"); -import express = require("express"); - -export interface RequestMock { - app?: any; - body?: any; - session?: any; - headers?: any; - get?: any; - query?: any; - originalUrl: string; -} - -export interface ResponseMock { - send: sinon.SinonStub | sinon.SinonSpy; - sendStatus: sinon.SinonStub; - sendFile: sinon.SinonStub; - sendfile: sinon.SinonStub; - status: sinon.SinonStub | sinon.SinonSpy; - json: sinon.SinonStub | sinon.SinonSpy; - links: sinon.SinonStub; - jsonp: sinon.SinonStub; - download: sinon.SinonStub; - contentType: sinon.SinonStub; - type: sinon.SinonStub; - format: sinon.SinonStub; - attachment: sinon.SinonStub; - set: sinon.SinonStub; - header: sinon.SinonStub; - headersSent: boolean; - get: sinon.SinonStub; - clearCookie: sinon.SinonStub; - cookie: sinon.SinonStub; - location: sinon.SinonStub; - redirect: sinon.SinonStub | sinon.SinonSpy; - render: sinon.SinonStub | sinon.SinonSpy; - locals: sinon.SinonStub; - charset: string; - vary: sinon.SinonStub; - app: any; - write: sinon.SinonStub; - writeContinue: sinon.SinonStub; - writeHead: sinon.SinonStub; - statusCode: number; - statusMessage: string; - setHeader: sinon.SinonStub; - setTimeout: sinon.SinonStub; - sendDate: boolean; - getHeader: sinon.SinonStub; -} - -export function RequestMock(): RequestMock { - return { - originalUrl: "/non-api/xxx", - app: { - get: sinon.stub() - }, - headers: { - "x-forwarded-for": "127.0.0.1" - }, - session: {} - }; -} -export function ResponseMock(): ResponseMock { - return { - send: sinon.stub(), - status: sinon.stub(), - json: sinon.stub(), - sendStatus: sinon.stub(), - links: sinon.stub(), - jsonp: sinon.stub(), - sendFile: sinon.stub(), - sendfile: sinon.stub(), - download: sinon.stub(), - contentType: sinon.stub(), - type: sinon.stub(), - format: sinon.stub(), - attachment: sinon.stub(), - set: sinon.stub(), - header: sinon.stub(), - headersSent: true, - get: sinon.stub(), - clearCookie: sinon.stub(), - cookie: sinon.stub(), - location: sinon.stub(), - redirect: sinon.stub(), - render: sinon.stub(), - locals: sinon.stub(), - charset: "utf-8", - vary: sinon.stub(), - app: sinon.stub(), - write: sinon.stub(), - writeContinue: sinon.stub(), - writeHead: sinon.stub(), - statusCode: 200, - statusMessage: "message", - setHeader: sinon.stub(), - setTimeout: sinon.stub(), - sendDate: true, - getHeader: sinon.stub() - }; -} diff --git a/themes/triangles/server/src/lib/stubs/ldapjs.spec.ts b/themes/triangles/server/src/lib/stubs/ldapjs.spec.ts deleted file mode 100644 index 045c0e11b..000000000 --- a/themes/triangles/server/src/lib/stubs/ldapjs.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ - -import Sinon = require("sinon"); - -export class LdapjsMock { - createClientStub: sinon.SinonStub; - - constructor() { - this.createClientStub = Sinon.stub(); - } - - createClient(params: any) { - return this.createClientStub(params); - } -} - -export class LdapjsClientMock { - bindStub: sinon.SinonStub; - unbindStub: sinon.SinonStub; - searchStub: sinon.SinonStub; - modifyStub: sinon.SinonStub; - onStub: sinon.SinonStub; - - constructor() { - this.bindStub = Sinon.stub(); - this.unbindStub = Sinon.stub(); - this.searchStub = Sinon.stub(); - this.modifyStub = Sinon.stub(); - this.onStub = Sinon.stub(); - } - - bind() { - return this.bindStub(); - } - - unbind() { - return this.unbindStub(); - } - - search() { - return this.searchStub(); - } - - modify() { - return this.modifyStub(); - } - - on() { - return this.onStub(); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/stubs/speakeasy.spec.ts b/themes/triangles/server/src/lib/stubs/speakeasy.spec.ts deleted file mode 100644 index 023614dcd..000000000 --- a/themes/triangles/server/src/lib/stubs/speakeasy.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ - -import sinon = require("sinon"); - -export = { - totp: sinon.stub(), - generateSecret: sinon.stub() -}; diff --git a/themes/triangles/server/src/lib/stubs/u2f.spec.ts b/themes/triangles/server/src/lib/stubs/u2f.spec.ts deleted file mode 100644 index 234b28c11..000000000 --- a/themes/triangles/server/src/lib/stubs/u2f.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ - -import sinon = require("sinon"); - -export interface U2FMock { - request: sinon.SinonStub; - checkSignature: sinon.SinonStub; - checkRegistration: sinon.SinonStub; -} - -export function U2FMock(): U2FMock { - return { - request: sinon.stub(), - checkSignature: sinon.stub(), - checkRegistration: sinon.stub() - }; -} diff --git a/themes/triangles/server/src/lib/utils/HashGenerator.spec.ts b/themes/triangles/server/src/lib/utils/HashGenerator.spec.ts deleted file mode 100644 index f19619a65..000000000 --- a/themes/triangles/server/src/lib/utils/HashGenerator.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Assert = require("assert"); -import { HashGenerator } from "./HashGenerator"; - -describe("utils/HashGenerator", function () { - it("should compute correct ssha512 (password)", function () { - return HashGenerator.ssha512("password", 500000, "jgiCMRyGXzoqpxS3") - .then(function (hash: string) { - Assert.equal(hash, "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"); - }); - }); - - it("should compute correct ssha512 (test)", function () { - return HashGenerator.ssha512("test", 500000, "abcdefghijklmnop") - .then(function (hash: string) { - Assert.equal(hash, "{CRYPT}$6$rounds=500000$abcdefghijklmnop$sTlNGf0VO/HTQIOXemmaBbV28HUch/qhWOA1/4dsDj6CDQYhUgXbYSPL6gccAsWMr2zD5fFWwhKmPdG.yxphs."); - }); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/utils/HashGenerator.ts b/themes/triangles/server/src/lib/utils/HashGenerator.ts deleted file mode 100644 index e67de32b7..000000000 --- a/themes/triangles/server/src/lib/utils/HashGenerator.ts +++ /dev/null @@ -1,23 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import RandomString = require("randomstring"); -import Util = require("util"); -const crypt = require("crypt3"); - -export class HashGenerator { - static ssha512( - password: string, - rounds: number = 500000, - salt?: string): BluebirdPromise { - const saltSize = 16; - // $6 means SHA512 - const _salt = Util.format("$6$rounds=%d$%s", rounds, - (salt) ? salt : RandomString.generate(16)); - - const cryptAsync = BluebirdPromise.promisify(crypt); - - return cryptAsync(password, _salt) - .then(function (hash: string) { - return BluebirdPromise.resolve(Util.format("{CRYPT}%s", hash)); - }); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/utils/ObjectCloner.ts b/themes/triangles/server/src/lib/utils/ObjectCloner.ts deleted file mode 100644 index 3e125d749..000000000 --- a/themes/triangles/server/src/lib/utils/ObjectCloner.ts +++ /dev/null @@ -1,6 +0,0 @@ - -export class ObjectCloner { - static clone(obj: any): any { - return JSON.parse(JSON.stringify(obj)); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/utils/SafeRedirection.spec.ts b/themes/triangles/server/src/lib/utils/SafeRedirection.spec.ts deleted file mode 100644 index 4126949fd..000000000 --- a/themes/triangles/server/src/lib/utils/SafeRedirection.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import Assert = require("assert"); -import Sinon = require("sinon"); -import { SafeRedirector } from "./SafeRedirection"; - -describe("web_server/middlewares/SafeRedirection", () => { - describe("Url is in protected domain", () => { - before(() => { - this.redirector = new SafeRedirector("example.com"); - this.res = {redirect: Sinon.stub()}; - }); - - it("should redirect to provided url", () => { - this.redirector.redirectOrElse(this.res, - "https://mysubdomain.example.com:8080/abc", - "https://authelia.example.com"); - Assert(this.res.redirect.calledWith("https://mysubdomain.example.com:8080/abc")); - }); - - it("should redirect to default url when wrong domain", () => { - this.redirector.redirectOrElse(this.res, - "https://mysubdomain.domain.rtf:8080/abc", - "https://authelia.example.com"); - Assert(this.res.redirect.calledWith("https://authelia.example.com")); - }); - - it("should redirect to default url when not terminating by domain", () => { - this.redirector.redirectOrElse(this.res, - "https://mysubdomain.example.com.rtf:8080/abc", - "https://authelia.example.com"); - Assert(this.res.redirect.calledWith("https://authelia.example.com")); - }); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/utils/SafeRedirection.ts b/themes/triangles/server/src/lib/utils/SafeRedirection.ts deleted file mode 100644 index 9e6a32e0c..000000000 --- a/themes/triangles/server/src/lib/utils/SafeRedirection.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Express = require("express"); -import { DomainExtractor } from "../../../../shared/DomainExtractor"; -import { BelongToDomain } from "../../../../shared/BelongToDomain"; - - -export class SafeRedirector { - private domain: string; - - constructor(domain: string) { - this.domain = domain; - } - - redirectOrElse( - res: Express.Response, - url: string, - defaultUrl: string): void { - if (BelongToDomain(url, this.domain)) { - res.redirect(url); - } - res.redirect(defaultUrl); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/utils/URLDecomposer.spec.ts b/themes/triangles/server/src/lib/utils/URLDecomposer.spec.ts deleted file mode 100644 index cbb038738..000000000 --- a/themes/triangles/server/src/lib/utils/URLDecomposer.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { URLDecomposer } from "./URLDecomposer"; -import Assert = require("assert"); - -describe("utils/URLDecomposer", function () { - describe("test fromUrl", function () { - it("should return domain from https url", function () { - const d = URLDecomposer.fromUrl("https://www.example.com/test/abc"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/test/abc"); - }); - - it("should return domain from http url", function () { - const d = URLDecomposer.fromUrl("http://www.example.com/test/abc"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/test/abc"); - }); - - it("should return domain when url contains port", function () { - const d = URLDecomposer.fromUrl("https://www.example.com:8080/test/abc"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/test/abc"); - }); - - it("should return default path when no path provided", function () { - const d = URLDecomposer.fromUrl("https://www.example.com:8080"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/"); - }); - - it("should return default path when provided", function () { - const d = URLDecomposer.fromUrl("https://www.example.com:8080/"); - Assert.equal(d.domain, "www.example.com"); - Assert.equal(d.path, "/"); - }); - - it("should return undefined when does not match", function () { - const d = URLDecomposer.fromUrl("https:///abc/test"); - Assert.equal(d, undefined); - }); - - it("should return undefined when does not match", function () { - const d = URLDecomposer.fromUrl("https:///abc/test"); - Assert.equal(d, undefined); - }); - }); -}); \ No newline at end of file diff --git a/themes/triangles/server/src/lib/utils/URLDecomposer.ts b/themes/triangles/server/src/lib/utils/URLDecomposer.ts deleted file mode 100644 index 9bdf2e9d2..000000000 --- a/themes/triangles/server/src/lib/utils/URLDecomposer.ts +++ /dev/null @@ -1,15 +0,0 @@ -export class URLDecomposer { - static fromUrl(url: string): {domain: string, path: string} { - if (!url) return; - const match = url.match(/https?:\/\/([a-z0-9_.-]+)(:[0-9]+)?(.*)/); - - if (!match) return; - - if (match[1] && !match[3]) { - return {domain: match[1], path: "/"}; - } else if (match[1] && match[3]) { - return {domain: match[1], path: match[3]}; - } - return; - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/web_server/Configurator.ts b/themes/triangles/server/src/lib/web_server/Configurator.ts deleted file mode 100644 index 6e404874a..000000000 --- a/themes/triangles/server/src/lib/web_server/Configurator.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Configuration } from "../configuration/schema/Configuration"; -import { GlobalDependencies } from "../../../types/Dependencies"; -import { SessionConfigurationBuilder } from - "../configuration/SessionConfigurationBuilder"; -import Path = require("path"); -import Express = require("express"); -import * as BodyParser from "body-parser"; -import { RestApi } from "./RestApi"; -import { WithHeadersLogged } from "./middlewares/WithHeadersLogged"; -import { ServerVariables } from "../ServerVariables"; -import Helmet = require("helmet"); - -const addRequestId = require("express-request-id")(); - -// Constants -const TRUST_PROXY = "trust proxy"; -const X_POWERED_BY = "x-powered-by"; -const VIEWS = "views"; -const VIEW_ENGINE = "view engine"; -const PUG = "pug"; - -export class Configurator { - static configure(config: Configuration, - app: Express.Application, - vars: ServerVariables, - deps: GlobalDependencies): void { - const viewsDirectory = Path.resolve(__dirname, "../../views"); - const publicHtmlDirectory = Path.resolve(__dirname, "../../public_html"); - - const expressSessionOptions = SessionConfigurationBuilder.build(config, deps); - - app.use(Express.static(publicHtmlDirectory)); - app.use(BodyParser.urlencoded({ extended: false })); - app.use(BodyParser.json()); - app.use(deps.session(expressSessionOptions)); - app.use(addRequestId); - app.use(WithHeadersLogged.middleware(vars.logger)); - app.disable(X_POWERED_BY); - app.enable(TRUST_PROXY); - app.use(Helmet()); - - app.set(VIEWS, viewsDirectory); - app.set(VIEW_ENGINE, PUG); - - RestApi.setup(app, vars); - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/web_server/RestApi.ts b/themes/triangles/server/src/lib/web_server/RestApi.ts deleted file mode 100644 index 9144a15b9..000000000 --- a/themes/triangles/server/src/lib/web_server/RestApi.ts +++ /dev/null @@ -1,125 +0,0 @@ -import Express = require("express"); - -import FirstFactorGet = require("../routes/firstfactor/get"); -import SecondFactorGet = require("../routes/secondfactor/get"); - -import FirstFactorPost = require("../routes/firstfactor/post"); -import LogoutGet = require("../routes/logout/get"); -import VerifyGet = require("../routes/verify/get"); -import TOTPSignGet = require("../routes/secondfactor/totp/sign/post"); - -import IdentityCheckMiddleware = require("../IdentityCheckMiddleware"); - -import TOTPRegistrationIdentityHandler from "../routes/secondfactor/totp/identity/RegistrationHandler"; -import U2FRegistrationIdentityHandler from "../routes/secondfactor/u2f/identity/RegistrationHandler"; -import ResetPasswordIdentityHandler from "../routes/password-reset/identity/PasswordResetHandler"; - -import U2FSignPost = require("../routes/secondfactor/u2f/sign/post"); -import U2FSignRequestGet = require("../routes/secondfactor/u2f/sign_request/get"); - -import U2FRegisterPost = require("../routes/secondfactor/u2f/register/post"); -import U2FRegisterRequestGet = require("../routes/secondfactor/u2f/register_request/get"); - -import ResetPasswordFormPost = require("../routes/password-reset/form/post"); -import ResetPasswordRequestPost = require("../routes/password-reset/request/get"); - -import Error401Get = require("../routes/error/401/get"); -import Error403Get = require("../routes/error/403/get"); -import Error404Get = require("../routes/error/404/get"); - -import LoggedIn = require("../routes/loggedin/get"); - -import { ServerVariables } from "../ServerVariables"; -import Endpoints = require("../../../../shared/api"); -import { RequireValidatedFirstFactor } from "./middlewares/RequireValidatedFirstFactor"; - -function setupTotp(app: Express.Application, vars: ServerVariables) { - app.post(Endpoints.SECOND_FACTOR_TOTP_POST, - RequireValidatedFirstFactor.middleware(vars.logger), - TOTPSignGet.default(vars)); - - app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, - RequireValidatedFirstFactor.middleware(vars.logger)); - - app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, - RequireValidatedFirstFactor.middleware(vars.logger)); - - IdentityCheckMiddleware.register(app, - Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, - Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, - new TOTPRegistrationIdentityHandler(vars.logger, - vars.userDataStore, vars.totpHandler, vars.config.totp), - vars); -} - -function setupU2f(app: Express.Application, vars: ServerVariables) { - app.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, - RequireValidatedFirstFactor.middleware(vars.logger), - U2FSignRequestGet.default(vars)); - - app.post(Endpoints.SECOND_FACTOR_U2F_SIGN_POST, - RequireValidatedFirstFactor.middleware(vars.logger), - U2FSignPost.default(vars)); - - app.get(Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, - RequireValidatedFirstFactor.middleware(vars.logger), - U2FRegisterRequestGet.default(vars)); - - app.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, - RequireValidatedFirstFactor.middleware(vars.logger), - U2FRegisterPost.default(vars)); - - app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, - RequireValidatedFirstFactor.middleware(vars.logger)); - - app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, - RequireValidatedFirstFactor.middleware(vars.logger)); - - IdentityCheckMiddleware.register(app, - Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, - Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, - new U2FRegistrationIdentityHandler(vars.logger), vars); -} - -function setupResetPassword(app: Express.Application, vars: ServerVariables) { - IdentityCheckMiddleware.register(app, - Endpoints.RESET_PASSWORD_IDENTITY_START_GET, - Endpoints.RESET_PASSWORD_IDENTITY_FINISH_GET, - new ResetPasswordIdentityHandler(vars.logger, vars.usersDatabase), - vars); - - app.get(Endpoints.RESET_PASSWORD_REQUEST_GET, - ResetPasswordRequestPost.default); - app.post(Endpoints.RESET_PASSWORD_FORM_POST, - ResetPasswordFormPost.default(vars)); -} - -function setupErrors(app: Express.Application, vars: ServerVariables) { - app.get(Endpoints.ERROR_401_GET, Error401Get.default(vars)); - app.get(Endpoints.ERROR_403_GET, Error403Get.default(vars)); - app.get(Endpoints.ERROR_404_GET, Error404Get.default); -} - -export class RestApi { - static setup(app: Express.Application, vars: ServerVariables): void { - app.get(Endpoints.FIRST_FACTOR_GET, FirstFactorGet.default(vars)); - - app.get(Endpoints.SECOND_FACTOR_GET, - RequireValidatedFirstFactor.middleware(vars.logger), - SecondFactorGet.default(vars)); - - app.get(Endpoints.LOGOUT_GET, LogoutGet.default(vars)); - - app.get(Endpoints.VERIFY_GET, VerifyGet.default(vars)); - app.post(Endpoints.FIRST_FACTOR_POST, FirstFactorPost.default(vars)); - - setupTotp(app, vars); - setupU2f(app, vars); - setupResetPassword(app, vars); - setupErrors(app, vars); - - app.get(Endpoints.LOGGED_IN, - RequireValidatedFirstFactor.middleware(vars.logger), - LoggedIn.default(vars)); - } -} diff --git a/themes/triangles/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts b/themes/triangles/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts deleted file mode 100644 index ecfd75765..000000000 --- a/themes/triangles/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts +++ /dev/null @@ -1,27 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import ErrorReplies = require("../../ErrorReplies"); -import { IRequestLogger } from "../../logging/IRequestLogger"; -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import Exceptions = require("../../Exceptions"); -import { Level } from "../../authentication/Level"; - -export class RequireValidatedFirstFactor { - static middleware(logger: IRequestLogger) { - return function (req: Express.Request, res: Express.Response, - next: Express.NextFunction): BluebirdPromise { - - 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.")); - - next(); - resolve(); - }) - .catch(ErrorReplies.replyWithError401(req, res, logger)); - }; - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/lib/web_server/middlewares/WithHeadersLogged.ts b/themes/triangles/server/src/lib/web_server/middlewares/WithHeadersLogged.ts deleted file mode 100644 index 139db1141..000000000 --- a/themes/triangles/server/src/lib/web_server/middlewares/WithHeadersLogged.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Express = require("express"); -import { IRequestLogger } from "../../logging/IRequestLogger"; - -export class WithHeadersLogged { - static middleware(logger: IRequestLogger) { - return function (req: Express.Request, res: Express.Response, - next: Express.NextFunction): void { - logger.debug(req, "Headers = %s", JSON.stringify(req.headers)); - next(); - }; - } -} \ No newline at end of file diff --git a/themes/triangles/server/src/resources/email-template.ejs b/themes/triangles/server/src/resources/email-template.ejs old mode 100644 new mode 100755 diff --git a/themes/triangles/server/src/views/already-logged-in.pug b/themes/triangles/server/src/views/already-logged-in.pug old mode 100644 new mode 100755 diff --git a/themes/triangles/server/src/views/errors/.directory b/themes/triangles/server/src/views/errors/.directory old mode 100644 new mode 100755 diff --git a/themes/triangles/server/src/views/errors/401.pug b/themes/triangles/server/src/views/errors/401.pug old mode 100644 new mode 100755 diff --git a/themes/triangles/server/src/views/errors/403.pug b/themes/triangles/server/src/views/errors/403.pug old mode 100644 new mode 100755 diff --git a/themes/triangles/server/src/views/errors/404.pug b/themes/triangles/server/src/views/errors/404.pug old mode 100644 new mode 100755 diff --git a/themes/triangles/server/src/views/firstfactor.pug b/themes/triangles/server/src/views/firstfactor.pug old mode 100644 new mode 100755 diff --git a/themes/triangles/server/src/views/layout/layout.pug b/themes/triangles/server/src/views/layout/layout.pug old mode 100644 new mode 100755 diff --git a/themes/triangles/server/src/views/need-identity-validation.pug b/themes/triangles/server/src/views/need-identity-validation.pug old mode 100644 new mode 100755 diff --git a/themes/triangles/server/src/views/password-reset-form.pug b/themes/triangles/server/src/views/password-reset-form.pug old mode 100644 new mode 100755 diff --git a/themes/triangles/server/src/views/password-reset-request.pug b/themes/triangles/server/src/views/password-reset-request.pug old mode 100644 new mode 100755 diff --git a/themes/triangles/server/src/views/secondfactor.pug b/themes/triangles/server/src/views/secondfactor.pug old mode 100644 new mode 100755 diff --git a/themes/triangles/server/src/views/totp-register.pug b/themes/triangles/server/src/views/totp-register.pug old mode 100644 new mode 100755 diff --git a/themes/triangles/server/src/views/u2f-register.pug b/themes/triangles/server/src/views/u2f-register.pug old mode 100644 new mode 100755 diff --git a/themes/triangles/server/test/requests.ts b/themes/triangles/server/test/requests.ts deleted file mode 100644 index 93fa0de47..000000000 --- a/themes/triangles/server/test/requests.ts +++ /dev/null @@ -1,94 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import request = require("request"); -import assert = require("assert"); -import express = require("express"); -import nodemailer = require("nodemailer"); -import Endpoints = require("../../shared/api"); - -declare module "request" { - export interface RequestAPI { - getAsync(uri: string, options?: RequiredUriUrl): BluebirdPromise; - getAsync(uri: string): BluebirdPromise; - getAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; - - postAsync(uri: string, options?: CoreOptions): BluebirdPromise; - postAsync(uri: string): BluebirdPromise; - postAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; - } -} - -const requestAsync: typeof request = BluebirdPromise.promisifyAll(request) as typeof request; - -export = function (port: number) { - const PORT = port; - const BASE_URL = "http://localhost:" + PORT; - - function execute_totp(jar: request.CookieJar, token: string) { - return requestAsync.postAsync({ - url: BASE_URL + Endpoints.SECOND_FACTOR_TOTP_POST, - jar: jar, - form: { - token: token - } - }); - } - - function execute_u2f_authentication(jar: request.CookieJar) { - return requestAsync.getAsync({ - url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, - jar: jar - }) - .then(function (res: request.RequestResponse) { - assert.equal(res.statusCode, 200); - return requestAsync.postAsync({ - url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_POST, - jar: jar, - form: { - } - }); - }); - } - - function execute_verification(jar: request.CookieJar) { - return requestAsync.getAsync({ url: BASE_URL + Endpoints.VERIFY_GET, jar: jar }); - } - - function execute_login(jar: request.CookieJar) { - return requestAsync.getAsync({ url: BASE_URL + Endpoints.FIRST_FACTOR_GET, jar: jar }); - } - - function execute_first_factor(jar: request.CookieJar) { - return requestAsync.postAsync({ - url: BASE_URL + Endpoints.FIRST_FACTOR_POST, - jar: jar, - form: { - username: "test_ok", - password: "password" - } - }); - } - - function execute_failing_first_factor(jar: request.CookieJar) { - return requestAsync.postAsync({ - url: BASE_URL + Endpoints.FIRST_FACTOR_POST, - jar: jar, - form: { - username: "test_nok", - password: "password" - } - }); - } - - return { - login: execute_login, - verify: execute_verification, - u2f_authentication: execute_u2f_authentication, - first_factor: execute_first_factor, - failing_first_factor: execute_failing_first_factor, - totp: execute_totp, - }; -}; - diff --git a/themes/triangles/server/tsconfig.json b/themes/triangles/server/tsconfig.json deleted file mode 100644 index ebe98c5ed..000000000 --- a/themes/triangles/server/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "moduleResolution": "node", - "noImplicitAny": true, - "sourceMap": true, - "removeComments": true, - "outDir": "../dist", - "baseUrl": ".", - "paths": { - "*": [ - "./types/*", - "../shared/types/*" - ] - } - }, - "exclude": [ - "src/**/*.spec.ts" - ] -} diff --git a/themes/triangles/server/tslint.json b/themes/triangles/server/tslint.json deleted file mode 100644 index c2c1b7501..000000000 --- a/themes/triangles/server/tslint.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "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 - } -} diff --git a/themes/triangles/server/types/.directory b/themes/triangles/server/types/.directory deleted file mode 100644 index 1e65000ea..000000000 --- a/themes/triangles/server/types/.directory +++ /dev/null @@ -1,4 +0,0 @@ -[Dolphin] -Timestamp=2018,12,17,20,58,27 -Version=3 -ViewMode=1 diff --git a/themes/triangles/server/types/AuthenticationSession.ts b/themes/triangles/server/types/AuthenticationSession.ts deleted file mode 100644 index bbed0e715..000000000 --- a/themes/triangles/server/types/AuthenticationSession.ts +++ /dev/null @@ -1,18 +0,0 @@ -import U2f = require("u2f"); -import { Level } from "../src/lib/authentication/Level"; - -export interface AuthenticationSession { - userid: string; - authentication_level: Level; - keep_me_logged_in: boolean; - last_activity_datetime: number; - identity_check?: { - challenge: string; - userid: string; - }; - register_request?: U2f.Request; - sign_request?: U2f.Request; - email: string; - groups: string[]; - redirect?: string; -} \ No newline at end of file diff --git a/themes/triangles/server/types/Dependencies.ts b/themes/triangles/server/types/Dependencies.ts deleted file mode 100644 index f20404dbc..000000000 --- a/themes/triangles/server/types/Dependencies.ts +++ /dev/null @@ -1,29 +0,0 @@ -import winston = require("winston"); -import speakeasy = require("speakeasy"); -import nodemailer = require("nodemailer"); -import session = require("express-session"); -import nedb = require("nedb"); -import ldapjs = require("ldapjs"); -import u2f = require("u2f"); -import RedisSession = require("connect-redis"); -import Redis = require("redis"); - -export type Speakeasy = typeof speakeasy; -export type Winston = typeof winston; -export type Session = typeof session; -export type Nedb = typeof nedb; -export type Ldapjs = typeof ldapjs; -export type U2f = typeof u2f; -export type ConnectRedis = typeof RedisSession; -export type Redis = typeof Redis; - -export interface GlobalDependencies { - u2f: U2f; - ldapjs: Ldapjs; - session: Session; - Redis: Redis; - ConnectRedis: ConnectRedis; - winston: Winston; - speakeasy: Speakeasy; - nedb: Nedb; -} \ No newline at end of file diff --git a/themes/triangles/server/types/Identity.ts b/themes/triangles/server/types/Identity.ts deleted file mode 100644 index e985984e2..000000000 --- a/themes/triangles/server/types/Identity.ts +++ /dev/null @@ -1,6 +0,0 @@ - - -export interface Identity { - userid: string; - email: string; -} \ No newline at end of file diff --git a/themes/triangles/server/types/TOTPSecret.ts b/themes/triangles/server/types/TOTPSecret.ts deleted file mode 100644 index d6775f2f0..000000000 --- a/themes/triangles/server/types/TOTPSecret.ts +++ /dev/null @@ -1,11 +0,0 @@ - -export interface TOTPSecret { - ascii: string; - hex: string; - base32: string; - qr_code_ascii: string; - qr_code_hex: string; - qr_code_base32: string; - google_auth_qr: string; - otpauth_url: string; - } \ No newline at end of file diff --git a/themes/triangles/server/types/U2FRegistration.ts b/themes/triangles/server/types/U2FRegistration.ts deleted file mode 100644 index b6080af07..000000000 --- a/themes/triangles/server/types/U2FRegistration.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface U2FRegistration { - keyHandle: string; - publicKey: string; -} \ No newline at end of file diff --git a/themes/triangles/server/types/dovehash.d.ts b/themes/triangles/server/types/dovehash.d.ts deleted file mode 100644 index c354609c0..000000000 --- a/themes/triangles/server/types/dovehash.d.ts +++ /dev/null @@ -1,4 +0,0 @@ - -declare module "dovehash" { - function encode(algo: string, text: string): string; -} \ No newline at end of file diff --git a/themes/triangles/server/types/speakeasy.d.ts b/themes/triangles/server/types/speakeasy.d.ts deleted file mode 100644 index 6ea06948b..000000000 --- a/themes/triangles/server/types/speakeasy.d.ts +++ /dev/null @@ -1,96 +0,0 @@ -declare module "speakeasy" { - export = speakeasy - - interface SharedOptions { - encoding?: string - algorithm?: string - } - - interface DigestOptions extends SharedOptions { - secret: string - counter: number - } - - interface HOTPOptions extends SharedOptions { - secret: string - counter: number - digest?: Buffer - digits?: number - } - - interface HOTPVerifyOptions extends SharedOptions { - secret: string - token: string - counter: number - digits?: number - window?: number - } - - interface TOTPOptions extends SharedOptions { - secret: string - time?: number - step?: number - epoch?: number - counter?: number - digits?: number - } - - interface TOTPVerifyOptions extends SharedOptions { - secret: string - token: string - time?: number - step?: number - epoch?: number - counter?: number - digits?: number - window?: number - } - - interface GenerateSecretOptions { - length?: number - symbols?: boolean - otpauth_url?: boolean - name?: string - issuer?: string - } - - interface GeneratedSecret { - ascii: string - hex: string - base32: string - qr_code_ascii: string - qr_code_hex: string - qr_code_base32: string - google_auth_qr: string - otpauth_url: string - } - - interface OTPAuthURLOptions extends SharedOptions { - secret: string - label: string - type?: string - counter?: number - issuer?: string - digits?: number - period?: number - } - - interface Speakeasy { - digest: (options: DigestOptions) => Buffer - hotp: { - (options: HOTPOptions): string, - verifyDelta: (options: HOTPVerifyOptions) => boolean, - verify: (options: HOTPVerifyOptions) => boolean, - } - totp: { - (options: TOTPOptions): string - verifyDelta: (options: TOTPVerifyOptions) => boolean, - verify: (options: TOTPVerifyOptions) => boolean, - } - generateSecret: (options?: GenerateSecretOptions) => GeneratedSecret - generateSecretASCII: (length?: number, symbols?: boolean) => string - otpauthURL: (options: OTPAuthURLOptions) => string - } - - const speakeasy: Speakeasy -} \ No newline at end of file diff --git a/users_database.yml b/users_database.yml old mode 100644 new mode 100755 From 3d1448d3cc535538ea96ac7a03ff43779fd99dd6 Mon Sep 17 00:00:00 2001 From: BankaiNoJutsu Date: Tue, 18 Dec 2018 16:34:56 +0100 Subject: [PATCH 06/11] fix permissions --- .gitignore | 0 .npmignore | 0 .travis.yml | 0 CHANGELOG.md | 0 CONTRIBUTORS.md | 0 Dockerfile | 0 Dockerfile.dev | 0 Gruntfile.js | 0 LICENSE | 0 README.md | 0 client/src/css/00-bootstrap.min.css | 0 client/src/css/01-main.css | 0 client/src/css/02-login.css | 0 client/src/css/03-errors.css | 0 client/src/css/03-password-reset-form.css | 0 client/src/css/03-password-reset-request.css | 0 client/src/css/03-totp-register.css | 0 client/src/css/03-u2f-register.css | 0 client/src/img/background.svg | 0 client/src/img/icon.png | Bin client/src/img/mail.png | Bin client/src/img/notifications/error.png | Bin client/src/img/notifications/info.png | Bin client/src/img/notifications/success.png | Bin client/src/img/notifications/warning.png | Bin client/src/img/padlock.png | Bin client/src/img/password.png | Bin client/src/img/pendrive.png | Bin client/src/img/stores/applestore-badge.svg | 0 client/src/img/stores/googleplay-badge.svg | 0 client/src/img/success.png | Bin client/src/img/user.png | Bin client/src/img/warning.png | Bin client/src/index.ts | 0 client/src/lib/GetPromised.ts | 0 client/src/lib/INotifier.ts | 0 client/src/lib/Notifier.ts | 0 client/src/lib/QueryParametersRetriever.ts | 0 client/src/lib/SafeRedirect.ts | 0 client/src/lib/firstfactor/FirstFactorValidator.ts | 0 client/src/lib/firstfactor/UISelectors.ts | 0 client/src/lib/firstfactor/index.ts | 0 client/src/lib/reset-password/constants.ts | 0 .../src/lib/reset-password/reset-password-form.ts | 0 .../lib/reset-password/reset-password-request.ts | 0 client/src/lib/secondfactor/TOTPValidator.ts | 0 client/src/lib/secondfactor/U2FValidator.ts | 0 client/src/lib/secondfactor/constants.ts | 0 client/src/lib/secondfactor/index.ts | 0 client/src/lib/totp-register/totp-register.ts | 0 client/src/lib/totp-register/ui-selector.ts | 0 client/src/lib/u2f-register/u2f-register.ts | 0 client/src/thirdparties/qrcode.min.js | 0 client/test/Notifier.test.ts | 0 .../test/firstfactor/FirstFactorValidator.test.ts | 0 client/test/mocks/NotifierStub.ts | 0 client/test/mocks/jquery.ts | 0 client/test/mocks/u2f-api.ts | 0 client/test/secondfactor/TOTPValidator.test.ts | 0 client/test/totp-register/totp-register.test.ts | 0 client/tsconfig.json | 0 client/tslint.json | 0 config.minimal.yml | 0 config.template.yml | 0 docker-compose.dev.yml | 0 docker-compose.dockerhub.yml | 0 docker-compose.minimal.dev.yml | 0 docker-compose.minimal.yml | 0 docker-compose.swarm.minimal.yml | 0 docker-compose.test.yml | 0 docker-compose.yml | 0 docs/build.md | 0 docs/configuration.md | 0 docs/deployment-dev.md | 0 docs/deployment-production.md | 0 docs/features.md | 0 docs/getting-started.md | 0 docs/security.md | 0 example/compose/authelia/docker-compose.test.yml | 0 example/compose/docker-compose.base.yml | 0 example/compose/httpbin/docker-compose.yml | 0 example/compose/ldap/access.rules | 0 example/compose/ldap/base.ldif | 0 example/compose/ldap/docker-compose.admin.yml | 0 example/compose/ldap/docker-compose.yml | 0 example/compose/mongo/docker-compose.yml | 0 example/compose/nginx/backend/docker-compose.yml | 0 .../compose/nginx/backend/html/admin/secret.html | 0 .../nginx/backend/html/dev/groups/admin/secret.html | 0 .../nginx/backend/html/dev/groups/dev/secret.html | 0 .../nginx/backend/html/dev/users/bob/secret.html | 0 .../nginx/backend/html/dev/users/harry/secret.html | 0 .../nginx/backend/html/dev/users/john/secret.html | 0 example/compose/nginx/backend/html/home/index.html | 0 example/compose/nginx/backend/html/icon.png | Bin example/compose/nginx/backend/html/mail/secret.html | 0 .../compose/nginx/backend/html/public/index.html | 0 .../compose/nginx/backend/html/public/secret.html | 0 .../nginx/backend/html/single_factor/secret.html | 0 example/compose/nginx/backend/nginx.conf | 0 example/compose/nginx/minimal/docker-compose.yml | 0 .../compose/nginx/minimal/html/admin/secret.html | 0 example/compose/nginx/minimal/html/home/index.html | 0 example/compose/nginx/minimal/nginx.conf | 0 example/compose/nginx/minimal/ssl/server.crt | 0 example/compose/nginx/minimal/ssl/server.csr | 0 example/compose/nginx/minimal/ssl/server.key | 0 example/compose/nginx/portal/docker-compose.yml | 0 example/compose/nginx/portal/nginx.conf | 0 example/compose/nginx/portal/ssl/server.crt | 0 example/compose/nginx/portal/ssl/server.csr | 0 example/compose/nginx/portal/ssl/server.key | 0 example/compose/redis/docker-compose.yml | 0 example/compose/smtp/docker-compose.yml | 0 example/kube/README.md | 0 example/kube/apps/app-home/deployment.yml | 0 example/kube/apps/app-home/index.html | 0 example/kube/apps/app-home/service.yml | 0 example/kube/apps/app1/deployment.yml | 0 example/kube/apps/app1/index.html | 0 example/kube/apps/app1/service.yml | 0 example/kube/apps/app1/ssl/tls.crt | 0 example/kube/apps/app1/ssl/tls.csr | 0 example/kube/apps/app1/ssl/tls.key | 0 example/kube/apps/app2/deployment.yml | 0 example/kube/apps/app2/index.html | 0 example/kube/apps/app2/service.yml | 0 example/kube/apps/app2/ssl/tls.crt | 0 example/kube/apps/app2/ssl/tls.csr | 0 example/kube/apps/app2/ssl/tls.key | 0 example/kube/apps/insecure-ingress.yml | 0 example/kube/apps/secure-ingress.yml | 0 example/kube/authelia/configs/config.yml | 0 example/kube/authelia/deployment.yml | 0 example/kube/authelia/ingress.yml | 0 example/kube/authelia/service.yml | 0 example/kube/authelia/ssl/tls.crt | 0 example/kube/authelia/ssl/tls.csr | 0 example/kube/authelia/ssl/tls.key | 0 example/kube/bootstrap.sh | 0 example/kube/build_and_push.sh | 0 example/kube/docker-registry/daemonset.yml | 0 example/kube/docker-registry/ingress.yml | 0 .../kube/docker-registry/replicationcontroller.yml | 0 example/kube/docker-registry/service.yml | 0 example/kube/ingress-controller/default-backend.yml | 0 example/kube/ingress-controller/deployment.yml | 0 example/kube/ingress-controller/service.yml | 0 example/kube/ldap/Dockerfile | 0 example/kube/ldap/deployment.yml | 0 example/kube/ldap/service.yml | 0 example/kube/mailcatcher/deployment.yml | 0 example/kube/mailcatcher/ingress.yml | 0 example/kube/mailcatcher/service.yml | 0 example/kube/namespace.yml | 0 example/kube/storage/mongo.yml | 0 example/kube/storage/redis.yml | 0 images/authelia-title-white.png | Bin images/authelia-title.png | Bin images/email_confirmation.png | Bin images/first_factor.png | Bin images/icon.png | Bin images/kube-logo.png | Bin images/reset_password.png | Bin images/second_factor.png | Bin images/totp.png | Bin images/u2f.png | Bin package-lock.json | 0 package.json | 0 scripts/dc-dev.sh | 0 scripts/docker-publish.sh | 0 scripts/example-commit/dc-example.sh | 0 scripts/example-commit/deploy-example.sh | 0 scripts/example-commit/undeploy-example.sh | 0 scripts/example-dockerhub/dc-example.sh | 0 scripts/example-dockerhub/deploy-example.sh | 0 scripts/example-dockerhub/undeploy-example.sh | 0 scripts/integration-tests.sh | 0 scripts/npm-deployment-test.sh | 0 scripts/run-cucumber.sh | 0 scripts/travis.sh | 0 server/src/index.ts | 0 server/src/lib/AuthenticationSessionHandler.ts | 0 server/src/lib/ErrorReplies.ts | 0 server/src/lib/Exceptions.ts | 0 server/src/lib/FirstFactorValidator.ts | 0 server/src/lib/IdentityCheckMiddleware.spec.ts | 0 server/src/lib/IdentityCheckMiddleware.ts | 0 .../src/lib/IdentityCheckPreValidationTemplate.ts | 0 server/src/lib/IdentityValidable.ts | 0 server/src/lib/IdentityValidableStub.spec.ts | 0 server/src/lib/Server.spec.ts | 0 server/src/lib/Server.ts | 0 server/src/lib/ServerVariables.ts | 0 server/src/lib/ServerVariablesInitializer.ts | 0 server/src/lib/ServerVariablesMockBuilder.spec.ts | 0 server/src/lib/authentication/Level.ts | 0 .../lib/authentication/backends/GroupsAndEmails.ts | 0 .../lib/authentication/backends/IUsersDatabase.ts | 0 .../backends/IUsersDatabaseStub.spec.ts | 0 .../backends/file/FileUsersDatabase.spec.ts | 0 .../backends/file/FileUsersDatabase.ts | 0 .../authentication/backends/file/ReadWriteQueue.ts | 0 .../lib/authentication/backends/ldap/ISession.ts | 0 .../authentication/backends/ldap/ISessionFactory.ts | 0 .../backends/ldap/LdapUsersDatabase.spec.ts | 0 .../backends/ldap/LdapUsersDatabase.ts | 0 .../backends/ldap/SafeSession.spec.ts | 0 .../lib/authentication/backends/ldap/SafeSession.ts | 0 .../authentication/backends/ldap/Sanitizer.spec.ts | 0 .../lib/authentication/backends/ldap/Sanitizer.ts | 0 .../authentication/backends/ldap/Session.spec.ts | 0 .../src/lib/authentication/backends/ldap/Session.ts | 0 .../authentication/backends/ldap/SessionFactory.ts | 0 .../backends/ldap/SessionFactoryStub.spec.ts | 0 .../backends/ldap/SessionStub.spec.ts | 0 .../backends/ldap/connector/Connector.ts | 0 .../backends/ldap/connector/ConnectorFactory.ts | 0 .../ldap/connector/ConnectorFactoryStub.spec.ts | 0 .../backends/ldap/connector/ConnectorStub.spec.ts | 0 .../backends/ldap/connector/IConnector.ts | 0 .../backends/ldap/connector/IConnectorFactory.ts | 0 server/src/lib/authentication/totp/ITotpHandler.ts | 0 .../src/lib/authentication/totp/TotpHandler.spec.ts | 0 server/src/lib/authentication/totp/TotpHandler.ts | 0 .../lib/authentication/totp/TotpHandlerStub.spec.ts | 0 server/src/lib/authentication/u2f/IU2fHandler.ts | 0 server/src/lib/authentication/u2f/U2fHandler.ts | 0 .../lib/authentication/u2f/U2fHandlerStub.spec.ts | 0 server/src/lib/authorization/Authorizer.spec.ts | 0 server/src/lib/authorization/Authorizer.ts | 0 server/src/lib/authorization/AuthorizerStub.spec.ts | 0 server/src/lib/authorization/IAuthorizer.ts | 0 server/src/lib/authorization/Level.ts | 0 .../src/lib/authorization/MultipleDomainMatcher.ts | 0 server/src/lib/authorization/Object.ts | 0 server/src/lib/authorization/Subject.ts | 0 .../lib/configuration/ConfigurationParser.spec.ts | 0 server/src/lib/configuration/ConfigurationParser.ts | 0 .../SessionConfigurationBuilder.spec.ts | 0 .../configuration/SessionConfigurationBuilder.ts | 0 .../configuration/schema/AclConfiguration.spec.ts | 0 .../lib/configuration/schema/AclConfiguration.ts | 0 .../AuthenticationBackendConfiguration.spec.ts | 0 .../schema/AuthenticationBackendConfiguration.ts | 0 .../src/lib/configuration/schema/Configuration.ts | 0 .../schema/FileUsersDatabaseConfiguration.ts | 0 .../configuration/schema/LdapConfiguration.spec.ts | 0 .../lib/configuration/schema/LdapConfiguration.ts | 0 .../schema/NotifierConfiguration.spec.ts | 0 .../configuration/schema/NotifierConfiguration.ts | 0 .../schema/RegulationConfiguration.spec.ts | 0 .../configuration/schema/RegulationConfiguration.ts | 0 .../schema/SessionConfiguration.spec.ts | 0 .../configuration/schema/SessionConfiguration.ts | 0 .../schema/StorageConfiguration.spec.ts | 0 .../configuration/schema/StorageConfiguration.ts | 0 .../lib/configuration/schema/TotpConfiguration.ts | 0 .../schema/UserDatabaseConfiguration.ts | 0 server/src/lib/connectors/mongo/IMongoClient.d.ts | 0 server/src/lib/connectors/mongo/MongoClient.spec.ts | 0 server/src/lib/connectors/mongo/MongoClient.ts | 0 .../lib/connectors/mongo/MongoClientStub.spec.ts | 0 server/src/lib/logging/GlobalLogger.ts | 0 server/src/lib/logging/GlobalLoggerStub.spec.ts | 0 server/src/lib/logging/IGlobalLogger.ts | 0 server/src/lib/logging/IRequestLogger.ts | 0 server/src/lib/logging/RequestLogger.ts | 0 server/src/lib/logging/RequestLoggerStub.spec.ts | 0 server/src/lib/notifiers/AbstractEmailNotifier.ts | 0 server/src/lib/notifiers/EmailNotifier.spec.ts | 0 server/src/lib/notifiers/EmailNotifier.ts | 0 server/src/lib/notifiers/FileSystemNotifier.ts | 0 server/src/lib/notifiers/IMailSender.ts | 0 server/src/lib/notifiers/IMailSenderBuilder.ts | 0 server/src/lib/notifiers/INotifier.ts | 0 server/src/lib/notifiers/MailSender.ts | 0 server/src/lib/notifiers/MailSenderBuilder.spec.ts | 0 server/src/lib/notifiers/MailSenderBuilder.ts | 0 .../src/lib/notifiers/MailSenderBuilderStub.spec.ts | 0 server/src/lib/notifiers/MailSenderStub.spec.ts | 0 server/src/lib/notifiers/NotifierFactory.spec.ts | 0 server/src/lib/notifiers/NotifierFactory.ts | 0 server/src/lib/notifiers/NotifierStub.spec.ts | 0 server/src/lib/notifiers/SmtpNotifier.ts | 0 server/src/lib/regulation/IRegulator.ts | 0 server/src/lib/regulation/Regulator.spec.ts | 0 server/src/lib/regulation/Regulator.ts | 0 server/src/lib/regulation/RegulatorStub.spec.ts | 0 server/src/lib/routes/error/401/get.spec.ts | 0 server/src/lib/routes/error/401/get.ts | 0 server/src/lib/routes/error/403/get.spec.ts | 0 server/src/lib/routes/error/403/get.ts | 0 server/src/lib/routes/error/404/get.spec.ts | 0 server/src/lib/routes/error/404/get.ts | 0 server/src/lib/routes/error/redirector.ts | 0 server/src/lib/routes/firstfactor/get.ts | 0 server/src/lib/routes/firstfactor/post.spec.ts | 0 server/src/lib/routes/firstfactor/post.ts | 0 server/src/lib/routes/loggedin/get.ts | 0 server/src/lib/routes/logout/get.ts | 0 server/src/lib/routes/password-reset/constants.ts | 0 .../src/lib/routes/password-reset/form/post.spec.ts | 0 server/src/lib/routes/password-reset/form/post.ts | 0 .../identity/PasswordResetHandler.spec.ts | 0 .../password-reset/identity/PasswordResetHandler.ts | 0 server/src/lib/routes/password-reset/request/get.ts | 0 server/src/lib/routes/secondfactor/get.spec.ts | 0 server/src/lib/routes/secondfactor/get.ts | 0 server/src/lib/routes/secondfactor/redirect.spec.ts | 0 server/src/lib/routes/secondfactor/redirect.ts | 0 .../src/lib/routes/secondfactor/totp/constants.ts | 0 .../totp/identity/RegistrationHandler.spec.ts | 0 .../totp/identity/RegistrationHandler.ts | 0 .../lib/routes/secondfactor/totp/sign/post.spec.ts | 0 .../src/lib/routes/secondfactor/totp/sign/post.ts | 0 server/src/lib/routes/secondfactor/u2f/U2FCommon.ts | 0 .../u2f/identity/RegistrationHandler.spec.ts | 0 .../u2f/identity/RegistrationHandler.ts | 0 .../routes/secondfactor/u2f/register/post.spec.ts | 0 .../lib/routes/secondfactor/u2f/register/post.ts | 0 .../secondfactor/u2f/register_request/get.spec.ts | 0 .../routes/secondfactor/u2f/register_request/get.ts | 0 .../lib/routes/secondfactor/u2f/sign/post.spec.ts | 0 server/src/lib/routes/secondfactor/u2f/sign/post.ts | 0 .../secondfactor/u2f/sign_request/get.spec.ts | 0 .../lib/routes/secondfactor/u2f/sign_request/get.ts | 0 server/src/lib/routes/verify/access_control.ts | 0 server/src/lib/routes/verify/get.spec.ts | 0 server/src/lib/routes/verify/get.ts | 0 server/src/lib/routes/verify/get_basic_auth.ts | 0 server/src/lib/routes/verify/get_session_cookie.ts | 0 .../lib/storage/AuthenticationTraceDocument.d.ts | 0 server/src/lib/storage/CollectionFactoryFactory.ts | 0 .../src/lib/storage/CollectionFactoryStub.spec.ts | 0 server/src/lib/storage/CollectionStub.spec.ts | 0 server/src/lib/storage/ICollection.d.ts | 0 server/src/lib/storage/ICollectionFactory.d.ts | 0 server/src/lib/storage/IUserDataStore.d.ts | 0 .../src/lib/storage/IdentityValidationDocument.d.ts | 0 server/src/lib/storage/TOTPSecretDocument.d.ts | 0 server/src/lib/storage/U2FRegistrationDocument.d.ts | 0 server/src/lib/storage/UserDataStore.spec.ts | 0 server/src/lib/storage/UserDataStore.ts | 0 server/src/lib/storage/UserDataStoreStub.spec.ts | 0 .../src/lib/storage/mongo/MongoCollection.spec.ts | 0 server/src/lib/storage/mongo/MongoCollection.ts | 0 .../storage/mongo/MongoCollectionFactory.spec.ts | 0 .../src/lib/storage/mongo/MongoCollectionFactory.ts | 0 server/src/lib/storage/nedb/NedbCollection.spec.ts | 0 server/src/lib/storage/nedb/NedbCollection.ts | 0 .../lib/storage/nedb/NedbCollectionFactory.spec.ts | 0 .../src/lib/storage/nedb/NedbCollectionFactory.ts | 0 server/src/lib/stubs/express.spec.ts | 0 server/src/lib/stubs/ldapjs.spec.ts | 0 server/src/lib/stubs/speakeasy.spec.ts | 0 server/src/lib/stubs/u2f.spec.ts | 0 server/src/lib/utils/HashGenerator.spec.ts | 0 server/src/lib/utils/HashGenerator.ts | 0 server/src/lib/utils/ObjectCloner.ts | 0 server/src/lib/utils/SafeRedirection.spec.ts | 0 server/src/lib/utils/SafeRedirection.ts | 0 server/src/lib/utils/URLDecomposer.spec.ts | 0 server/src/lib/utils/URLDecomposer.ts | 0 server/src/lib/web_server/Configurator.ts | 0 server/src/lib/web_server/RestApi.ts | 0 .../middlewares/RequireValidatedFirstFactor.ts | 0 .../lib/web_server/middlewares/WithHeadersLogged.ts | 0 server/src/resources/email-template.ejs | 0 server/src/views/already-logged-in.pug | 0 server/src/views/errors/401.pug | 0 server/src/views/errors/403.pug | 0 server/src/views/errors/404.pug | 0 server/src/views/firstfactor.pug | 0 server/src/views/layout/layout.pug | 0 server/src/views/need-identity-validation.pug | 0 server/src/views/password-reset-form.pug | 0 server/src/views/password-reset-request.pug | 0 server/src/views/secondfactor.pug | 0 server/src/views/totp-register.pug | 0 server/src/views/u2f-register.pug | 0 server/test/requests.ts | 0 server/tsconfig.json | 0 server/tslint.json | 0 server/types/AuthenticationSession.ts | 0 server/types/Dependencies.ts | 0 server/types/Identity.ts | 0 server/types/TOTPSecret.ts | 0 server/types/U2FRegistration.ts | 0 server/types/dovehash.d.ts | 0 server/types/speakeasy.d.ts | 0 shared/BelongToDomain.ts | 0 shared/DomainExtractor.spec.ts | 0 shared/DomainExtractor.ts | 0 shared/ErrorMessage.ts | 0 shared/RedirectionMessage.ts | 0 shared/SignMessage.ts | 0 shared/UserMessages.ts | 0 shared/api.ts | 0 shared/constants.ts | 0 shared/types/u2f.d.ts | 0 test/complete-config/00-suite.ts | 0 test/complete-config/closed-redirection.ts | 0 test/complete-config/mongo-broken-connection.ts | 0 test/configuration.ts | 0 test/environment.ts | 0 test/features/access-control.feature | 0 test/features/auth-portal-redirection.feature | 0 test/features/authelia.feature | 0 test/features/authentication.feature | 0 test/features/forward-headers.feature | 0 test/features/redirection.feature | 0 test/features/registration.feature | 0 test/features/regulation.feature | 0 test/features/reset-password.feature | 0 test/features/resilience.feature | 0 test/features/restrictions.feature | 0 test/features/session-timeout.feature | 0 test/features/single-factor-domain.feature | 0 test/features/step_definitions/access-control.ts | 0 test/features/step_definitions/authelia.ts | 0 test/features/step_definitions/authentication.ts | 0 test/features/step_definitions/forward-headers.ts | 0 test/features/step_definitions/hooks.ts | 0 test/features/step_definitions/notifications.ts | 0 test/features/step_definitions/redirection.ts | 0 test/features/step_definitions/registration.ts | 0 test/features/step_definitions/regulation.ts | 0 test/features/step_definitions/reset-password.ts | 0 test/features/step_definitions/resilience.ts | 0 test/features/step_definitions/restrictions.ts | 0 test/features/step_definitions/session-timeout.ts | 0 test/features/step_definitions/single-factor.ts | 0 test/features/support/world.ts | 0 test/helpers/access-secret.ts | 0 test/helpers/click-on-button.ts | 0 test/helpers/click-on-link.ts | 0 test/helpers/fill-field.ts | 0 test/helpers/fill-login-page-and-click.ts | 0 test/helpers/full-login.ts | 0 test/helpers/get-identity-link.ts | 0 test/helpers/login-and-register-totp.ts | 0 test/helpers/login-as.ts | 0 test/helpers/register-totp.ts | 0 test/helpers/see-notification.ts | 0 test/helpers/validate-totp.ts | 0 test/helpers/visit-page.ts | 0 test/helpers/wait-redirected.ts | 0 test/helpers/with-driver.ts | 0 test/inactivity/00-suite.ts | 0 test/inactivity/keep_me_logged_in.ts | 0 test/minimal-config/00-suite.ts | 0 test/minimal-config/bad_password.ts | 0 test/minimal-config/fail_totp.ts | 0 test/minimal-config/register_totp.ts | 0 test/minimal-config/reset_password.ts | 0 test/minimal-config/validate_totp.ts | 0 themes/black/client/src/css/.directory | 0 themes/black/client/src/css/00-bootstrap.min.css | 0 themes/black/client/src/css/01-main.css | 0 themes/black/client/src/css/02-login.css | 0 themes/black/client/src/css/03-errors.css | 0 .../black/client/src/css/03-password-reset-form.css | 0 .../client/src/css/03-password-reset-request.css | 0 themes/black/client/src/css/03-totp-register.css | 0 themes/black/client/src/css/03-u2f-register.css | 0 themes/black/client/src/img/RandomizedPattern.svg | 0 themes/black/client/src/img/background.jpg | Bin themes/black/client/src/img/icon.png | Bin themes/black/client/src/img/mail.png | Bin .../black/client/src/img/notifications/.directory | 0 themes/black/client/src/img/notifications/error.png | Bin themes/black/client/src/img/notifications/info.png | Bin .../black/client/src/img/notifications/success.png | Bin .../black/client/src/img/notifications/warning.png | Bin themes/black/client/src/img/padlock.png | Bin themes/black/client/src/img/password_white.png | Bin themes/black/client/src/img/pendrive.png | Bin themes/black/client/src/img/sharingan.png | Bin themes/black/client/src/img/stores/.directory | 0 .../client/src/img/stores/applestore-badge.svg | 0 .../client/src/img/stores/googleplay-badge.svg | 0 themes/black/client/src/img/success.png | Bin themes/black/client/src/img/user.png | Bin themes/black/client/src/img/warning.png | Bin themes/black/client/src/thirdparties/qrcode.min.js | 0 themes/black/client/src/thirdparties/u2f-api.js | 0 themes/black/server/.directory | 0 .../black/server/src/resources/email-template.ejs | 0 themes/black/server/src/views/already-logged-in.pug | 0 themes/black/server/src/views/errors/.directory | 0 themes/black/server/src/views/errors/401.pug | 0 themes/black/server/src/views/errors/403.pug | 0 themes/black/server/src/views/errors/404.pug | 0 themes/black/server/src/views/firstfactor.pug | 0 themes/black/server/src/views/layout/layout.pug | 0 .../server/src/views/need-identity-validation.pug | 0 .../black/server/src/views/password-reset-form.pug | 0 .../server/src/views/password-reset-request.pug | 0 themes/black/server/src/views/secondfactor.pug | 0 themes/black/server/src/views/totp-register.pug | 0 themes/black/server/src/views/u2f-register.pug | 0 themes/default/client/src/css/.directory | 0 themes/default/client/src/css/00-bootstrap.min.css | 0 themes/default/client/src/css/01-main.css | 0 themes/default/client/src/css/02-login.css | 0 themes/default/client/src/css/03-errors.css | 0 .../client/src/css/03-password-reset-form.css | 0 .../client/src/css/03-password-reset-request.css | 0 themes/default/client/src/css/03-totp-register.css | 0 themes/default/client/src/css/03-u2f-register.css | 0 themes/default/client/src/img/background.svg | 0 themes/default/client/src/img/icon.png | Bin themes/default/client/src/img/mail.png | Bin .../default/client/src/img/notifications/.directory | 0 .../default/client/src/img/notifications/error.png | Bin .../default/client/src/img/notifications/info.png | Bin .../client/src/img/notifications/success.png | Bin .../client/src/img/notifications/warning.png | Bin themes/default/client/src/img/padlock.png | Bin themes/default/client/src/img/password.png | Bin themes/default/client/src/img/pendrive.png | Bin themes/default/client/src/img/stores/.directory | 0 .../client/src/img/stores/applestore-badge.svg | 0 .../client/src/img/stores/googleplay-badge.svg | 0 themes/default/client/src/img/success.png | Bin themes/default/client/src/img/user.png | Bin themes/default/client/src/img/warning.png | Bin .../default/client/src/thirdparties/qrcode.min.js | 0 themes/default/server/.directory | 0 .../default/server/src/resources/email-template.ejs | 0 .../default/server/src/views/already-logged-in.pug | 0 themes/default/server/src/views/errors/.directory | 0 themes/default/server/src/views/errors/401.pug | 0 themes/default/server/src/views/errors/403.pug | 0 themes/default/server/src/views/errors/404.pug | 0 themes/default/server/src/views/firstfactor.pug | 0 themes/default/server/src/views/layout/layout.pug | 0 .../server/src/views/need-identity-validation.pug | 0 .../server/src/views/password-reset-form.pug | 0 .../server/src/views/password-reset-request.pug | 0 themes/default/server/src/views/secondfactor.pug | 0 themes/default/server/src/views/totp-register.pug | 0 themes/default/server/src/views/u2f-register.pug | 0 themes/matrix/client/src/css/.directory | 0 themes/matrix/client/src/css/00-bootstrap.min.css | 0 themes/matrix/client/src/css/01-main.css | 0 themes/matrix/client/src/css/02-login.css | 0 themes/matrix/client/src/css/03-errors.css | 0 .../client/src/css/03-password-reset-form.css | 0 .../client/src/css/03-password-reset-request.css | 0 themes/matrix/client/src/css/03-totp-register.css | 0 themes/matrix/client/src/css/03-u2f-register.css | 0 themes/matrix/client/src/img/background.jpg | Bin themes/matrix/client/src/img/icon.png | Bin themes/matrix/client/src/img/mail.png | Bin .../matrix/client/src/img/matrix_circle_128x128.png | Bin .../matrix/client/src/img/notifications/.directory | 0 .../matrix/client/src/img/notifications/error.png | Bin themes/matrix/client/src/img/notifications/info.png | Bin .../matrix/client/src/img/notifications/success.png | Bin .../matrix/client/src/img/notifications/warning.png | Bin themes/matrix/client/src/img/padlock.png | Bin themes/matrix/client/src/img/password_white.png | Bin themes/matrix/client/src/img/pendrive.png | Bin themes/matrix/client/src/img/stores/.directory | 0 .../client/src/img/stores/applestore-badge.svg | 0 .../client/src/img/stores/googleplay-badge.svg | 0 themes/matrix/client/src/img/success.png | Bin themes/matrix/client/src/img/user.png | Bin themes/matrix/client/src/img/warning.png | Bin themes/matrix/client/src/thirdparties/matrix.js | 0 themes/matrix/client/src/thirdparties/qrcode.min.js | 0 themes/matrix/client/src/thirdparties/u2f-api.js | 0 themes/matrix/server/.directory | 0 .../matrix/server/src/resources/email-template.ejs | 0 .../matrix/server/src/views/already-logged-in.pug | 0 themes/matrix/server/src/views/errors/.directory | 0 themes/matrix/server/src/views/errors/401.pug | 0 themes/matrix/server/src/views/errors/403.pug | 0 themes/matrix/server/src/views/errors/404.pug | 0 themes/matrix/server/src/views/firstfactor.pug | 0 themes/matrix/server/src/views/layout/layout.pug | 0 .../server/src/views/need-identity-validation.pug | 0 .../matrix/server/src/views/password-reset-form.pug | 0 .../server/src/views/password-reset-request.pug | 0 themes/matrix/server/src/views/secondfactor.pug | 0 themes/matrix/server/src/views/totp-register.pug | 0 themes/matrix/server/src/views/u2f-register.pug | 0 themes/squares/client/src/css/.directory | 0 themes/squares/client/src/css/00-bootstrap.min.css | 0 themes/squares/client/src/css/01-main.css | 0 themes/squares/client/src/css/02-login.css | 0 themes/squares/client/src/css/03-errors.css | 0 .../client/src/css/03-password-reset-form.css | 0 .../client/src/css/03-password-reset-request.css | 0 themes/squares/client/src/css/03-totp-register.css | 0 themes/squares/client/src/css/03-u2f-register.css | 0 themes/squares/client/src/img/LargeTriangles.svg | 0 themes/squares/client/src/img/RandomizedPattern.svg | 0 themes/squares/client/src/img/background.jpg | Bin themes/squares/client/src/img/background.svg | 0 themes/squares/client/src/img/icon.png | Bin themes/squares/client/src/img/mail.png | Bin .../client/src/img/matrix_circle_128x128.png | Bin .../squares/client/src/img/notifications/.directory | 0 .../squares/client/src/img/notifications/error.png | Bin .../squares/client/src/img/notifications/info.png | Bin .../client/src/img/notifications/success.png | Bin .../client/src/img/notifications/warning.png | Bin themes/squares/client/src/img/padlock.png | Bin themes/squares/client/src/img/password_white.png | Bin themes/squares/client/src/img/pendrive.png | Bin themes/squares/client/src/img/sharingan.png | Bin themes/squares/client/src/img/stores/.directory | 0 .../client/src/img/stores/applestore-badge.svg | 0 .../client/src/img/stores/googleplay-badge.svg | 0 themes/squares/client/src/img/success.png | Bin themes/squares/client/src/img/user.png | Bin themes/squares/client/src/img/warning.png | Bin .../squares/client/src/thirdparties/qrcode.min.js | 0 themes/squares/client/src/thirdparties/u2f-api.js | 0 themes/squares/server/.directory | 0 .../squares/server/src/resources/email-template.ejs | 0 .../squares/server/src/views/already-logged-in.pug | 0 themes/squares/server/src/views/errors/.directory | 0 themes/squares/server/src/views/errors/401.pug | 0 themes/squares/server/src/views/errors/403.pug | 0 themes/squares/server/src/views/errors/404.pug | 0 themes/squares/server/src/views/firstfactor.pug | 0 themes/squares/server/src/views/layout/layout.pug | 0 .../server/src/views/need-identity-validation.pug | 0 .../server/src/views/password-reset-form.pug | 0 .../server/src/views/password-reset-request.pug | 0 themes/squares/server/src/views/secondfactor.pug | 0 themes/squares/server/src/views/totp-register.pug | 0 themes/squares/server/src/views/u2f-register.pug | 0 themes/triangles/client/src/.directory | 0 themes/triangles/client/src/css/.directory | 0 .../triangles/client/src/css/00-bootstrap.min.css | 0 themes/triangles/client/src/css/01-main.css | 0 themes/triangles/client/src/css/02-login.css | 0 themes/triangles/client/src/css/03-errors.css | 0 .../client/src/css/03-password-reset-form.css | 0 .../client/src/css/03-password-reset-request.css | 0 .../triangles/client/src/css/03-totp-register.css | 0 themes/triangles/client/src/css/03-u2f-register.css | 0 themes/triangles/client/src/img/LargeTriangles.svg | 0 themes/triangles/client/src/img/background.jpg | Bin themes/triangles/client/src/img/icon.png | Bin themes/triangles/client/src/img/mail.png | Bin .../client/src/img/matrix_circle_128x128.png | Bin .../client/src/img/notifications/.directory | 0 .../client/src/img/notifications/error.png | Bin .../triangles/client/src/img/notifications/info.png | Bin .../client/src/img/notifications/success.png | Bin .../client/src/img/notifications/warning.png | Bin themes/triangles/client/src/img/padlock.png | Bin themes/triangles/client/src/img/password_white.png | Bin themes/triangles/client/src/img/pendrive.png | Bin themes/triangles/client/src/img/sharingan.png | Bin themes/triangles/client/src/img/stores/.directory | 0 .../client/src/img/stores/applestore-badge.svg | 0 .../client/src/img/stores/googleplay-badge.svg | 0 themes/triangles/client/src/img/success.png | Bin themes/triangles/client/src/img/user.png | Bin themes/triangles/client/src/img/warning.png | Bin .../triangles/client/src/thirdparties/qrcode.min.js | 0 themes/triangles/client/src/thirdparties/u2f-api.js | 0 themes/triangles/server/.directory | 0 .../server/src/resources/email-template.ejs | 0 .../server/src/views/already-logged-in.pug | 0 themes/triangles/server/src/views/errors/.directory | 0 themes/triangles/server/src/views/errors/401.pug | 0 themes/triangles/server/src/views/errors/403.pug | 0 themes/triangles/server/src/views/errors/404.pug | 0 themes/triangles/server/src/views/firstfactor.pug | 0 themes/triangles/server/src/views/layout/layout.pug | 0 .../server/src/views/need-identity-validation.pug | 0 .../server/src/views/password-reset-form.pug | 0 .../server/src/views/password-reset-request.pug | 0 themes/triangles/server/src/views/secondfactor.pug | 0 themes/triangles/server/src/views/totp-register.pug | 0 themes/triangles/server/src/views/u2f-register.pug | 0 users_database.yml | 0 685 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 .gitignore mode change 100755 => 100644 .npmignore mode change 100755 => 100644 .travis.yml mode change 100755 => 100644 CHANGELOG.md mode change 100755 => 100644 CONTRIBUTORS.md mode change 100755 => 100644 Dockerfile mode change 100755 => 100644 Dockerfile.dev mode change 100755 => 100644 Gruntfile.js mode change 100755 => 100644 LICENSE mode change 100755 => 100644 README.md mode change 100755 => 100644 client/src/css/00-bootstrap.min.css mode change 100755 => 100644 client/src/css/01-main.css mode change 100755 => 100644 client/src/css/02-login.css mode change 100755 => 100644 client/src/css/03-errors.css mode change 100755 => 100644 client/src/css/03-password-reset-form.css mode change 100755 => 100644 client/src/css/03-password-reset-request.css mode change 100755 => 100644 client/src/css/03-totp-register.css mode change 100755 => 100644 client/src/css/03-u2f-register.css mode change 100755 => 100644 client/src/img/background.svg mode change 100755 => 100644 client/src/img/icon.png mode change 100755 => 100644 client/src/img/mail.png mode change 100755 => 100644 client/src/img/notifications/error.png mode change 100755 => 100644 client/src/img/notifications/info.png mode change 100755 => 100644 client/src/img/notifications/success.png mode change 100755 => 100644 client/src/img/notifications/warning.png mode change 100755 => 100644 client/src/img/padlock.png mode change 100755 => 100644 client/src/img/password.png mode change 100755 => 100644 client/src/img/pendrive.png mode change 100755 => 100644 client/src/img/stores/applestore-badge.svg mode change 100755 => 100644 client/src/img/stores/googleplay-badge.svg mode change 100755 => 100644 client/src/img/success.png mode change 100755 => 100644 client/src/img/user.png mode change 100755 => 100644 client/src/img/warning.png mode change 100755 => 100644 client/src/index.ts mode change 100755 => 100644 client/src/lib/GetPromised.ts mode change 100755 => 100644 client/src/lib/INotifier.ts mode change 100755 => 100644 client/src/lib/Notifier.ts mode change 100755 => 100644 client/src/lib/QueryParametersRetriever.ts mode change 100755 => 100644 client/src/lib/SafeRedirect.ts mode change 100755 => 100644 client/src/lib/firstfactor/FirstFactorValidator.ts mode change 100755 => 100644 client/src/lib/firstfactor/UISelectors.ts mode change 100755 => 100644 client/src/lib/firstfactor/index.ts mode change 100755 => 100644 client/src/lib/reset-password/constants.ts mode change 100755 => 100644 client/src/lib/reset-password/reset-password-form.ts mode change 100755 => 100644 client/src/lib/reset-password/reset-password-request.ts mode change 100755 => 100644 client/src/lib/secondfactor/TOTPValidator.ts mode change 100755 => 100644 client/src/lib/secondfactor/U2FValidator.ts mode change 100755 => 100644 client/src/lib/secondfactor/constants.ts mode change 100755 => 100644 client/src/lib/secondfactor/index.ts mode change 100755 => 100644 client/src/lib/totp-register/totp-register.ts mode change 100755 => 100644 client/src/lib/totp-register/ui-selector.ts mode change 100755 => 100644 client/src/lib/u2f-register/u2f-register.ts mode change 100755 => 100644 client/src/thirdparties/qrcode.min.js mode change 100755 => 100644 client/test/Notifier.test.ts mode change 100755 => 100644 client/test/firstfactor/FirstFactorValidator.test.ts mode change 100755 => 100644 client/test/mocks/NotifierStub.ts mode change 100755 => 100644 client/test/mocks/jquery.ts mode change 100755 => 100644 client/test/mocks/u2f-api.ts mode change 100755 => 100644 client/test/secondfactor/TOTPValidator.test.ts mode change 100755 => 100644 client/test/totp-register/totp-register.test.ts mode change 100755 => 100644 client/tsconfig.json mode change 100755 => 100644 client/tslint.json mode change 100755 => 100644 config.minimal.yml mode change 100755 => 100644 config.template.yml mode change 100755 => 100644 docker-compose.dev.yml mode change 100755 => 100644 docker-compose.dockerhub.yml mode change 100755 => 100644 docker-compose.minimal.dev.yml mode change 100755 => 100644 docker-compose.minimal.yml mode change 100755 => 100644 docker-compose.swarm.minimal.yml mode change 100755 => 100644 docker-compose.test.yml mode change 100755 => 100644 docker-compose.yml mode change 100755 => 100644 docs/build.md mode change 100755 => 100644 docs/configuration.md mode change 100755 => 100644 docs/deployment-dev.md mode change 100755 => 100644 docs/deployment-production.md mode change 100755 => 100644 docs/features.md mode change 100755 => 100644 docs/getting-started.md mode change 100755 => 100644 docs/security.md mode change 100755 => 100644 example/compose/authelia/docker-compose.test.yml mode change 100755 => 100644 example/compose/docker-compose.base.yml mode change 100755 => 100644 example/compose/httpbin/docker-compose.yml mode change 100755 => 100644 example/compose/ldap/access.rules mode change 100755 => 100644 example/compose/ldap/base.ldif mode change 100755 => 100644 example/compose/ldap/docker-compose.admin.yml mode change 100755 => 100644 example/compose/ldap/docker-compose.yml mode change 100755 => 100644 example/compose/mongo/docker-compose.yml mode change 100755 => 100644 example/compose/nginx/backend/docker-compose.yml mode change 100755 => 100644 example/compose/nginx/backend/html/admin/secret.html mode change 100755 => 100644 example/compose/nginx/backend/html/dev/groups/admin/secret.html mode change 100755 => 100644 example/compose/nginx/backend/html/dev/groups/dev/secret.html mode change 100755 => 100644 example/compose/nginx/backend/html/dev/users/bob/secret.html mode change 100755 => 100644 example/compose/nginx/backend/html/dev/users/harry/secret.html mode change 100755 => 100644 example/compose/nginx/backend/html/dev/users/john/secret.html mode change 100755 => 100644 example/compose/nginx/backend/html/home/index.html mode change 100755 => 100644 example/compose/nginx/backend/html/icon.png mode change 100755 => 100644 example/compose/nginx/backend/html/mail/secret.html mode change 100755 => 100644 example/compose/nginx/backend/html/public/index.html mode change 100755 => 100644 example/compose/nginx/backend/html/public/secret.html mode change 100755 => 100644 example/compose/nginx/backend/html/single_factor/secret.html mode change 100755 => 100644 example/compose/nginx/backend/nginx.conf mode change 100755 => 100644 example/compose/nginx/minimal/docker-compose.yml mode change 100755 => 100644 example/compose/nginx/minimal/html/admin/secret.html mode change 100755 => 100644 example/compose/nginx/minimal/html/home/index.html mode change 100755 => 100644 example/compose/nginx/minimal/nginx.conf mode change 100755 => 100644 example/compose/nginx/minimal/ssl/server.crt mode change 100755 => 100644 example/compose/nginx/minimal/ssl/server.csr mode change 100755 => 100644 example/compose/nginx/minimal/ssl/server.key mode change 100755 => 100644 example/compose/nginx/portal/docker-compose.yml mode change 100755 => 100644 example/compose/nginx/portal/nginx.conf mode change 100755 => 100644 example/compose/nginx/portal/ssl/server.crt mode change 100755 => 100644 example/compose/nginx/portal/ssl/server.csr mode change 100755 => 100644 example/compose/nginx/portal/ssl/server.key mode change 100755 => 100644 example/compose/redis/docker-compose.yml mode change 100755 => 100644 example/compose/smtp/docker-compose.yml mode change 100755 => 100644 example/kube/README.md mode change 100755 => 100644 example/kube/apps/app-home/deployment.yml mode change 100755 => 100644 example/kube/apps/app-home/index.html mode change 100755 => 100644 example/kube/apps/app-home/service.yml mode change 100755 => 100644 example/kube/apps/app1/deployment.yml mode change 100755 => 100644 example/kube/apps/app1/index.html mode change 100755 => 100644 example/kube/apps/app1/service.yml mode change 100755 => 100644 example/kube/apps/app1/ssl/tls.crt mode change 100755 => 100644 example/kube/apps/app1/ssl/tls.csr mode change 100755 => 100644 example/kube/apps/app1/ssl/tls.key mode change 100755 => 100644 example/kube/apps/app2/deployment.yml mode change 100755 => 100644 example/kube/apps/app2/index.html mode change 100755 => 100644 example/kube/apps/app2/service.yml mode change 100755 => 100644 example/kube/apps/app2/ssl/tls.crt mode change 100755 => 100644 example/kube/apps/app2/ssl/tls.csr mode change 100755 => 100644 example/kube/apps/app2/ssl/tls.key mode change 100755 => 100644 example/kube/apps/insecure-ingress.yml mode change 100755 => 100644 example/kube/apps/secure-ingress.yml mode change 100755 => 100644 example/kube/authelia/configs/config.yml mode change 100755 => 100644 example/kube/authelia/deployment.yml mode change 100755 => 100644 example/kube/authelia/ingress.yml mode change 100755 => 100644 example/kube/authelia/service.yml mode change 100755 => 100644 example/kube/authelia/ssl/tls.crt mode change 100755 => 100644 example/kube/authelia/ssl/tls.csr mode change 100755 => 100644 example/kube/authelia/ssl/tls.key mode change 100755 => 100644 example/kube/bootstrap.sh mode change 100755 => 100644 example/kube/build_and_push.sh mode change 100755 => 100644 example/kube/docker-registry/daemonset.yml mode change 100755 => 100644 example/kube/docker-registry/ingress.yml mode change 100755 => 100644 example/kube/docker-registry/replicationcontroller.yml mode change 100755 => 100644 example/kube/docker-registry/service.yml mode change 100755 => 100644 example/kube/ingress-controller/default-backend.yml mode change 100755 => 100644 example/kube/ingress-controller/deployment.yml mode change 100755 => 100644 example/kube/ingress-controller/service.yml mode change 100755 => 100644 example/kube/ldap/Dockerfile mode change 100755 => 100644 example/kube/ldap/deployment.yml mode change 100755 => 100644 example/kube/ldap/service.yml mode change 100755 => 100644 example/kube/mailcatcher/deployment.yml mode change 100755 => 100644 example/kube/mailcatcher/ingress.yml mode change 100755 => 100644 example/kube/mailcatcher/service.yml mode change 100755 => 100644 example/kube/namespace.yml mode change 100755 => 100644 example/kube/storage/mongo.yml mode change 100755 => 100644 example/kube/storage/redis.yml mode change 100755 => 100644 images/authelia-title-white.png mode change 100755 => 100644 images/authelia-title.png mode change 100755 => 100644 images/email_confirmation.png mode change 100755 => 100644 images/first_factor.png mode change 100755 => 100644 images/icon.png mode change 100755 => 100644 images/kube-logo.png mode change 100755 => 100644 images/reset_password.png mode change 100755 => 100644 images/second_factor.png mode change 100755 => 100644 images/totp.png mode change 100755 => 100644 images/u2f.png mode change 100755 => 100644 package-lock.json mode change 100755 => 100644 package.json mode change 100755 => 100644 scripts/dc-dev.sh mode change 100755 => 100644 scripts/docker-publish.sh mode change 100755 => 100644 scripts/example-commit/dc-example.sh mode change 100755 => 100644 scripts/example-commit/deploy-example.sh mode change 100755 => 100644 scripts/example-commit/undeploy-example.sh mode change 100755 => 100644 scripts/example-dockerhub/dc-example.sh mode change 100755 => 100644 scripts/example-dockerhub/deploy-example.sh mode change 100755 => 100644 scripts/example-dockerhub/undeploy-example.sh mode change 100755 => 100644 scripts/integration-tests.sh mode change 100755 => 100644 scripts/npm-deployment-test.sh mode change 100755 => 100644 scripts/run-cucumber.sh mode change 100755 => 100644 scripts/travis.sh mode change 100755 => 100644 server/src/index.ts mode change 100755 => 100644 server/src/lib/AuthenticationSessionHandler.ts mode change 100755 => 100644 server/src/lib/ErrorReplies.ts mode change 100755 => 100644 server/src/lib/Exceptions.ts mode change 100755 => 100644 server/src/lib/FirstFactorValidator.ts mode change 100755 => 100644 server/src/lib/IdentityCheckMiddleware.spec.ts mode change 100755 => 100644 server/src/lib/IdentityCheckMiddleware.ts mode change 100755 => 100644 server/src/lib/IdentityCheckPreValidationTemplate.ts mode change 100755 => 100644 server/src/lib/IdentityValidable.ts mode change 100755 => 100644 server/src/lib/IdentityValidableStub.spec.ts mode change 100755 => 100644 server/src/lib/Server.spec.ts mode change 100755 => 100644 server/src/lib/Server.ts mode change 100755 => 100644 server/src/lib/ServerVariables.ts mode change 100755 => 100644 server/src/lib/ServerVariablesInitializer.ts mode change 100755 => 100644 server/src/lib/ServerVariablesMockBuilder.spec.ts mode change 100755 => 100644 server/src/lib/authentication/Level.ts mode change 100755 => 100644 server/src/lib/authentication/backends/GroupsAndEmails.ts mode change 100755 => 100644 server/src/lib/authentication/backends/IUsersDatabase.ts mode change 100755 => 100644 server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts mode change 100755 => 100644 server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts mode change 100755 => 100644 server/src/lib/authentication/backends/file/FileUsersDatabase.ts mode change 100755 => 100644 server/src/lib/authentication/backends/file/ReadWriteQueue.ts mode change 100755 => 100644 server/src/lib/authentication/backends/ldap/ISession.ts mode change 100755 => 100644 server/src/lib/authentication/backends/ldap/ISessionFactory.ts mode change 100755 => 100644 server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts mode change 100755 => 100644 server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts mode change 100755 => 100644 server/src/lib/authentication/backends/ldap/SafeSession.spec.ts mode change 100755 => 100644 server/src/lib/authentication/backends/ldap/SafeSession.ts mode change 100755 => 100644 server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts mode change 100755 => 100644 server/src/lib/authentication/backends/ldap/Sanitizer.ts mode change 100755 => 100644 server/src/lib/authentication/backends/ldap/Session.spec.ts mode change 100755 => 100644 server/src/lib/authentication/backends/ldap/Session.ts mode change 100755 => 100644 server/src/lib/authentication/backends/ldap/SessionFactory.ts mode change 100755 => 100644 server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts mode change 100755 => 100644 server/src/lib/authentication/backends/ldap/SessionStub.spec.ts mode change 100755 => 100644 server/src/lib/authentication/backends/ldap/connector/Connector.ts mode change 100755 => 100644 server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts mode change 100755 => 100644 server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts mode change 100755 => 100644 server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts mode change 100755 => 100644 server/src/lib/authentication/backends/ldap/connector/IConnector.ts mode change 100755 => 100644 server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts mode change 100755 => 100644 server/src/lib/authentication/totp/ITotpHandler.ts mode change 100755 => 100644 server/src/lib/authentication/totp/TotpHandler.spec.ts mode change 100755 => 100644 server/src/lib/authentication/totp/TotpHandler.ts mode change 100755 => 100644 server/src/lib/authentication/totp/TotpHandlerStub.spec.ts mode change 100755 => 100644 server/src/lib/authentication/u2f/IU2fHandler.ts mode change 100755 => 100644 server/src/lib/authentication/u2f/U2fHandler.ts mode change 100755 => 100644 server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts mode change 100755 => 100644 server/src/lib/authorization/Authorizer.spec.ts mode change 100755 => 100644 server/src/lib/authorization/Authorizer.ts mode change 100755 => 100644 server/src/lib/authorization/AuthorizerStub.spec.ts mode change 100755 => 100644 server/src/lib/authorization/IAuthorizer.ts mode change 100755 => 100644 server/src/lib/authorization/Level.ts mode change 100755 => 100644 server/src/lib/authorization/MultipleDomainMatcher.ts mode change 100755 => 100644 server/src/lib/authorization/Object.ts mode change 100755 => 100644 server/src/lib/authorization/Subject.ts mode change 100755 => 100644 server/src/lib/configuration/ConfigurationParser.spec.ts mode change 100755 => 100644 server/src/lib/configuration/ConfigurationParser.ts mode change 100755 => 100644 server/src/lib/configuration/SessionConfigurationBuilder.spec.ts mode change 100755 => 100644 server/src/lib/configuration/SessionConfigurationBuilder.ts mode change 100755 => 100644 server/src/lib/configuration/schema/AclConfiguration.spec.ts mode change 100755 => 100644 server/src/lib/configuration/schema/AclConfiguration.ts mode change 100755 => 100644 server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts mode change 100755 => 100644 server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts mode change 100755 => 100644 server/src/lib/configuration/schema/Configuration.ts mode change 100755 => 100644 server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts mode change 100755 => 100644 server/src/lib/configuration/schema/LdapConfiguration.spec.ts mode change 100755 => 100644 server/src/lib/configuration/schema/LdapConfiguration.ts mode change 100755 => 100644 server/src/lib/configuration/schema/NotifierConfiguration.spec.ts mode change 100755 => 100644 server/src/lib/configuration/schema/NotifierConfiguration.ts mode change 100755 => 100644 server/src/lib/configuration/schema/RegulationConfiguration.spec.ts mode change 100755 => 100644 server/src/lib/configuration/schema/RegulationConfiguration.ts mode change 100755 => 100644 server/src/lib/configuration/schema/SessionConfiguration.spec.ts mode change 100755 => 100644 server/src/lib/configuration/schema/SessionConfiguration.ts mode change 100755 => 100644 server/src/lib/configuration/schema/StorageConfiguration.spec.ts mode change 100755 => 100644 server/src/lib/configuration/schema/StorageConfiguration.ts mode change 100755 => 100644 server/src/lib/configuration/schema/TotpConfiguration.ts mode change 100755 => 100644 server/src/lib/configuration/schema/UserDatabaseConfiguration.ts mode change 100755 => 100644 server/src/lib/connectors/mongo/IMongoClient.d.ts mode change 100755 => 100644 server/src/lib/connectors/mongo/MongoClient.spec.ts mode change 100755 => 100644 server/src/lib/connectors/mongo/MongoClient.ts mode change 100755 => 100644 server/src/lib/connectors/mongo/MongoClientStub.spec.ts mode change 100755 => 100644 server/src/lib/logging/GlobalLogger.ts mode change 100755 => 100644 server/src/lib/logging/GlobalLoggerStub.spec.ts mode change 100755 => 100644 server/src/lib/logging/IGlobalLogger.ts mode change 100755 => 100644 server/src/lib/logging/IRequestLogger.ts mode change 100755 => 100644 server/src/lib/logging/RequestLogger.ts mode change 100755 => 100644 server/src/lib/logging/RequestLoggerStub.spec.ts mode change 100755 => 100644 server/src/lib/notifiers/AbstractEmailNotifier.ts mode change 100755 => 100644 server/src/lib/notifiers/EmailNotifier.spec.ts mode change 100755 => 100644 server/src/lib/notifiers/EmailNotifier.ts mode change 100755 => 100644 server/src/lib/notifiers/FileSystemNotifier.ts mode change 100755 => 100644 server/src/lib/notifiers/IMailSender.ts mode change 100755 => 100644 server/src/lib/notifiers/IMailSenderBuilder.ts mode change 100755 => 100644 server/src/lib/notifiers/INotifier.ts mode change 100755 => 100644 server/src/lib/notifiers/MailSender.ts mode change 100755 => 100644 server/src/lib/notifiers/MailSenderBuilder.spec.ts mode change 100755 => 100644 server/src/lib/notifiers/MailSenderBuilder.ts mode change 100755 => 100644 server/src/lib/notifiers/MailSenderBuilderStub.spec.ts mode change 100755 => 100644 server/src/lib/notifiers/MailSenderStub.spec.ts mode change 100755 => 100644 server/src/lib/notifiers/NotifierFactory.spec.ts mode change 100755 => 100644 server/src/lib/notifiers/NotifierFactory.ts mode change 100755 => 100644 server/src/lib/notifiers/NotifierStub.spec.ts mode change 100755 => 100644 server/src/lib/notifiers/SmtpNotifier.ts mode change 100755 => 100644 server/src/lib/regulation/IRegulator.ts mode change 100755 => 100644 server/src/lib/regulation/Regulator.spec.ts mode change 100755 => 100644 server/src/lib/regulation/Regulator.ts mode change 100755 => 100644 server/src/lib/regulation/RegulatorStub.spec.ts mode change 100755 => 100644 server/src/lib/routes/error/401/get.spec.ts mode change 100755 => 100644 server/src/lib/routes/error/401/get.ts mode change 100755 => 100644 server/src/lib/routes/error/403/get.spec.ts mode change 100755 => 100644 server/src/lib/routes/error/403/get.ts mode change 100755 => 100644 server/src/lib/routes/error/404/get.spec.ts mode change 100755 => 100644 server/src/lib/routes/error/404/get.ts mode change 100755 => 100644 server/src/lib/routes/error/redirector.ts mode change 100755 => 100644 server/src/lib/routes/firstfactor/get.ts mode change 100755 => 100644 server/src/lib/routes/firstfactor/post.spec.ts mode change 100755 => 100644 server/src/lib/routes/firstfactor/post.ts mode change 100755 => 100644 server/src/lib/routes/loggedin/get.ts mode change 100755 => 100644 server/src/lib/routes/logout/get.ts mode change 100755 => 100644 server/src/lib/routes/password-reset/constants.ts mode change 100755 => 100644 server/src/lib/routes/password-reset/form/post.spec.ts mode change 100755 => 100644 server/src/lib/routes/password-reset/form/post.ts mode change 100755 => 100644 server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts mode change 100755 => 100644 server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts mode change 100755 => 100644 server/src/lib/routes/password-reset/request/get.ts mode change 100755 => 100644 server/src/lib/routes/secondfactor/get.spec.ts mode change 100755 => 100644 server/src/lib/routes/secondfactor/get.ts mode change 100755 => 100644 server/src/lib/routes/secondfactor/redirect.spec.ts mode change 100755 => 100644 server/src/lib/routes/secondfactor/redirect.ts mode change 100755 => 100644 server/src/lib/routes/secondfactor/totp/constants.ts mode change 100755 => 100644 server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts mode change 100755 => 100644 server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts mode change 100755 => 100644 server/src/lib/routes/secondfactor/totp/sign/post.spec.ts mode change 100755 => 100644 server/src/lib/routes/secondfactor/totp/sign/post.ts mode change 100755 => 100644 server/src/lib/routes/secondfactor/u2f/U2FCommon.ts mode change 100755 => 100644 server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts mode change 100755 => 100644 server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts mode change 100755 => 100644 server/src/lib/routes/secondfactor/u2f/register/post.spec.ts mode change 100755 => 100644 server/src/lib/routes/secondfactor/u2f/register/post.ts mode change 100755 => 100644 server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts mode change 100755 => 100644 server/src/lib/routes/secondfactor/u2f/register_request/get.ts mode change 100755 => 100644 server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts mode change 100755 => 100644 server/src/lib/routes/secondfactor/u2f/sign/post.ts mode change 100755 => 100644 server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts mode change 100755 => 100644 server/src/lib/routes/secondfactor/u2f/sign_request/get.ts mode change 100755 => 100644 server/src/lib/routes/verify/access_control.ts mode change 100755 => 100644 server/src/lib/routes/verify/get.spec.ts mode change 100755 => 100644 server/src/lib/routes/verify/get.ts mode change 100755 => 100644 server/src/lib/routes/verify/get_basic_auth.ts mode change 100755 => 100644 server/src/lib/routes/verify/get_session_cookie.ts mode change 100755 => 100644 server/src/lib/storage/AuthenticationTraceDocument.d.ts mode change 100755 => 100644 server/src/lib/storage/CollectionFactoryFactory.ts mode change 100755 => 100644 server/src/lib/storage/CollectionFactoryStub.spec.ts mode change 100755 => 100644 server/src/lib/storage/CollectionStub.spec.ts mode change 100755 => 100644 server/src/lib/storage/ICollection.d.ts mode change 100755 => 100644 server/src/lib/storage/ICollectionFactory.d.ts mode change 100755 => 100644 server/src/lib/storage/IUserDataStore.d.ts mode change 100755 => 100644 server/src/lib/storage/IdentityValidationDocument.d.ts mode change 100755 => 100644 server/src/lib/storage/TOTPSecretDocument.d.ts mode change 100755 => 100644 server/src/lib/storage/U2FRegistrationDocument.d.ts mode change 100755 => 100644 server/src/lib/storage/UserDataStore.spec.ts mode change 100755 => 100644 server/src/lib/storage/UserDataStore.ts mode change 100755 => 100644 server/src/lib/storage/UserDataStoreStub.spec.ts mode change 100755 => 100644 server/src/lib/storage/mongo/MongoCollection.spec.ts mode change 100755 => 100644 server/src/lib/storage/mongo/MongoCollection.ts mode change 100755 => 100644 server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts mode change 100755 => 100644 server/src/lib/storage/mongo/MongoCollectionFactory.ts mode change 100755 => 100644 server/src/lib/storage/nedb/NedbCollection.spec.ts mode change 100755 => 100644 server/src/lib/storage/nedb/NedbCollection.ts mode change 100755 => 100644 server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts mode change 100755 => 100644 server/src/lib/storage/nedb/NedbCollectionFactory.ts mode change 100755 => 100644 server/src/lib/stubs/express.spec.ts mode change 100755 => 100644 server/src/lib/stubs/ldapjs.spec.ts mode change 100755 => 100644 server/src/lib/stubs/speakeasy.spec.ts mode change 100755 => 100644 server/src/lib/stubs/u2f.spec.ts mode change 100755 => 100644 server/src/lib/utils/HashGenerator.spec.ts mode change 100755 => 100644 server/src/lib/utils/HashGenerator.ts mode change 100755 => 100644 server/src/lib/utils/ObjectCloner.ts mode change 100755 => 100644 server/src/lib/utils/SafeRedirection.spec.ts mode change 100755 => 100644 server/src/lib/utils/SafeRedirection.ts mode change 100755 => 100644 server/src/lib/utils/URLDecomposer.spec.ts mode change 100755 => 100644 server/src/lib/utils/URLDecomposer.ts mode change 100755 => 100644 server/src/lib/web_server/Configurator.ts mode change 100755 => 100644 server/src/lib/web_server/RestApi.ts mode change 100755 => 100644 server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts mode change 100755 => 100644 server/src/lib/web_server/middlewares/WithHeadersLogged.ts mode change 100755 => 100644 server/src/resources/email-template.ejs mode change 100755 => 100644 server/src/views/already-logged-in.pug mode change 100755 => 100644 server/src/views/errors/401.pug mode change 100755 => 100644 server/src/views/errors/403.pug mode change 100755 => 100644 server/src/views/errors/404.pug mode change 100755 => 100644 server/src/views/firstfactor.pug mode change 100755 => 100644 server/src/views/layout/layout.pug mode change 100755 => 100644 server/src/views/need-identity-validation.pug mode change 100755 => 100644 server/src/views/password-reset-form.pug mode change 100755 => 100644 server/src/views/password-reset-request.pug mode change 100755 => 100644 server/src/views/secondfactor.pug mode change 100755 => 100644 server/src/views/totp-register.pug mode change 100755 => 100644 server/src/views/u2f-register.pug mode change 100755 => 100644 server/test/requests.ts mode change 100755 => 100644 server/tsconfig.json mode change 100755 => 100644 server/tslint.json mode change 100755 => 100644 server/types/AuthenticationSession.ts mode change 100755 => 100644 server/types/Dependencies.ts mode change 100755 => 100644 server/types/Identity.ts mode change 100755 => 100644 server/types/TOTPSecret.ts mode change 100755 => 100644 server/types/U2FRegistration.ts mode change 100755 => 100644 server/types/dovehash.d.ts mode change 100755 => 100644 server/types/speakeasy.d.ts mode change 100755 => 100644 shared/BelongToDomain.ts mode change 100755 => 100644 shared/DomainExtractor.spec.ts mode change 100755 => 100644 shared/DomainExtractor.ts mode change 100755 => 100644 shared/ErrorMessage.ts mode change 100755 => 100644 shared/RedirectionMessage.ts mode change 100755 => 100644 shared/SignMessage.ts mode change 100755 => 100644 shared/UserMessages.ts mode change 100755 => 100644 shared/api.ts mode change 100755 => 100644 shared/constants.ts mode change 100755 => 100644 shared/types/u2f.d.ts mode change 100755 => 100644 test/complete-config/00-suite.ts mode change 100755 => 100644 test/complete-config/closed-redirection.ts mode change 100755 => 100644 test/complete-config/mongo-broken-connection.ts mode change 100755 => 100644 test/configuration.ts mode change 100755 => 100644 test/environment.ts mode change 100755 => 100644 test/features/access-control.feature mode change 100755 => 100644 test/features/auth-portal-redirection.feature mode change 100755 => 100644 test/features/authelia.feature mode change 100755 => 100644 test/features/authentication.feature mode change 100755 => 100644 test/features/forward-headers.feature mode change 100755 => 100644 test/features/redirection.feature mode change 100755 => 100644 test/features/registration.feature mode change 100755 => 100644 test/features/regulation.feature mode change 100755 => 100644 test/features/reset-password.feature mode change 100755 => 100644 test/features/resilience.feature mode change 100755 => 100644 test/features/restrictions.feature mode change 100755 => 100644 test/features/session-timeout.feature mode change 100755 => 100644 test/features/single-factor-domain.feature mode change 100755 => 100644 test/features/step_definitions/access-control.ts mode change 100755 => 100644 test/features/step_definitions/authelia.ts mode change 100755 => 100644 test/features/step_definitions/authentication.ts mode change 100755 => 100644 test/features/step_definitions/forward-headers.ts mode change 100755 => 100644 test/features/step_definitions/hooks.ts mode change 100755 => 100644 test/features/step_definitions/notifications.ts mode change 100755 => 100644 test/features/step_definitions/redirection.ts mode change 100755 => 100644 test/features/step_definitions/registration.ts mode change 100755 => 100644 test/features/step_definitions/regulation.ts mode change 100755 => 100644 test/features/step_definitions/reset-password.ts mode change 100755 => 100644 test/features/step_definitions/resilience.ts mode change 100755 => 100644 test/features/step_definitions/restrictions.ts mode change 100755 => 100644 test/features/step_definitions/session-timeout.ts mode change 100755 => 100644 test/features/step_definitions/single-factor.ts mode change 100755 => 100644 test/features/support/world.ts mode change 100755 => 100644 test/helpers/access-secret.ts mode change 100755 => 100644 test/helpers/click-on-button.ts mode change 100755 => 100644 test/helpers/click-on-link.ts mode change 100755 => 100644 test/helpers/fill-field.ts mode change 100755 => 100644 test/helpers/fill-login-page-and-click.ts mode change 100755 => 100644 test/helpers/full-login.ts mode change 100755 => 100644 test/helpers/get-identity-link.ts mode change 100755 => 100644 test/helpers/login-and-register-totp.ts mode change 100755 => 100644 test/helpers/login-as.ts mode change 100755 => 100644 test/helpers/register-totp.ts mode change 100755 => 100644 test/helpers/see-notification.ts mode change 100755 => 100644 test/helpers/validate-totp.ts mode change 100755 => 100644 test/helpers/visit-page.ts mode change 100755 => 100644 test/helpers/wait-redirected.ts mode change 100755 => 100644 test/helpers/with-driver.ts mode change 100755 => 100644 test/inactivity/00-suite.ts mode change 100755 => 100644 test/inactivity/keep_me_logged_in.ts mode change 100755 => 100644 test/minimal-config/00-suite.ts mode change 100755 => 100644 test/minimal-config/bad_password.ts mode change 100755 => 100644 test/minimal-config/fail_totp.ts mode change 100755 => 100644 test/minimal-config/register_totp.ts mode change 100755 => 100644 test/minimal-config/reset_password.ts mode change 100755 => 100644 test/minimal-config/validate_totp.ts mode change 100755 => 100644 themes/black/client/src/css/.directory mode change 100755 => 100644 themes/black/client/src/css/00-bootstrap.min.css mode change 100755 => 100644 themes/black/client/src/css/01-main.css mode change 100755 => 100644 themes/black/client/src/css/02-login.css mode change 100755 => 100644 themes/black/client/src/css/03-errors.css mode change 100755 => 100644 themes/black/client/src/css/03-password-reset-form.css mode change 100755 => 100644 themes/black/client/src/css/03-password-reset-request.css mode change 100755 => 100644 themes/black/client/src/css/03-totp-register.css mode change 100755 => 100644 themes/black/client/src/css/03-u2f-register.css mode change 100755 => 100644 themes/black/client/src/img/RandomizedPattern.svg mode change 100755 => 100644 themes/black/client/src/img/background.jpg mode change 100755 => 100644 themes/black/client/src/img/icon.png mode change 100755 => 100644 themes/black/client/src/img/mail.png mode change 100755 => 100644 themes/black/client/src/img/notifications/.directory mode change 100755 => 100644 themes/black/client/src/img/notifications/error.png mode change 100755 => 100644 themes/black/client/src/img/notifications/info.png mode change 100755 => 100644 themes/black/client/src/img/notifications/success.png mode change 100755 => 100644 themes/black/client/src/img/notifications/warning.png mode change 100755 => 100644 themes/black/client/src/img/padlock.png mode change 100755 => 100644 themes/black/client/src/img/password_white.png mode change 100755 => 100644 themes/black/client/src/img/pendrive.png mode change 100755 => 100644 themes/black/client/src/img/sharingan.png mode change 100755 => 100644 themes/black/client/src/img/stores/.directory mode change 100755 => 100644 themes/black/client/src/img/stores/applestore-badge.svg mode change 100755 => 100644 themes/black/client/src/img/stores/googleplay-badge.svg mode change 100755 => 100644 themes/black/client/src/img/success.png mode change 100755 => 100644 themes/black/client/src/img/user.png mode change 100755 => 100644 themes/black/client/src/img/warning.png mode change 100755 => 100644 themes/black/client/src/thirdparties/qrcode.min.js mode change 100755 => 100644 themes/black/client/src/thirdparties/u2f-api.js mode change 100755 => 100644 themes/black/server/.directory mode change 100755 => 100644 themes/black/server/src/resources/email-template.ejs mode change 100755 => 100644 themes/black/server/src/views/already-logged-in.pug mode change 100755 => 100644 themes/black/server/src/views/errors/.directory mode change 100755 => 100644 themes/black/server/src/views/errors/401.pug mode change 100755 => 100644 themes/black/server/src/views/errors/403.pug mode change 100755 => 100644 themes/black/server/src/views/errors/404.pug mode change 100755 => 100644 themes/black/server/src/views/firstfactor.pug mode change 100755 => 100644 themes/black/server/src/views/layout/layout.pug mode change 100755 => 100644 themes/black/server/src/views/need-identity-validation.pug mode change 100755 => 100644 themes/black/server/src/views/password-reset-form.pug mode change 100755 => 100644 themes/black/server/src/views/password-reset-request.pug mode change 100755 => 100644 themes/black/server/src/views/secondfactor.pug mode change 100755 => 100644 themes/black/server/src/views/totp-register.pug mode change 100755 => 100644 themes/black/server/src/views/u2f-register.pug mode change 100755 => 100644 themes/default/client/src/css/.directory mode change 100755 => 100644 themes/default/client/src/css/00-bootstrap.min.css mode change 100755 => 100644 themes/default/client/src/css/01-main.css mode change 100755 => 100644 themes/default/client/src/css/02-login.css mode change 100755 => 100644 themes/default/client/src/css/03-errors.css mode change 100755 => 100644 themes/default/client/src/css/03-password-reset-form.css mode change 100755 => 100644 themes/default/client/src/css/03-password-reset-request.css mode change 100755 => 100644 themes/default/client/src/css/03-totp-register.css mode change 100755 => 100644 themes/default/client/src/css/03-u2f-register.css mode change 100755 => 100644 themes/default/client/src/img/background.svg mode change 100755 => 100644 themes/default/client/src/img/icon.png mode change 100755 => 100644 themes/default/client/src/img/mail.png mode change 100755 => 100644 themes/default/client/src/img/notifications/.directory mode change 100755 => 100644 themes/default/client/src/img/notifications/error.png mode change 100755 => 100644 themes/default/client/src/img/notifications/info.png mode change 100755 => 100644 themes/default/client/src/img/notifications/success.png mode change 100755 => 100644 themes/default/client/src/img/notifications/warning.png mode change 100755 => 100644 themes/default/client/src/img/padlock.png mode change 100755 => 100644 themes/default/client/src/img/password.png mode change 100755 => 100644 themes/default/client/src/img/pendrive.png mode change 100755 => 100644 themes/default/client/src/img/stores/.directory mode change 100755 => 100644 themes/default/client/src/img/stores/applestore-badge.svg mode change 100755 => 100644 themes/default/client/src/img/stores/googleplay-badge.svg mode change 100755 => 100644 themes/default/client/src/img/success.png mode change 100755 => 100644 themes/default/client/src/img/user.png mode change 100755 => 100644 themes/default/client/src/img/warning.png mode change 100755 => 100644 themes/default/client/src/thirdparties/qrcode.min.js mode change 100755 => 100644 themes/default/server/.directory mode change 100755 => 100644 themes/default/server/src/resources/email-template.ejs mode change 100755 => 100644 themes/default/server/src/views/already-logged-in.pug mode change 100755 => 100644 themes/default/server/src/views/errors/.directory mode change 100755 => 100644 themes/default/server/src/views/errors/401.pug mode change 100755 => 100644 themes/default/server/src/views/errors/403.pug mode change 100755 => 100644 themes/default/server/src/views/errors/404.pug mode change 100755 => 100644 themes/default/server/src/views/firstfactor.pug mode change 100755 => 100644 themes/default/server/src/views/layout/layout.pug mode change 100755 => 100644 themes/default/server/src/views/need-identity-validation.pug mode change 100755 => 100644 themes/default/server/src/views/password-reset-form.pug mode change 100755 => 100644 themes/default/server/src/views/password-reset-request.pug mode change 100755 => 100644 themes/default/server/src/views/secondfactor.pug mode change 100755 => 100644 themes/default/server/src/views/totp-register.pug mode change 100755 => 100644 themes/default/server/src/views/u2f-register.pug mode change 100755 => 100644 themes/matrix/client/src/css/.directory mode change 100755 => 100644 themes/matrix/client/src/css/00-bootstrap.min.css mode change 100755 => 100644 themes/matrix/client/src/css/01-main.css mode change 100755 => 100644 themes/matrix/client/src/css/02-login.css mode change 100755 => 100644 themes/matrix/client/src/css/03-errors.css mode change 100755 => 100644 themes/matrix/client/src/css/03-password-reset-form.css mode change 100755 => 100644 themes/matrix/client/src/css/03-password-reset-request.css mode change 100755 => 100644 themes/matrix/client/src/css/03-totp-register.css mode change 100755 => 100644 themes/matrix/client/src/css/03-u2f-register.css mode change 100755 => 100644 themes/matrix/client/src/img/background.jpg mode change 100755 => 100644 themes/matrix/client/src/img/icon.png mode change 100755 => 100644 themes/matrix/client/src/img/mail.png mode change 100755 => 100644 themes/matrix/client/src/img/matrix_circle_128x128.png mode change 100755 => 100644 themes/matrix/client/src/img/notifications/.directory mode change 100755 => 100644 themes/matrix/client/src/img/notifications/error.png mode change 100755 => 100644 themes/matrix/client/src/img/notifications/info.png mode change 100755 => 100644 themes/matrix/client/src/img/notifications/success.png mode change 100755 => 100644 themes/matrix/client/src/img/notifications/warning.png mode change 100755 => 100644 themes/matrix/client/src/img/padlock.png mode change 100755 => 100644 themes/matrix/client/src/img/password_white.png mode change 100755 => 100644 themes/matrix/client/src/img/pendrive.png mode change 100755 => 100644 themes/matrix/client/src/img/stores/.directory mode change 100755 => 100644 themes/matrix/client/src/img/stores/applestore-badge.svg mode change 100755 => 100644 themes/matrix/client/src/img/stores/googleplay-badge.svg mode change 100755 => 100644 themes/matrix/client/src/img/success.png mode change 100755 => 100644 themes/matrix/client/src/img/user.png mode change 100755 => 100644 themes/matrix/client/src/img/warning.png mode change 100755 => 100644 themes/matrix/client/src/thirdparties/matrix.js mode change 100755 => 100644 themes/matrix/client/src/thirdparties/qrcode.min.js mode change 100755 => 100644 themes/matrix/client/src/thirdparties/u2f-api.js mode change 100755 => 100644 themes/matrix/server/.directory mode change 100755 => 100644 themes/matrix/server/src/resources/email-template.ejs mode change 100755 => 100644 themes/matrix/server/src/views/already-logged-in.pug mode change 100755 => 100644 themes/matrix/server/src/views/errors/.directory mode change 100755 => 100644 themes/matrix/server/src/views/errors/401.pug mode change 100755 => 100644 themes/matrix/server/src/views/errors/403.pug mode change 100755 => 100644 themes/matrix/server/src/views/errors/404.pug mode change 100755 => 100644 themes/matrix/server/src/views/firstfactor.pug mode change 100755 => 100644 themes/matrix/server/src/views/layout/layout.pug mode change 100755 => 100644 themes/matrix/server/src/views/need-identity-validation.pug mode change 100755 => 100644 themes/matrix/server/src/views/password-reset-form.pug mode change 100755 => 100644 themes/matrix/server/src/views/password-reset-request.pug mode change 100755 => 100644 themes/matrix/server/src/views/secondfactor.pug mode change 100755 => 100644 themes/matrix/server/src/views/totp-register.pug mode change 100755 => 100644 themes/matrix/server/src/views/u2f-register.pug mode change 100755 => 100644 themes/squares/client/src/css/.directory mode change 100755 => 100644 themes/squares/client/src/css/00-bootstrap.min.css mode change 100755 => 100644 themes/squares/client/src/css/01-main.css mode change 100755 => 100644 themes/squares/client/src/css/02-login.css mode change 100755 => 100644 themes/squares/client/src/css/03-errors.css mode change 100755 => 100644 themes/squares/client/src/css/03-password-reset-form.css mode change 100755 => 100644 themes/squares/client/src/css/03-password-reset-request.css mode change 100755 => 100644 themes/squares/client/src/css/03-totp-register.css mode change 100755 => 100644 themes/squares/client/src/css/03-u2f-register.css mode change 100755 => 100644 themes/squares/client/src/img/LargeTriangles.svg mode change 100755 => 100644 themes/squares/client/src/img/RandomizedPattern.svg mode change 100755 => 100644 themes/squares/client/src/img/background.jpg mode change 100755 => 100644 themes/squares/client/src/img/background.svg mode change 100755 => 100644 themes/squares/client/src/img/icon.png mode change 100755 => 100644 themes/squares/client/src/img/mail.png mode change 100755 => 100644 themes/squares/client/src/img/matrix_circle_128x128.png mode change 100755 => 100644 themes/squares/client/src/img/notifications/.directory mode change 100755 => 100644 themes/squares/client/src/img/notifications/error.png mode change 100755 => 100644 themes/squares/client/src/img/notifications/info.png mode change 100755 => 100644 themes/squares/client/src/img/notifications/success.png mode change 100755 => 100644 themes/squares/client/src/img/notifications/warning.png mode change 100755 => 100644 themes/squares/client/src/img/padlock.png mode change 100755 => 100644 themes/squares/client/src/img/password_white.png mode change 100755 => 100644 themes/squares/client/src/img/pendrive.png mode change 100755 => 100644 themes/squares/client/src/img/sharingan.png mode change 100755 => 100644 themes/squares/client/src/img/stores/.directory mode change 100755 => 100644 themes/squares/client/src/img/stores/applestore-badge.svg mode change 100755 => 100644 themes/squares/client/src/img/stores/googleplay-badge.svg mode change 100755 => 100644 themes/squares/client/src/img/success.png mode change 100755 => 100644 themes/squares/client/src/img/user.png mode change 100755 => 100644 themes/squares/client/src/img/warning.png mode change 100755 => 100644 themes/squares/client/src/thirdparties/qrcode.min.js mode change 100755 => 100644 themes/squares/client/src/thirdparties/u2f-api.js mode change 100755 => 100644 themes/squares/server/.directory mode change 100755 => 100644 themes/squares/server/src/resources/email-template.ejs mode change 100755 => 100644 themes/squares/server/src/views/already-logged-in.pug mode change 100755 => 100644 themes/squares/server/src/views/errors/.directory mode change 100755 => 100644 themes/squares/server/src/views/errors/401.pug mode change 100755 => 100644 themes/squares/server/src/views/errors/403.pug mode change 100755 => 100644 themes/squares/server/src/views/errors/404.pug mode change 100755 => 100644 themes/squares/server/src/views/firstfactor.pug mode change 100755 => 100644 themes/squares/server/src/views/layout/layout.pug mode change 100755 => 100644 themes/squares/server/src/views/need-identity-validation.pug mode change 100755 => 100644 themes/squares/server/src/views/password-reset-form.pug mode change 100755 => 100644 themes/squares/server/src/views/password-reset-request.pug mode change 100755 => 100644 themes/squares/server/src/views/secondfactor.pug mode change 100755 => 100644 themes/squares/server/src/views/totp-register.pug mode change 100755 => 100644 themes/squares/server/src/views/u2f-register.pug mode change 100755 => 100644 themes/triangles/client/src/.directory mode change 100755 => 100644 themes/triangles/client/src/css/.directory mode change 100755 => 100644 themes/triangles/client/src/css/00-bootstrap.min.css mode change 100755 => 100644 themes/triangles/client/src/css/01-main.css mode change 100755 => 100644 themes/triangles/client/src/css/02-login.css mode change 100755 => 100644 themes/triangles/client/src/css/03-errors.css mode change 100755 => 100644 themes/triangles/client/src/css/03-password-reset-form.css mode change 100755 => 100644 themes/triangles/client/src/css/03-password-reset-request.css mode change 100755 => 100644 themes/triangles/client/src/css/03-totp-register.css mode change 100755 => 100644 themes/triangles/client/src/css/03-u2f-register.css mode change 100755 => 100644 themes/triangles/client/src/img/LargeTriangles.svg mode change 100755 => 100644 themes/triangles/client/src/img/background.jpg mode change 100755 => 100644 themes/triangles/client/src/img/icon.png mode change 100755 => 100644 themes/triangles/client/src/img/mail.png mode change 100755 => 100644 themes/triangles/client/src/img/matrix_circle_128x128.png mode change 100755 => 100644 themes/triangles/client/src/img/notifications/.directory mode change 100755 => 100644 themes/triangles/client/src/img/notifications/error.png mode change 100755 => 100644 themes/triangles/client/src/img/notifications/info.png mode change 100755 => 100644 themes/triangles/client/src/img/notifications/success.png mode change 100755 => 100644 themes/triangles/client/src/img/notifications/warning.png mode change 100755 => 100644 themes/triangles/client/src/img/padlock.png mode change 100755 => 100644 themes/triangles/client/src/img/password_white.png mode change 100755 => 100644 themes/triangles/client/src/img/pendrive.png mode change 100755 => 100644 themes/triangles/client/src/img/sharingan.png mode change 100755 => 100644 themes/triangles/client/src/img/stores/.directory mode change 100755 => 100644 themes/triangles/client/src/img/stores/applestore-badge.svg mode change 100755 => 100644 themes/triangles/client/src/img/stores/googleplay-badge.svg mode change 100755 => 100644 themes/triangles/client/src/img/success.png mode change 100755 => 100644 themes/triangles/client/src/img/user.png mode change 100755 => 100644 themes/triangles/client/src/img/warning.png mode change 100755 => 100644 themes/triangles/client/src/thirdparties/qrcode.min.js mode change 100755 => 100644 themes/triangles/client/src/thirdparties/u2f-api.js mode change 100755 => 100644 themes/triangles/server/.directory mode change 100755 => 100644 themes/triangles/server/src/resources/email-template.ejs mode change 100755 => 100644 themes/triangles/server/src/views/already-logged-in.pug mode change 100755 => 100644 themes/triangles/server/src/views/errors/.directory mode change 100755 => 100644 themes/triangles/server/src/views/errors/401.pug mode change 100755 => 100644 themes/triangles/server/src/views/errors/403.pug mode change 100755 => 100644 themes/triangles/server/src/views/errors/404.pug mode change 100755 => 100644 themes/triangles/server/src/views/firstfactor.pug mode change 100755 => 100644 themes/triangles/server/src/views/layout/layout.pug mode change 100755 => 100644 themes/triangles/server/src/views/need-identity-validation.pug mode change 100755 => 100644 themes/triangles/server/src/views/password-reset-form.pug mode change 100755 => 100644 themes/triangles/server/src/views/password-reset-request.pug mode change 100755 => 100644 themes/triangles/server/src/views/secondfactor.pug mode change 100755 => 100644 themes/triangles/server/src/views/totp-register.pug mode change 100755 => 100644 themes/triangles/server/src/views/u2f-register.pug mode change 100755 => 100644 users_database.yml diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 diff --git a/.npmignore b/.npmignore old mode 100755 new mode 100644 diff --git a/.travis.yml b/.travis.yml old mode 100755 new mode 100644 diff --git a/CHANGELOG.md b/CHANGELOG.md old mode 100755 new mode 100644 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md old mode 100755 new mode 100644 diff --git a/Dockerfile b/Dockerfile old mode 100755 new mode 100644 diff --git a/Dockerfile.dev b/Dockerfile.dev old mode 100755 new mode 100644 diff --git a/Gruntfile.js b/Gruntfile.js old mode 100755 new mode 100644 diff --git a/LICENSE b/LICENSE old mode 100755 new mode 100644 diff --git a/README.md b/README.md old mode 100755 new mode 100644 diff --git a/client/src/css/00-bootstrap.min.css b/client/src/css/00-bootstrap.min.css old mode 100755 new mode 100644 diff --git a/client/src/css/01-main.css b/client/src/css/01-main.css old mode 100755 new mode 100644 diff --git a/client/src/css/02-login.css b/client/src/css/02-login.css old mode 100755 new mode 100644 diff --git a/client/src/css/03-errors.css b/client/src/css/03-errors.css old mode 100755 new mode 100644 diff --git a/client/src/css/03-password-reset-form.css b/client/src/css/03-password-reset-form.css old mode 100755 new mode 100644 diff --git a/client/src/css/03-password-reset-request.css b/client/src/css/03-password-reset-request.css old mode 100755 new mode 100644 diff --git a/client/src/css/03-totp-register.css b/client/src/css/03-totp-register.css old mode 100755 new mode 100644 diff --git a/client/src/css/03-u2f-register.css b/client/src/css/03-u2f-register.css old mode 100755 new mode 100644 diff --git a/client/src/img/background.svg b/client/src/img/background.svg old mode 100755 new mode 100644 diff --git a/client/src/img/icon.png b/client/src/img/icon.png old mode 100755 new mode 100644 diff --git a/client/src/img/mail.png b/client/src/img/mail.png old mode 100755 new mode 100644 diff --git a/client/src/img/notifications/error.png b/client/src/img/notifications/error.png old mode 100755 new mode 100644 diff --git a/client/src/img/notifications/info.png b/client/src/img/notifications/info.png old mode 100755 new mode 100644 diff --git a/client/src/img/notifications/success.png b/client/src/img/notifications/success.png old mode 100755 new mode 100644 diff --git a/client/src/img/notifications/warning.png b/client/src/img/notifications/warning.png old mode 100755 new mode 100644 diff --git a/client/src/img/padlock.png b/client/src/img/padlock.png old mode 100755 new mode 100644 diff --git a/client/src/img/password.png b/client/src/img/password.png old mode 100755 new mode 100644 diff --git a/client/src/img/pendrive.png b/client/src/img/pendrive.png old mode 100755 new mode 100644 diff --git a/client/src/img/stores/applestore-badge.svg b/client/src/img/stores/applestore-badge.svg old mode 100755 new mode 100644 diff --git a/client/src/img/stores/googleplay-badge.svg b/client/src/img/stores/googleplay-badge.svg old mode 100755 new mode 100644 diff --git a/client/src/img/success.png b/client/src/img/success.png old mode 100755 new mode 100644 diff --git a/client/src/img/user.png b/client/src/img/user.png old mode 100755 new mode 100644 diff --git a/client/src/img/warning.png b/client/src/img/warning.png old mode 100755 new mode 100644 diff --git a/client/src/index.ts b/client/src/index.ts old mode 100755 new mode 100644 diff --git a/client/src/lib/GetPromised.ts b/client/src/lib/GetPromised.ts old mode 100755 new mode 100644 diff --git a/client/src/lib/INotifier.ts b/client/src/lib/INotifier.ts old mode 100755 new mode 100644 diff --git a/client/src/lib/Notifier.ts b/client/src/lib/Notifier.ts old mode 100755 new mode 100644 diff --git a/client/src/lib/QueryParametersRetriever.ts b/client/src/lib/QueryParametersRetriever.ts old mode 100755 new mode 100644 diff --git a/client/src/lib/SafeRedirect.ts b/client/src/lib/SafeRedirect.ts old mode 100755 new mode 100644 diff --git a/client/src/lib/firstfactor/FirstFactorValidator.ts b/client/src/lib/firstfactor/FirstFactorValidator.ts old mode 100755 new mode 100644 diff --git a/client/src/lib/firstfactor/UISelectors.ts b/client/src/lib/firstfactor/UISelectors.ts old mode 100755 new mode 100644 diff --git a/client/src/lib/firstfactor/index.ts b/client/src/lib/firstfactor/index.ts old mode 100755 new mode 100644 diff --git a/client/src/lib/reset-password/constants.ts b/client/src/lib/reset-password/constants.ts old mode 100755 new mode 100644 diff --git a/client/src/lib/reset-password/reset-password-form.ts b/client/src/lib/reset-password/reset-password-form.ts old mode 100755 new mode 100644 diff --git a/client/src/lib/reset-password/reset-password-request.ts b/client/src/lib/reset-password/reset-password-request.ts old mode 100755 new mode 100644 diff --git a/client/src/lib/secondfactor/TOTPValidator.ts b/client/src/lib/secondfactor/TOTPValidator.ts old mode 100755 new mode 100644 diff --git a/client/src/lib/secondfactor/U2FValidator.ts b/client/src/lib/secondfactor/U2FValidator.ts old mode 100755 new mode 100644 diff --git a/client/src/lib/secondfactor/constants.ts b/client/src/lib/secondfactor/constants.ts old mode 100755 new mode 100644 diff --git a/client/src/lib/secondfactor/index.ts b/client/src/lib/secondfactor/index.ts old mode 100755 new mode 100644 diff --git a/client/src/lib/totp-register/totp-register.ts b/client/src/lib/totp-register/totp-register.ts old mode 100755 new mode 100644 diff --git a/client/src/lib/totp-register/ui-selector.ts b/client/src/lib/totp-register/ui-selector.ts old mode 100755 new mode 100644 diff --git a/client/src/lib/u2f-register/u2f-register.ts b/client/src/lib/u2f-register/u2f-register.ts old mode 100755 new mode 100644 diff --git a/client/src/thirdparties/qrcode.min.js b/client/src/thirdparties/qrcode.min.js old mode 100755 new mode 100644 diff --git a/client/test/Notifier.test.ts b/client/test/Notifier.test.ts old mode 100755 new mode 100644 diff --git a/client/test/firstfactor/FirstFactorValidator.test.ts b/client/test/firstfactor/FirstFactorValidator.test.ts old mode 100755 new mode 100644 diff --git a/client/test/mocks/NotifierStub.ts b/client/test/mocks/NotifierStub.ts old mode 100755 new mode 100644 diff --git a/client/test/mocks/jquery.ts b/client/test/mocks/jquery.ts old mode 100755 new mode 100644 diff --git a/client/test/mocks/u2f-api.ts b/client/test/mocks/u2f-api.ts old mode 100755 new mode 100644 diff --git a/client/test/secondfactor/TOTPValidator.test.ts b/client/test/secondfactor/TOTPValidator.test.ts old mode 100755 new mode 100644 diff --git a/client/test/totp-register/totp-register.test.ts b/client/test/totp-register/totp-register.test.ts old mode 100755 new mode 100644 diff --git a/client/tsconfig.json b/client/tsconfig.json old mode 100755 new mode 100644 diff --git a/client/tslint.json b/client/tslint.json old mode 100755 new mode 100644 diff --git a/config.minimal.yml b/config.minimal.yml old mode 100755 new mode 100644 diff --git a/config.template.yml b/config.template.yml old mode 100755 new mode 100644 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml old mode 100755 new mode 100644 diff --git a/docker-compose.dockerhub.yml b/docker-compose.dockerhub.yml old mode 100755 new mode 100644 diff --git a/docker-compose.minimal.dev.yml b/docker-compose.minimal.dev.yml old mode 100755 new mode 100644 diff --git a/docker-compose.minimal.yml b/docker-compose.minimal.yml old mode 100755 new mode 100644 diff --git a/docker-compose.swarm.minimal.yml b/docker-compose.swarm.minimal.yml old mode 100755 new mode 100644 diff --git a/docker-compose.test.yml b/docker-compose.test.yml old mode 100755 new mode 100644 diff --git a/docker-compose.yml b/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docs/build.md b/docs/build.md old mode 100755 new mode 100644 diff --git a/docs/configuration.md b/docs/configuration.md old mode 100755 new mode 100644 diff --git a/docs/deployment-dev.md b/docs/deployment-dev.md old mode 100755 new mode 100644 diff --git a/docs/deployment-production.md b/docs/deployment-production.md old mode 100755 new mode 100644 diff --git a/docs/features.md b/docs/features.md old mode 100755 new mode 100644 diff --git a/docs/getting-started.md b/docs/getting-started.md old mode 100755 new mode 100644 diff --git a/docs/security.md b/docs/security.md old mode 100755 new mode 100644 diff --git a/example/compose/authelia/docker-compose.test.yml b/example/compose/authelia/docker-compose.test.yml old mode 100755 new mode 100644 diff --git a/example/compose/docker-compose.base.yml b/example/compose/docker-compose.base.yml old mode 100755 new mode 100644 diff --git a/example/compose/httpbin/docker-compose.yml b/example/compose/httpbin/docker-compose.yml old mode 100755 new mode 100644 diff --git a/example/compose/ldap/access.rules b/example/compose/ldap/access.rules old mode 100755 new mode 100644 diff --git a/example/compose/ldap/base.ldif b/example/compose/ldap/base.ldif old mode 100755 new mode 100644 diff --git a/example/compose/ldap/docker-compose.admin.yml b/example/compose/ldap/docker-compose.admin.yml old mode 100755 new mode 100644 diff --git a/example/compose/ldap/docker-compose.yml b/example/compose/ldap/docker-compose.yml old mode 100755 new mode 100644 diff --git a/example/compose/mongo/docker-compose.yml b/example/compose/mongo/docker-compose.yml old mode 100755 new mode 100644 diff --git a/example/compose/nginx/backend/docker-compose.yml b/example/compose/nginx/backend/docker-compose.yml old mode 100755 new mode 100644 diff --git a/example/compose/nginx/backend/html/admin/secret.html b/example/compose/nginx/backend/html/admin/secret.html old mode 100755 new mode 100644 diff --git a/example/compose/nginx/backend/html/dev/groups/admin/secret.html b/example/compose/nginx/backend/html/dev/groups/admin/secret.html old mode 100755 new mode 100644 diff --git a/example/compose/nginx/backend/html/dev/groups/dev/secret.html b/example/compose/nginx/backend/html/dev/groups/dev/secret.html old mode 100755 new mode 100644 diff --git a/example/compose/nginx/backend/html/dev/users/bob/secret.html b/example/compose/nginx/backend/html/dev/users/bob/secret.html old mode 100755 new mode 100644 diff --git a/example/compose/nginx/backend/html/dev/users/harry/secret.html b/example/compose/nginx/backend/html/dev/users/harry/secret.html old mode 100755 new mode 100644 diff --git a/example/compose/nginx/backend/html/dev/users/john/secret.html b/example/compose/nginx/backend/html/dev/users/john/secret.html old mode 100755 new mode 100644 diff --git a/example/compose/nginx/backend/html/home/index.html b/example/compose/nginx/backend/html/home/index.html old mode 100755 new mode 100644 diff --git a/example/compose/nginx/backend/html/icon.png b/example/compose/nginx/backend/html/icon.png old mode 100755 new mode 100644 diff --git a/example/compose/nginx/backend/html/mail/secret.html b/example/compose/nginx/backend/html/mail/secret.html old mode 100755 new mode 100644 diff --git a/example/compose/nginx/backend/html/public/index.html b/example/compose/nginx/backend/html/public/index.html old mode 100755 new mode 100644 diff --git a/example/compose/nginx/backend/html/public/secret.html b/example/compose/nginx/backend/html/public/secret.html old mode 100755 new mode 100644 diff --git a/example/compose/nginx/backend/html/single_factor/secret.html b/example/compose/nginx/backend/html/single_factor/secret.html old mode 100755 new mode 100644 diff --git a/example/compose/nginx/backend/nginx.conf b/example/compose/nginx/backend/nginx.conf old mode 100755 new mode 100644 diff --git a/example/compose/nginx/minimal/docker-compose.yml b/example/compose/nginx/minimal/docker-compose.yml old mode 100755 new mode 100644 diff --git a/example/compose/nginx/minimal/html/admin/secret.html b/example/compose/nginx/minimal/html/admin/secret.html old mode 100755 new mode 100644 diff --git a/example/compose/nginx/minimal/html/home/index.html b/example/compose/nginx/minimal/html/home/index.html old mode 100755 new mode 100644 diff --git a/example/compose/nginx/minimal/nginx.conf b/example/compose/nginx/minimal/nginx.conf old mode 100755 new mode 100644 diff --git a/example/compose/nginx/minimal/ssl/server.crt b/example/compose/nginx/minimal/ssl/server.crt old mode 100755 new mode 100644 diff --git a/example/compose/nginx/minimal/ssl/server.csr b/example/compose/nginx/minimal/ssl/server.csr old mode 100755 new mode 100644 diff --git a/example/compose/nginx/minimal/ssl/server.key b/example/compose/nginx/minimal/ssl/server.key old mode 100755 new mode 100644 diff --git a/example/compose/nginx/portal/docker-compose.yml b/example/compose/nginx/portal/docker-compose.yml old mode 100755 new mode 100644 diff --git a/example/compose/nginx/portal/nginx.conf b/example/compose/nginx/portal/nginx.conf old mode 100755 new mode 100644 diff --git a/example/compose/nginx/portal/ssl/server.crt b/example/compose/nginx/portal/ssl/server.crt old mode 100755 new mode 100644 diff --git a/example/compose/nginx/portal/ssl/server.csr b/example/compose/nginx/portal/ssl/server.csr old mode 100755 new mode 100644 diff --git a/example/compose/nginx/portal/ssl/server.key b/example/compose/nginx/portal/ssl/server.key old mode 100755 new mode 100644 diff --git a/example/compose/redis/docker-compose.yml b/example/compose/redis/docker-compose.yml old mode 100755 new mode 100644 diff --git a/example/compose/smtp/docker-compose.yml b/example/compose/smtp/docker-compose.yml old mode 100755 new mode 100644 diff --git a/example/kube/README.md b/example/kube/README.md old mode 100755 new mode 100644 diff --git a/example/kube/apps/app-home/deployment.yml b/example/kube/apps/app-home/deployment.yml old mode 100755 new mode 100644 diff --git a/example/kube/apps/app-home/index.html b/example/kube/apps/app-home/index.html old mode 100755 new mode 100644 diff --git a/example/kube/apps/app-home/service.yml b/example/kube/apps/app-home/service.yml old mode 100755 new mode 100644 diff --git a/example/kube/apps/app1/deployment.yml b/example/kube/apps/app1/deployment.yml old mode 100755 new mode 100644 diff --git a/example/kube/apps/app1/index.html b/example/kube/apps/app1/index.html old mode 100755 new mode 100644 diff --git a/example/kube/apps/app1/service.yml b/example/kube/apps/app1/service.yml old mode 100755 new mode 100644 diff --git a/example/kube/apps/app1/ssl/tls.crt b/example/kube/apps/app1/ssl/tls.crt old mode 100755 new mode 100644 diff --git a/example/kube/apps/app1/ssl/tls.csr b/example/kube/apps/app1/ssl/tls.csr old mode 100755 new mode 100644 diff --git a/example/kube/apps/app1/ssl/tls.key b/example/kube/apps/app1/ssl/tls.key old mode 100755 new mode 100644 diff --git a/example/kube/apps/app2/deployment.yml b/example/kube/apps/app2/deployment.yml old mode 100755 new mode 100644 diff --git a/example/kube/apps/app2/index.html b/example/kube/apps/app2/index.html old mode 100755 new mode 100644 diff --git a/example/kube/apps/app2/service.yml b/example/kube/apps/app2/service.yml old mode 100755 new mode 100644 diff --git a/example/kube/apps/app2/ssl/tls.crt b/example/kube/apps/app2/ssl/tls.crt old mode 100755 new mode 100644 diff --git a/example/kube/apps/app2/ssl/tls.csr b/example/kube/apps/app2/ssl/tls.csr old mode 100755 new mode 100644 diff --git a/example/kube/apps/app2/ssl/tls.key b/example/kube/apps/app2/ssl/tls.key old mode 100755 new mode 100644 diff --git a/example/kube/apps/insecure-ingress.yml b/example/kube/apps/insecure-ingress.yml old mode 100755 new mode 100644 diff --git a/example/kube/apps/secure-ingress.yml b/example/kube/apps/secure-ingress.yml old mode 100755 new mode 100644 diff --git a/example/kube/authelia/configs/config.yml b/example/kube/authelia/configs/config.yml old mode 100755 new mode 100644 diff --git a/example/kube/authelia/deployment.yml b/example/kube/authelia/deployment.yml old mode 100755 new mode 100644 diff --git a/example/kube/authelia/ingress.yml b/example/kube/authelia/ingress.yml old mode 100755 new mode 100644 diff --git a/example/kube/authelia/service.yml b/example/kube/authelia/service.yml old mode 100755 new mode 100644 diff --git a/example/kube/authelia/ssl/tls.crt b/example/kube/authelia/ssl/tls.crt old mode 100755 new mode 100644 diff --git a/example/kube/authelia/ssl/tls.csr b/example/kube/authelia/ssl/tls.csr old mode 100755 new mode 100644 diff --git a/example/kube/authelia/ssl/tls.key b/example/kube/authelia/ssl/tls.key old mode 100755 new mode 100644 diff --git a/example/kube/bootstrap.sh b/example/kube/bootstrap.sh old mode 100755 new mode 100644 diff --git a/example/kube/build_and_push.sh b/example/kube/build_and_push.sh old mode 100755 new mode 100644 diff --git a/example/kube/docker-registry/daemonset.yml b/example/kube/docker-registry/daemonset.yml old mode 100755 new mode 100644 diff --git a/example/kube/docker-registry/ingress.yml b/example/kube/docker-registry/ingress.yml old mode 100755 new mode 100644 diff --git a/example/kube/docker-registry/replicationcontroller.yml b/example/kube/docker-registry/replicationcontroller.yml old mode 100755 new mode 100644 diff --git a/example/kube/docker-registry/service.yml b/example/kube/docker-registry/service.yml old mode 100755 new mode 100644 diff --git a/example/kube/ingress-controller/default-backend.yml b/example/kube/ingress-controller/default-backend.yml old mode 100755 new mode 100644 diff --git a/example/kube/ingress-controller/deployment.yml b/example/kube/ingress-controller/deployment.yml old mode 100755 new mode 100644 diff --git a/example/kube/ingress-controller/service.yml b/example/kube/ingress-controller/service.yml old mode 100755 new mode 100644 diff --git a/example/kube/ldap/Dockerfile b/example/kube/ldap/Dockerfile old mode 100755 new mode 100644 diff --git a/example/kube/ldap/deployment.yml b/example/kube/ldap/deployment.yml old mode 100755 new mode 100644 diff --git a/example/kube/ldap/service.yml b/example/kube/ldap/service.yml old mode 100755 new mode 100644 diff --git a/example/kube/mailcatcher/deployment.yml b/example/kube/mailcatcher/deployment.yml old mode 100755 new mode 100644 diff --git a/example/kube/mailcatcher/ingress.yml b/example/kube/mailcatcher/ingress.yml old mode 100755 new mode 100644 diff --git a/example/kube/mailcatcher/service.yml b/example/kube/mailcatcher/service.yml old mode 100755 new mode 100644 diff --git a/example/kube/namespace.yml b/example/kube/namespace.yml old mode 100755 new mode 100644 diff --git a/example/kube/storage/mongo.yml b/example/kube/storage/mongo.yml old mode 100755 new mode 100644 diff --git a/example/kube/storage/redis.yml b/example/kube/storage/redis.yml old mode 100755 new mode 100644 diff --git a/images/authelia-title-white.png b/images/authelia-title-white.png old mode 100755 new mode 100644 diff --git a/images/authelia-title.png b/images/authelia-title.png old mode 100755 new mode 100644 diff --git a/images/email_confirmation.png b/images/email_confirmation.png old mode 100755 new mode 100644 diff --git a/images/first_factor.png b/images/first_factor.png old mode 100755 new mode 100644 diff --git a/images/icon.png b/images/icon.png old mode 100755 new mode 100644 diff --git a/images/kube-logo.png b/images/kube-logo.png old mode 100755 new mode 100644 diff --git a/images/reset_password.png b/images/reset_password.png old mode 100755 new mode 100644 diff --git a/images/second_factor.png b/images/second_factor.png old mode 100755 new mode 100644 diff --git a/images/totp.png b/images/totp.png old mode 100755 new mode 100644 diff --git a/images/u2f.png b/images/u2f.png old mode 100755 new mode 100644 diff --git a/package-lock.json b/package-lock.json old mode 100755 new mode 100644 diff --git a/package.json b/package.json old mode 100755 new mode 100644 diff --git a/scripts/dc-dev.sh b/scripts/dc-dev.sh old mode 100755 new mode 100644 diff --git a/scripts/docker-publish.sh b/scripts/docker-publish.sh old mode 100755 new mode 100644 diff --git a/scripts/example-commit/dc-example.sh b/scripts/example-commit/dc-example.sh old mode 100755 new mode 100644 diff --git a/scripts/example-commit/deploy-example.sh b/scripts/example-commit/deploy-example.sh old mode 100755 new mode 100644 diff --git a/scripts/example-commit/undeploy-example.sh b/scripts/example-commit/undeploy-example.sh old mode 100755 new mode 100644 diff --git a/scripts/example-dockerhub/dc-example.sh b/scripts/example-dockerhub/dc-example.sh old mode 100755 new mode 100644 diff --git a/scripts/example-dockerhub/deploy-example.sh b/scripts/example-dockerhub/deploy-example.sh old mode 100755 new mode 100644 diff --git a/scripts/example-dockerhub/undeploy-example.sh b/scripts/example-dockerhub/undeploy-example.sh old mode 100755 new mode 100644 diff --git a/scripts/integration-tests.sh b/scripts/integration-tests.sh old mode 100755 new mode 100644 diff --git a/scripts/npm-deployment-test.sh b/scripts/npm-deployment-test.sh old mode 100755 new mode 100644 diff --git a/scripts/run-cucumber.sh b/scripts/run-cucumber.sh old mode 100755 new mode 100644 diff --git a/scripts/travis.sh b/scripts/travis.sh old mode 100755 new mode 100644 diff --git a/server/src/index.ts b/server/src/index.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/AuthenticationSessionHandler.ts b/server/src/lib/AuthenticationSessionHandler.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/ErrorReplies.ts b/server/src/lib/ErrorReplies.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/Exceptions.ts b/server/src/lib/Exceptions.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/FirstFactorValidator.ts b/server/src/lib/FirstFactorValidator.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/IdentityCheckMiddleware.spec.ts b/server/src/lib/IdentityCheckMiddleware.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/IdentityCheckMiddleware.ts b/server/src/lib/IdentityCheckMiddleware.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/IdentityCheckPreValidationTemplate.ts b/server/src/lib/IdentityCheckPreValidationTemplate.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/IdentityValidable.ts b/server/src/lib/IdentityValidable.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/IdentityValidableStub.spec.ts b/server/src/lib/IdentityValidableStub.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/Server.spec.ts b/server/src/lib/Server.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/Server.ts b/server/src/lib/Server.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/ServerVariables.ts b/server/src/lib/ServerVariables.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/ServerVariablesInitializer.ts b/server/src/lib/ServerVariablesInitializer.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/ServerVariablesMockBuilder.spec.ts b/server/src/lib/ServerVariablesMockBuilder.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/Level.ts b/server/src/lib/authentication/Level.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/GroupsAndEmails.ts b/server/src/lib/authentication/backends/GroupsAndEmails.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/IUsersDatabase.ts b/server/src/lib/authentication/backends/IUsersDatabase.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts b/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts b/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/file/FileUsersDatabase.ts b/server/src/lib/authentication/backends/file/FileUsersDatabase.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/file/ReadWriteQueue.ts b/server/src/lib/authentication/backends/file/ReadWriteQueue.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/ldap/ISession.ts b/server/src/lib/authentication/backends/ldap/ISession.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/ldap/ISessionFactory.ts b/server/src/lib/authentication/backends/ldap/ISessionFactory.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts b/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts b/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts b/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/ldap/SafeSession.ts b/server/src/lib/authentication/backends/ldap/SafeSession.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts b/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/ldap/Sanitizer.ts b/server/src/lib/authentication/backends/ldap/Sanitizer.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/ldap/Session.spec.ts b/server/src/lib/authentication/backends/ldap/Session.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/ldap/Session.ts b/server/src/lib/authentication/backends/ldap/Session.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/ldap/SessionFactory.ts b/server/src/lib/authentication/backends/ldap/SessionFactory.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts b/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts b/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/ldap/connector/Connector.ts b/server/src/lib/authentication/backends/ldap/connector/Connector.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts b/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts b/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts b/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/ldap/connector/IConnector.ts b/server/src/lib/authentication/backends/ldap/connector/IConnector.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts b/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/totp/ITotpHandler.ts b/server/src/lib/authentication/totp/ITotpHandler.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/totp/TotpHandler.spec.ts b/server/src/lib/authentication/totp/TotpHandler.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/totp/TotpHandler.ts b/server/src/lib/authentication/totp/TotpHandler.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts b/server/src/lib/authentication/totp/TotpHandlerStub.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/u2f/IU2fHandler.ts b/server/src/lib/authentication/u2f/IU2fHandler.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/u2f/U2fHandler.ts b/server/src/lib/authentication/u2f/U2fHandler.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts b/server/src/lib/authentication/u2f/U2fHandlerStub.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authorization/Authorizer.spec.ts b/server/src/lib/authorization/Authorizer.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authorization/Authorizer.ts b/server/src/lib/authorization/Authorizer.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authorization/AuthorizerStub.spec.ts b/server/src/lib/authorization/AuthorizerStub.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authorization/IAuthorizer.ts b/server/src/lib/authorization/IAuthorizer.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authorization/Level.ts b/server/src/lib/authorization/Level.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authorization/MultipleDomainMatcher.ts b/server/src/lib/authorization/MultipleDomainMatcher.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authorization/Object.ts b/server/src/lib/authorization/Object.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/authorization/Subject.ts b/server/src/lib/authorization/Subject.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/ConfigurationParser.spec.ts b/server/src/lib/configuration/ConfigurationParser.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/ConfigurationParser.ts b/server/src/lib/configuration/ConfigurationParser.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts b/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/SessionConfigurationBuilder.ts b/server/src/lib/configuration/SessionConfigurationBuilder.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/schema/AclConfiguration.spec.ts b/server/src/lib/configuration/schema/AclConfiguration.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/schema/AclConfiguration.ts b/server/src/lib/configuration/schema/AclConfiguration.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts b/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts b/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/schema/Configuration.ts b/server/src/lib/configuration/schema/Configuration.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts b/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/schema/LdapConfiguration.spec.ts b/server/src/lib/configuration/schema/LdapConfiguration.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/schema/LdapConfiguration.ts b/server/src/lib/configuration/schema/LdapConfiguration.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts b/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/schema/NotifierConfiguration.ts b/server/src/lib/configuration/schema/NotifierConfiguration.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts b/server/src/lib/configuration/schema/RegulationConfiguration.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/schema/RegulationConfiguration.ts b/server/src/lib/configuration/schema/RegulationConfiguration.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/schema/SessionConfiguration.spec.ts b/server/src/lib/configuration/schema/SessionConfiguration.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/schema/SessionConfiguration.ts b/server/src/lib/configuration/schema/SessionConfiguration.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/schema/StorageConfiguration.spec.ts b/server/src/lib/configuration/schema/StorageConfiguration.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/schema/StorageConfiguration.ts b/server/src/lib/configuration/schema/StorageConfiguration.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/schema/TotpConfiguration.ts b/server/src/lib/configuration/schema/TotpConfiguration.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts b/server/src/lib/configuration/schema/UserDatabaseConfiguration.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/connectors/mongo/IMongoClient.d.ts b/server/src/lib/connectors/mongo/IMongoClient.d.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/connectors/mongo/MongoClient.spec.ts b/server/src/lib/connectors/mongo/MongoClient.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/connectors/mongo/MongoClient.ts b/server/src/lib/connectors/mongo/MongoClient.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/connectors/mongo/MongoClientStub.spec.ts b/server/src/lib/connectors/mongo/MongoClientStub.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/logging/GlobalLogger.ts b/server/src/lib/logging/GlobalLogger.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/logging/GlobalLoggerStub.spec.ts b/server/src/lib/logging/GlobalLoggerStub.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/logging/IGlobalLogger.ts b/server/src/lib/logging/IGlobalLogger.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/logging/IRequestLogger.ts b/server/src/lib/logging/IRequestLogger.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/logging/RequestLogger.ts b/server/src/lib/logging/RequestLogger.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/logging/RequestLoggerStub.spec.ts b/server/src/lib/logging/RequestLoggerStub.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/notifiers/AbstractEmailNotifier.ts b/server/src/lib/notifiers/AbstractEmailNotifier.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/notifiers/EmailNotifier.spec.ts b/server/src/lib/notifiers/EmailNotifier.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/notifiers/EmailNotifier.ts b/server/src/lib/notifiers/EmailNotifier.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/notifiers/FileSystemNotifier.ts b/server/src/lib/notifiers/FileSystemNotifier.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/notifiers/IMailSender.ts b/server/src/lib/notifiers/IMailSender.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/notifiers/IMailSenderBuilder.ts b/server/src/lib/notifiers/IMailSenderBuilder.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/notifiers/INotifier.ts b/server/src/lib/notifiers/INotifier.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/notifiers/MailSender.ts b/server/src/lib/notifiers/MailSender.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/notifiers/MailSenderBuilder.spec.ts b/server/src/lib/notifiers/MailSenderBuilder.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/notifiers/MailSenderBuilder.ts b/server/src/lib/notifiers/MailSenderBuilder.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts b/server/src/lib/notifiers/MailSenderBuilderStub.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/notifiers/MailSenderStub.spec.ts b/server/src/lib/notifiers/MailSenderStub.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/notifiers/NotifierFactory.spec.ts b/server/src/lib/notifiers/NotifierFactory.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/notifiers/NotifierFactory.ts b/server/src/lib/notifiers/NotifierFactory.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/notifiers/NotifierStub.spec.ts b/server/src/lib/notifiers/NotifierStub.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/notifiers/SmtpNotifier.ts b/server/src/lib/notifiers/SmtpNotifier.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/regulation/IRegulator.ts b/server/src/lib/regulation/IRegulator.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/regulation/Regulator.spec.ts b/server/src/lib/regulation/Regulator.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/regulation/Regulator.ts b/server/src/lib/regulation/Regulator.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/regulation/RegulatorStub.spec.ts b/server/src/lib/regulation/RegulatorStub.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/error/401/get.spec.ts b/server/src/lib/routes/error/401/get.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/error/401/get.ts b/server/src/lib/routes/error/401/get.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/error/403/get.spec.ts b/server/src/lib/routes/error/403/get.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/error/403/get.ts b/server/src/lib/routes/error/403/get.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/error/404/get.spec.ts b/server/src/lib/routes/error/404/get.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/error/404/get.ts b/server/src/lib/routes/error/404/get.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/error/redirector.ts b/server/src/lib/routes/error/redirector.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/firstfactor/get.ts b/server/src/lib/routes/firstfactor/get.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/firstfactor/post.spec.ts b/server/src/lib/routes/firstfactor/post.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/firstfactor/post.ts b/server/src/lib/routes/firstfactor/post.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/loggedin/get.ts b/server/src/lib/routes/loggedin/get.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/logout/get.ts b/server/src/lib/routes/logout/get.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/password-reset/constants.ts b/server/src/lib/routes/password-reset/constants.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/password-reset/form/post.spec.ts b/server/src/lib/routes/password-reset/form/post.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/password-reset/form/post.ts b/server/src/lib/routes/password-reset/form/post.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts b/server/src/lib/routes/password-reset/identity/PasswordResetHandler.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts b/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/password-reset/request/get.ts b/server/src/lib/routes/password-reset/request/get.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/secondfactor/get.spec.ts b/server/src/lib/routes/secondfactor/get.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/secondfactor/get.ts b/server/src/lib/routes/secondfactor/get.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/secondfactor/redirect.spec.ts b/server/src/lib/routes/secondfactor/redirect.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/secondfactor/redirect.ts b/server/src/lib/routes/secondfactor/redirect.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/secondfactor/totp/constants.ts b/server/src/lib/routes/secondfactor/totp/constants.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts b/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts b/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts b/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/secondfactor/totp/sign/post.ts b/server/src/lib/routes/secondfactor/totp/sign/post.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts b/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts b/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts b/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts b/server/src/lib/routes/secondfactor/u2f/register/post.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/secondfactor/u2f/register/post.ts b/server/src/lib/routes/secondfactor/u2f/register/post.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts b/server/src/lib/routes/secondfactor/u2f/register_request/get.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/secondfactor/u2f/register_request/get.ts b/server/src/lib/routes/secondfactor/u2f/register_request/get.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts b/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/secondfactor/u2f/sign/post.ts b/server/src/lib/routes/secondfactor/u2f/sign/post.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts b/server/src/lib/routes/secondfactor/u2f/sign_request/get.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts b/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/verify/access_control.ts b/server/src/lib/routes/verify/access_control.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/verify/get.spec.ts b/server/src/lib/routes/verify/get.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/verify/get.ts b/server/src/lib/routes/verify/get.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/verify/get_basic_auth.ts b/server/src/lib/routes/verify/get_basic_auth.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/routes/verify/get_session_cookie.ts b/server/src/lib/routes/verify/get_session_cookie.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/AuthenticationTraceDocument.d.ts b/server/src/lib/storage/AuthenticationTraceDocument.d.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/CollectionFactoryFactory.ts b/server/src/lib/storage/CollectionFactoryFactory.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/CollectionFactoryStub.spec.ts b/server/src/lib/storage/CollectionFactoryStub.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/CollectionStub.spec.ts b/server/src/lib/storage/CollectionStub.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/ICollection.d.ts b/server/src/lib/storage/ICollection.d.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/ICollectionFactory.d.ts b/server/src/lib/storage/ICollectionFactory.d.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/IUserDataStore.d.ts b/server/src/lib/storage/IUserDataStore.d.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/IdentityValidationDocument.d.ts b/server/src/lib/storage/IdentityValidationDocument.d.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/TOTPSecretDocument.d.ts b/server/src/lib/storage/TOTPSecretDocument.d.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/U2FRegistrationDocument.d.ts b/server/src/lib/storage/U2FRegistrationDocument.d.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/UserDataStore.spec.ts b/server/src/lib/storage/UserDataStore.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/UserDataStore.ts b/server/src/lib/storage/UserDataStore.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/UserDataStoreStub.spec.ts b/server/src/lib/storage/UserDataStoreStub.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/mongo/MongoCollection.spec.ts b/server/src/lib/storage/mongo/MongoCollection.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/mongo/MongoCollection.ts b/server/src/lib/storage/mongo/MongoCollection.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts b/server/src/lib/storage/mongo/MongoCollectionFactory.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/mongo/MongoCollectionFactory.ts b/server/src/lib/storage/mongo/MongoCollectionFactory.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/nedb/NedbCollection.spec.ts b/server/src/lib/storage/nedb/NedbCollection.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/nedb/NedbCollection.ts b/server/src/lib/storage/nedb/NedbCollection.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts b/server/src/lib/storage/nedb/NedbCollectionFactory.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/storage/nedb/NedbCollectionFactory.ts b/server/src/lib/storage/nedb/NedbCollectionFactory.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/stubs/express.spec.ts b/server/src/lib/stubs/express.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/stubs/ldapjs.spec.ts b/server/src/lib/stubs/ldapjs.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/stubs/speakeasy.spec.ts b/server/src/lib/stubs/speakeasy.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/stubs/u2f.spec.ts b/server/src/lib/stubs/u2f.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/utils/HashGenerator.spec.ts b/server/src/lib/utils/HashGenerator.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/utils/HashGenerator.ts b/server/src/lib/utils/HashGenerator.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/utils/ObjectCloner.ts b/server/src/lib/utils/ObjectCloner.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/utils/SafeRedirection.spec.ts b/server/src/lib/utils/SafeRedirection.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/utils/SafeRedirection.ts b/server/src/lib/utils/SafeRedirection.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/utils/URLDecomposer.spec.ts b/server/src/lib/utils/URLDecomposer.spec.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/utils/URLDecomposer.ts b/server/src/lib/utils/URLDecomposer.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/web_server/Configurator.ts b/server/src/lib/web_server/Configurator.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/web_server/RestApi.ts b/server/src/lib/web_server/RestApi.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts b/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts old mode 100755 new mode 100644 diff --git a/server/src/lib/web_server/middlewares/WithHeadersLogged.ts b/server/src/lib/web_server/middlewares/WithHeadersLogged.ts old mode 100755 new mode 100644 diff --git a/server/src/resources/email-template.ejs b/server/src/resources/email-template.ejs old mode 100755 new mode 100644 diff --git a/server/src/views/already-logged-in.pug b/server/src/views/already-logged-in.pug old mode 100755 new mode 100644 diff --git a/server/src/views/errors/401.pug b/server/src/views/errors/401.pug old mode 100755 new mode 100644 diff --git a/server/src/views/errors/403.pug b/server/src/views/errors/403.pug old mode 100755 new mode 100644 diff --git a/server/src/views/errors/404.pug b/server/src/views/errors/404.pug old mode 100755 new mode 100644 diff --git a/server/src/views/firstfactor.pug b/server/src/views/firstfactor.pug old mode 100755 new mode 100644 diff --git a/server/src/views/layout/layout.pug b/server/src/views/layout/layout.pug old mode 100755 new mode 100644 diff --git a/server/src/views/need-identity-validation.pug b/server/src/views/need-identity-validation.pug old mode 100755 new mode 100644 diff --git a/server/src/views/password-reset-form.pug b/server/src/views/password-reset-form.pug old mode 100755 new mode 100644 diff --git a/server/src/views/password-reset-request.pug b/server/src/views/password-reset-request.pug old mode 100755 new mode 100644 diff --git a/server/src/views/secondfactor.pug b/server/src/views/secondfactor.pug old mode 100755 new mode 100644 diff --git a/server/src/views/totp-register.pug b/server/src/views/totp-register.pug old mode 100755 new mode 100644 diff --git a/server/src/views/u2f-register.pug b/server/src/views/u2f-register.pug old mode 100755 new mode 100644 diff --git a/server/test/requests.ts b/server/test/requests.ts old mode 100755 new mode 100644 diff --git a/server/tsconfig.json b/server/tsconfig.json old mode 100755 new mode 100644 diff --git a/server/tslint.json b/server/tslint.json old mode 100755 new mode 100644 diff --git a/server/types/AuthenticationSession.ts b/server/types/AuthenticationSession.ts old mode 100755 new mode 100644 diff --git a/server/types/Dependencies.ts b/server/types/Dependencies.ts old mode 100755 new mode 100644 diff --git a/server/types/Identity.ts b/server/types/Identity.ts old mode 100755 new mode 100644 diff --git a/server/types/TOTPSecret.ts b/server/types/TOTPSecret.ts old mode 100755 new mode 100644 diff --git a/server/types/U2FRegistration.ts b/server/types/U2FRegistration.ts old mode 100755 new mode 100644 diff --git a/server/types/dovehash.d.ts b/server/types/dovehash.d.ts old mode 100755 new mode 100644 diff --git a/server/types/speakeasy.d.ts b/server/types/speakeasy.d.ts old mode 100755 new mode 100644 diff --git a/shared/BelongToDomain.ts b/shared/BelongToDomain.ts old mode 100755 new mode 100644 diff --git a/shared/DomainExtractor.spec.ts b/shared/DomainExtractor.spec.ts old mode 100755 new mode 100644 diff --git a/shared/DomainExtractor.ts b/shared/DomainExtractor.ts old mode 100755 new mode 100644 diff --git a/shared/ErrorMessage.ts b/shared/ErrorMessage.ts old mode 100755 new mode 100644 diff --git a/shared/RedirectionMessage.ts b/shared/RedirectionMessage.ts old mode 100755 new mode 100644 diff --git a/shared/SignMessage.ts b/shared/SignMessage.ts old mode 100755 new mode 100644 diff --git a/shared/UserMessages.ts b/shared/UserMessages.ts old mode 100755 new mode 100644 diff --git a/shared/api.ts b/shared/api.ts old mode 100755 new mode 100644 diff --git a/shared/constants.ts b/shared/constants.ts old mode 100755 new mode 100644 diff --git a/shared/types/u2f.d.ts b/shared/types/u2f.d.ts old mode 100755 new mode 100644 diff --git a/test/complete-config/00-suite.ts b/test/complete-config/00-suite.ts old mode 100755 new mode 100644 diff --git a/test/complete-config/closed-redirection.ts b/test/complete-config/closed-redirection.ts old mode 100755 new mode 100644 diff --git a/test/complete-config/mongo-broken-connection.ts b/test/complete-config/mongo-broken-connection.ts old mode 100755 new mode 100644 diff --git a/test/configuration.ts b/test/configuration.ts old mode 100755 new mode 100644 diff --git a/test/environment.ts b/test/environment.ts old mode 100755 new mode 100644 diff --git a/test/features/access-control.feature b/test/features/access-control.feature old mode 100755 new mode 100644 diff --git a/test/features/auth-portal-redirection.feature b/test/features/auth-portal-redirection.feature old mode 100755 new mode 100644 diff --git a/test/features/authelia.feature b/test/features/authelia.feature old mode 100755 new mode 100644 diff --git a/test/features/authentication.feature b/test/features/authentication.feature old mode 100755 new mode 100644 diff --git a/test/features/forward-headers.feature b/test/features/forward-headers.feature old mode 100755 new mode 100644 diff --git a/test/features/redirection.feature b/test/features/redirection.feature old mode 100755 new mode 100644 diff --git a/test/features/registration.feature b/test/features/registration.feature old mode 100755 new mode 100644 diff --git a/test/features/regulation.feature b/test/features/regulation.feature old mode 100755 new mode 100644 diff --git a/test/features/reset-password.feature b/test/features/reset-password.feature old mode 100755 new mode 100644 diff --git a/test/features/resilience.feature b/test/features/resilience.feature old mode 100755 new mode 100644 diff --git a/test/features/restrictions.feature b/test/features/restrictions.feature old mode 100755 new mode 100644 diff --git a/test/features/session-timeout.feature b/test/features/session-timeout.feature old mode 100755 new mode 100644 diff --git a/test/features/single-factor-domain.feature b/test/features/single-factor-domain.feature old mode 100755 new mode 100644 diff --git a/test/features/step_definitions/access-control.ts b/test/features/step_definitions/access-control.ts old mode 100755 new mode 100644 diff --git a/test/features/step_definitions/authelia.ts b/test/features/step_definitions/authelia.ts old mode 100755 new mode 100644 diff --git a/test/features/step_definitions/authentication.ts b/test/features/step_definitions/authentication.ts old mode 100755 new mode 100644 diff --git a/test/features/step_definitions/forward-headers.ts b/test/features/step_definitions/forward-headers.ts old mode 100755 new mode 100644 diff --git a/test/features/step_definitions/hooks.ts b/test/features/step_definitions/hooks.ts old mode 100755 new mode 100644 diff --git a/test/features/step_definitions/notifications.ts b/test/features/step_definitions/notifications.ts old mode 100755 new mode 100644 diff --git a/test/features/step_definitions/redirection.ts b/test/features/step_definitions/redirection.ts old mode 100755 new mode 100644 diff --git a/test/features/step_definitions/registration.ts b/test/features/step_definitions/registration.ts old mode 100755 new mode 100644 diff --git a/test/features/step_definitions/regulation.ts b/test/features/step_definitions/regulation.ts old mode 100755 new mode 100644 diff --git a/test/features/step_definitions/reset-password.ts b/test/features/step_definitions/reset-password.ts old mode 100755 new mode 100644 diff --git a/test/features/step_definitions/resilience.ts b/test/features/step_definitions/resilience.ts old mode 100755 new mode 100644 diff --git a/test/features/step_definitions/restrictions.ts b/test/features/step_definitions/restrictions.ts old mode 100755 new mode 100644 diff --git a/test/features/step_definitions/session-timeout.ts b/test/features/step_definitions/session-timeout.ts old mode 100755 new mode 100644 diff --git a/test/features/step_definitions/single-factor.ts b/test/features/step_definitions/single-factor.ts old mode 100755 new mode 100644 diff --git a/test/features/support/world.ts b/test/features/support/world.ts old mode 100755 new mode 100644 diff --git a/test/helpers/access-secret.ts b/test/helpers/access-secret.ts old mode 100755 new mode 100644 diff --git a/test/helpers/click-on-button.ts b/test/helpers/click-on-button.ts old mode 100755 new mode 100644 diff --git a/test/helpers/click-on-link.ts b/test/helpers/click-on-link.ts old mode 100755 new mode 100644 diff --git a/test/helpers/fill-field.ts b/test/helpers/fill-field.ts old mode 100755 new mode 100644 diff --git a/test/helpers/fill-login-page-and-click.ts b/test/helpers/fill-login-page-and-click.ts old mode 100755 new mode 100644 diff --git a/test/helpers/full-login.ts b/test/helpers/full-login.ts old mode 100755 new mode 100644 diff --git a/test/helpers/get-identity-link.ts b/test/helpers/get-identity-link.ts old mode 100755 new mode 100644 diff --git a/test/helpers/login-and-register-totp.ts b/test/helpers/login-and-register-totp.ts old mode 100755 new mode 100644 diff --git a/test/helpers/login-as.ts b/test/helpers/login-as.ts old mode 100755 new mode 100644 diff --git a/test/helpers/register-totp.ts b/test/helpers/register-totp.ts old mode 100755 new mode 100644 diff --git a/test/helpers/see-notification.ts b/test/helpers/see-notification.ts old mode 100755 new mode 100644 diff --git a/test/helpers/validate-totp.ts b/test/helpers/validate-totp.ts old mode 100755 new mode 100644 diff --git a/test/helpers/visit-page.ts b/test/helpers/visit-page.ts old mode 100755 new mode 100644 diff --git a/test/helpers/wait-redirected.ts b/test/helpers/wait-redirected.ts old mode 100755 new mode 100644 diff --git a/test/helpers/with-driver.ts b/test/helpers/with-driver.ts old mode 100755 new mode 100644 diff --git a/test/inactivity/00-suite.ts b/test/inactivity/00-suite.ts old mode 100755 new mode 100644 diff --git a/test/inactivity/keep_me_logged_in.ts b/test/inactivity/keep_me_logged_in.ts old mode 100755 new mode 100644 diff --git a/test/minimal-config/00-suite.ts b/test/minimal-config/00-suite.ts old mode 100755 new mode 100644 diff --git a/test/minimal-config/bad_password.ts b/test/minimal-config/bad_password.ts old mode 100755 new mode 100644 diff --git a/test/minimal-config/fail_totp.ts b/test/minimal-config/fail_totp.ts old mode 100755 new mode 100644 diff --git a/test/minimal-config/register_totp.ts b/test/minimal-config/register_totp.ts old mode 100755 new mode 100644 diff --git a/test/minimal-config/reset_password.ts b/test/minimal-config/reset_password.ts old mode 100755 new mode 100644 diff --git a/test/minimal-config/validate_totp.ts b/test/minimal-config/validate_totp.ts old mode 100755 new mode 100644 diff --git a/themes/black/client/src/css/.directory b/themes/black/client/src/css/.directory old mode 100755 new mode 100644 diff --git a/themes/black/client/src/css/00-bootstrap.min.css b/themes/black/client/src/css/00-bootstrap.min.css old mode 100755 new mode 100644 diff --git a/themes/black/client/src/css/01-main.css b/themes/black/client/src/css/01-main.css old mode 100755 new mode 100644 diff --git a/themes/black/client/src/css/02-login.css b/themes/black/client/src/css/02-login.css old mode 100755 new mode 100644 diff --git a/themes/black/client/src/css/03-errors.css b/themes/black/client/src/css/03-errors.css old mode 100755 new mode 100644 diff --git a/themes/black/client/src/css/03-password-reset-form.css b/themes/black/client/src/css/03-password-reset-form.css old mode 100755 new mode 100644 diff --git a/themes/black/client/src/css/03-password-reset-request.css b/themes/black/client/src/css/03-password-reset-request.css old mode 100755 new mode 100644 diff --git a/themes/black/client/src/css/03-totp-register.css b/themes/black/client/src/css/03-totp-register.css old mode 100755 new mode 100644 diff --git a/themes/black/client/src/css/03-u2f-register.css b/themes/black/client/src/css/03-u2f-register.css old mode 100755 new mode 100644 diff --git a/themes/black/client/src/img/RandomizedPattern.svg b/themes/black/client/src/img/RandomizedPattern.svg old mode 100755 new mode 100644 diff --git a/themes/black/client/src/img/background.jpg b/themes/black/client/src/img/background.jpg old mode 100755 new mode 100644 diff --git a/themes/black/client/src/img/icon.png b/themes/black/client/src/img/icon.png old mode 100755 new mode 100644 diff --git a/themes/black/client/src/img/mail.png b/themes/black/client/src/img/mail.png old mode 100755 new mode 100644 diff --git a/themes/black/client/src/img/notifications/.directory b/themes/black/client/src/img/notifications/.directory old mode 100755 new mode 100644 diff --git a/themes/black/client/src/img/notifications/error.png b/themes/black/client/src/img/notifications/error.png old mode 100755 new mode 100644 diff --git a/themes/black/client/src/img/notifications/info.png b/themes/black/client/src/img/notifications/info.png old mode 100755 new mode 100644 diff --git a/themes/black/client/src/img/notifications/success.png b/themes/black/client/src/img/notifications/success.png old mode 100755 new mode 100644 diff --git a/themes/black/client/src/img/notifications/warning.png b/themes/black/client/src/img/notifications/warning.png old mode 100755 new mode 100644 diff --git a/themes/black/client/src/img/padlock.png b/themes/black/client/src/img/padlock.png old mode 100755 new mode 100644 diff --git a/themes/black/client/src/img/password_white.png b/themes/black/client/src/img/password_white.png old mode 100755 new mode 100644 diff --git a/themes/black/client/src/img/pendrive.png b/themes/black/client/src/img/pendrive.png old mode 100755 new mode 100644 diff --git a/themes/black/client/src/img/sharingan.png b/themes/black/client/src/img/sharingan.png old mode 100755 new mode 100644 diff --git a/themes/black/client/src/img/stores/.directory b/themes/black/client/src/img/stores/.directory old mode 100755 new mode 100644 diff --git a/themes/black/client/src/img/stores/applestore-badge.svg b/themes/black/client/src/img/stores/applestore-badge.svg old mode 100755 new mode 100644 diff --git a/themes/black/client/src/img/stores/googleplay-badge.svg b/themes/black/client/src/img/stores/googleplay-badge.svg old mode 100755 new mode 100644 diff --git a/themes/black/client/src/img/success.png b/themes/black/client/src/img/success.png old mode 100755 new mode 100644 diff --git a/themes/black/client/src/img/user.png b/themes/black/client/src/img/user.png old mode 100755 new mode 100644 diff --git a/themes/black/client/src/img/warning.png b/themes/black/client/src/img/warning.png old mode 100755 new mode 100644 diff --git a/themes/black/client/src/thirdparties/qrcode.min.js b/themes/black/client/src/thirdparties/qrcode.min.js old mode 100755 new mode 100644 diff --git a/themes/black/client/src/thirdparties/u2f-api.js b/themes/black/client/src/thirdparties/u2f-api.js old mode 100755 new mode 100644 diff --git a/themes/black/server/.directory b/themes/black/server/.directory old mode 100755 new mode 100644 diff --git a/themes/black/server/src/resources/email-template.ejs b/themes/black/server/src/resources/email-template.ejs old mode 100755 new mode 100644 diff --git a/themes/black/server/src/views/already-logged-in.pug b/themes/black/server/src/views/already-logged-in.pug old mode 100755 new mode 100644 diff --git a/themes/black/server/src/views/errors/.directory b/themes/black/server/src/views/errors/.directory old mode 100755 new mode 100644 diff --git a/themes/black/server/src/views/errors/401.pug b/themes/black/server/src/views/errors/401.pug old mode 100755 new mode 100644 diff --git a/themes/black/server/src/views/errors/403.pug b/themes/black/server/src/views/errors/403.pug old mode 100755 new mode 100644 diff --git a/themes/black/server/src/views/errors/404.pug b/themes/black/server/src/views/errors/404.pug old mode 100755 new mode 100644 diff --git a/themes/black/server/src/views/firstfactor.pug b/themes/black/server/src/views/firstfactor.pug old mode 100755 new mode 100644 diff --git a/themes/black/server/src/views/layout/layout.pug b/themes/black/server/src/views/layout/layout.pug old mode 100755 new mode 100644 diff --git a/themes/black/server/src/views/need-identity-validation.pug b/themes/black/server/src/views/need-identity-validation.pug old mode 100755 new mode 100644 diff --git a/themes/black/server/src/views/password-reset-form.pug b/themes/black/server/src/views/password-reset-form.pug old mode 100755 new mode 100644 diff --git a/themes/black/server/src/views/password-reset-request.pug b/themes/black/server/src/views/password-reset-request.pug old mode 100755 new mode 100644 diff --git a/themes/black/server/src/views/secondfactor.pug b/themes/black/server/src/views/secondfactor.pug old mode 100755 new mode 100644 diff --git a/themes/black/server/src/views/totp-register.pug b/themes/black/server/src/views/totp-register.pug old mode 100755 new mode 100644 diff --git a/themes/black/server/src/views/u2f-register.pug b/themes/black/server/src/views/u2f-register.pug old mode 100755 new mode 100644 diff --git a/themes/default/client/src/css/.directory b/themes/default/client/src/css/.directory old mode 100755 new mode 100644 diff --git a/themes/default/client/src/css/00-bootstrap.min.css b/themes/default/client/src/css/00-bootstrap.min.css old mode 100755 new mode 100644 diff --git a/themes/default/client/src/css/01-main.css b/themes/default/client/src/css/01-main.css old mode 100755 new mode 100644 diff --git a/themes/default/client/src/css/02-login.css b/themes/default/client/src/css/02-login.css old mode 100755 new mode 100644 diff --git a/themes/default/client/src/css/03-errors.css b/themes/default/client/src/css/03-errors.css old mode 100755 new mode 100644 diff --git a/themes/default/client/src/css/03-password-reset-form.css b/themes/default/client/src/css/03-password-reset-form.css old mode 100755 new mode 100644 diff --git a/themes/default/client/src/css/03-password-reset-request.css b/themes/default/client/src/css/03-password-reset-request.css old mode 100755 new mode 100644 diff --git a/themes/default/client/src/css/03-totp-register.css b/themes/default/client/src/css/03-totp-register.css old mode 100755 new mode 100644 diff --git a/themes/default/client/src/css/03-u2f-register.css b/themes/default/client/src/css/03-u2f-register.css old mode 100755 new mode 100644 diff --git a/themes/default/client/src/img/background.svg b/themes/default/client/src/img/background.svg old mode 100755 new mode 100644 diff --git a/themes/default/client/src/img/icon.png b/themes/default/client/src/img/icon.png old mode 100755 new mode 100644 diff --git a/themes/default/client/src/img/mail.png b/themes/default/client/src/img/mail.png old mode 100755 new mode 100644 diff --git a/themes/default/client/src/img/notifications/.directory b/themes/default/client/src/img/notifications/.directory old mode 100755 new mode 100644 diff --git a/themes/default/client/src/img/notifications/error.png b/themes/default/client/src/img/notifications/error.png old mode 100755 new mode 100644 diff --git a/themes/default/client/src/img/notifications/info.png b/themes/default/client/src/img/notifications/info.png old mode 100755 new mode 100644 diff --git a/themes/default/client/src/img/notifications/success.png b/themes/default/client/src/img/notifications/success.png old mode 100755 new mode 100644 diff --git a/themes/default/client/src/img/notifications/warning.png b/themes/default/client/src/img/notifications/warning.png old mode 100755 new mode 100644 diff --git a/themes/default/client/src/img/padlock.png b/themes/default/client/src/img/padlock.png old mode 100755 new mode 100644 diff --git a/themes/default/client/src/img/password.png b/themes/default/client/src/img/password.png old mode 100755 new mode 100644 diff --git a/themes/default/client/src/img/pendrive.png b/themes/default/client/src/img/pendrive.png old mode 100755 new mode 100644 diff --git a/themes/default/client/src/img/stores/.directory b/themes/default/client/src/img/stores/.directory old mode 100755 new mode 100644 diff --git a/themes/default/client/src/img/stores/applestore-badge.svg b/themes/default/client/src/img/stores/applestore-badge.svg old mode 100755 new mode 100644 diff --git a/themes/default/client/src/img/stores/googleplay-badge.svg b/themes/default/client/src/img/stores/googleplay-badge.svg old mode 100755 new mode 100644 diff --git a/themes/default/client/src/img/success.png b/themes/default/client/src/img/success.png old mode 100755 new mode 100644 diff --git a/themes/default/client/src/img/user.png b/themes/default/client/src/img/user.png old mode 100755 new mode 100644 diff --git a/themes/default/client/src/img/warning.png b/themes/default/client/src/img/warning.png old mode 100755 new mode 100644 diff --git a/themes/default/client/src/thirdparties/qrcode.min.js b/themes/default/client/src/thirdparties/qrcode.min.js old mode 100755 new mode 100644 diff --git a/themes/default/server/.directory b/themes/default/server/.directory old mode 100755 new mode 100644 diff --git a/themes/default/server/src/resources/email-template.ejs b/themes/default/server/src/resources/email-template.ejs old mode 100755 new mode 100644 diff --git a/themes/default/server/src/views/already-logged-in.pug b/themes/default/server/src/views/already-logged-in.pug old mode 100755 new mode 100644 diff --git a/themes/default/server/src/views/errors/.directory b/themes/default/server/src/views/errors/.directory old mode 100755 new mode 100644 diff --git a/themes/default/server/src/views/errors/401.pug b/themes/default/server/src/views/errors/401.pug old mode 100755 new mode 100644 diff --git a/themes/default/server/src/views/errors/403.pug b/themes/default/server/src/views/errors/403.pug old mode 100755 new mode 100644 diff --git a/themes/default/server/src/views/errors/404.pug b/themes/default/server/src/views/errors/404.pug old mode 100755 new mode 100644 diff --git a/themes/default/server/src/views/firstfactor.pug b/themes/default/server/src/views/firstfactor.pug old mode 100755 new mode 100644 diff --git a/themes/default/server/src/views/layout/layout.pug b/themes/default/server/src/views/layout/layout.pug old mode 100755 new mode 100644 diff --git a/themes/default/server/src/views/need-identity-validation.pug b/themes/default/server/src/views/need-identity-validation.pug old mode 100755 new mode 100644 diff --git a/themes/default/server/src/views/password-reset-form.pug b/themes/default/server/src/views/password-reset-form.pug old mode 100755 new mode 100644 diff --git a/themes/default/server/src/views/password-reset-request.pug b/themes/default/server/src/views/password-reset-request.pug old mode 100755 new mode 100644 diff --git a/themes/default/server/src/views/secondfactor.pug b/themes/default/server/src/views/secondfactor.pug old mode 100755 new mode 100644 diff --git a/themes/default/server/src/views/totp-register.pug b/themes/default/server/src/views/totp-register.pug old mode 100755 new mode 100644 diff --git a/themes/default/server/src/views/u2f-register.pug b/themes/default/server/src/views/u2f-register.pug old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/css/.directory b/themes/matrix/client/src/css/.directory old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/css/00-bootstrap.min.css b/themes/matrix/client/src/css/00-bootstrap.min.css old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/css/01-main.css b/themes/matrix/client/src/css/01-main.css old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/css/02-login.css b/themes/matrix/client/src/css/02-login.css old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/css/03-errors.css b/themes/matrix/client/src/css/03-errors.css old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/css/03-password-reset-form.css b/themes/matrix/client/src/css/03-password-reset-form.css old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/css/03-password-reset-request.css b/themes/matrix/client/src/css/03-password-reset-request.css old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/css/03-totp-register.css b/themes/matrix/client/src/css/03-totp-register.css old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/css/03-u2f-register.css b/themes/matrix/client/src/css/03-u2f-register.css old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/img/background.jpg b/themes/matrix/client/src/img/background.jpg old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/img/icon.png b/themes/matrix/client/src/img/icon.png old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/img/mail.png b/themes/matrix/client/src/img/mail.png old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/img/matrix_circle_128x128.png b/themes/matrix/client/src/img/matrix_circle_128x128.png old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/img/notifications/.directory b/themes/matrix/client/src/img/notifications/.directory old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/img/notifications/error.png b/themes/matrix/client/src/img/notifications/error.png old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/img/notifications/info.png b/themes/matrix/client/src/img/notifications/info.png old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/img/notifications/success.png b/themes/matrix/client/src/img/notifications/success.png old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/img/notifications/warning.png b/themes/matrix/client/src/img/notifications/warning.png old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/img/padlock.png b/themes/matrix/client/src/img/padlock.png old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/img/password_white.png b/themes/matrix/client/src/img/password_white.png old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/img/pendrive.png b/themes/matrix/client/src/img/pendrive.png old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/img/stores/.directory b/themes/matrix/client/src/img/stores/.directory old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/img/stores/applestore-badge.svg b/themes/matrix/client/src/img/stores/applestore-badge.svg old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/img/stores/googleplay-badge.svg b/themes/matrix/client/src/img/stores/googleplay-badge.svg old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/img/success.png b/themes/matrix/client/src/img/success.png old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/img/user.png b/themes/matrix/client/src/img/user.png old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/img/warning.png b/themes/matrix/client/src/img/warning.png old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/thirdparties/matrix.js b/themes/matrix/client/src/thirdparties/matrix.js old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/thirdparties/qrcode.min.js b/themes/matrix/client/src/thirdparties/qrcode.min.js old mode 100755 new mode 100644 diff --git a/themes/matrix/client/src/thirdparties/u2f-api.js b/themes/matrix/client/src/thirdparties/u2f-api.js old mode 100755 new mode 100644 diff --git a/themes/matrix/server/.directory b/themes/matrix/server/.directory old mode 100755 new mode 100644 diff --git a/themes/matrix/server/src/resources/email-template.ejs b/themes/matrix/server/src/resources/email-template.ejs old mode 100755 new mode 100644 diff --git a/themes/matrix/server/src/views/already-logged-in.pug b/themes/matrix/server/src/views/already-logged-in.pug old mode 100755 new mode 100644 diff --git a/themes/matrix/server/src/views/errors/.directory b/themes/matrix/server/src/views/errors/.directory old mode 100755 new mode 100644 diff --git a/themes/matrix/server/src/views/errors/401.pug b/themes/matrix/server/src/views/errors/401.pug old mode 100755 new mode 100644 diff --git a/themes/matrix/server/src/views/errors/403.pug b/themes/matrix/server/src/views/errors/403.pug old mode 100755 new mode 100644 diff --git a/themes/matrix/server/src/views/errors/404.pug b/themes/matrix/server/src/views/errors/404.pug old mode 100755 new mode 100644 diff --git a/themes/matrix/server/src/views/firstfactor.pug b/themes/matrix/server/src/views/firstfactor.pug old mode 100755 new mode 100644 diff --git a/themes/matrix/server/src/views/layout/layout.pug b/themes/matrix/server/src/views/layout/layout.pug old mode 100755 new mode 100644 diff --git a/themes/matrix/server/src/views/need-identity-validation.pug b/themes/matrix/server/src/views/need-identity-validation.pug old mode 100755 new mode 100644 diff --git a/themes/matrix/server/src/views/password-reset-form.pug b/themes/matrix/server/src/views/password-reset-form.pug old mode 100755 new mode 100644 diff --git a/themes/matrix/server/src/views/password-reset-request.pug b/themes/matrix/server/src/views/password-reset-request.pug old mode 100755 new mode 100644 diff --git a/themes/matrix/server/src/views/secondfactor.pug b/themes/matrix/server/src/views/secondfactor.pug old mode 100755 new mode 100644 diff --git a/themes/matrix/server/src/views/totp-register.pug b/themes/matrix/server/src/views/totp-register.pug old mode 100755 new mode 100644 diff --git a/themes/matrix/server/src/views/u2f-register.pug b/themes/matrix/server/src/views/u2f-register.pug old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/css/.directory b/themes/squares/client/src/css/.directory old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/css/00-bootstrap.min.css b/themes/squares/client/src/css/00-bootstrap.min.css old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/css/01-main.css b/themes/squares/client/src/css/01-main.css old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/css/02-login.css b/themes/squares/client/src/css/02-login.css old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/css/03-errors.css b/themes/squares/client/src/css/03-errors.css old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/css/03-password-reset-form.css b/themes/squares/client/src/css/03-password-reset-form.css old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/css/03-password-reset-request.css b/themes/squares/client/src/css/03-password-reset-request.css old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/css/03-totp-register.css b/themes/squares/client/src/css/03-totp-register.css old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/css/03-u2f-register.css b/themes/squares/client/src/css/03-u2f-register.css old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/LargeTriangles.svg b/themes/squares/client/src/img/LargeTriangles.svg old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/RandomizedPattern.svg b/themes/squares/client/src/img/RandomizedPattern.svg old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/background.jpg b/themes/squares/client/src/img/background.jpg old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/background.svg b/themes/squares/client/src/img/background.svg old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/icon.png b/themes/squares/client/src/img/icon.png old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/mail.png b/themes/squares/client/src/img/mail.png old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/matrix_circle_128x128.png b/themes/squares/client/src/img/matrix_circle_128x128.png old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/notifications/.directory b/themes/squares/client/src/img/notifications/.directory old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/notifications/error.png b/themes/squares/client/src/img/notifications/error.png old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/notifications/info.png b/themes/squares/client/src/img/notifications/info.png old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/notifications/success.png b/themes/squares/client/src/img/notifications/success.png old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/notifications/warning.png b/themes/squares/client/src/img/notifications/warning.png old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/padlock.png b/themes/squares/client/src/img/padlock.png old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/password_white.png b/themes/squares/client/src/img/password_white.png old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/pendrive.png b/themes/squares/client/src/img/pendrive.png old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/sharingan.png b/themes/squares/client/src/img/sharingan.png old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/stores/.directory b/themes/squares/client/src/img/stores/.directory old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/stores/applestore-badge.svg b/themes/squares/client/src/img/stores/applestore-badge.svg old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/stores/googleplay-badge.svg b/themes/squares/client/src/img/stores/googleplay-badge.svg old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/success.png b/themes/squares/client/src/img/success.png old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/user.png b/themes/squares/client/src/img/user.png old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/img/warning.png b/themes/squares/client/src/img/warning.png old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/thirdparties/qrcode.min.js b/themes/squares/client/src/thirdparties/qrcode.min.js old mode 100755 new mode 100644 diff --git a/themes/squares/client/src/thirdparties/u2f-api.js b/themes/squares/client/src/thirdparties/u2f-api.js old mode 100755 new mode 100644 diff --git a/themes/squares/server/.directory b/themes/squares/server/.directory old mode 100755 new mode 100644 diff --git a/themes/squares/server/src/resources/email-template.ejs b/themes/squares/server/src/resources/email-template.ejs old mode 100755 new mode 100644 diff --git a/themes/squares/server/src/views/already-logged-in.pug b/themes/squares/server/src/views/already-logged-in.pug old mode 100755 new mode 100644 diff --git a/themes/squares/server/src/views/errors/.directory b/themes/squares/server/src/views/errors/.directory old mode 100755 new mode 100644 diff --git a/themes/squares/server/src/views/errors/401.pug b/themes/squares/server/src/views/errors/401.pug old mode 100755 new mode 100644 diff --git a/themes/squares/server/src/views/errors/403.pug b/themes/squares/server/src/views/errors/403.pug old mode 100755 new mode 100644 diff --git a/themes/squares/server/src/views/errors/404.pug b/themes/squares/server/src/views/errors/404.pug old mode 100755 new mode 100644 diff --git a/themes/squares/server/src/views/firstfactor.pug b/themes/squares/server/src/views/firstfactor.pug old mode 100755 new mode 100644 diff --git a/themes/squares/server/src/views/layout/layout.pug b/themes/squares/server/src/views/layout/layout.pug old mode 100755 new mode 100644 diff --git a/themes/squares/server/src/views/need-identity-validation.pug b/themes/squares/server/src/views/need-identity-validation.pug old mode 100755 new mode 100644 diff --git a/themes/squares/server/src/views/password-reset-form.pug b/themes/squares/server/src/views/password-reset-form.pug old mode 100755 new mode 100644 diff --git a/themes/squares/server/src/views/password-reset-request.pug b/themes/squares/server/src/views/password-reset-request.pug old mode 100755 new mode 100644 diff --git a/themes/squares/server/src/views/secondfactor.pug b/themes/squares/server/src/views/secondfactor.pug old mode 100755 new mode 100644 diff --git a/themes/squares/server/src/views/totp-register.pug b/themes/squares/server/src/views/totp-register.pug old mode 100755 new mode 100644 diff --git a/themes/squares/server/src/views/u2f-register.pug b/themes/squares/server/src/views/u2f-register.pug old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/.directory b/themes/triangles/client/src/.directory old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/css/.directory b/themes/triangles/client/src/css/.directory old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/css/00-bootstrap.min.css b/themes/triangles/client/src/css/00-bootstrap.min.css old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/css/01-main.css b/themes/triangles/client/src/css/01-main.css old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/css/02-login.css b/themes/triangles/client/src/css/02-login.css old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/css/03-errors.css b/themes/triangles/client/src/css/03-errors.css old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/css/03-password-reset-form.css b/themes/triangles/client/src/css/03-password-reset-form.css old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/css/03-password-reset-request.css b/themes/triangles/client/src/css/03-password-reset-request.css old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/css/03-totp-register.css b/themes/triangles/client/src/css/03-totp-register.css old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/css/03-u2f-register.css b/themes/triangles/client/src/css/03-u2f-register.css old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/img/LargeTriangles.svg b/themes/triangles/client/src/img/LargeTriangles.svg old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/img/background.jpg b/themes/triangles/client/src/img/background.jpg old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/img/icon.png b/themes/triangles/client/src/img/icon.png old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/img/mail.png b/themes/triangles/client/src/img/mail.png old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/img/matrix_circle_128x128.png b/themes/triangles/client/src/img/matrix_circle_128x128.png old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/img/notifications/.directory b/themes/triangles/client/src/img/notifications/.directory old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/img/notifications/error.png b/themes/triangles/client/src/img/notifications/error.png old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/img/notifications/info.png b/themes/triangles/client/src/img/notifications/info.png old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/img/notifications/success.png b/themes/triangles/client/src/img/notifications/success.png old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/img/notifications/warning.png b/themes/triangles/client/src/img/notifications/warning.png old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/img/padlock.png b/themes/triangles/client/src/img/padlock.png old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/img/password_white.png b/themes/triangles/client/src/img/password_white.png old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/img/pendrive.png b/themes/triangles/client/src/img/pendrive.png old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/img/sharingan.png b/themes/triangles/client/src/img/sharingan.png old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/img/stores/.directory b/themes/triangles/client/src/img/stores/.directory old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/img/stores/applestore-badge.svg b/themes/triangles/client/src/img/stores/applestore-badge.svg old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/img/stores/googleplay-badge.svg b/themes/triangles/client/src/img/stores/googleplay-badge.svg old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/img/success.png b/themes/triangles/client/src/img/success.png old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/img/user.png b/themes/triangles/client/src/img/user.png old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/img/warning.png b/themes/triangles/client/src/img/warning.png old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/thirdparties/qrcode.min.js b/themes/triangles/client/src/thirdparties/qrcode.min.js old mode 100755 new mode 100644 diff --git a/themes/triangles/client/src/thirdparties/u2f-api.js b/themes/triangles/client/src/thirdparties/u2f-api.js old mode 100755 new mode 100644 diff --git a/themes/triangles/server/.directory b/themes/triangles/server/.directory old mode 100755 new mode 100644 diff --git a/themes/triangles/server/src/resources/email-template.ejs b/themes/triangles/server/src/resources/email-template.ejs old mode 100755 new mode 100644 diff --git a/themes/triangles/server/src/views/already-logged-in.pug b/themes/triangles/server/src/views/already-logged-in.pug old mode 100755 new mode 100644 diff --git a/themes/triangles/server/src/views/errors/.directory b/themes/triangles/server/src/views/errors/.directory old mode 100755 new mode 100644 diff --git a/themes/triangles/server/src/views/errors/401.pug b/themes/triangles/server/src/views/errors/401.pug old mode 100755 new mode 100644 diff --git a/themes/triangles/server/src/views/errors/403.pug b/themes/triangles/server/src/views/errors/403.pug old mode 100755 new mode 100644 diff --git a/themes/triangles/server/src/views/errors/404.pug b/themes/triangles/server/src/views/errors/404.pug old mode 100755 new mode 100644 diff --git a/themes/triangles/server/src/views/firstfactor.pug b/themes/triangles/server/src/views/firstfactor.pug old mode 100755 new mode 100644 diff --git a/themes/triangles/server/src/views/layout/layout.pug b/themes/triangles/server/src/views/layout/layout.pug old mode 100755 new mode 100644 diff --git a/themes/triangles/server/src/views/need-identity-validation.pug b/themes/triangles/server/src/views/need-identity-validation.pug old mode 100755 new mode 100644 diff --git a/themes/triangles/server/src/views/password-reset-form.pug b/themes/triangles/server/src/views/password-reset-form.pug old mode 100755 new mode 100644 diff --git a/themes/triangles/server/src/views/password-reset-request.pug b/themes/triangles/server/src/views/password-reset-request.pug old mode 100755 new mode 100644 diff --git a/themes/triangles/server/src/views/secondfactor.pug b/themes/triangles/server/src/views/secondfactor.pug old mode 100755 new mode 100644 diff --git a/themes/triangles/server/src/views/totp-register.pug b/themes/triangles/server/src/views/totp-register.pug old mode 100755 new mode 100644 diff --git a/themes/triangles/server/src/views/u2f-register.pug b/themes/triangles/server/src/views/u2f-register.pug old mode 100755 new mode 100644 diff --git a/users_database.yml b/users_database.yml old mode 100755 new mode 100644 From 55c06b975e6c2a5030f635a1b509326af85c1661 Mon Sep 17 00:00:00 2001 From: BankaiNoJutsu Date: Tue, 18 Dec 2018 17:36:01 +0100 Subject: [PATCH 07/11] Add readme in themes folder, and remove uneccessary check in gruntfile --- Gruntfile.js | 5 +---- package-lock.json | 13 +++++++++---- themes/README | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 themes/README diff --git a/Gruntfile.js b/Gruntfile.js index 2bcb4f8f7..5dea75530 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -233,11 +233,8 @@ module.exports = function (grunt) { grunt.registerTask('docker-build', ['run:docker-build']); grunt.registerTask('check', function() { - if (grunt.option('theme') == 'undefined') { - grunt.log.writeln('1- Valid argmuents are just "grunt" (will use default) or "grunt --theme=|default|black|matrix|squares|triangles"'); - } if ((theme != 'default') && (theme != 'black') && (theme != 'matrix') && (theme != 'squares') && (theme != 'triangles')) { - grunt.warn('2- Valid argmuents are just "grunt" (will use default) or "grunt --theme=|default|black|matrix|squares|triangles"'); + grunt.warn('Valid argmuents are just "grunt" (will use default) or "grunt --theme=|default|black|matrix|squares|triangles"'); } if (grunt.option('theme') == 'default' || 'black' || 'matrix' || 'squares' || 'triangles') { grunt.log.ok(); diff --git a/package-lock.json b/package-lock.json index 890819932..feb359488 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2950,7 +2950,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -3365,7 +3366,8 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -3421,6 +3423,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3464,12 +3467,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, diff --git a/themes/README b/themes/README new file mode 100644 index 000000000..86bb0c1d5 --- /dev/null +++ b/themes/README @@ -0,0 +1,17 @@ +In order to build a specific Theme you need to run: + +grunt --theme= + +Available themes are: default, black, matrix, squares, triangles + +Ex. grunt --theme=black + +By default the original theme will be built. + +If you want to create a new theme: +- Use the themes/default as source material +- Make a copy in themes folder with a new name +- Add your theme folder name on line 237,239 and 242 +- And then build as above, with your theme folder/name. + +That's it! From bace1159f51d6fa4e53a6d2d508ae6995a170e79 Mon Sep 17 00:00:00 2001 From: BankaiNoJutsu Date: Tue, 18 Dec 2018 17:48:48 +0100 Subject: [PATCH 08/11] fixed perm on travis.sh --- scripts/travis.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/travis.sh diff --git a/scripts/travis.sh b/scripts/travis.sh old mode 100644 new mode 100755 From 5a11641ff37a237f3c154b3715d22e65bcbf285f Mon Sep 17 00:00:00 2001 From: BankaiNoJutsu Date: Tue, 18 Dec 2018 17:55:37 +0100 Subject: [PATCH 09/11] fixed all scripts permissions +x --- scripts/dc-dev.sh | 0 scripts/docker-publish.sh | 0 scripts/integration-tests.sh | 0 scripts/npm-deployment-test.sh | 0 scripts/run-cucumber.sh | 0 5 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/dc-dev.sh mode change 100644 => 100755 scripts/docker-publish.sh mode change 100644 => 100755 scripts/integration-tests.sh mode change 100644 => 100755 scripts/npm-deployment-test.sh mode change 100644 => 100755 scripts/run-cucumber.sh diff --git a/scripts/dc-dev.sh b/scripts/dc-dev.sh old mode 100644 new mode 100755 diff --git a/scripts/docker-publish.sh b/scripts/docker-publish.sh old mode 100644 new mode 100755 diff --git a/scripts/integration-tests.sh b/scripts/integration-tests.sh old mode 100644 new mode 100755 diff --git a/scripts/npm-deployment-test.sh b/scripts/npm-deployment-test.sh old mode 100644 new mode 100755 diff --git a/scripts/run-cucumber.sh b/scripts/run-cucumber.sh old mode 100644 new mode 100755 From 7d4a9c566b1b1dcae0e8d3423753fc87e423a44f Mon Sep 17 00:00:00 2001 From: BankaiNoJutsu Date: Tue, 18 Dec 2018 18:02:20 +0100 Subject: [PATCH 10/11] fix +x permissions on all scripts in folder --- scripts/example-commit/dc-example.sh | 0 scripts/example-commit/deploy-example.sh | 0 scripts/example-commit/undeploy-example.sh | 0 scripts/example-dockerhub/dc-example.sh | 0 scripts/example-dockerhub/deploy-example.sh | 0 scripts/example-dockerhub/undeploy-example.sh | 0 6 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/example-commit/dc-example.sh mode change 100644 => 100755 scripts/example-commit/deploy-example.sh mode change 100644 => 100755 scripts/example-commit/undeploy-example.sh mode change 100644 => 100755 scripts/example-dockerhub/dc-example.sh mode change 100644 => 100755 scripts/example-dockerhub/deploy-example.sh mode change 100644 => 100755 scripts/example-dockerhub/undeploy-example.sh diff --git a/scripts/example-commit/dc-example.sh b/scripts/example-commit/dc-example.sh old mode 100644 new mode 100755 diff --git a/scripts/example-commit/deploy-example.sh b/scripts/example-commit/deploy-example.sh old mode 100644 new mode 100755 diff --git a/scripts/example-commit/undeploy-example.sh b/scripts/example-commit/undeploy-example.sh old mode 100644 new mode 100755 diff --git a/scripts/example-dockerhub/dc-example.sh b/scripts/example-dockerhub/dc-example.sh old mode 100644 new mode 100755 diff --git a/scripts/example-dockerhub/deploy-example.sh b/scripts/example-dockerhub/deploy-example.sh old mode 100644 new mode 100755 diff --git a/scripts/example-dockerhub/undeploy-example.sh b/scripts/example-dockerhub/undeploy-example.sh old mode 100644 new mode 100755 From 17cc93425ae6cece06e38355ef884376d9b4b8e1 Mon Sep 17 00:00:00 2001 From: BankaiNoJutsu Date: Tue, 18 Dec 2018 19:12:09 +0100 Subject: [PATCH 11/11] fix config.minimal.yml user db path --- config.minimal.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.minimal.yml b/config.minimal.yml index c43828f2e..8da7bc5e1 100644 --- a/config.minimal.yml +++ b/config.minimal.yml @@ -4,7 +4,7 @@ authentication_backend: file: - path: users_database.yml + path: /etc/authelia/users_database.yml session: secret: unsecure_session_secret