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
parent
9dab40c2ce
commit
67f84b97c8
|
@ -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.
|
||||
#
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -2,5 +2,6 @@ version: '2'
|
|||
services:
|
||||
redis:
|
||||
image: redis:4.0-alpine
|
||||
command: redis-server --requirepass authelia
|
||||
networks:
|
||||
- example-network
|
||||
|
|
|
@ -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"));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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"
|
||||
}));
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export interface SessionRedisOptions {
|
||||
host: string;
|
||||
port: number;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface SessionConfiguration {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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());
|
||||
});
|
||||
})
|
||||
});
|
||||
|
|
|
@ -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.");
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in New Issue