Add support for users database on disk. (#262)
In order to simplify the deployment of Authelia for testing, LDAP is now optional made optional thanks to users database stored in a file. One can update the file manually even while Authelia is running. With this feature the minimal configuration requires only two components: Authelia and nginx. The users database is obviously made for development environments only as it prevents Authelia to be scaled to more than one instance. Note: Configuration has been updated. Key `ldap` has been nested in `authentication_backend`.pull/263/head
parent
1f5a18d12a
commit
9dab40c2ce
|
@ -34,3 +34,5 @@ dist/
|
|||
example/ldap/private.ldif
|
||||
|
||||
Configuration.schema.json
|
||||
|
||||
users_database.test.yml
|
||||
|
|
|
@ -2,17 +2,10 @@
|
|||
# Authelia minimal configuration #
|
||||
###############################################################
|
||||
|
||||
ldap:
|
||||
url: ldap://openldap
|
||||
base_dn: dc=example,dc=com
|
||||
|
||||
additional_users_dn: ou=users
|
||||
additional_groups_dn: ou=groups
|
||||
|
||||
groups_filter: (&(member={dn})(objectclass=groupOfNames))
|
||||
|
||||
user: cn=admin,dc=example,dc=com
|
||||
password: password
|
||||
authentication_backend:
|
||||
file:
|
||||
# The path to the database file. The file is at the root of the repo.
|
||||
path: /etc/authelia/users_database.yml
|
||||
|
||||
session:
|
||||
# The secret to encrypt the session cookies with.
|
||||
|
|
|
@ -29,43 +29,61 @@ default_redirection_url: https://home.example.com:8080/
|
|||
totp:
|
||||
issuer: authelia.com
|
||||
|
||||
|
||||
# LDAP configuration
|
||||
# The authentication backend to use for verifying user passwords
|
||||
# and retrieve information such as email address and groups
|
||||
# users belong to.
|
||||
#
|
||||
# Example: for user john, the DN will be cn=john,ou=users,dc=example,dc=com
|
||||
ldap:
|
||||
# The url of the ldap server
|
||||
url: ldap://openldap
|
||||
# There are two supported backends: `ldap` and `file`.
|
||||
authentication_backend:
|
||||
# LDAP backend configuration.
|
||||
#
|
||||
# This backend allows Authelia to be scaled to more
|
||||
# than one instance and therefore is recommended for
|
||||
# production.
|
||||
ldap:
|
||||
# The url of the ldap server
|
||||
url: ldap://openldap
|
||||
|
||||
# The base dn for every entries
|
||||
base_dn: dc=example,dc=com
|
||||
# The base dn for every entries
|
||||
base_dn: dc=example,dc=com
|
||||
|
||||
# An additional dn to define the scope to all users
|
||||
additional_users_dn: ou=users
|
||||
# An additional dn to define the scope to all users
|
||||
additional_users_dn: ou=users
|
||||
|
||||
# The users filter used to find the user DN
|
||||
# {0} is a matcher replaced by username.
|
||||
# 'cn={0}' by default.
|
||||
users_filter: cn={0}
|
||||
# The users filter used to find the user DN
|
||||
# {0} is a matcher replaced by username.
|
||||
# 'cn={0}' by default.
|
||||
users_filter: cn={0}
|
||||
|
||||
# An additional dn to define the scope of groups
|
||||
additional_groups_dn: ou=groups
|
||||
# An additional dn to define the scope of groups
|
||||
additional_groups_dn: ou=groups
|
||||
|
||||
# The groups filter used for retrieving groups of a given user.
|
||||
# {0} is a matcher replaced by username.
|
||||
# {dn} is a matcher replaced by user DN.
|
||||
# 'member={dn}' by default.
|
||||
groups_filter: (&(member={dn})(objectclass=groupOfNames))
|
||||
# The groups filter used for retrieving groups of a given user.
|
||||
# {0} is a matcher replaced by username.
|
||||
# {dn} is a matcher replaced by user DN.
|
||||
# 'member={dn}' by default.
|
||||
groups_filter: (&(member={dn})(objectclass=groupOfNames))
|
||||
|
||||
# The attribute holding the name of the group
|
||||
group_name_attribute: cn
|
||||
# The attribute holding the name of the group
|
||||
group_name_attribute: cn
|
||||
|
||||
# The attribute holding the mail address of the user
|
||||
mail_attribute: mail
|
||||
# The attribute holding the mail address of the user
|
||||
mail_attribute: mail
|
||||
|
||||
# The username and password of the admin user.
|
||||
user: cn=admin,dc=example,dc=com
|
||||
password: password
|
||||
# The username and password of the admin user.
|
||||
user: cn=admin,dc=example,dc=com
|
||||
password: password
|
||||
|
||||
# File backend configuration.
|
||||
#
|
||||
# With this backend, the users database is stored in a file
|
||||
# which is updated when users reset their passwords.
|
||||
# Therefore, this backend is meant to be used in a dev environment
|
||||
# and not in production since it prevents Authelia to be scaled to
|
||||
# more than one instance.
|
||||
#
|
||||
## file:
|
||||
## path: ./users_database.yml
|
||||
|
||||
|
||||
# Authentication methods
|
||||
|
@ -87,6 +105,7 @@ authentication_methods:
|
|||
per_subdomain_methods:
|
||||
single_factor.example.com: single_factor
|
||||
|
||||
|
||||
# Access Control
|
||||
#
|
||||
# Access control is a set of rules you can use to restrict user access to certain
|
||||
|
@ -217,8 +236,8 @@ regulation:
|
|||
# You must use only an available configuration: local, mongo
|
||||
storage:
|
||||
# The directory where the DB files will be saved
|
||||
# local:
|
||||
# path: /var/lib/authelia/store
|
||||
## local:
|
||||
## path: /var/lib/authelia/store
|
||||
|
||||
# Settings to connect to mongo server
|
||||
mongo:
|
||||
|
@ -232,16 +251,16 @@ storage:
|
|||
# Use only an available configuration: filesystem, gmail
|
||||
notifier:
|
||||
# For testing purpose, notifications can be sent in a file
|
||||
# filesystem:
|
||||
# filename: /tmp/authelia/notification.txt
|
||||
## filesystem:
|
||||
## filename: /tmp/authelia/notification.txt
|
||||
|
||||
# Use your email account to send the notifications. You can use an app password.
|
||||
# List of valid services can be found here: https://nodemailer.com/smtp/well-known/
|
||||
# email:
|
||||
# username: user@example.com
|
||||
# password: yourpassword
|
||||
# sender: admin@example.com
|
||||
# service: gmail
|
||||
## email:
|
||||
## username: user@example.com
|
||||
## password: yourpassword
|
||||
## sender: admin@example.com
|
||||
## service: gmail
|
||||
|
||||
# Use a SMTP server for sending notifications
|
||||
smtp:
|
||||
|
|
|
@ -5,8 +5,9 @@ services:
|
|||
restart: always
|
||||
volumes:
|
||||
- ./config.minimal.yml:/etc/authelia/config.yml:ro
|
||||
- ./users_database.test.yml:/etc/authelia/users_database.yml:rw
|
||||
- /tmp/authelia:/tmp/authelia
|
||||
environment:
|
||||
- NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
networks:
|
||||
- example-network
|
||||
- example-network
|
||||
|
|
|
@ -42,11 +42,13 @@ describe("Server", function () {
|
|||
domain: "example.com",
|
||||
secret: "secret"
|
||||
},
|
||||
ldap: {
|
||||
url: "http://ldap",
|
||||
user: "user",
|
||||
password: "password",
|
||||
base_dn: "dc=example,dc=com"
|
||||
authentication_backend: {
|
||||
ldap: {
|
||||
url: "http://ldap",
|
||||
user: "user",
|
||||
password: "password",
|
||||
base_dn: "dc=example,dc=com"
|
||||
},
|
||||
},
|
||||
notifier: {
|
||||
email: {
|
||||
|
|
|
@ -35,7 +35,10 @@ export default class Server {
|
|||
const displayableConfiguration: Configuration = clone(configuration);
|
||||
const STARS = "*****";
|
||||
|
||||
displayableConfiguration.ldap.password = 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;
|
||||
|
|
|
@ -6,7 +6,7 @@ import { INotifier } from "./notifiers/INotifier";
|
|||
import { IRegulator } from "./regulation/IRegulator";
|
||||
import { Configuration } from "./configuration/schema/Configuration";
|
||||
import { IAccessController } from "./access_control/IAccessController";
|
||||
import { IUsersDatabase } from "./ldap/IUsersDatabase";
|
||||
import { IUsersDatabase } from "./authentication/backends/IUsersDatabase";
|
||||
|
||||
export interface ServerVariables {
|
||||
logger: IRequestLogger;
|
||||
|
|
|
@ -11,8 +11,8 @@ 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 "./ldap/LdapUsersDatabase";
|
||||
import { ConnectorFactory } from "./ldap/connector/ConnectorFactory";
|
||||
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";
|
||||
|
@ -32,7 +32,9 @@ import { ServerVariables } from "./ServerVariables";
|
|||
import { MethodCalculator } from "./authentication/MethodCalculator";
|
||||
import { MongoClient } from "./connectors/mongo/MongoClient";
|
||||
import { IGlobalLogger } from "./logging/IGlobalLogger";
|
||||
import { SessionFactory } from "./ldap/SessionFactory";
|
||||
import { SessionFactory } from "./authentication/backends/ldap/SessionFactory";
|
||||
import { IUsersDatabase } from "./authentication/backends/IUsersDatabase";
|
||||
import { FileUsersDatabase } from "./authentication/backends/file/FileUsersDatabase";
|
||||
|
||||
class UserDataStoreFactory {
|
||||
static create(config: Configuration.Configuration, globalLogger: IGlobalLogger): BluebirdPromise<UserDataStore> {
|
||||
|
@ -58,24 +60,44 @@ class UserDataStoreFactory {
|
|||
}
|
||||
|
||||
export class ServerVariablesInitializer {
|
||||
static createUsersDatabase(
|
||||
config: Configuration.Configuration,
|
||||
deps: GlobalDependencies)
|
||||
: IUsersDatabase {
|
||||
|
||||
if (config.authentication_backend.ldap) {
|
||||
const ldapConfig = config.authentication_backend.ldap;
|
||||
return new LdapUsersDatabase(
|
||||
new SessionFactory(
|
||||
ldapConfig,
|
||||
new ConnectorFactory(ldapConfig, deps.ldapjs),
|
||||
deps.winston
|
||||
),
|
||||
ldapConfig
|
||||
);
|
||||
}
|
||||
else if (config.authentication_backend.file) {
|
||||
return new FileUsersDatabase(config.authentication_backend.file);
|
||||
}
|
||||
}
|
||||
|
||||
static initialize(
|
||||
config: Configuration.Configuration,
|
||||
globalLogger: IGlobalLogger,
|
||||
requestLogger: IRequestLogger,
|
||||
deps: GlobalDependencies): BluebirdPromise<ServerVariables> {
|
||||
deps: GlobalDependencies)
|
||||
: BluebirdPromise<ServerVariables> {
|
||||
|
||||
const mailSenderBuilder = new MailSenderBuilder(Nodemailer);
|
||||
const notifier = NotifierFactory.build(config.notifier, mailSenderBuilder);
|
||||
const ldapUsersDatabase = new LdapUsersDatabase(
|
||||
new SessionFactory(
|
||||
config.ldap,
|
||||
new ConnectorFactory(config.ldap, deps.ldapjs),
|
||||
deps.winston
|
||||
),
|
||||
config.ldap
|
||||
);
|
||||
const accessController = new AccessController(config.access_control, deps.winston);
|
||||
const totpHandler = new TotpHandler(deps.speakeasy);
|
||||
const mailSenderBuilder =
|
||||
new MailSenderBuilder(Nodemailer);
|
||||
const notifier = NotifierFactory.build(
|
||||
config.notifier, mailSenderBuilder);
|
||||
const accessController = new AccessController(
|
||||
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) {
|
||||
|
@ -85,7 +107,7 @@ export class ServerVariablesInitializer {
|
|||
const variables: ServerVariables = {
|
||||
accessController: accessController,
|
||||
config: config,
|
||||
usersDatabase: ldapUsersDatabase,
|
||||
usersDatabase: usersDatabase,
|
||||
logger: requestLogger,
|
||||
notifier: notifier,
|
||||
regulator: regulator,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ServerVariables } from "./ServerVariables";
|
||||
|
||||
import { Configuration } from "./configuration/schema/Configuration";
|
||||
import { UsersDatabaseStub } from "./ldap/UsersDatabaseStub.spec";
|
||||
import { IUsersDatabaseStub } from "./authentication/backends/IUsersDatabaseStub.spec";
|
||||
import { AccessControllerStub } from "./access_control/AccessControllerStub.spec";
|
||||
import { RequestLoggerStub } from "./logging/RequestLoggerStub.spec";
|
||||
import { NotifierStub } from "./notifiers/NotifierStub.spec";
|
||||
|
@ -13,7 +13,7 @@ import { U2fHandlerStub } from "./authentication/u2f/U2fHandlerStub.spec";
|
|||
export interface ServerVariablesMock {
|
||||
accessController: AccessControllerStub;
|
||||
config: Configuration;
|
||||
usersDatabase: UsersDatabaseStub;
|
||||
usersDatabase: IUsersDatabaseStub;
|
||||
logger: RequestLoggerStub;
|
||||
notifier: NotifierStub;
|
||||
regulator: RegulatorStub;
|
||||
|
@ -34,17 +34,19 @@ export class ServerVariablesMockBuilder {
|
|||
totp: {
|
||||
issuer: "authelia.com"
|
||||
},
|
||||
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"
|
||||
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: {},
|
||||
|
@ -60,7 +62,7 @@ export class ServerVariablesMockBuilder {
|
|||
},
|
||||
storage: {}
|
||||
},
|
||||
usersDatabase: new UsersDatabaseStub(),
|
||||
usersDatabase: new IUsersDatabaseStub(),
|
||||
logger: new RequestLoggerStub(enableLogging),
|
||||
notifier: new NotifierStub(),
|
||||
regulator: new RegulatorStub(),
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
export interface GroupsAndEmails {
|
||||
groups: string[];
|
||||
emails: string[];
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import Bluebird = require("bluebird");
|
||||
import { GroupsAndEmails } from "./ISession";
|
||||
|
||||
import { GroupsAndEmails } from "./GroupsAndEmails";
|
||||
|
||||
export interface IUsersDatabase {
|
||||
checkUserPassword(username: string, password: string): Bluebird<GroupsAndEmails>;
|
|
@ -2,9 +2,9 @@ import Bluebird = require("bluebird");
|
|||
import Sinon = require("sinon");
|
||||
|
||||
import { IUsersDatabase } from "./IUsersDatabase";
|
||||
import { GroupsAndEmails } from "./ISession";
|
||||
import { GroupsAndEmails } from "./GroupsAndEmails";
|
||||
|
||||
export class UsersDatabaseStub implements IUsersDatabase {
|
||||
export class IUsersDatabaseStub implements IUsersDatabase {
|
||||
checkUserPasswordStub: Sinon.SinonStub;
|
||||
getEmailsStub: Sinon.SinonStub;
|
||||
getGroupsStub: Sinon.SinonStub;
|
|
@ -0,0 +1,224 @@
|
|||
import Assert = require("assert");
|
||||
import Bluebird = require("bluebird");
|
||||
import Fs = require("fs");
|
||||
import Sinon = require("sinon");
|
||||
import Tmp = require("tmp");
|
||||
|
||||
import { FileUsersDatabase } from "./FileUsersDatabase";
|
||||
import { FileUsersDatabaseConfiguration } from "../../../configuration/schema/FileUsersDatabaseConfiguration";
|
||||
import { HashGenerator } from "../../../utils/HashGenerator";
|
||||
|
||||
const GOOD_DATABASE = `
|
||||
users:
|
||||
john:
|
||||
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||
email: john.doe@authelia.com
|
||||
groups:
|
||||
- admins
|
||||
- dev
|
||||
|
||||
harry:
|
||||
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||
emails: harry.potter@authelia.com
|
||||
groups: []
|
||||
`;
|
||||
|
||||
const BAD_HASH = `
|
||||
users:
|
||||
john:
|
||||
password: "{CRYPT}$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||
email: john.doe@authelia.com
|
||||
groups:
|
||||
- admins
|
||||
- dev
|
||||
`;
|
||||
|
||||
const NO_PASSWORD_DATABASE = `
|
||||
users:
|
||||
john:
|
||||
email: john.doe@authelia.com
|
||||
groups:
|
||||
- admins
|
||||
- dev
|
||||
`;
|
||||
|
||||
const NO_EMAIL_DATABASE = `
|
||||
users:
|
||||
john:
|
||||
password: "{CRYPT}$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||
groups:
|
||||
- admins
|
||||
- dev
|
||||
`;
|
||||
|
||||
const SINGLE_USER_DATABASE = `
|
||||
users:
|
||||
john:
|
||||
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||
email: john.doe@authelia.com
|
||||
groups:
|
||||
- admins
|
||||
- dev
|
||||
`
|
||||
|
||||
function createTmpFileFrom(yaml: string) {
|
||||
const tmpFileAsync = Bluebird.promisify(Tmp.file);
|
||||
return tmpFileAsync()
|
||||
.then((path: string) => {
|
||||
Fs.writeFileSync(path, yaml, "utf-8");
|
||||
return Bluebird.resolve(path);
|
||||
});
|
||||
}
|
||||
|
||||
describe("authentication/backends/file/FileUsersDatabase", function() {
|
||||
let configuration: FileUsersDatabaseConfiguration;
|
||||
|
||||
describe("checkUserPassword", () => {
|
||||
describe("good config", () => {
|
||||
beforeEach(() => {
|
||||
return createTmpFileFrom(GOOD_DATABASE)
|
||||
.then((path: string) => configuration = {
|
||||
path: path
|
||||
});
|
||||
});
|
||||
|
||||
it("should succeed", () => {
|
||||
const usersDatabase = new FileUsersDatabase(configuration);
|
||||
return usersDatabase.checkUserPassword("john", "password")
|
||||
.then((groupsAndEmails) => {
|
||||
Assert.deepEqual(groupsAndEmails.groups, ["admins", "dev"]);
|
||||
Assert.deepEqual(groupsAndEmails.emails, ["john.doe@authelia.com"]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail when password is wrong", () => {
|
||||
const usersDatabase = new FileUsersDatabase(configuration);
|
||||
return usersDatabase.checkUserPassword("john", "bad_password")
|
||||
.then(() => Bluebird.reject(new Error("should not be here.")))
|
||||
.catch((err) => {
|
||||
return Bluebird.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail when user does not exist", () => {
|
||||
const usersDatabase = new FileUsersDatabase(configuration);
|
||||
return usersDatabase.checkUserPassword("no_user", "password")
|
||||
.then(() => Bluebird.reject(new Error("should not be here.")))
|
||||
.catch((err) => {
|
||||
return Bluebird.resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("bad hash", () => {
|
||||
beforeEach(() => {
|
||||
return createTmpFileFrom(GOOD_DATABASE)
|
||||
.then((path: string) => configuration = {
|
||||
path: path
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail when hash is wrong", () => {
|
||||
const usersDatabase = new FileUsersDatabase(configuration);
|
||||
return usersDatabase.checkUserPassword("john", "password")
|
||||
.then(() => Bluebird.reject(new Error("should not be here.")))
|
||||
.catch((err) => {
|
||||
return Bluebird.resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("no password", () => {
|
||||
beforeEach(() => {
|
||||
return createTmpFileFrom(NO_PASSWORD_DATABASE)
|
||||
.then((path: string) => configuration = {
|
||||
path: path
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail", () => {
|
||||
const usersDatabase = new FileUsersDatabase(configuration);
|
||||
return usersDatabase.checkUserPassword("john", "password")
|
||||
.then(() => Bluebird.reject(new Error("should not be here.")))
|
||||
.catch((err) => {
|
||||
return Bluebird.resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getEmails", () => {
|
||||
describe("good config", () => {
|
||||
beforeEach(() => {
|
||||
return createTmpFileFrom(GOOD_DATABASE)
|
||||
.then((path: string) => configuration = {
|
||||
path: path
|
||||
});
|
||||
});
|
||||
|
||||
it("should succeed", () => {
|
||||
const usersDatabase = new FileUsersDatabase(configuration);
|
||||
return usersDatabase.getEmails("john")
|
||||
.then((emails) => {
|
||||
Assert.deepEqual(emails, ["john.doe@authelia.com"]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail when user does not exist", () => {
|
||||
const usersDatabase = new FileUsersDatabase(configuration);
|
||||
return usersDatabase.getEmails("no_user")
|
||||
.then(() => Bluebird.reject(new Error("should not be here.")))
|
||||
.catch((err) => {
|
||||
return Bluebird.resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("no email provided", () => {
|
||||
beforeEach(() => {
|
||||
return createTmpFileFrom(NO_EMAIL_DATABASE)
|
||||
.then((path: string) => configuration = {
|
||||
path: path
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail", () => {
|
||||
const usersDatabase = new FileUsersDatabase(configuration);
|
||||
return usersDatabase.getEmails("john")
|
||||
.then(() => Bluebird.reject(new Error("should not be here.")))
|
||||
.catch((err) => {
|
||||
return Bluebird.resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("updatePassword", () => {
|
||||
beforeEach(() => {
|
||||
return createTmpFileFrom(SINGLE_USER_DATABASE)
|
||||
.then((path: string) => configuration = {
|
||||
path: path
|
||||
});
|
||||
});
|
||||
|
||||
it("should succeed", () => {
|
||||
const usersDatabase = new FileUsersDatabase(configuration);
|
||||
const NEW_HASH = "{CRYPT}$6$rounds=500000$Qw6MhgADvLyYMEq9$ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const stub = Sinon.stub(HashGenerator, "ssha512").returns(Bluebird.resolve(NEW_HASH));
|
||||
return usersDatabase.updatePassword("john", "mypassword")
|
||||
.then(() => {
|
||||
const content = Fs.readFileSync(configuration.path, "utf-8");
|
||||
const matches = content.match(/password: '(.+)'/);
|
||||
Assert.equal(matches[1], NEW_HASH);
|
||||
})
|
||||
.finally(() => stub.restore());
|
||||
});
|
||||
|
||||
it("should fail when user does not exist", () => {
|
||||
const usersDatabase = new FileUsersDatabase(configuration);
|
||||
return usersDatabase.updatePassword("bad_user", "mypassword")
|
||||
.then(() => Bluebird.reject(new Error("should not be here")))
|
||||
.catch(() => Bluebird.resolve());
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,182 @@
|
|||
import Bluebird = require("bluebird");
|
||||
import Fs = require("fs");
|
||||
import Yaml = require("yamljs");
|
||||
|
||||
import { FileUsersDatabaseConfiguration }
|
||||
from "../../../configuration/schema/FileUsersDatabaseConfiguration";
|
||||
import { GroupsAndEmails } from "../GroupsAndEmails";
|
||||
import { IUsersDatabase } from "../IUsersDatabase";
|
||||
import { HashGenerator } from "../../../utils/HashGenerator";
|
||||
import { ReadWriteQueue } from "./ReadWriteQueue";
|
||||
|
||||
const loadAsync = Bluebird.promisify(Yaml.load);
|
||||
|
||||
export class FileUsersDatabase implements IUsersDatabase {
|
||||
private configuration: FileUsersDatabaseConfiguration;
|
||||
private queue: ReadWriteQueue;
|
||||
|
||||
constructor(configuration: FileUsersDatabaseConfiguration) {
|
||||
this.configuration = configuration;
|
||||
this.queue = new ReadWriteQueue(this.configuration.path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read database from file.
|
||||
* It enqueues the read task so that it is scheduled
|
||||
* between other reads and writes.
|
||||
*/
|
||||
private readDatabase(): Bluebird<any> {
|
||||
return new Bluebird<string>((resolve, reject) => {
|
||||
this.queue.read((err: Error, data: string) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(data);
|
||||
this.queue.next();
|
||||
});
|
||||
})
|
||||
.then((content) => {
|
||||
const database = Yaml.parse(content);
|
||||
if (!database) {
|
||||
return Bluebird.reject(new Error("Unable to parse YAML file."));
|
||||
}
|
||||
return Bluebird.resolve(database);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the user exists in the database.
|
||||
*/
|
||||
private checkUserExists(
|
||||
database: any,
|
||||
username: string)
|
||||
: Bluebird<void> {
|
||||
if (!(username in database.users)) {
|
||||
return Bluebird.reject(
|
||||
new Error(`User ${username} does not exist in database.`));
|
||||
}
|
||||
return Bluebird.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the password of a given user.
|
||||
*/
|
||||
private checkPassword(
|
||||
database: any,
|
||||
username: string,
|
||||
password: string)
|
||||
: Bluebird<void> {
|
||||
const storedHash: string = database.users[username].password;
|
||||
const matches = storedHash.match(/rounds=([0-9]+)\$([a-zA-z0-9]+)\$/);
|
||||
if (!(matches && matches.length == 3)) {
|
||||
return Bluebird.reject(new Error("Unable to detect the hash salt and rounds. " +
|
||||
"Make sure the password is hashed with SSHA512."));
|
||||
}
|
||||
|
||||
const rounds: number = parseInt(matches[1]);
|
||||
const salt = matches[2];
|
||||
|
||||
return HashGenerator.ssha512(password, rounds, salt)
|
||||
.then((hash: string) => {
|
||||
if (hash !== storedHash) {
|
||||
return Bluebird.reject(new Error("Wrong username/password."));
|
||||
}
|
||||
return Bluebird.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve email addresses of a given user.
|
||||
*/
|
||||
private retrieveEmails(
|
||||
database: any,
|
||||
username: string)
|
||||
: Bluebird<string[]> {
|
||||
if (!("email" in database.users[username])) {
|
||||
return Bluebird.reject(
|
||||
new Error(`User ${username} has no email address.`));
|
||||
}
|
||||
return Bluebird.resolve(
|
||||
[database.users[username].email]);
|
||||
}
|
||||
|
||||
private retrieveGroups(
|
||||
database: any,
|
||||
username: string)
|
||||
: Bluebird<string[]> {
|
||||
if (!("groups" in database.users[username])) {
|
||||
return Bluebird.resolve([]);
|
||||
}
|
||||
return Bluebird.resolve(
|
||||
database.users[username].groups);
|
||||
}
|
||||
|
||||
private replacePassword(
|
||||
database: any,
|
||||
username: string,
|
||||
newPassword: string)
|
||||
: Bluebird<void> {
|
||||
const that = this;
|
||||
return HashGenerator.ssha512(newPassword)
|
||||
.then((hash) => {
|
||||
database.users[username].password = hash;
|
||||
const str = Yaml.stringify(database, 4, 2);
|
||||
return Bluebird.resolve(str);
|
||||
})
|
||||
.then((content: string) => {
|
||||
return new Bluebird((resolve, reject) => {
|
||||
that.queue.write(content, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve();
|
||||
that.queue.next();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
checkUserPassword(
|
||||
username: string,
|
||||
password: string)
|
||||
: Bluebird<GroupsAndEmails> {
|
||||
return this.readDatabase()
|
||||
.then((database) => {
|
||||
return this.checkUserExists(database, username)
|
||||
.then(() => this.checkPassword(database, username, password))
|
||||
.then(() => {
|
||||
return Bluebird.join(
|
||||
this.retrieveEmails(database, username),
|
||||
this.retrieveGroups(database, username)
|
||||
).spread((emails: string[], groups: string[]) => {
|
||||
return { emails: emails, groups: groups };
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getEmails(username: string): Bluebird<string[]> {
|
||||
return this.readDatabase()
|
||||
.then((database) => {
|
||||
return this.checkUserExists(database, username)
|
||||
.then(() => this.retrieveEmails(database, username));
|
||||
});
|
||||
}
|
||||
|
||||
getGroups(username: string): Bluebird<string[]> {
|
||||
return this.readDatabase()
|
||||
.then((database) => {
|
||||
return this.checkUserExists(database, username)
|
||||
.then(() => this.retrieveGroups(database, username));
|
||||
});
|
||||
}
|
||||
|
||||
updatePassword(username: string, newPassword: string): Bluebird<void> {
|
||||
return this.readDatabase()
|
||||
.then((database) => {
|
||||
return this.checkUserExists(database, username)
|
||||
.then(() => this.replacePassword(database, username, newPassword));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import Fs = require("fs");
|
||||
|
||||
type Callback = (err: Error, data?: string) => void;
|
||||
type ContentAndCallback = [string, Callback] | [string, string, Callback];
|
||||
|
||||
/**
|
||||
* WriteQueue is a queue synchronizing writes to a file.
|
||||
*
|
||||
* Example of use:
|
||||
*
|
||||
* queue.add(mycontent, (err) => {
|
||||
* // do whatever you want here.
|
||||
* queue.next();
|
||||
* })
|
||||
*/
|
||||
export class ReadWriteQueue {
|
||||
private filePath: string;
|
||||
private queue: ContentAndCallback[];
|
||||
|
||||
constructor (filePath: string) {
|
||||
this.queue = [];
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
next () {
|
||||
if (this.queue.length === 0)
|
||||
return;
|
||||
|
||||
const task = this.queue[0];
|
||||
|
||||
if (task[0] == "write") {
|
||||
Fs.writeFile(this.filePath, task[1], "utf-8", (err) => {
|
||||
this.queue.shift();
|
||||
const cb = task[2] as Callback;
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
else if (task[0] == "read") {
|
||||
Fs.readFile(this.filePath, { encoding: "utf-8"} , (err, data) => {
|
||||
this.queue.shift();
|
||||
const cb = task[1] as Callback;
|
||||
cb(err, data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
write (content: string, cb: Callback) {
|
||||
this.queue.push(["write", content, cb]);
|
||||
if (this.queue.length === 1) {
|
||||
this.next();
|
||||
}
|
||||
}
|
||||
|
||||
read (cb: Callback) {
|
||||
this.queue.push(["read", cb]);
|
||||
if (this.queue.length === 1) {
|
||||
this.next();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,6 @@
|
|||
|
||||
import BluebirdPromise = require("bluebird");
|
||||
|
||||
export interface GroupsAndEmails {
|
||||
groups: string[];
|
||||
emails: string[];
|
||||
}
|
||||
|
||||
export interface ISession {
|
||||
open(): BluebirdPromise<void>;
|
||||
close(): BluebirdPromise<void>;
|
|
@ -1,9 +1,10 @@
|
|||
import Bluebird = require("bluebird");
|
||||
import { IUsersDatabase } from "./IUsersDatabase";
|
||||
import { IUsersDatabase } from "../IUsersDatabase";
|
||||
import { ISessionFactory } from "./ISessionFactory";
|
||||
import { LdapConfiguration } from "../configuration/schema/LdapConfiguration";
|
||||
import { ISession, GroupsAndEmails } from "./ISession";
|
||||
import Exceptions = require("../Exceptions");
|
||||
import { LdapConfiguration } from "../../../configuration/schema/LdapConfiguration";
|
||||
import { ISession } from "./ISession";
|
||||
import { GroupsAndEmails } from "../GroupsAndEmails";
|
||||
import Exceptions = require("../../../Exceptions");
|
||||
|
||||
type SessionCallback<T> = (session: ISession) => Bluebird<T>;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import BluebirdPromise = require("bluebird");
|
||||
import { ISession, GroupsAndEmails } from "./ISession";
|
||||
import { ISession } from "./ISession";
|
||||
import { Sanitizer } from "./Sanitizer";
|
||||
|
||||
const SPECIAL_CHAR_USED_MESSAGE = "Special character used in LDAP query.";
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
import { LdapConfiguration } from "../configuration/schema/LdapConfiguration";
|
||||
import { LdapConfiguration } from "../../../configuration/schema/LdapConfiguration";
|
||||
import { Session } from "./Session";
|
||||
import { ConnectorFactoryStub } from "./connector/ConnectorFactoryStub.spec";
|
||||
import { ConnectorStub } from "./connector/ConnectorStub.spec";
|
|
@ -1,11 +1,11 @@
|
|||
import BluebirdPromise = require("bluebird");
|
||||
import exceptions = require("../Exceptions");
|
||||
import exceptions = require("../../../Exceptions");
|
||||
import { EventEmitter } from "events";
|
||||
import { ISession, GroupsAndEmails } from "./ISession";
|
||||
import { LdapConfiguration } from "../configuration/schema/LdapConfiguration";
|
||||
import { Winston } from "../../../types/Dependencies";
|
||||
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 { HashGenerator } from "../../../utils/HashGenerator";
|
||||
import { IConnector } from "./connector/IConnector";
|
||||
|
||||
export class Session implements ISession {
|
|
@ -4,7 +4,7 @@ import Winston = require("winston");
|
|||
import { IConnectorFactory } from "./connector/IConnectorFactory";
|
||||
import { ISessionFactory } from "./ISessionFactory";
|
||||
import { ISession } from "./ISession";
|
||||
import { LdapConfiguration } from "../configuration/schema/LdapConfiguration";
|
||||
import { LdapConfiguration } from "../../../configuration/schema/LdapConfiguration";
|
||||
import { Session } from "./Session";
|
||||
import { SafeSession } from "./SafeSession";
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import Bluebird = require("bluebird");
|
||||
import Sinon = require("sinon");
|
||||
|
||||
import { ISession, GroupsAndEmails } from "./ISession";
|
||||
import { ISession } from "./ISession";
|
||||
|
||||
export class SessionStub implements ISession {
|
||||
openStub: Sinon.SinonStub;
|
|
@ -2,7 +2,7 @@ import LdapJs = require("ldapjs");
|
|||
import EventEmitter = require("events");
|
||||
import Bluebird = require("bluebird");
|
||||
import { IConnector } from "./IConnector";
|
||||
import Exceptions = require("../../Exceptions");
|
||||
import Exceptions = require("../../../../Exceptions");
|
||||
|
||||
interface SearchEntry {
|
||||
object: any;
|
|
@ -1,6 +1,6 @@
|
|||
import { IConnector } from "./IConnector";
|
||||
import { Connector } from "./Connector";
|
||||
import { LdapConfiguration } from "../../configuration/schema/LdapConfiguration";
|
||||
import { LdapConfiguration } from "../../../../configuration/schema/LdapConfiguration";
|
||||
import { Ldapjs } from "Dependencies";
|
||||
|
||||
export class ConnectorFactory {
|
|
@ -1,5 +1,6 @@
|
|||
import Sinon = require("sinon");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import Sinon = require("sinon");
|
||||
|
||||
import { IConnectorFactory } from "./IConnectorFactory";
|
||||
import { IConnector } from "./IConnector";
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import Sinon = require("sinon");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import Sinon = require("sinon");
|
||||
|
||||
import { IConnector } from "./IConnector";
|
||||
|
||||
export class ConnectorStub implements IConnector {
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import Bluebird = require("bluebird");
|
||||
import EventEmitter = require("events");
|
||||
|
|
@ -7,13 +7,15 @@ describe("configuration/ConfigurationParser", function () {
|
|||
function buildYamlConfig(): Configuration {
|
||||
const yaml_config: Configuration = {
|
||||
port: 8080,
|
||||
ldap: {
|
||||
url: "http://ldap",
|
||||
base_dn: "dc=example,dc=com",
|
||||
additional_users_dn: "ou=users",
|
||||
additional_groups_dn: "ou=groups",
|
||||
user: "user",
|
||||
password: "pass"
|
||||
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",
|
||||
|
|
|
@ -19,17 +19,19 @@ describe("configuration/SessionConfigurationBuilder", function () {
|
|||
totp: {
|
||||
issuer: "authelia.com"
|
||||
},
|
||||
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: ""
|
||||
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: {
|
||||
|
@ -100,17 +102,19 @@ describe("configuration/SessionConfigurationBuilder", function () {
|
|||
totp: {
|
||||
issuer: "authelia.com"
|
||||
},
|
||||
ldap: {
|
||||
url: "ldap://ldap",
|
||||
user: "user",
|
||||
password: "password",
|
||||
base_dn: "dc=example,dc=com",
|
||||
additional_groups_dn: "ou=groups",
|
||||
additional_users_dn: "ou=users",
|
||||
group_name_attribute: "",
|
||||
groups_filter: "",
|
||||
mail_attribute: "",
|
||||
users_filter: ""
|
||||
authentication_backend: {
|
||||
ldap: {
|
||||
url: "ldap://ldap",
|
||||
user: "user",
|
||||
password: "password",
|
||||
base_dn: "dc=example,dc=com",
|
||||
additional_groups_dn: "ou=groups",
|
||||
additional_users_dn: "ou=users",
|
||||
group_name_attribute: "",
|
||||
groups_filter: "",
|
||||
mail_attribute: "",
|
||||
users_filter: ""
|
||||
},
|
||||
},
|
||||
logs_level: "debug",
|
||||
notifier: {
|
||||
|
|
|
@ -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`");
|
||||
});
|
||||
});
|
|
@ -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];
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { ACLConfiguration, complete as AclConfigurationComplete } from "./AclConfiguration";
|
||||
import { AuthenticationMethodsConfiguration, complete as AuthenticationMethodsConfigurationComplete } from "./AuthenticationMethodsConfiguration";
|
||||
import { LdapConfiguration, complete as LdapConfigurationComplete } from "./LdapConfiguration";
|
||||
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";
|
||||
|
@ -10,7 +10,7 @@ import { MethodCalculator } from "../../authentication/MethodCalculator";
|
|||
|
||||
export interface Configuration {
|
||||
access_control?: ACLConfiguration;
|
||||
ldap: LdapConfiguration;
|
||||
authentication_backend: AuthenticationBackendConfiguration;
|
||||
authentication_methods?: AuthenticationMethodsConfiguration;
|
||||
default_redirection_url?: string;
|
||||
logs_level?: string;
|
||||
|
@ -30,10 +30,16 @@ export function complete(
|
|||
JSON.stringify(configuration));
|
||||
const errors: string[] = [];
|
||||
|
||||
newConfiguration.access_control = AclConfigurationComplete(
|
||||
newConfiguration.access_control);
|
||||
newConfiguration.ldap = LdapConfigurationComplete(
|
||||
newConfiguration.ldap);
|
||||
newConfiguration.access_control =
|
||||
AclConfigurationComplete(
|
||||
newConfiguration.access_control);
|
||||
|
||||
const [backend, error] =
|
||||
AuthenticationBackendComplete(
|
||||
newConfiguration.authentication_backend);
|
||||
|
||||
if (error) errors.push(error);
|
||||
newConfiguration.authentication_backend = backend;
|
||||
|
||||
newConfiguration.authentication_methods =
|
||||
AuthenticationMethodsConfigurationComplete(
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
export interface FileUsersDatabaseConfiguration {
|
||||
path: string;
|
||||
}
|
|
@ -14,7 +14,7 @@ import UserMessages = require("../../../../../shared/UserMessages");
|
|||
import { MethodCalculator } from "../../authentication/MethodCalculator";
|
||||
import { ServerVariables } from "../../ServerVariables";
|
||||
import { AuthenticationSession } from "../../../../types/AuthenticationSession";
|
||||
import { GroupsAndEmails } from "../../ldap/ISession";
|
||||
import { GroupsAndEmails } from "../../authentication/backends/GroupsAndEmails";
|
||||
|
||||
export default function (vars: ServerVariables) {
|
||||
return function (req: express.Request, res: express.Response)
|
||||
|
|
|
@ -42,7 +42,7 @@ describe("routes/password-reset/form/post", function () {
|
|||
mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({}));
|
||||
mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({}));
|
||||
|
||||
mocks.config.ldap = {
|
||||
mocks.config.authentication_backend.ldap = {
|
||||
url: "ldap://ldapjs",
|
||||
mail_attribute: "mail",
|
||||
user: "user",
|
||||
|
|
|
@ -8,7 +8,7 @@ import { IdentityValidable } from "../../../IdentityValidable";
|
|||
import { PRE_VALIDATION_TEMPLATE } from "../../../IdentityCheckPreValidationTemplate";
|
||||
import Constants = require("../constants");
|
||||
import { IRequestLogger } from "../../../logging/IRequestLogger";
|
||||
import { IUsersDatabase } from "../../../ldap/IUsersDatabase";
|
||||
import { IUsersDatabase } from "../../../authentication/backends/IUsersDatabase";
|
||||
|
||||
export const TEMPLATE_NAME = "password-reset-form";
|
||||
|
||||
|
|
|
@ -3,14 +3,14 @@ import { HashGenerator } from "./HashGenerator";
|
|||
|
||||
describe("utils/HashGenerator", function () {
|
||||
it("should compute correct ssha512 (password)", function () {
|
||||
return HashGenerator.ssha512("password", "jgiCMRyGXzoqpxS3")
|
||||
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", "abcdefghijklmnop")
|
||||
return HashGenerator.ssha512("test", 500000, "abcdefghijklmnop")
|
||||
.then(function (hash: string) {
|
||||
Assert.equal(hash, "{CRYPT}$6$rounds=500000$abcdefghijklmnop$sTlNGf0VO/HTQIOXemmaBbV28HUch/qhWOA1/4dsDj6CDQYhUgXbYSPL6gccAsWMr2zD5fFWwhKmPdG.yxphs.");
|
||||
});
|
||||
|
|
|
@ -4,8 +4,10 @@ import Util = require("util");
|
|||
const crypt = require("crypt3");
|
||||
|
||||
export class HashGenerator {
|
||||
static ssha512(password: string, salt?: string): BluebirdPromise<string> {
|
||||
const rounds = 500000;
|
||||
static ssha512(
|
||||
password: string,
|
||||
rounds: number = 500000,
|
||||
salt?: string): BluebirdPromise<string> {
|
||||
const saltSize = 16;
|
||||
// $6 means SHA512
|
||||
const _salt = Util.format("$6$rounds=%d$%s", rounds,
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import SeleniumWebdriver = require("selenium-webdriver");
|
||||
|
||||
export default function(driver: any, linkText: string) {
|
||||
return driver.wait(
|
||||
SeleniumWebdriver.until.elementLocated(
|
||||
SeleniumWebdriver.By.linkText(linkText)), 5000)
|
||||
.then(function (el) {
|
||||
return el.click();
|
||||
});
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
import SeleniumWebdriver = require("selenium-webdriver");
|
||||
|
||||
export default function(driver: any, fieldName: string, text: string) {
|
||||
return driver.wait(
|
||||
SeleniumWebdriver.until.elementLocated(
|
||||
SeleniumWebdriver.By.name(fieldName)), 5000)
|
||||
.then(function (el) {
|
||||
return el.sendKeys(text);
|
||||
});
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
import Bluebird = require("bluebird");
|
||||
import Fs = require("fs");
|
||||
import Request = require("request-promise");
|
||||
|
||||
export function GetLinkFromFile(): Bluebird<string> {
|
||||
return Bluebird.promisify(Fs.readFile)("/tmp/authelia/notification.txt")
|
||||
.then(function (data: any) {
|
||||
const regexp = new RegExp(/Link: (.+)/);
|
||||
const match = regexp.exec(data);
|
||||
const link = match[1];
|
||||
return Bluebird.resolve(link);
|
||||
});
|
||||
};
|
||||
|
||||
export function GetLinkFromEmail(): Bluebird<string> {
|
||||
return Request({
|
||||
method: "GET",
|
||||
uri: "http://localhost:8085/messages",
|
||||
json: true
|
||||
})
|
||||
.then(function (data: any) {
|
||||
const messageId = data[data.length - 1].id;
|
||||
return Request({
|
||||
method: "GET",
|
||||
uri: `http://localhost:8085/messages/${messageId}.html`
|
||||
});
|
||||
})
|
||||
.then(function (data: any) {
|
||||
const regexp = new RegExp(/<a href="(.+)" class="button">Continue<\/a>/);
|
||||
const match = regexp.exec(data);
|
||||
const link = match[1];
|
||||
return Bluebird.resolve(link);
|
||||
});
|
||||
};
|
|
@ -1,38 +1,6 @@
|
|||
import Bluebird = require("bluebird");
|
||||
import SeleniumWebdriver = require("selenium-webdriver");
|
||||
import Fs = require("fs");
|
||||
import Request = require("request-promise");
|
||||
|
||||
function retrieveValidationLinkFromNotificationFile(): Bluebird<string> {
|
||||
return Bluebird.promisify(Fs.readFile)("/tmp/authelia/notification.txt")
|
||||
.then(function (data: any) {
|
||||
const regexp = new RegExp(/Link: (.+)/);
|
||||
const match = regexp.exec(data);
|
||||
const link = match[1];
|
||||
return Bluebird.resolve(link);
|
||||
});
|
||||
};
|
||||
|
||||
function retrieveValidationLinkFromEmail(): Bluebird<string> {
|
||||
return Request({
|
||||
method: "GET",
|
||||
uri: "http://localhost:8085/messages",
|
||||
json: true
|
||||
})
|
||||
.then(function (data: any) {
|
||||
const messageId = data[data.length - 1].id;
|
||||
return Request({
|
||||
method: "GET",
|
||||
uri: `http://localhost:8085/messages/${messageId}.html`
|
||||
});
|
||||
})
|
||||
.then(function (data: any) {
|
||||
const regexp = new RegExp(/<a href="(.+)" class="button">Continue<\/a>/);
|
||||
const match = regexp.exec(data);
|
||||
const link = match[1];
|
||||
return Bluebird.resolve(link);
|
||||
});
|
||||
};
|
||||
import {GetLinkFromFile, GetLinkFromEmail} from '../helpers/get-identity-link';
|
||||
|
||||
export default function(driver: any, email?: boolean): Bluebird<string> {
|
||||
return driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.className("register-totp")), 5000)
|
||||
|
@ -40,8 +8,8 @@ export default function(driver: any, email?: boolean): Bluebird<string> {
|
|||
return driver.findElement(SeleniumWebdriver.By.className("register-totp")).click();
|
||||
})
|
||||
.then(function () {
|
||||
if(email) return retrieveValidationLinkFromEmail();
|
||||
else return retrieveValidationLinkFromNotificationFile();
|
||||
if(email) return GetLinkFromEmail();
|
||||
else return GetLinkFromFile();
|
||||
})
|
||||
.then(function (link: string) {
|
||||
return driver.get(link);
|
||||
|
|
|
@ -1,21 +1,32 @@
|
|||
require("chromedriver");
|
||||
import ChildProcess = require('child_process');
|
||||
import Bluebird = require("bluebird");
|
||||
|
||||
import Environment = require('../environment');
|
||||
|
||||
const execAsync = Bluebird.promisify(ChildProcess.exec);
|
||||
|
||||
const includes = [
|
||||
"docker-compose.minimal.yml",
|
||||
"example/compose/docker-compose.base.yml",
|
||||
"example/compose/nginx/minimal/docker-compose.yml",
|
||||
"example/compose/ldap/docker-compose.yml"
|
||||
]
|
||||
|
||||
|
||||
before(function() {
|
||||
this.timeout(20000);
|
||||
this.environment = new Environment.Environment(includes);
|
||||
return this.environment.setup(2000);
|
||||
|
||||
return execAsync("cp users_database.yml users_database.test.yml")
|
||||
.then(() => this.environment.setup(2000));
|
||||
});
|
||||
|
||||
after(function() {
|
||||
this.timeout(30000);
|
||||
return this.environment.cleanup();
|
||||
return execAsync("rm users_database.test.yml")
|
||||
.then(() => {
|
||||
if(process.env.KEEP_ENV != "true") {
|
||||
return this.environment.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
require("chromedriver");
|
||||
import Bluebird = require("bluebird");
|
||||
import ChildProcess = require("child_process");
|
||||
import SeleniumWebdriver = require("selenium-webdriver");
|
||||
|
||||
import WithDriver from '../helpers/with-driver';
|
||||
import VisitPage from '../helpers/visit-page';
|
||||
import ClickOnLink from '../helpers/click-on-link';
|
||||
import ClickOnButton from '../helpers/click-on-button';
|
||||
import WaitRedirect from '../helpers/wait-redirected';
|
||||
import FillField from "../helpers/fill-field";
|
||||
import {GetLinkFromFile} from "../helpers/get-identity-link";
|
||||
import FillLoginPageAndClick from "../helpers/fill-login-page-and-click";
|
||||
|
||||
const execAsync = Bluebird.promisify(ChildProcess.exec);
|
||||
|
||||
describe('Reset password', function() {
|
||||
this.timeout(10000);
|
||||
WithDriver();
|
||||
|
||||
after(() => {
|
||||
return execAsync("cp users_database.yml users_database.test.yml");
|
||||
})
|
||||
|
||||
describe('click on reset password', function() {
|
||||
it("should reset password for john", function() {
|
||||
return VisitPage(this.driver, "https://login.example.com:8080/")
|
||||
.then(() => ClickOnLink(this.driver, "Forgot password\?"))
|
||||
.then(() => WaitRedirect(this.driver, "https://login.example.com:8080/password-reset/request"))
|
||||
.then(() => FillField(this.driver, "username", "john"))
|
||||
.then(() => ClickOnButton(this.driver, "Reset Password"))
|
||||
.then(() => this.driver.sleep(1000)) // Simulate the time to read it from mailbox.
|
||||
.then(() => GetLinkFromFile())
|
||||
.then((link) => VisitPage(this.driver, link))
|
||||
.then(() => FillField(this.driver, "password1", "newpass"))
|
||||
.then(() => FillField(this.driver, "password2", "newpass"))
|
||||
.then(() => ClickOnButton(this.driver, "Reset Password"))
|
||||
.then(() => WaitRedirect(this.driver, "https://login.example.com:8080/"))
|
||||
.then(() => FillLoginPageAndClick(this.driver, "john", "newpass"))
|
||||
.then(() => WaitRedirect(this.driver, "https://login.example.com:8080/secondfactor"))
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
###############################################################
|
||||
# Users Database #
|
||||
###############################################################
|
||||
|
||||
# This file can be used if you do not have an LDAP set up.
|
||||
|
||||
# List of users
|
||||
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: []
|
||||
|
||||
bob:
|
||||
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||
email: bob.dylan@authelia.com
|
||||
groups:
|
||||
- dev
|
||||
|
||||
james:
|
||||
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||
email: james.dean@authelia.com
|
Loading…
Reference in New Issue