Merge pull request #131 from clems4ever/disable-second-factor

Allow basic authentication in configuration
pull/140/head
Clément Michaud 2017-10-09 23:27:36 +02:00 committed by GitHub
commit d5035b8704
38 changed files with 569 additions and 211 deletions

View File

@ -53,7 +53,7 @@ module.exports = function (grunt) {
}, },
"include-minified-script": { "include-minified-script": {
cmd: "sed", cmd: "sed",
args: ["-i", "s/authelia\.min/authelia/", `${buildDir}/server/src/views/layout/layout.pug`] args: ["-i", "s/authelia\.js/authelia.min.js/", `${buildDir}/server/src/views/layout/layout.pug`]
} }
}, },
copy: { copy: {

View File

@ -22,7 +22,8 @@ used in production to secure internal services in a small docker swarm cluster.
3. [Second factor with U2F security keys](#second-factor-with-u2f-security-keys) 3. [Second factor with U2F security keys](#second-factor-with-u2f-security-keys)
4. [Password reset](#password-reset) 4. [Password reset](#password-reset)
5. [Access control](#access-control) 5. [Access control](#access-control)
6. [Session management with Redis](#session-management-with-redis) 6. [Basic authentication](#basic-authentication)
7. [Session management with Redis](#session-management-with-redis)
4. [Documentation](#documentation) 4. [Documentation](#documentation)
1. [Authelia configuration](#authelia-configuration) 1. [Authelia configuration](#authelia-configuration)
1. [API documentation](#api-documentation) 1. [API documentation](#api-documentation)
@ -37,6 +38,7 @@ used in production to secure internal services in a small docker swarm cluster.
as 2nd factor. as 2nd factor.
* Password reset with identity verification by sending links to user email * Password reset with identity verification by sending links to user email
address. address.
* Two-factor and basic authentication methods available.
* Access restriction after too many authentication attempts. * Access restriction after too many authentication attempts.
* Session management using Redis key/value store. * Session management using Redis key/value store.
* User-defined access control per subdomain and resource. * User-defined access control per subdomain and resource.
@ -187,6 +189,11 @@ user access to some resources and subdomains. Those rules are defined and fully
in the configuration file. They can apply to users, groups or everyone. in the configuration file. They can apply to users, groups or everyone.
Check out [config.template.yml] to see how they are defined. Check out [config.template.yml] to see how they are defined.
### Basic Authentication
Authelia allows you to customize the authentication method to use for each sub-domain.
The supported methods are either "basic_auth" and "two_factor".
Please see [config.template.yml] to see an example of configuration.
### Session management with Redis ### Session management with Redis
When your users authenticate against Authelia, sessions are stored in a Redis key/value store. You can specify your own Redis instance in [config.template.yml]. When your users authenticate against Authelia, sessions are stored in a Redis key/value store. You can specify your own Redis instance in [config.template.yml].

View File

@ -2,12 +2,19 @@
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import Endpoints = require("../../../../shared/api"); import Endpoints = require("../../../../shared/api");
import Constants = require("../../../../shared/constants"); import Constants = require("../../../../shared/constants");
import Util = require("util");
export function validate(username: string, password: string, export function validate(username: string, password: string,
redirectUrl: string, onlyBasicAuth: boolean, $: JQueryStatic): BluebirdPromise<string> { redirectUrl: string, $: JQueryStatic): BluebirdPromise<string> {
return new BluebirdPromise<string>(function (resolve, reject) { return new BluebirdPromise<string>(function (resolve, reject) {
const url = Endpoints.FIRST_FACTOR_POST + "?" + Constants.REDIRECT_QUERY_PARAM + "=" + redirectUrl let url: string;
+ "&" + Constants.ONLY_BASIC_AUTH_QUERY_PARAM + "=" + onlyBasicAuth; if (redirectUrl != undefined) {
const redirectParam = Util.format("%s=%s", Constants.REDIRECT_QUERY_PARAM, redirectUrl);
url = Util.format("%s?%s", Endpoints.FIRST_FACTOR_POST, redirectParam);
}
else {
url = Util.format("%s", Endpoints.FIRST_FACTOR_POST);
}
$.ajax({ $.ajax({
method: "POST", method: "POST",

View File

@ -17,8 +17,7 @@ export default function (window: Window, $: JQueryStatic,
$(UISelectors.PASSWORD_FIELD_ID).val(""); $(UISelectors.PASSWORD_FIELD_ID).val("");
const redirectUrl = QueryParametersRetriever.get(Constants.REDIRECT_QUERY_PARAM); const redirectUrl = QueryParametersRetriever.get(Constants.REDIRECT_QUERY_PARAM);
const onlyBasicAuth = QueryParametersRetriever.get(Constants.ONLY_BASIC_AUTH_QUERY_PARAM) ? true : false; firstFactorValidator.validate(username, password, redirectUrl, $)
firstFactorValidator.validate(username, password, redirectUrl, onlyBasicAuth, $)
.then(onFirstFactorSuccess, onFirstFactorFailure); .then(onFirstFactorSuccess, onFirstFactorFailure);
return false; return false;
} }

View File

@ -13,7 +13,7 @@ describe("test FirstFactorValidator", function () {
const jqueryMock = JQueryMock.JQueryMock(); const jqueryMock = JQueryMock.JQueryMock();
jqueryMock.jquery.ajax.returns(postPromise); jqueryMock.jquery.ajax.returns(postPromise);
return FirstFactorValidator.validate("username", "password", "http://redirect", false, jqueryMock.jquery as any); return FirstFactorValidator.validate("username", "password", "http://redirect", jqueryMock.jquery as any);
}); });
function should_fail_first_factor_validation(errorMessage: string) { function should_fail_first_factor_validation(errorMessage: string) {
@ -27,7 +27,7 @@ describe("test FirstFactorValidator", function () {
const jqueryMock = JQueryMock.JQueryMock(); const jqueryMock = JQueryMock.JQueryMock();
jqueryMock.jquery.ajax.returns(postPromise); jqueryMock.jquery.ajax.returns(postPromise);
return FirstFactorValidator.validate("username", "password", "http://redirect", false, jqueryMock.jquery as any) return FirstFactorValidator.validate("username", "password", "http://redirect", jqueryMock.jquery as any)
.then(function () { .then(function () {
return BluebirdPromise.reject(new Error("First factor validation successfully finished while it should have not.")); return BluebirdPromise.reject(new Error("First factor validation successfully finished while it should have not."));
}, function (err: Error) { }, function (err: Error) {

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

@ -29,6 +29,9 @@ import ResetPasswordRequestPost = require("./routes/password-reset/request/get")
import Error401Get = require("./routes/error/401/get"); import Error401Get = require("./routes/error/401/get");
import Error403Get = require("./routes/error/403/get"); import Error403Get = require("./routes/error/403/get");
import Error404Get = require("./routes/error/404/get"); import Error404Get = require("./routes/error/404/get");
import LoggedIn = require("./routes/loggedin/get");
import { ServerVariablesHandler } from "./ServerVariablesHandler"; import { ServerVariablesHandler } from "./ServerVariablesHandler";
import Endpoints = require("../../../shared/api"); import Endpoints = require("../../../shared/api");
@ -72,5 +75,6 @@ export class RestApi {
app.get(Endpoints.ERROR_401_GET, withLog(Error401Get.default)); app.get(Endpoints.ERROR_401_GET, withLog(Error401Get.default));
app.get(Endpoints.ERROR_403_GET, withLog(Error403Get.default)); app.get(Endpoints.ERROR_403_GET, withLog(Error403Get.default));
app.get(Endpoints.ERROR_404_GET, withLog(Error404Get.default)); app.get(Endpoints.ERROR_404_GET, withLog(Error404Get.default));
app.get(Endpoints.LOGGED_IN, withLog(LoggedIn.default));
} }
} }

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

@ -8,6 +8,7 @@ import {
} from "./Configuration"; } from "./Configuration";
import Util = require("util"); import Util = require("util");
import { ACLAdapter } from "./adapters/ACLAdapter"; import { ACLAdapter } from "./adapters/ACLAdapter";
import { AuthenticationMethodsAdapter } from "./adapters/AuthenticationMethodsAdapter";
const LDAP_URL_ENV_VARIABLE = "LDAP_URL"; const LDAP_URL_ENV_VARIABLE = "LDAP_URL";
@ -55,15 +56,16 @@ function adaptLdapConfiguration(userConfig: UserLdapConfiguration): LdapConfigur
}; };
} }
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 = AuthenticationMethodsAdapter
.adapt(userConfiguration.authentication_methods);
return { return {
port: port, port: port,
@ -81,7 +83,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

@ -1,8 +1,5 @@
import { ACLConfiguration } from "../Configuration"; import { ACLConfiguration } from "../Configuration";
import { ObjectCloner } from "../../utils/ObjectCloner";
function clone(obj: any): any {
return JSON.parse(JSON.stringify(obj));
}
const DEFAULT_POLICY = "deny"; const DEFAULT_POLICY = "deny";
@ -32,7 +29,7 @@ export class ACLAdapter {
static adapt(configuration: ACLConfiguration): ACLConfiguration { static adapt(configuration: ACLConfiguration): ACLConfiguration {
if (!configuration) return; if (!configuration) return;
const newConfiguration: ACLConfiguration = clone(configuration); const newConfiguration: ACLConfiguration = ObjectCloner.clone(configuration);
adaptDefaultPolicy(newConfiguration); adaptDefaultPolicy(newConfiguration);
adaptAny(newConfiguration); adaptAny(newConfiguration);
adaptGroups(newConfiguration); adaptGroups(newConfiguration);

View File

@ -0,0 +1,30 @@
import { AuthenticationMethodsConfiguration } from "../Configuration";
import { ObjectCloner } from "../../utils/ObjectCloner";
function clone(obj: any): any {
return JSON.parse(JSON.stringify(obj));
}
export class AuthenticationMethodsAdapter {
static adapt(authentication_methods: AuthenticationMethodsConfiguration)
: AuthenticationMethodsConfiguration {
if (!authentication_methods) {
return {
default_method: "two_factor",
per_subdomain_methods: {}
};
}
const newAuthMethods: AuthenticationMethodsConfiguration
= ObjectCloner.clone(authentication_methods);
if (!newAuthMethods.default_method)
newAuthMethods.default_method = "two_factor";
if (!newAuthMethods.per_subdomain_methods ||
newAuthMethods.per_subdomain_methods.constructor !== Object)
newAuthMethods.per_subdomain_methods = {};
return newAuthMethods;
}
}

View File

@ -6,11 +6,52 @@ import Endpoints = require("../../../../../shared/api");
import AuthenticationValidator = require("../../AuthenticationValidator"); import AuthenticationValidator = require("../../AuthenticationValidator");
import { ServerVariablesHandler } from "../../ServerVariablesHandler"; import { ServerVariablesHandler } from "../../ServerVariablesHandler";
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import AuthenticationSession = require("../../AuthenticationSession");
import Constants = require("../../../../../shared/constants");
import Util = require("util");
export default function (req: express.Request, res: express.Response): BluebirdPromise<void> { function getRedirectParam(req: express.Request) {
return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined"
? req.query[Constants.REDIRECT_QUERY_PARAM]
: undefined;
}
function redirectToSecondFactorPage(req: express.Request, res: express.Response) {
const redirectUrl = getRedirectParam(req);
if (!redirectUrl)
res.redirect(Endpoints.SECOND_FACTOR_GET);
else
res.redirect(Util.format("%s?redirect=%s", Endpoints.SECOND_FACTOR_GET,
encodeURIComponent(redirectUrl)));
}
function redirectToService(req: express.Request, res: express.Response) {
const redirectUrl = getRedirectParam(req);
if (!redirectUrl)
res.redirect(Endpoints.LOGGED_IN);
else
res.redirect(redirectUrl);
}
function renderFirstFactor(res: express.Response) {
res.render("firstfactor", { res.render("firstfactor", {
first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST, first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST,
reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET
}); });
return BluebirdPromise.resolve(); }
export default function (req: express.Request, res: express.Response): BluebirdPromise<void> {
return AuthenticationSession.get(req)
.then(function (authSession) {
if (authSession.first_factor) {
if (authSession.second_factor)
redirectToService(req, res);
else
redirectToSecondFactorPage(req, res);
return BluebirdPromise.resolve();
}
renderFirstFactor(res);
return BluebirdPromise.resolve();
});
} }

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);
@ -45,11 +48,16 @@ export default function (req: express.Request, res: express.Response): BluebirdP
JSON.stringify(groupsAndEmails)); JSON.stringify(groupsAndEmails));
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] !== "undefined"
const onlyBasicAuth = req.query[Constants.ONLY_BASIC_AUTH_QUERY_PARAM] === "true"; // Fuck, don't know why it is a string!
? req.query[Constants.REDIRECT_QUERY_PARAM]
: undefined;
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,22 +71,26 @@ 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) {
newRedirectUrl += "?redirect=" + encodeURIComponent(redirectUrl); newRedirectUrl += "?" + Constants.REDIRECT_QUERY_PARAM + "="
+ encodeURIComponent(redirectUrl);
} }
logger.debug(req, "Redirect to '%s'", newRedirectUrl, typeof redirectUrl); logger.debug(req, "Redirect to '%s'", newRedirectUrl, typeof redirectUrl);
res.send({ res.send({
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

@ -0,0 +1,8 @@
import Express = require("express");
import Endpoints = require("../../../../../shared/api");
export default function(req: Express.Request, res: Express.Response) {
res.render("already-logged-in", {
logout_endpoint: Endpoints.LOGOUT_GET
});
}

View File

@ -4,15 +4,25 @@ import Endpoints = require("../../../../../shared/api");
import FirstFactorBlocker = require("../FirstFactorBlocker"); import FirstFactorBlocker = require("../FirstFactorBlocker");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import { ServerVariablesHandler } from "../../ServerVariablesHandler"; import { ServerVariablesHandler } from "../../ServerVariablesHandler";
import AuthenticationSession = require("../../AuthenticationSession");
const TEMPLATE_NAME = "secondfactor"; const TEMPLATE_NAME = "secondfactor";
export default FirstFactorBlocker.default(handler); export default FirstFactorBlocker.default(handler);
function handler(req: Express.Request, res: Express.Response): BluebirdPromise<void> { function handler(req: Express.Request, res: Express.Response): BluebirdPromise<void> {
return AuthenticationSession.get(req)
.then(function (authSession) {
if (authSession.first_factor && authSession.second_factor) {
res.redirect(Endpoints.LOGGED_IN);
return BluebirdPromise.resolve();
}
res.render(TEMPLATE_NAME, { res.render(TEMPLATE_NAME, {
username: authSession.userid,
totp_identity_start_endpoint: Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, totp_identity_start_endpoint: Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET,
u2f_identity_start_endpoint: Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET u2f_identity_start_endpoint: Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET
}); });
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
});
} }

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,11 @@
export class DomainExtractor {
static fromUrl(url: string): string {
if (!url) return "";
return url.match(/https?:\/\/([^\/:]+).*/)[1];
}
static fromHostHeader(host: string): string {
if (!host) return "";
return host.split(":")[0];
}
}

View File

@ -0,0 +1,6 @@
export class ObjectCloner {
static clone(obj: any): any {
return JSON.parse(JSON.stringify(obj));
}
}

View File

@ -26,5 +26,5 @@ html
</div> </div>
</div> </div>
</div> </div>
script(src="/js/authelia.min.js") script(src="/js/authelia.js")
block entrypoint block entrypoint

View File

@ -5,6 +5,7 @@ block form-header
<img class="header-img" src="../img/padlock.png" alt=""> <img class="header-img" src="../img/padlock.png" alt="">
block content block content
p Hi <b>#{username}</b>, please complete second factor or <a href="/logout">logout</a>.
<div class="notification notification-totp"></div> <div class="notification notification-totp"></div>
<form class="form-signin totp"> <form class="form-signin totp">
<div class="form-inputs"> <div class="form-inputs">

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

@ -48,6 +48,10 @@ describe("test session configuration builder", function () {
local: { local: {
in_memory: true in_memory: true
} }
},
authentication_methods: {
default_method: "two_factor",
per_subdomain_methods: {}
} }
}; };
@ -122,6 +126,10 @@ describe("test session configuration builder", function () {
local: { local: {
in_memory: true in_memory: true
} }
},
authentication_methods: {
default_method: "two_factor",
per_subdomain_methods: {}
} }
}; };

View File

@ -0,0 +1,59 @@
import { AuthenticationMethodsAdapter } from "../../../src/lib/configuration/adapters/AuthenticationMethodsAdapter";
import Assert = require("assert");
describe("test authentication methods configuration adapter", function () {
describe("no authentication methods defined", function () {
it("should adapt a configuration when no authentication methods config is defined", function () {
const userConfiguration: any = undefined;
const appConfiguration = AuthenticationMethodsAdapter.adapt(userConfiguration);
Assert.deepStrictEqual(appConfiguration, {
default_method: "two_factor",
per_subdomain_methods: {}
});
});
});
describe("partial authentication methods config", function() {
it("should adapt a configuration when default_method is not defined", function () {
const userConfiguration: any = {
per_subdomain_methods: {
"example.com": "basic_auth"
}
};
const appConfiguration = AuthenticationMethodsAdapter.adapt(userConfiguration);
Assert.deepStrictEqual(appConfiguration, {
default_method: "two_factor",
per_subdomain_methods: {
"example.com": "basic_auth"
}
});
});
it("should adapt a configuration when per_subdomain_methods is not defined", function () {
const userConfiguration: any = {
default_method: "basic_auth"
};
const appConfiguration = AuthenticationMethodsAdapter.adapt(userConfiguration);
Assert.deepStrictEqual(appConfiguration, {
default_method: "basic_auth",
per_subdomain_methods: {}
});
});
it("should adapt a configuration when per_subdomain_methods has wrong type", function () {
const userConfiguration: any = {
default_method: "basic_auth",
per_subdomain_methods: []
};
const appConfiguration = AuthenticationMethodsAdapter.adapt(userConfiguration);
Assert.deepStrictEqual(appConfiguration, {
default_method: "basic_auth",
per_subdomain_methods: {}
});
});
});
});

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

View File

@ -297,3 +297,5 @@ export const LOGOUT_GET = "/logout";
export const ERROR_401_GET = "/error/401"; export const ERROR_401_GET = "/error/401";
export const ERROR_403_GET = "/error/403"; export const ERROR_403_GET = "/error/403";
export const ERROR_404_GET = "/error/404"; export const ERROR_404_GET = "/error/404";
export const LOGGED_IN = "/loggedin";

View File

@ -1,4 +1 @@
export const ONLY_BASIC_AUTH_QUERY_PARAM = "only_basic_auth";
export const REDIRECT_QUERY_PARAM = "redirect"; export const REDIRECT_QUERY_PARAM = "redirect";

View File

@ -0,0 +1,32 @@
Feature: User is redirected when factors are already validated
@need-registered-user-john
Scenario: User has validated first factor and tries to access service protected by second factor. He is then redirect to second factor step.
When I visit "https://basicauth.test.local:8080/secret.html"
And I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fbasicauth.test.local%3A8080%2Fsecret.html"
And I login with user "john" and password "password"
And I'm redirected to "https://basicauth.test.local:8080/secret.html"
And I visit "https://public.test.local:8080/secret.html"
Then I'm redirected to "https://auth.test.local:8080/secondfactor?redirect=https%3A%2F%2Fpublic.test.local%3A8080%2Fsecret.html"
@need-registered-user-john
Scenario: User who has validated second factor and access auth portal should be redirected to "Already logged in page"
When I visit "https://public.test.local:8080/secret.html"
And I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fpublic.test.local%3A8080%2Fsecret.html"
And I login with user "john" and password "password"
And I use "REGISTERED" as TOTP token handle
And I click on "TOTP"
And I'm redirected to "https://public.test.local:8080/secret.html"
And I visit "https://auth.test.local:8080"
Then I'm redirected to "https://auth.test.local:8080/loggedin"
@need-registered-user-john
Scenario: User who has validated second factor and access auth portal with rediction param should be redirected to that URL
When I visit "https://public.test.local:8080/secret.html"
And I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fpublic.test.local%3A8080%2Fsecret.html"
And I login with user "john" and password "password"
And I use "REGISTERED" as TOTP token handle
And I click on "TOTP"
And I'm redirected to "https://public.test.local:8080/secret.html"
And I visit "https://auth.test.local:8080?redirect=https://public.test.local:8080/secret.html"
Then I'm redirected to "https://public.test.local:8080/secret.html"

View File

@ -1,4 +1,4 @@
Feature: User validate first factor Feature: Authentication scenarii
Scenario: User succeeds first factor Scenario: User succeeds first factor
Given I visit "https://auth.test.local:8080/" Given I visit "https://auth.test.local:8080/"

View File

@ -2,18 +2,12 @@ Feature: User can access certain subdomains with basic auth
@need-registered-user-john @need-registered-user-john
Scenario: User is redirected to service after first factor if allowed Scenario: User is redirected to service after first factor if allowed
When I visit "https://auth.test.local:8080/?redirect=https%3A%2F%2Fbasicauth.test.local%3A8080%2Fsecret.html&only_basic_auth=true" When I visit "https://auth.test.local:8080/?redirect=https%3A%2F%2Fbasicauth.test.local%3A8080%2Fsecret.html"
And I login with user "john" and password "password" And I login with user "john" and password "password"
Then I'm redirected to "https://basicauth.test.local:8080/secret.html" Then I'm redirected to "https://basicauth.test.local:8080/secret.html"
@need-registered-user-john @need-registered-user-john
Scenario: Redirection after first factor fails if basic_auth not allowed. It redirects user to first factor. Scenario: Redirection after first factor fails if basic_auth not allowed. It redirects user to first factor.
When I visit "https://auth.test.local:8080/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html&only_basic_auth=true"
And I login with user "john" and password "password"
Then I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html"
@need-registered-user-john
Scenario: User is redirected to second factor after first factor
When I visit "https://auth.test.local:8080/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html" When I visit "https://auth.test.local:8080/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html"
And I login with user "john" and password "password" And I login with user "john" and password "password"
Then I'm redirected to "https://auth.test.local:8080/secondfactor?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html" Then I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html"