1
0
mirror of https://github.com/0rangebananaspy/authelia.git synced 2024-09-14 22:47:21 +07:00

Customize the authentication method to be used by a sub-domain

One can now customize the default authentication method for all sub-domains,
i.e., either 'two_factor' or 'basic_auth' and define specific authentication
method per sub-domain.

For example, one can specify that every sub-domain must be authenticated with
two factor except one sub-domain that must be authenticated with basic auth.
This commit is contained in:
Clement Michaud 2017-10-07 18:37:08 +02:00
parent 6940e15ffa
commit c061dbfda4
18 changed files with 340 additions and 174 deletions

View File

@ -47,6 +47,20 @@ ldap:
password: password password: password
# Authentication methods
#
# Authentication methods can be defined per subdomain.
# There are currently two available methods: "basic_auth" and "two_factor"
#
# Note: by default a domain uses "two_factor" method.
#
# Note: 'overriden_methods' is a dictionary where keys must be subdomains and
# values must be one of the two possible methods.
authentication_methods:
default_method: two_factor
per_subdomain_methods:
basicauth.test.local: basic_auth
# Access Control # Access Control
# #
# Access control is a set of rules you can use to restrict user access to certain # Access control is a set of rules you can use to restrict user access to certain

View File

@ -221,7 +221,7 @@ http {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header Content-Length ""; proxy_set_header Content-Length "";
proxy_pass http://authelia/verify?only_basic_auth=true; proxy_pass http://authelia/verify;
} }
location / { location / {
@ -236,7 +236,7 @@ http {
auth_request_set $groups $upstream_http_remote_groups; auth_request_set $groups $upstream_http_remote_groups;
proxy_set_header Remote-Groups $groups; proxy_set_header Remote-Groups $groups;
error_page 401 =302 https://auth.test.local:8080?redirect=$redirect&only_basic_auth=true; error_page 401 =302 https://auth.test.local:8080?redirect=$redirect;
error_page 403 = https://auth.test.local:8080/error/403; error_page 403 = https://auth.test.local:8080/error/403;
} }
} }

View File

@ -0,0 +1,15 @@
import { AuthenticationMethod, AuthenticationMethodsConfiguration } from "./configuration/Configuration";
export class AuthenticationMethodCalculator {
private configuration: AuthenticationMethodsConfiguration;
constructor(config: AuthenticationMethodsConfiguration) {
this.configuration = config;
}
compute(subDomain: string): AuthenticationMethod {
if (subDomain in this.configuration.per_subdomain_methods)
return this.configuration.per_subdomain_methods[subDomain];
return this.configuration.default_method;
}
}

View File

@ -1,5 +1,3 @@
import U2F = require("u2f"); import U2F = require("u2f");
import { IRequestLogger } from "./logging/IRequestLogger"; import { IRequestLogger } from "./logging/IRequestLogger";
@ -14,6 +12,8 @@ import { INotifier } from "./notifiers/INotifier";
import { AuthenticationRegulator } from "./AuthenticationRegulator"; import { AuthenticationRegulator } from "./AuthenticationRegulator";
import Configuration = require("./configuration/Configuration"); import Configuration = require("./configuration/Configuration");
import { AccessController } from "./access_control/AccessController"; import { AccessController } from "./access_control/AccessController";
import { AuthenticationMethodCalculator } from "./AuthenticationMethodCalculator";
export interface ServerVariables { export interface ServerVariables {
@ -29,4 +29,5 @@ export interface ServerVariables {
regulator: AuthenticationRegulator; regulator: AuthenticationRegulator;
config: Configuration.AppConfiguration; config: Configuration.AppConfiguration;
accessController: AccessController; accessController: AccessController;
authenticationMethodsCalculator: AuthenticationMethodCalculator;
} }

View File

@ -33,8 +33,11 @@ import { ICollectionFactory } from "./storage/ICollectionFactory";
import { MongoCollectionFactory } from "./storage/mongo/MongoCollectionFactory"; import { MongoCollectionFactory } from "./storage/mongo/MongoCollectionFactory";
import { MongoConnectorFactory } from "./connectors/mongo/MongoConnectorFactory"; import { MongoConnectorFactory } from "./connectors/mongo/MongoConnectorFactory";
import { IMongoClient } from "./connectors/mongo/IMongoClient"; import { IMongoClient } from "./connectors/mongo/IMongoClient";
import { GlobalDependencies } from "../../types/Dependencies"; import { GlobalDependencies } from "../../types/Dependencies";
import { ServerVariables } from "./ServerVariables"; import { ServerVariables } from "./ServerVariables";
import { AuthenticationMethodCalculator } from "./AuthenticationMethodCalculator";
import express = require("express"); import express = require("express");
@ -78,6 +81,7 @@ export class ServerVariablesHandler {
const accessController = new AccessController(config.access_control, deps.winston); const accessController = new AccessController(config.access_control, deps.winston);
const totpValidator = new TOTPValidator(deps.speakeasy); const totpValidator = new TOTPValidator(deps.speakeasy);
const totpGenerator = new TOTPGenerator(deps.speakeasy); const totpGenerator = new TOTPGenerator(deps.speakeasy);
const authenticationMethodCalculator = new AuthenticationMethodCalculator(config.authentication_methods);
return UserDataStoreFactory.create(config) return UserDataStoreFactory.create(config)
.then(function (userDataStore: UserDataStore) { .then(function (userDataStore: UserDataStore) {
@ -97,6 +101,7 @@ export class ServerVariablesHandler {
totpValidator: totpValidator, totpValidator: totpValidator,
u2f: deps.u2f, u2f: deps.u2f,
userDataStore: userDataStore, userDataStore: userDataStore,
authenticationMethodsCalculator: authenticationMethodCalculator
}; };
app.set(VARIABLES_KEY, variables); app.set(VARIABLES_KEY, variables);
@ -150,4 +155,8 @@ export class ServerVariablesHandler {
static getU2F(app: express.Application): typeof U2F { static getU2F(app: express.Application): typeof U2F {
return (app.get(VARIABLES_KEY) as ServerVariables).u2f; return (app.get(VARIABLES_KEY) as ServerVariables).u2f;
} }
static getAuthenticationMethodCalculator(app: express.Application): AuthenticationMethodCalculator {
return (app.get(VARIABLES_KEY) as ServerVariables).authenticationMethodsCalculator;
}
} }

View File

@ -2,7 +2,7 @@
import { ACLConfiguration, ACLPolicy, ACLRule } from "../configuration/Configuration"; import { ACLConfiguration, ACLPolicy, ACLRule } from "../configuration/Configuration";
import { IAccessController } from "./IAccessController"; import { IAccessController } from "./IAccessController";
import { Winston } from "../../../types/Dependencies"; import { Winston } from "../../../types/Dependencies";
import { DomainMatcher } from "./DomainMatcher"; import { MultipleDomainMatcher } from "./MultipleDomainMatcher";
enum AccessReturn { enum AccessReturn {
@ -17,7 +17,7 @@ function AllowedRule(rule: ACLRule) {
function MatchDomain(actualDomain: string) { function MatchDomain(actualDomain: string) {
return function (rule: ACLRule): boolean { return function (rule: ACLRule): boolean {
return DomainMatcher.match(actualDomain, rule.domain); return MultipleDomainMatcher.match(actualDomain, rule.domain);
}; };
} }

View File

@ -1,12 +0,0 @@
export class DomainMatcher {
static match(domain: string, allowedDomain: string): boolean {
if (allowedDomain.startsWith("*") &&
domain.endsWith(allowedDomain.substr(1))) {
return true;
}
else if (domain == allowedDomain) {
return true;
}
}
}

View File

@ -0,0 +1,12 @@
export class MultipleDomainMatcher {
static match(domain: string, pattern: string): boolean {
if (pattern.startsWith("*") &&
domain.endsWith(pattern.substr(1))) {
return true;
}
else if (domain == pattern) {
return true;
}
}
}

View File

@ -109,6 +109,14 @@ export interface RegulationConfiguration {
ban_time: number; ban_time: number;
} }
declare type AuthenticationMethod = 'two_factor' | 'basic_auth';
declare type AuthenticationMethodPerSubdomain = { [subdomain: string]: AuthenticationMethod }
export interface AuthenticationMethodsConfiguration {
default_method: AuthenticationMethod;
per_subdomain_methods: AuthenticationMethodPerSubdomain;
}
export interface UserConfiguration { export interface UserConfiguration {
port?: number; port?: number;
logs_level?: string; logs_level?: string;
@ -116,6 +124,7 @@ export interface UserConfiguration {
session: SessionCookieConfiguration; session: SessionCookieConfiguration;
storage: StorageConfiguration; storage: StorageConfiguration;
notifier: NotifierConfiguration; notifier: NotifierConfiguration;
authentication_methods?: AuthenticationMethodsConfiguration;
access_control?: ACLConfiguration; access_control?: ACLConfiguration;
regulation: RegulationConfiguration; regulation: RegulationConfiguration;
} }
@ -127,6 +136,7 @@ export interface AppConfiguration {
session: SessionCookieConfiguration; session: SessionCookieConfiguration;
storage: StorageConfiguration; storage: StorageConfiguration;
notifier: NotifierConfiguration; notifier: NotifierConfiguration;
authentication_methods: AuthenticationMethodsConfiguration;
access_control?: ACLConfiguration; access_control?: ACLConfiguration;
regulation: RegulationConfiguration; regulation: RegulationConfiguration;
} }

View File

@ -4,7 +4,7 @@ import {
AppConfiguration, UserConfiguration, NotifierConfiguration, AppConfiguration, UserConfiguration, NotifierConfiguration,
ACLConfiguration, LdapConfiguration, SessionRedisOptions, ACLConfiguration, LdapConfiguration, SessionRedisOptions,
MongoStorageConfiguration, LocalStorageConfiguration, MongoStorageConfiguration, LocalStorageConfiguration,
UserLdapConfiguration UserLdapConfiguration, AuthenticationMethodsConfiguration
} from "./Configuration"; } from "./Configuration";
import Util = require("util"); import Util = require("util");
import { ACLAdapter } from "./adapters/ACLAdapter"; import { ACLAdapter } from "./adapters/ACLAdapter";
@ -55,15 +55,25 @@ function adaptLdapConfiguration(userConfig: UserLdapConfiguration): LdapConfigur
}; };
} }
function adaptAuthenticationMethods(authentication_methods: AuthenticationMethodsConfiguration)
: AuthenticationMethodsConfiguration {
if (!authentication_methods) {
return {
default_method: "two_factor",
per_subdomain_methods: {}
};
}
return authentication_methods;
}
function adaptFromUserConfiguration(userConfiguration: UserConfiguration): AppConfiguration { function adaptFromUserConfiguration(userConfiguration: UserConfiguration): AppConfiguration {
ensure_key_existence(userConfiguration, "ldap"); ensure_key_existence(userConfiguration, "ldap");
// ensure_key_existence(userConfiguration, "ldap.url");
// ensure_key_existence(userConfiguration, "ldap.base_dn");
ensure_key_existence(userConfiguration, "session.secret"); ensure_key_existence(userConfiguration, "session.secret");
ensure_key_existence(userConfiguration, "regulation"); ensure_key_existence(userConfiguration, "regulation");
const port = userConfiguration.port || 8080; const port = userConfiguration.port || 8080;
const ldapConfiguration = adaptLdapConfiguration(userConfiguration.ldap); const ldapConfiguration = adaptLdapConfiguration(userConfiguration.ldap);
const authenticationMethods = adaptAuthenticationMethods(userConfiguration.authentication_methods);
return { return {
port: port, port: port,
@ -81,7 +91,8 @@ function adaptFromUserConfiguration(userConfiguration: UserConfiguration): AppCo
logs_level: get_optional<string>(userConfiguration, "logs_level", "info"), logs_level: get_optional<string>(userConfiguration, "logs_level", "info"),
notifier: ObjectPath.get<object, NotifierConfiguration>(userConfiguration, "notifier"), notifier: ObjectPath.get<object, NotifierConfiguration>(userConfiguration, "notifier"),
access_control: ACLAdapter.adapt(userConfiguration.access_control), access_control: ACLAdapter.adapt(userConfiguration.access_control),
regulation: userConfiguration.regulation regulation: userConfiguration.regulation,
authentication_methods: authenticationMethods
}; };
} }

View File

@ -11,6 +11,7 @@ import ErrorReplies = require("../../ErrorReplies");
import { ServerVariablesHandler } from "../../ServerVariablesHandler"; import { ServerVariablesHandler } from "../../ServerVariablesHandler";
import AuthenticationSession = require("../../AuthenticationSession"); import AuthenticationSession = require("../../AuthenticationSession");
import Constants = require("../../../../../shared/constants"); import Constants = require("../../../../../shared/constants");
import { DomainExtractor } from "../../utils/DomainExtractor";
export default function (req: express.Request, res: express.Response): BluebirdPromise<void> { export default function (req: express.Request, res: express.Response): BluebirdPromise<void> {
const username: string = req.body.username; const username: string = req.body.username;
@ -28,6 +29,8 @@ export default function (req: express.Request, res: express.Response): BluebirdP
const regulator = ServerVariablesHandler.getAuthenticationRegulator(req.app); const regulator = ServerVariablesHandler.getAuthenticationRegulator(req.app);
const accessController = ServerVariablesHandler.getAccessController(req.app); const accessController = ServerVariablesHandler.getAccessController(req.app);
const authenticationMethodsCalculator =
ServerVariablesHandler.getAuthenticationMethodCalculator(req.app);
let authSession: AuthenticationSession.AuthenticationSession; let authSession: AuthenticationSession.AuthenticationSession;
logger.info(req, "Starting authentication of user \"%s\"", username); logger.info(req, "Starting authentication of user \"%s\"", username);
@ -46,10 +49,12 @@ export default function (req: express.Request, res: express.Response): BluebirdP
authSession.userid = username; authSession.userid = username;
authSession.first_factor = true; authSession.first_factor = true;
const redirectUrl = req.query[Constants.REDIRECT_QUERY_PARAM]; const redirectUrl = req.query[Constants.REDIRECT_QUERY_PARAM];
const onlyBasicAuth = req.query[Constants.ONLY_BASIC_AUTH_QUERY_PARAM] === "true";
const emails: string[] = groupsAndEmails.emails; const emails: string[] = groupsAndEmails.emails;
const groups: string[] = groupsAndEmails.groups; const groups: string[] = groupsAndEmails.groups;
const redirectHost: string = DomainExtractor.fromUrl(redirectUrl);
const authMethod = authenticationMethodsCalculator.compute(redirectHost);
logger.debug(req, "Authentication method for \"%s\" is \"%s\"", redirectHost, authMethod);
if (!emails || emails.length <= 0) { if (!emails || emails.length <= 0) {
const errMessage = "No emails found. The user should have at least one email address to reset password."; const errMessage = "No emails found. The user should have at least one email address to reset password.";
@ -63,13 +68,13 @@ export default function (req: express.Request, res: express.Response): BluebirdP
logger.debug(req, "Mark successful authentication to regulator."); logger.debug(req, "Mark successful authentication to regulator.");
regulator.mark(username, true); regulator.mark(username, true);
if (onlyBasicAuth) { if (authMethod == "basic_auth") {
res.send({ res.send({
redirect: redirectUrl redirect: redirectUrl
}); });
logger.debug(req, "Redirect to '%s'", redirectUrl); logger.debug(req, "Redirect to '%s'", redirectUrl);
} }
else { else if (authMethod == "two_factor") {
let newRedirectUrl = Endpoint.SECOND_FACTOR_GET; let newRedirectUrl = Endpoint.SECOND_FACTOR_GET;
if (redirectUrl !== "undefined") { if (redirectUrl !== "undefined") {
newRedirectUrl += "?redirect=" + encodeURIComponent(redirectUrl); newRedirectUrl += "?redirect=" + encodeURIComponent(redirectUrl);
@ -79,6 +84,9 @@ export default function (req: express.Request, res: express.Response): BluebirdP
redirect: newRedirectUrl redirect: newRedirectUrl
}); });
} }
else {
return BluebirdPromise.reject(new Error("Unknown authentication method for this domain."));
}
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}) })
.catch(exceptions.LdapSearchError, ErrorReplies.replyWithError500(req, res, logger)) .catch(exceptions.LdapSearchError, ErrorReplies.replyWithError500(req, res, logger))

View File

@ -10,6 +10,7 @@ import { ServerVariablesHandler } from "../../ServerVariablesHandler";
import AuthenticationSession = require("../../AuthenticationSession"); import AuthenticationSession = require("../../AuthenticationSession");
import Constants = require("../../../../../shared/constants"); import Constants = require("../../../../../shared/constants");
import Util = require("util"); import Util = require("util");
import { DomainExtractor } from "../../utils/DomainExtractor";
const FIRST_FACTOR_NOT_VALIDATED_MESSAGE = "First factor not yet validated"; const FIRST_FACTOR_NOT_VALIDATED_MESSAGE = "First factor not yet validated";
const SECOND_FACTOR_NOT_VALIDATED_MESSAGE = "Second factor not yet validated"; const SECOND_FACTOR_NOT_VALIDATED_MESSAGE = "Second factor not yet validated";
@ -17,6 +18,7 @@ const SECOND_FACTOR_NOT_VALIDATED_MESSAGE = "Second factor not yet validated";
function verify_filter(req: express.Request, res: express.Response): BluebirdPromise<void> { function verify_filter(req: express.Request, res: express.Response): BluebirdPromise<void> {
const logger = ServerVariablesHandler.getLogger(req.app); const logger = ServerVariablesHandler.getLogger(req.app);
const accessController = ServerVariablesHandler.getAccessController(req.app); const accessController = ServerVariablesHandler.getAccessController(req.app);
const authenticationMethodsCalculator = ServerVariablesHandler.getAuthenticationMethodCalculator(req.app);
return AuthenticationSession.get(req) return AuthenticationSession.get(req)
.then(function (authSession) { .then(function (authSession) {
@ -29,12 +31,11 @@ function verify_filter(req: express.Request, res: express.Response): BluebirdPro
return BluebirdPromise.reject( return BluebirdPromise.reject(
new exceptions.AccessDeniedError(FIRST_FACTOR_NOT_VALIDATED_MESSAGE)); new exceptions.AccessDeniedError(FIRST_FACTOR_NOT_VALIDATED_MESSAGE));
const onlyBasicAuth = req.query[Constants.ONLY_BASIC_AUTH_QUERY_PARAM] === "true";
const host = objectPath.get<express.Request, string>(req, "headers.host"); const host = objectPath.get<express.Request, string>(req, "headers.host");
const path = objectPath.get<express.Request, string>(req, "headers.x-original-uri"); const path = objectPath.get<express.Request, string>(req, "headers.x-original-uri");
const domain = host.split(":")[0]; const domain = DomainExtractor.fromHostHeader(host);
const authenticationMethod = authenticationMethodsCalculator.compute(domain);
logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", domain, path, logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", domain, path,
username, groups.join(",")); username, groups.join(","));
@ -47,7 +48,7 @@ function verify_filter(req: express.Request, res: express.Response): BluebirdPro
new exceptions.DomainAccessDenied(Util.format("User '%s' does not have access to '%'", new exceptions.DomainAccessDenied(Util.format("User '%s' does not have access to '%'",
username, domain))); username, domain)));
if (!onlyBasicAuth && !authSession.second_factor) if (authenticationMethod == "two_factor" && !authSession.second_factor)
return BluebirdPromise.reject( return BluebirdPromise.reject(
new exceptions.AccessDeniedError(SECOND_FACTOR_NOT_VALIDATED_MESSAGE)); new exceptions.AccessDeniedError(SECOND_FACTOR_NOT_VALIDATED_MESSAGE));

View File

@ -0,0 +1,9 @@
export class DomainExtractor {
static fromUrl(url: string): string {
return url.match(/https?:\/\/([^\/:]+).*/)[1];
}
static fromHostHeader(host: string): string {
return host.split(":")[0];
}
}

View File

@ -0,0 +1,31 @@
import { AuthenticationMethodCalculator } from "../src/lib/AuthenticationMethodCalculator";
import { AuthenticationMethodsConfiguration } from "../src/lib/configuration/Configuration";
import Assert = require("assert");
describe("test authentication method calculator", function() {
it("should return default method when sub domain not overriden", function() {
const options1: AuthenticationMethodsConfiguration = {
default_method: "two_factor",
per_subdomain_methods: {}
};
const options2: AuthenticationMethodsConfiguration = {
default_method: "basic_auth",
per_subdomain_methods: {}
};
const calculator1 = new AuthenticationMethodCalculator(options1);
const calculator2 = new AuthenticationMethodCalculator(options2);
Assert.equal(calculator1.compute("www.example.com"), "two_factor");
Assert.equal(calculator2.compute("www.example.com"), "basic_auth");
});
it("should return overridden method when sub domain method is defined", function() {
const options1: AuthenticationMethodsConfiguration = {
default_method: "two_factor",
per_subdomain_methods: {
"www.example.com": "basic_auth"
}
};
const calculator1 = new AuthenticationMethodCalculator(options1);
Assert.equal(calculator1.compute("www.example.com"), "basic_auth");
});
});

View File

@ -8,155 +8,163 @@ import Sinon = require("sinon");
import Assert = require("assert"); import Assert = require("assert");
describe("test session configuration builder", function () { describe("test session configuration builder", function () {
it("should return session options without redis options", function () { it("should return session options without redis options", function () {
const configuration: AppConfiguration = { const configuration: AppConfiguration = {
access_control: { access_control: {
default_policy: "deny", default_policy: "deny",
any: [], any: [],
users: {}, users: {},
groups: {} groups: {}
}, },
ldap: { ldap: {
url: "ldap://ldap", url: "ldap://ldap",
user: "user", user: "user",
password: "password", password: "password",
groups_dn: "ou=groups,dc=example,dc=com", groups_dn: "ou=groups,dc=example,dc=com",
users_dn: "ou=users,dc=example,dc=com", users_dn: "ou=users,dc=example,dc=com",
group_name_attribute: "", group_name_attribute: "",
groups_filter: "", groups_filter: "",
mail_attribute: "", mail_attribute: "",
users_filter: "" users_filter: ""
}, },
logs_level: "debug", logs_level: "debug",
notifier: { notifier: {
filesystem: { filesystem: {
filename: "/test" filename: "/test"
} }
}, },
port: 8080, port: 8080,
session: { session: {
domain: "example.com", domain: "example.com",
expiration: 3600, expiration: 3600,
secret: "secret" secret: "secret"
}, },
regulation: { regulation: {
max_retries: 3, max_retries: 3,
ban_time: 5 * 60, ban_time: 5 * 60,
find_time: 5 * 60 find_time: 5 * 60
}, },
storage: { storage: {
local: { local: {
in_memory: true 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,
dovehash: Sinon.spy() as any dovehash: Sinon.spy() as any
}; };
const options = SessionConfigurationBuilder.build(configuration, deps); const options = SessionConfigurationBuilder.build(configuration, deps);
const expectedOptions = { const expectedOptions = {
secret: "secret", secret: "secret",
resave: false, resave: false,
saveUninitialized: true, saveUninitialized: true,
cookie: { cookie: {
secure: false, secure: false,
maxAge: 3600, maxAge: 3600,
domain: "example.com" domain: "example.com"
} }
}; };
Assert.deepEqual(expectedOptions, options); Assert.deepEqual(expectedOptions, options);
}); });
it("should return session options with redis options", function () { it("should return session options with redis options", function () {
const configuration: AppConfiguration = { const configuration: AppConfiguration = {
access_control: { access_control: {
default_policy: "deny", default_policy: "deny",
any: [], any: [],
users: {}, users: {},
groups: {} groups: {}
}, },
ldap: { ldap: {
url: "ldap://ldap", url: "ldap://ldap",
user: "user", user: "user",
password: "password", password: "password",
groups_dn: "ou=groups,dc=example,dc=com", groups_dn: "ou=groups,dc=example,dc=com",
users_dn: "ou=users,dc=example,dc=com", users_dn: "ou=users,dc=example,dc=com",
group_name_attribute: "", group_name_attribute: "",
groups_filter: "", groups_filter: "",
mail_attribute: "", mail_attribute: "",
users_filter: "" users_filter: ""
}, },
logs_level: "debug", logs_level: "debug",
notifier: { notifier: {
filesystem: { filesystem: {
filename: "/test" filename: "/test"
} }
}, },
port: 8080, port: 8080,
session: { session: {
domain: "example.com", domain: "example.com",
expiration: 3600, expiration: 3600,
secret: "secret", secret: "secret",
redis: { redis: {
host: "redis.example.com", host: "redis.example.com",
port: 6379 port: 6379
} }
}, },
regulation: { regulation: {
max_retries: 3, max_retries: 3,
ban_time: 5 * 60, ban_time: 5 * 60,
find_time: 5 * 60 find_time: 5 * 60
}, },
storage: { storage: {
local: { local: {
in_memory: true in_memory: true
} }
} },
}; authentication_methods: {
default_method: "two_factor",
per_subdomain_methods: {}
}
};
const RedisStoreMock = Sinon.spy(); const RedisStoreMock = Sinon.spy();
const deps: GlobalDependencies = { const deps: GlobalDependencies = {
ConnectRedis: Sinon.stub().returns(RedisStoreMock) as any, ConnectRedis: Sinon.stub().returns(RedisStoreMock) 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,
dovehash: Sinon.spy() as any dovehash: Sinon.spy() as any
}; };
const options = SessionConfigurationBuilder.build(configuration, deps); const options = SessionConfigurationBuilder.build(configuration, deps);
const expectedOptions: ExpressSession.SessionOptions = { const expectedOptions: ExpressSession.SessionOptions = {
secret: "secret", secret: "secret",
resave: false, resave: false,
saveUninitialized: true, saveUninitialized: true,
cookie: { cookie: {
secure: false, secure: false,
maxAge: 3600, maxAge: 3600,
domain: "example.com" domain: "example.com"
}, },
store: Sinon.match.object as any store: Sinon.match.object as any
}; };
Assert((deps.ConnectRedis as Sinon.SinonStub).calledWith(deps.session)); Assert((deps.ConnectRedis as Sinon.SinonStub).calledWith(deps.session));
Assert.equal(options.secret, expectedOptions.secret); Assert.equal(options.secret, expectedOptions.secret);
Assert.equal(options.resave, expectedOptions.resave); Assert.equal(options.resave, expectedOptions.resave);
Assert.equal(options.saveUninitialized, expectedOptions.saveUninitialized); Assert.equal(options.saveUninitialized, expectedOptions.saveUninitialized);
Assert.deepEqual(options.cookie, expectedOptions.cookie); Assert.deepEqual(options.cookie, expectedOptions.cookie);
Assert(options.store != undefined); Assert(options.store != undefined);
}); });
}); });

View File

@ -2,6 +2,7 @@ import Sinon = require("sinon");
import express = require("express"); import express = require("express");
import { RequestLoggerStub } from "./RequestLoggerStub"; import { RequestLoggerStub } from "./RequestLoggerStub";
import { UserDataStoreStub } from "./storage/UserDataStoreStub"; import { UserDataStoreStub } from "./storage/UserDataStoreStub";
import { AuthenticationMethodCalculator } from "../../src/lib/AuthenticationMethodCalculator";
import { VARIABLES_KEY } from "../../src/lib/ServerVariablesHandler"; import { VARIABLES_KEY } from "../../src/lib/ServerVariablesHandler";
export interface ServerVariablesMock { export interface ServerVariablesMock {
@ -17,6 +18,7 @@ export interface ServerVariablesMock {
regulator: any; regulator: any;
config: any; config: any;
accessController: any; accessController: any;
authenticationMethodsCalculator: any;
} }
@ -33,7 +35,11 @@ export function mock(app: express.Application): ServerVariablesMock {
totpGenerator: Sinon.stub(), totpGenerator: Sinon.stub(),
totpValidator: Sinon.stub(), totpValidator: Sinon.stub(),
u2f: Sinon.stub(), u2f: Sinon.stub(),
userDataStore: new UserDataStoreStub() userDataStore: new UserDataStoreStub(),
authenticationMethodsCalculator: new AuthenticationMethodCalculator({
default_method: "two_factor",
per_subdomain_methods: {}
})
}; };
app.get = Sinon.stub().withArgs(VARIABLES_KEY).returns(mocks); app.get = Sinon.stub().withArgs(VARIABLES_KEY).returns(mocks);
return mocks; return mocks;

View File

@ -2,6 +2,8 @@
import Assert = require("assert"); import Assert = require("assert");
import VerifyGet = require("../../../src/lib/routes/verify/get"); import VerifyGet = require("../../../src/lib/routes/verify/get");
import AuthenticationSession = require("../../../src/lib/AuthenticationSession"); import AuthenticationSession = require("../../../src/lib/AuthenticationSession");
import { AuthenticationMethodCalculator } from "../../../src/lib/AuthenticationMethodCalculator";
import { AuthenticationMethodsConfiguration } from "../../../src/lib/configuration/Configuration";
import Sinon = require("sinon"); import Sinon = require("sinon");
import winston = require("winston"); import winston = require("winston");
@ -17,6 +19,7 @@ describe("test authentication token verification", function () {
let req: ExpressMock.RequestMock; let req: ExpressMock.RequestMock;
let res: ExpressMock.ResponseMock; let res: ExpressMock.ResponseMock;
let accessController: AccessControllerStub; let accessController: AccessControllerStub;
let mocks: any;
beforeEach(function () { beforeEach(function () {
accessController = new AccessControllerStub(); accessController = new AccessControllerStub();
@ -34,9 +37,16 @@ describe("test authentication token verification", function () {
AuthenticationSession.reset(req as any); AuthenticationSession.reset(req as any);
req.headers = {}; req.headers = {};
req.headers.host = "secret.example.com"; req.headers.host = "secret.example.com";
const mocks = ServerVariablesMock.mock(req.app); mocks = ServerVariablesMock.mock(req.app);
mocks.config = {} as any; mocks.config = {} as any;
mocks.accessController = accessController as any; mocks.accessController = accessController as any;
const options: AuthenticationMethodsConfiguration = {
default_method: "two_factor",
per_subdomain_methods: {
"redirect.url": "basic_auth"
}
};
mocks.authenticationMethodsCalculator = new AuthenticationMethodCalculator(options);
}); });
it("should be already authenticated", function () { it("should be already authenticated", function () {
@ -153,9 +163,9 @@ describe("test authentication token verification", function () {
describe("given user tries to access a basic auth endpoint", function () { describe("given user tries to access a basic auth endpoint", function () {
beforeEach(function () { beforeEach(function () {
req.query = { req.query = {
redirect: "http://redirect.url", redirect: "http://redirect.url"
only_basic_auth: "true"
}; };
req.headers["host"] = "redirect.url";
}); });
it("should be authenticated when first factor is validated and not second factor", function () { it("should be authenticated when first factor is validated and not second factor", function () {

View File

@ -0,0 +1,33 @@
import { DomainExtractor } from "../../src/lib/utils/DomainExtractor";
import Assert = require("assert");
describe("test DomainExtractor", function () {
describe("test fromUrl", function () {
it("should return domain from https url", function () {
const domain = DomainExtractor.fromUrl("https://www.example.com/test/abc");
Assert.equal(domain, "www.example.com");
});
it("should return domain from http url", function () {
const domain = DomainExtractor.fromUrl("http://www.example.com/test/abc");
Assert.equal(domain, "www.example.com");
});
it("should return domain when url contains port", function () {
const domain = DomainExtractor.fromUrl("https://www.example.com:8080/test/abc");
Assert.equal(domain, "www.example.com");
});
});
describe("test fromHostHeader", function () {
it("should return domain when default port is used", function () {
const domain = DomainExtractor.fromHostHeader("www.example.com");
Assert.equal(domain, "www.example.com");
});
it("should return domain when non default port is used", function () {
const domain = DomainExtractor.fromHostHeader("www.example.com:8080");
Assert.equal(domain, "www.example.com");
});
});
});