Enable authentication to Mongo and Redis. (#263)

* Fix issue in unit test of IdentityCheckMiddleware.

* Enable authentication to Mongo server.

* Enable authentication to Redis.
pull/259/head^2
Clément Michaud 2018-08-26 13:10:23 +02:00 committed by GitHub
parent 9dab40c2ce
commit 67f84b97c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 227 additions and 179 deletions

View File

@ -213,6 +213,7 @@ session:
redis: redis:
host: redis host: redis
port: 6379 port: 6379
password: authelia
# Configuration of the authentication regulation mechanism. # Configuration of the authentication regulation mechanism.
# #
@ -243,6 +244,9 @@ storage:
mongo: mongo:
url: mongodb://mongo url: mongodb://mongo
database: authelia database: authelia
auth:
username: authelia
password: authelia
# Configuration of the notification system. # Configuration of the notification system.
# #

View File

@ -2,6 +2,10 @@ version: '2'
services: services:
mongo: mongo:
image: mongo:3.4 image: mongo:3.4
command: mongod --auth
environment:
- MONGO_INITDB_ROOT_USERNAME=authelia
- MONGO_INITDB_ROOT_PASSWORD=authelia
ports: ports:
- "27017:27017" - "27017:27017"
networks: networks:

View File

@ -2,5 +2,6 @@ version: '2'
services: services:
redis: redis:
image: redis:4.0-alpine image: redis:4.0-alpine
command: redis-server --requirepass authelia
networks: networks:
- example-network - example-network

View File

@ -73,8 +73,7 @@ throws a first factor error", function () {
identityValidable, "/endpoint", vars); identityValidable, "/endpoint", vars);
return callback(req as any, res as any, undefined) return callback(req as any, res as any, undefined)
.then(function () { return BluebirdPromise.reject("Should fail"); }) .then(() => {
.catch(function () {
Assert(res.redirect.calledWith("/error/401")); Assert(res.redirect.calledWith("/error/401"));
}); });
}); });
@ -137,16 +136,12 @@ throws a first factor error", function () {
describe("test finish GET", 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 const callback = IdentityValidator
.get_finish_validation(identityValidable, vars); .get_finish_validation(identityValidable, vars);
return callback(req as any, res as any, undefined) return callback(req as any, res as any, undefined)
.then(function () { .then(function () {
return BluebirdPromise.reject("Should fail");
})
.catch(function () {
Assert(res.redirect.calledWith("/error/401")); Assert(res.redirect.calledWith("/error/401"));
}); });
}); });
@ -164,16 +159,16 @@ valid", function () {
function () { function () {
req.query.identity_token = "token"; req.query.identity_token = "token";
identityValidable.postValidationInitStub
.returns(BluebirdPromise.resolve());
mocks.userDataStore.consumeIdentityValidationTokenStub.reset();
mocks.userDataStore.consumeIdentityValidationTokenStub mocks.userDataStore.consumeIdentityValidationTokenStub
.returns(BluebirdPromise.reject(new Error("Invalid token"))); .returns(BluebirdPromise.reject(new Error("Invalid token")));
const callback = IdentityValidator const callback = IdentityValidator
.get_finish_validation(identityValidable, vars); .get_finish_validation(identityValidable, vars);
return callback(req as any, res as any, undefined) return callback(req as any, res as any, undefined)
.then(function () { .then(() => {
return BluebirdPromise.reject("Should fail");
})
.catch(function () {
Assert(res.redirect.calledWith("/error/401")); Assert(res.redirect.calledWith("/error/401"));
}); });
}); });

View File

@ -73,15 +73,15 @@ export function get_finish_validation(handler: IdentityValidable,
vars.logger.debug(req, "Identity token provided is %s", identityToken); vars.logger.debug(req, "Identity token provided is %s", identityToken);
return checkIdentityToken(req, identityToken) return checkIdentityToken(req, identityToken)
.then(function () { .then(() => {
authSession = AuthenticationSessionHandler.get(req, vars.logger); authSession = AuthenticationSessionHandler.get(req, vars.logger);
return handler.postValidationInit(req); return handler.postValidationInit(req);
}) })
.then(function () { .then(() => {
return consumeToken(identityToken, handler.challenge(), return consumeToken(identityToken, handler.challenge(),
vars.userDataStore); vars.userDataStore);
}) })
.then(function (doc: IdentityValidationDocument) { .then((doc: IdentityValidationDocument) => {
authSession.identity_check = { authSession.identity_check = {
challenge: handler.challenge(), challenge: handler.challenge(),
userid: doc.userId userid: doc.userId

View File

@ -18,7 +18,7 @@ export class IdentityValidableStub implements IdentityValidable {
this.challengeStub = Sinon.stub(); this.challengeStub = Sinon.stub();
this.preValidationInitStub = Sinon.stub(); this.preValidationInitStub = Sinon.stub();
this.postValidationResponseStub = Sinon.stub(); this.postValidationInitStub = Sinon.stub();
this.preValidationResponseStub = Sinon.stub(); this.preValidationResponseStub = Sinon.stub();
this.postValidationResponseStub = Sinon.stub(); this.postValidationResponseStub = Sinon.stub();

View File

@ -48,8 +48,7 @@ class UserDataStoreFactory {
} }
else if (config.storage.mongo) { else if (config.storage.mongo) {
const mongoClient = new MongoClient( const mongoClient = new MongoClient(
config.storage.mongo.url, config.storage.mongo,
config.storage.mongo.database,
globalLogger); globalLogger);
const collectionFactory = CollectionFactoryFactory.createMongo(mongoClient); const collectionFactory = CollectionFactoryFactory.createMongo(mongoClient);
return BluebirdPromise.resolve(new UserDataStore(collectionFactory)); return BluebirdPromise.resolve(new UserDataStore(collectionFactory));

View File

@ -8,73 +8,72 @@ import Sinon = require("sinon");
import Assert = require("assert"); import Assert = require("assert");
describe("configuration/SessionConfigurationBuilder", function () { describe("configuration/SessionConfigurationBuilder", function () {
it("should return session options without redis options", function () { const configuration: Configuration = {
const configuration: Configuration = { access_control: {
access_control: { default_policy: "deny",
default_policy: "deny", any: [],
any: [], users: {},
users: {}, groups: {}
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" logs_level: "debug",
}, notifier: {
authentication_backend: { filesystem: {
ldap: { filename: "/test"
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: {}
} }
}; },
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 = { const deps: GlobalDependencies = {
ConnectRedis: Sinon.spy() as any, ConnectRedis: Sinon.spy() as any,
ldapjs: Sinon.spy() as any, ldapjs: Sinon.spy() as any,
nedb: Sinon.spy() as any, nedb: Sinon.spy() as any,
session: Sinon.spy() as any, session: Sinon.spy() as any,
speakeasy: Sinon.spy() as any, speakeasy: Sinon.spy() as any,
u2f: Sinon.spy() as any, u2f: Sinon.spy() as any,
winston: Sinon.spy() as any, winston: Sinon.spy() as any,
Redis: 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 options = SessionConfigurationBuilder.build(configuration, deps);
const expectedOptions = { const expectedOptions = {
name: "authelia_session", name: "authelia_session",
secret: "secret", secret: "secret",
@ -92,79 +91,17 @@ describe("configuration/SessionConfigurationBuilder", function () {
}); });
it("should return session options with redis options", function () { it("should return session options with redis options", function () {
const configuration: Configuration = { configuration.session["redis"] = {
access_control: { host: "redis.example.com",
default_policy: "deny", port: 6379
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: {}
}
}; };
const RedisStoreMock = Sinon.spy(); const RedisStoreMock = Sinon.spy();
const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); const redisClient = Sinon.mock().returns({ on: Sinon.spy() });
const deps: GlobalDependencies = { deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any;
ConnectRedis: Sinon.stub().returns(RedisStoreMock) as any, deps.Redis = {
ldapjs: Sinon.spy() as any, createClient: Sinon.mock().returns(redisClient)
nedb: Sinon.spy() as any, } 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
};
const options = SessionConfigurationBuilder.build(configuration, deps); const options = SessionConfigurationBuilder.build(configuration, deps);
@ -189,4 +126,30 @@ describe("configuration/SessionConfigurationBuilder", function () {
Assert.deepEqual(options.cookie, expectedOptions.cookie); Assert.deepEqual(options.cookie, expectedOptions.cookie);
Assert(options.store != undefined); 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"
}));
});
}); });

View File

@ -1,7 +1,9 @@
import ExpressSession = require("express-session"); import ExpressSession = require("express-session");
import Redis = require("redis");
import { Configuration } from "./schema/Configuration"; import { Configuration } from "./schema/Configuration";
import { GlobalDependencies } from "../../../types/Dependencies"; import { GlobalDependencies } from "../../../types/Dependencies";
import { RedisStoreOptions } from "connect-redis";
export class SessionConfigurationBuilder { export class SessionConfigurationBuilder {
@ -21,20 +23,24 @@ export class SessionConfigurationBuilder {
if (configuration.session.redis) { if (configuration.session.redis) {
let redisOptions; let redisOptions;
if (configuration.session.redis.host const options: Redis.ClientOpts = {
&& configuration.session.redis.port) { host: configuration.session.redis.host,
const client = deps.Redis.createClient({ port: configuration.session.redis.port
host: configuration.session.redis.host, };
port: configuration.session.redis.port
}); if (configuration.session.redis.password) {
client.on("error", function (err: Error) { options["password"] = configuration.session.redis.password;
console.error("Redis error:", err);
});
redisOptions = {
client: client,
logErrors: true
};
} }
const client = deps.Redis.createClient(options);
client.on("error", function (err: Error) {
console.error("Redis error:", err);
});
redisOptions = {
client: client,
logErrors: true
};
if (redisOptions) { if (redisOptions) {
const RedisStore = deps.ConnectRedis(deps.session); const RedisStore = deps.ConnectRedis(deps.session);

View File

@ -1,6 +1,7 @@
export interface SessionRedisOptions { export interface SessionRedisOptions {
host: string; host: string;
port: number; port: number;
password?: string;
} }
export interface SessionConfiguration { export interface SessionConfiguration {

View File

@ -1,6 +1,10 @@
export interface MongoStorageConfiguration { export interface MongoStorageConfiguration {
url: string; url: string;
database: string; database: string;
auth?: {
username: string;
password: string;
};
} }
export interface LocalStorageConfiguration { export interface LocalStorageConfiguration {
@ -21,5 +25,6 @@ export function complete(configuration: StorageConfiguration): StorageConfigurat
in_memory: true in_memory: true
}; };
} }
return newConfiguration; return newConfiguration;
} }

View File

@ -1,9 +1,11 @@
import Assert = require("assert"); import Assert = require("assert");
import Sinon = require("sinon");
import MongoDB = require("mongodb");
import Bluebird = require("bluebird"); import Bluebird = require("bluebird");
import MongoDB = require("mongodb");
import Sinon = require("sinon");
import { MongoClient } from "./MongoClient"; import { MongoClient } from "./MongoClient";
import { GlobalLoggerStub } from "../../logging/GlobalLoggerStub.spec"; import { GlobalLoggerStub } from "../../logging/GlobalLoggerStub.spec";
import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration";
describe("connectors/mongo/MongoClient", function () { describe("connectors/mongo/MongoClient", function () {
let MongoClientStub: any; let MongoClientStub: any;
@ -11,7 +13,52 @@ describe("connectors/mongo/MongoClient", function () {
let mongoDatabaseStub: any; let mongoDatabaseStub: any;
let logger: GlobalLoggerStub = new GlobalLoggerStub(); 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() { before(function() {
mongoClientStub = { mongoClientStub = {
db: Sinon.stub() db: Sinon.stub()
@ -38,7 +85,7 @@ describe("connectors/mongo/MongoClient", function () {
it("should create a collection", function () { it("should create a collection", function () {
const COLLECTION_NAME = "mycollection"; const COLLECTION_NAME = "mycollection";
const client = new MongoClient("mongo://url", "databasename", logger); const client = new MongoClient(configuration, logger);
mongoDatabaseStub.collection.returns("COL"); mongoDatabaseStub.collection.returns("COL");
return client.collection(COLLECTION_NAME) return client.collection(COLLECTION_NAME)
@ -60,12 +107,12 @@ describe("connectors/mongo/MongoClient", function () {
it("should fail creating the collection", function() { it("should fail creating the collection", function() {
const COLLECTION_NAME = "mycollection"; const COLLECTION_NAME = "mycollection";
const client = new MongoClient("mongo://url", "databasename", logger); const client = new MongoClient(configuration, logger);
mongoDatabaseStub.collection.returns("COL"); mongoDatabaseStub.collection.returns("COL");
return client.collection(COLLECTION_NAME) return client.collection(COLLECTION_NAME)
.then((collection) => Bluebird.reject(new Error("should not be here"))) .then((collection) => Bluebird.reject(new Error("should not be here.")))
.error((err) => Bluebird.resolve()); .catch((err) => Bluebird.resolve());
}); });
}) })
}); });

View File

@ -4,31 +4,47 @@ import { IMongoClient } from "./IMongoClient";
import Bluebird = require("bluebird"); import Bluebird = require("bluebird");
import { AUTHENTICATION_FAILED } from "../../../../../shared/UserMessages"; import { AUTHENTICATION_FAILED } from "../../../../../shared/UserMessages";
import { IGlobalLogger } from "../../logging/IGlobalLogger"; import { IGlobalLogger } from "../../logging/IGlobalLogger";
import { MongoStorageConfiguration } from "../../configuration/schema/StorageConfiguration";
export class MongoClient implements IMongoClient { export class MongoClient implements IMongoClient {
private url: string; private configuration: MongoStorageConfiguration;
private databaseName: string;
private database: MongoDB.Db; private database: MongoDB.Db;
private client: MongoDB.MongoClient; private client: MongoDB.MongoClient;
private logger: IGlobalLogger; private logger: IGlobalLogger;
constructor( constructor(
url: string, configuration: MongoStorageConfiguration,
databaseName: string,
logger: IGlobalLogger) { logger: IGlobalLogger) {
this.url = url; this.configuration = configuration;
this.databaseName = databaseName;
this.logger = logger; this.logger = logger;
} }
connect(): Bluebird<void> { connect(): Bluebird<void> {
const that = this; const that = this;
const connectAsync = Bluebird.promisify(MongoDB.MongoClient.connect); const options: MongoDB.MongoClientOptions = {};
return connectAsync(this.url) 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) { .then(function (client: MongoDB.MongoClient) {
that.database = client.db(that.databaseName); that.database = client.db(that.configuration.database);
that.database.on("close", () => { that.database.on("close", () => {
that.logger.info("[MongoClient] Lost connection."); that.logger.info("[MongoClient] Lost connection.");
}); });

View File

@ -103,7 +103,14 @@ declareNeedsConfiguration("totp_issuer", createCustomTotpIssuerConfiguration);
function registerUser(context: any, username: string) { function registerUser(context: any, username: string) {
let secret: TOTPSecret; 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 collectionFactory = CollectionFactoryFactory.createMongo(mongoClient);
const userDataStore = new UserDataStore(collectionFactory); const userDataStore = new UserDataStore(collectionFactory);