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:
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.
#

View File

@ -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:

View File

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

View File

@ -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"));
});
});

View File

@ -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

View File

@ -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();

View File

@ -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));

View File

@ -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"
}));
});
});

View File

@ -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);

View File

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

View File

@ -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;
}

View File

@ -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());
});
})
});

View File

@ -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<void> {
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.");
});

View File

@ -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);