From 67f84b97c8f1ca6e5883e3f9c8d538631df85141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Michaud?= Date: Sun, 26 Aug 2018 13:10:23 +0200 Subject: [PATCH] Enable authentication to Mongo and Redis. (#263) * Fix issue in unit test of IdentityCheckMiddleware. * Enable authentication to Mongo server. * Enable authentication to Redis. --- config.template.yml | 4 + example/compose/mongo/docker-compose.yml | 4 + example/compose/redis/docker-compose.yml | 1 + .../src/lib/IdentityCheckMiddleware.spec.ts | 17 +- server/src/lib/IdentityCheckMiddleware.ts | 6 +- server/src/lib/IdentityValidableStub.spec.ts | 2 +- server/src/lib/ServerVariablesInitializer.ts | 3 +- .../SessionConfigurationBuilder.spec.ts | 225 ++++++++---------- .../SessionConfigurationBuilder.ts | 34 +-- .../schema/SessionConfiguration.ts | 1 + .../schema/StorageConfiguration.ts | 5 + .../lib/connectors/mongo/MongoClient.spec.ts | 61 ++++- .../src/lib/connectors/mongo/MongoClient.ts | 34 ++- test/features/step_definitions/hooks.ts | 9 +- 14 files changed, 227 insertions(+), 179 deletions(-) diff --git a/config.template.yml b/config.template.yml index 275a83ef2..abbca0e30 100644 --- a/config.template.yml +++ b/config.template.yml @@ -213,6 +213,7 @@ session: redis: host: redis port: 6379 + password: authelia # Configuration of the authentication regulation mechanism. # @@ -243,6 +244,9 @@ storage: mongo: url: mongodb://mongo database: authelia + auth: + username: authelia + password: authelia # Configuration of the notification system. # diff --git a/example/compose/mongo/docker-compose.yml b/example/compose/mongo/docker-compose.yml index e63fa39fa..5b95e931b 100644 --- a/example/compose/mongo/docker-compose.yml +++ b/example/compose/mongo/docker-compose.yml @@ -2,6 +2,10 @@ version: '2' services: mongo: image: mongo:3.4 + command: mongod --auth + environment: + - MONGO_INITDB_ROOT_USERNAME=authelia + - MONGO_INITDB_ROOT_PASSWORD=authelia ports: - "27017:27017" networks: diff --git a/example/compose/redis/docker-compose.yml b/example/compose/redis/docker-compose.yml index 62cc6edd1..d4aee009f 100644 --- a/example/compose/redis/docker-compose.yml +++ b/example/compose/redis/docker-compose.yml @@ -2,5 +2,6 @@ version: '2' services: redis: image: redis:4.0-alpine + command: redis-server --requirepass authelia networks: - example-network diff --git a/server/src/lib/IdentityCheckMiddleware.spec.ts b/server/src/lib/IdentityCheckMiddleware.spec.ts index 5f2b23a4b..842ed6bcb 100644 --- a/server/src/lib/IdentityCheckMiddleware.spec.ts +++ b/server/src/lib/IdentityCheckMiddleware.spec.ts @@ -73,8 +73,7 @@ throws a first factor error", function () { identityValidable, "/endpoint", vars); return callback(req as any, res as any, undefined) - .then(function () { return BluebirdPromise.reject("Should fail"); }) - .catch(function () { + .then(() => { Assert(res.redirect.calledWith("/error/401")); }); }); @@ -137,16 +136,12 @@ throws a first factor error", function () { describe("test finish GET", function () { - it("should send 401 if no identity_token is provided", 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 () { - return BluebirdPromise.reject("Should fail"); - }) - .catch(function () { Assert(res.redirect.calledWith("/error/401")); }); }); @@ -164,16 +159,16 @@ valid", function () { 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(function () { - return BluebirdPromise.reject("Should fail"); - }) - .catch(function () { + .then(() => { Assert(res.redirect.calledWith("/error/401")); }); }); diff --git a/server/src/lib/IdentityCheckMiddleware.ts b/server/src/lib/IdentityCheckMiddleware.ts index 1163aea68..fa525b4b2 100644 --- a/server/src/lib/IdentityCheckMiddleware.ts +++ b/server/src/lib/IdentityCheckMiddleware.ts @@ -73,15 +73,15 @@ export function get_finish_validation(handler: IdentityValidable, vars.logger.debug(req, "Identity token provided is %s", identityToken); return checkIdentityToken(req, identityToken) - .then(function () { + .then(() => { authSession = AuthenticationSessionHandler.get(req, vars.logger); return handler.postValidationInit(req); }) - .then(function () { + .then(() => { return consumeToken(identityToken, handler.challenge(), vars.userDataStore); }) - .then(function (doc: IdentityValidationDocument) { + .then((doc: IdentityValidationDocument) => { authSession.identity_check = { challenge: handler.challenge(), userid: doc.userId diff --git a/server/src/lib/IdentityValidableStub.spec.ts b/server/src/lib/IdentityValidableStub.spec.ts index f8722a63f..20a977140 100644 --- a/server/src/lib/IdentityValidableStub.spec.ts +++ b/server/src/lib/IdentityValidableStub.spec.ts @@ -18,7 +18,7 @@ export class IdentityValidableStub implements IdentityValidable { this.challengeStub = Sinon.stub(); this.preValidationInitStub = Sinon.stub(); - this.postValidationResponseStub = Sinon.stub(); + this.postValidationInitStub = Sinon.stub(); this.preValidationResponseStub = Sinon.stub(); this.postValidationResponseStub = Sinon.stub(); diff --git a/server/src/lib/ServerVariablesInitializer.ts b/server/src/lib/ServerVariablesInitializer.ts index ad332775c..7069ef1c8 100644 --- a/server/src/lib/ServerVariablesInitializer.ts +++ b/server/src/lib/ServerVariablesInitializer.ts @@ -48,8 +48,7 @@ class UserDataStoreFactory { } else if (config.storage.mongo) { const mongoClient = new MongoClient( - config.storage.mongo.url, - config.storage.mongo.database, + config.storage.mongo, globalLogger); const collectionFactory = CollectionFactoryFactory.createMongo(mongoClient); return BluebirdPromise.resolve(new UserDataStore(collectionFactory)); diff --git a/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts b/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts index 3f7eb592b..1ff48ea4e 100644 --- a/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts +++ b/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts @@ -8,73 +8,72 @@ import Sinon = require("sinon"); import Assert = require("assert"); describe("configuration/SessionConfigurationBuilder", function () { - it("should return session options without redis options", function () { - const configuration: Configuration = { - access_control: { - default_policy: "deny", - any: [], - users: {}, - groups: {} + const configuration: Configuration = { + access_control: { + default_policy: "deny", + any: [], + users: {}, + groups: {} + }, + 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: "" }, - 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 - } - }, - authentication_methods: { - default_method: "two_factor", - per_subdomain_methods: {} + }, + 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 + } + }, + authentication_methods: { + default_method: "two_factor", + per_subdomain_methods: {} + } + }; - 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 - }; + 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", @@ -92,79 +91,17 @@ describe("configuration/SessionConfigurationBuilder", function () { }); it("should return session options with redis options", function () { - const configuration: Configuration = { - access_control: { - default_policy: "deny", - any: [], - users: {}, - groups: {} - }, - totp: { - issuer: "authelia.com" - }, - 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: { - filesystem: { - filename: "/test" - } - }, - port: 8080, - session: { - name: "authelia_session", - domain: "example.com", - expiration: 3600, - secret: "secret", - inactivity: 4000, - redis: { - host: "redis.example.com", - port: 6379 - } - }, - regulation: { - max_retries: 3, - ban_time: 5 * 60, - find_time: 5 * 60 - }, - storage: { - local: { - in_memory: true - } - }, - authentication_methods: { - default_method: "two_factor", - per_subdomain_methods: {} - } + configuration.session["redis"] = { + host: "redis.example.com", + port: 6379 }; - const RedisStoreMock = Sinon.spy(); const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); - const deps: GlobalDependencies = { - ConnectRedis: Sinon.stub().returns(RedisStoreMock) 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: { - createClient: Sinon.mock().returns(redisClient) - } as any - }; + deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; + deps.Redis = { + createClient: Sinon.mock().returns(redisClient) + } as any; const options = SessionConfigurationBuilder.build(configuration, deps); @@ -189,4 +126,30 @@ describe("configuration/SessionConfigurationBuilder", function () { 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/server/src/lib/configuration/SessionConfigurationBuilder.ts b/server/src/lib/configuration/SessionConfigurationBuilder.ts index 716a1f74a..6ce643d9d 100644 --- a/server/src/lib/configuration/SessionConfigurationBuilder.ts +++ b/server/src/lib/configuration/SessionConfigurationBuilder.ts @@ -1,7 +1,9 @@ - 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 { @@ -21,20 +23,24 @@ export class SessionConfigurationBuilder { if (configuration.session.redis) { let redisOptions; - if (configuration.session.redis.host - && configuration.session.redis.port) { - const client = deps.Redis.createClient({ - host: configuration.session.redis.host, - port: configuration.session.redis.port - }); - client.on("error", function (err: Error) { - console.error("Redis error:", err); - }); - redisOptions = { - client: client, - logErrors: true - }; + 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); diff --git a/server/src/lib/configuration/schema/SessionConfiguration.ts b/server/src/lib/configuration/schema/SessionConfiguration.ts index 4b5d555b6..2c88bb215 100644 --- a/server/src/lib/configuration/schema/SessionConfiguration.ts +++ b/server/src/lib/configuration/schema/SessionConfiguration.ts @@ -1,6 +1,7 @@ export interface SessionRedisOptions { host: string; port: number; + password?: string; } export interface SessionConfiguration { diff --git a/server/src/lib/configuration/schema/StorageConfiguration.ts b/server/src/lib/configuration/schema/StorageConfiguration.ts index 9cbc6d170..47e356ef4 100644 --- a/server/src/lib/configuration/schema/StorageConfiguration.ts +++ b/server/src/lib/configuration/schema/StorageConfiguration.ts @@ -1,6 +1,10 @@ export interface MongoStorageConfiguration { url: string; database: string; + auth?: { + username: string; + password: string; + }; } export interface LocalStorageConfiguration { @@ -21,5 +25,6 @@ export function complete(configuration: StorageConfiguration): StorageConfigurat in_memory: true }; } + return newConfiguration; } \ No newline at end of file diff --git a/server/src/lib/connectors/mongo/MongoClient.spec.ts b/server/src/lib/connectors/mongo/MongoClient.spec.ts index da3e8d622..ca0c68593 100644 --- a/server/src/lib/connectors/mongo/MongoClient.spec.ts +++ b/server/src/lib/connectors/mongo/MongoClient.spec.ts @@ -1,9 +1,11 @@ import Assert = require("assert"); -import Sinon = require("sinon"); -import MongoDB = require("mongodb"); 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; @@ -11,7 +13,52 @@ describe("connectors/mongo/MongoClient", function () { let mongoDatabaseStub: any; let logger: GlobalLoggerStub = new GlobalLoggerStub(); - describe("collection", function () { + 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() @@ -38,7 +85,7 @@ describe("connectors/mongo/MongoClient", function () { it("should create a collection", function () { const COLLECTION_NAME = "mycollection"; - const client = new MongoClient("mongo://url", "databasename", logger); + const client = new MongoClient(configuration, logger); mongoDatabaseStub.collection.returns("COL"); return client.collection(COLLECTION_NAME) @@ -60,12 +107,12 @@ describe("connectors/mongo/MongoClient", function () { it("should fail creating the collection", function() { const COLLECTION_NAME = "mycollection"; - const client = new MongoClient("mongo://url", "databasename", logger); + 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"))) - .error((err) => Bluebird.resolve()); + .then((collection) => Bluebird.reject(new Error("should not be here."))) + .catch((err) => Bluebird.resolve()); }); }) }); diff --git a/server/src/lib/connectors/mongo/MongoClient.ts b/server/src/lib/connectors/mongo/MongoClient.ts index d4570e7ae..d15731e97 100644 --- a/server/src/lib/connectors/mongo/MongoClient.ts +++ b/server/src/lib/connectors/mongo/MongoClient.ts @@ -4,31 +4,47 @@ 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 url: string; - private databaseName: string; + private configuration: MongoStorageConfiguration; private database: MongoDB.Db; private client: MongoDB.MongoClient; private logger: IGlobalLogger; constructor( - url: string, - databaseName: string, + configuration: MongoStorageConfiguration, logger: IGlobalLogger) { - this.url = url; - this.databaseName = databaseName; + this.configuration = configuration; this.logger = logger; } connect(): Bluebird { const that = this; - const connectAsync = Bluebird.promisify(MongoDB.MongoClient.connect); - return connectAsync(this.url) + 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.databaseName); + that.database = client.db(that.configuration.database); that.database.on("close", () => { that.logger.info("[MongoClient] Lost connection."); }); diff --git a/test/features/step_definitions/hooks.ts b/test/features/step_definitions/hooks.ts index 095a996b6..fc250a7eb 100644 --- a/test/features/step_definitions/hooks.ts +++ b/test/features/step_definitions/hooks.ts @@ -103,7 +103,14 @@ declareNeedsConfiguration("totp_issuer", createCustomTotpIssuerConfiguration); function registerUser(context: any, username: string) { let secret: TOTPSecret; - const mongoClient = new MongoClient("mongodb://localhost:27017", "authelia", new GlobalLoggerStub()); + const mongoClient = new MongoClient({ + url: "mongodb://localhost:27017", + database: "authelia", + auth: { + username: "authelia", + password: "authelia" + } + }, new GlobalLoggerStub()); const collectionFactory = CollectionFactoryFactory.createMongo(mongoClient); const userDataStore = new UserDataStore(collectionFactory);