diff --git a/Gruntfile.js b/Gruntfile.js index 3837d2eeb..cf7c78a1b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -53,7 +53,7 @@ module.exports = function (grunt) { }, "include-minified-script": { 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: { diff --git a/README.md b/README.md index 2f6217f5f..7049052dd 100644 --- a/README.md +++ b/README.md @@ -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) 4. [Password reset](#password-reset) 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) 1. [Authelia configuration](#authelia-configuration) 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. * Password reset with identity verification by sending links to user email address. +* Two-factor and basic authentication methods available. * Access restriction after too many authentication attempts. * Session management using Redis key/value store. * 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. 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 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]. diff --git a/client/src/lib/firstfactor/FirstFactorValidator.ts b/client/src/lib/firstfactor/FirstFactorValidator.ts index b7bda3b4d..175f3c2c8 100644 --- a/client/src/lib/firstfactor/FirstFactorValidator.ts +++ b/client/src/lib/firstfactor/FirstFactorValidator.ts @@ -2,12 +2,19 @@ import BluebirdPromise = require("bluebird"); import Endpoints = require("../../../../shared/api"); import Constants = require("../../../../shared/constants"); +import Util = require("util"); export function validate(username: string, password: string, - redirectUrl: string, onlyBasicAuth: boolean, $: JQueryStatic): BluebirdPromise { + redirectUrl: string, $: JQueryStatic): BluebirdPromise { return new BluebirdPromise(function (resolve, reject) { - const url = Endpoints.FIRST_FACTOR_POST + "?" + Constants.REDIRECT_QUERY_PARAM + "=" + redirectUrl - + "&" + Constants.ONLY_BASIC_AUTH_QUERY_PARAM + "=" + onlyBasicAuth; + let url: string; + 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({ method: "POST", diff --git a/client/src/lib/firstfactor/index.ts b/client/src/lib/firstfactor/index.ts index fc897ff1d..91de7e001 100644 --- a/client/src/lib/firstfactor/index.ts +++ b/client/src/lib/firstfactor/index.ts @@ -17,8 +17,7 @@ export default function (window: Window, $: JQueryStatic, $(UISelectors.PASSWORD_FIELD_ID).val(""); 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, onlyBasicAuth, $) + firstFactorValidator.validate(username, password, redirectUrl, $) .then(onFirstFactorSuccess, onFirstFactorFailure); return false; } diff --git a/client/test/firstfactor/FirstFactorValidator.test.ts b/client/test/firstfactor/FirstFactorValidator.test.ts index 49e4f232c..acae7c0d7 100644 --- a/client/test/firstfactor/FirstFactorValidator.test.ts +++ b/client/test/firstfactor/FirstFactorValidator.test.ts @@ -13,7 +13,7 @@ describe("test FirstFactorValidator", function () { const jqueryMock = JQueryMock.JQueryMock(); 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) { @@ -27,7 +27,7 @@ describe("test FirstFactorValidator", function () { const jqueryMock = JQueryMock.JQueryMock(); 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 () { return BluebirdPromise.reject(new Error("First factor validation successfully finished while it should have not.")); }, function (err: Error) { diff --git a/config.template.yml b/config.template.yml index ad4246b77..810f0133a 100644 --- a/config.template.yml +++ b/config.template.yml @@ -47,6 +47,20 @@ ldap: 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 is a set of rules you can use to restrict user access to certain diff --git a/example/nginx/nginx.conf b/example/nginx/nginx.conf index 5a19ec5ff..542d6221d 100644 --- a/example/nginx/nginx.conf +++ b/example/nginx/nginx.conf @@ -221,7 +221,7 @@ http { proxy_set_header Host $http_host; proxy_set_header Content-Length ""; - proxy_pass http://authelia/verify?only_basic_auth=true; + proxy_pass http://authelia/verify; } location / { @@ -236,7 +236,7 @@ http { auth_request_set $groups $upstream_http_remote_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; } } diff --git a/server/src/lib/AuthenticationMethodCalculator.ts b/server/src/lib/AuthenticationMethodCalculator.ts new file mode 100644 index 000000000..3791a9dbe --- /dev/null +++ b/server/src/lib/AuthenticationMethodCalculator.ts @@ -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; + } +} \ No newline at end of file diff --git a/server/src/lib/RestApi.ts b/server/src/lib/RestApi.ts index 0006fffa7..34686c719 100644 --- a/server/src/lib/RestApi.ts +++ b/server/src/lib/RestApi.ts @@ -29,6 +29,9 @@ import ResetPasswordRequestPost = require("./routes/password-reset/request/get") import Error401Get = require("./routes/error/401/get"); import Error403Get = require("./routes/error/403/get"); import Error404Get = require("./routes/error/404/get"); + +import LoggedIn = require("./routes/loggedin/get"); + import { ServerVariablesHandler } from "./ServerVariablesHandler"; 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_403_GET, withLog(Error403Get.default)); app.get(Endpoints.ERROR_404_GET, withLog(Error404Get.default)); + app.get(Endpoints.LOGGED_IN, withLog(LoggedIn.default)); } } diff --git a/server/src/lib/ServerVariables.ts b/server/src/lib/ServerVariables.ts index d11c8f38c..065a1dafa 100644 --- a/server/src/lib/ServerVariables.ts +++ b/server/src/lib/ServerVariables.ts @@ -1,5 +1,3 @@ - - import U2F = require("u2f"); import { IRequestLogger } from "./logging/IRequestLogger"; @@ -14,6 +12,8 @@ import { INotifier } from "./notifiers/INotifier"; import { AuthenticationRegulator } from "./AuthenticationRegulator"; import Configuration = require("./configuration/Configuration"); import { AccessController } from "./access_control/AccessController"; +import { AuthenticationMethodCalculator } from "./AuthenticationMethodCalculator"; + export interface ServerVariables { @@ -29,4 +29,5 @@ export interface ServerVariables { regulator: AuthenticationRegulator; config: Configuration.AppConfiguration; accessController: AccessController; + authenticationMethodsCalculator: AuthenticationMethodCalculator; } \ No newline at end of file diff --git a/server/src/lib/ServerVariablesHandler.ts b/server/src/lib/ServerVariablesHandler.ts index f40d15331..caf7a6332 100644 --- a/server/src/lib/ServerVariablesHandler.ts +++ b/server/src/lib/ServerVariablesHandler.ts @@ -33,8 +33,11 @@ import { ICollectionFactory } from "./storage/ICollectionFactory"; import { MongoCollectionFactory } from "./storage/mongo/MongoCollectionFactory"; import { MongoConnectorFactory } from "./connectors/mongo/MongoConnectorFactory"; import { IMongoClient } from "./connectors/mongo/IMongoClient"; + import { GlobalDependencies } from "../../types/Dependencies"; import { ServerVariables } from "./ServerVariables"; +import { AuthenticationMethodCalculator } from "./AuthenticationMethodCalculator"; + import express = require("express"); @@ -78,6 +81,7 @@ export class ServerVariablesHandler { const accessController = new AccessController(config.access_control, deps.winston); const totpValidator = new TOTPValidator(deps.speakeasy); const totpGenerator = new TOTPGenerator(deps.speakeasy); + const authenticationMethodCalculator = new AuthenticationMethodCalculator(config.authentication_methods); return UserDataStoreFactory.create(config) .then(function (userDataStore: UserDataStore) { @@ -97,6 +101,7 @@ export class ServerVariablesHandler { totpValidator: totpValidator, u2f: deps.u2f, userDataStore: userDataStore, + authenticationMethodsCalculator: authenticationMethodCalculator }; app.set(VARIABLES_KEY, variables); @@ -150,4 +155,8 @@ export class ServerVariablesHandler { static getU2F(app: express.Application): typeof U2F { return (app.get(VARIABLES_KEY) as ServerVariables).u2f; } + + static getAuthenticationMethodCalculator(app: express.Application): AuthenticationMethodCalculator { + return (app.get(VARIABLES_KEY) as ServerVariables).authenticationMethodsCalculator; + } } diff --git a/server/src/lib/access_control/AccessController.ts b/server/src/lib/access_control/AccessController.ts index 192153a6e..a7a572080 100644 --- a/server/src/lib/access_control/AccessController.ts +++ b/server/src/lib/access_control/AccessController.ts @@ -2,7 +2,7 @@ import { ACLConfiguration, ACLPolicy, ACLRule } from "../configuration/Configuration"; import { IAccessController } from "./IAccessController"; import { Winston } from "../../../types/Dependencies"; -import { DomainMatcher } from "./DomainMatcher"; +import { MultipleDomainMatcher } from "./MultipleDomainMatcher"; enum AccessReturn { @@ -17,7 +17,7 @@ function AllowedRule(rule: ACLRule) { function MatchDomain(actualDomain: string) { return function (rule: ACLRule): boolean { - return DomainMatcher.match(actualDomain, rule.domain); + return MultipleDomainMatcher.match(actualDomain, rule.domain); }; } diff --git a/server/src/lib/access_control/DomainMatcher.ts b/server/src/lib/access_control/DomainMatcher.ts deleted file mode 100644 index 2afb14a3c..000000000 --- a/server/src/lib/access_control/DomainMatcher.ts +++ /dev/null @@ -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; - } - } -} \ No newline at end of file diff --git a/server/src/lib/access_control/MultipleDomainMatcher.ts b/server/src/lib/access_control/MultipleDomainMatcher.ts new file mode 100644 index 000000000..64c647a4a --- /dev/null +++ b/server/src/lib/access_control/MultipleDomainMatcher.ts @@ -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; + } + } +} \ No newline at end of file diff --git a/server/src/lib/configuration/Configuration.d.ts b/server/src/lib/configuration/Configuration.d.ts index 7df8c3c50..8faa487d5 100644 --- a/server/src/lib/configuration/Configuration.d.ts +++ b/server/src/lib/configuration/Configuration.d.ts @@ -109,6 +109,14 @@ export interface RegulationConfiguration { 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 { port?: number; logs_level?: string; @@ -116,6 +124,7 @@ export interface UserConfiguration { session: SessionCookieConfiguration; storage: StorageConfiguration; notifier: NotifierConfiguration; + authentication_methods?: AuthenticationMethodsConfiguration; access_control?: ACLConfiguration; regulation: RegulationConfiguration; } @@ -127,6 +136,7 @@ export interface AppConfiguration { session: SessionCookieConfiguration; storage: StorageConfiguration; notifier: NotifierConfiguration; + authentication_methods: AuthenticationMethodsConfiguration; access_control?: ACLConfiguration; regulation: RegulationConfiguration; } diff --git a/server/src/lib/configuration/ConfigurationAdapter.ts b/server/src/lib/configuration/ConfigurationAdapter.ts index 56afdd8a9..da5807bdc 100644 --- a/server/src/lib/configuration/ConfigurationAdapter.ts +++ b/server/src/lib/configuration/ConfigurationAdapter.ts @@ -8,6 +8,7 @@ import { } from "./Configuration"; import Util = require("util"); import { ACLAdapter } from "./adapters/ACLAdapter"; +import { AuthenticationMethodsAdapter } from "./adapters/AuthenticationMethodsAdapter"; 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.url"); - // ensure_key_existence(userConfiguration, "ldap.base_dn"); ensure_key_existence(userConfiguration, "session.secret"); ensure_key_existence(userConfiguration, "regulation"); const port = userConfiguration.port || 8080; const ldapConfiguration = adaptLdapConfiguration(userConfiguration.ldap); + const authenticationMethods = AuthenticationMethodsAdapter + .adapt(userConfiguration.authentication_methods); return { port: port, @@ -81,7 +83,8 @@ function adaptFromUserConfiguration(userConfiguration: UserConfiguration): AppCo logs_level: get_optional(userConfiguration, "logs_level", "info"), notifier: ObjectPath.get(userConfiguration, "notifier"), access_control: ACLAdapter.adapt(userConfiguration.access_control), - regulation: userConfiguration.regulation + regulation: userConfiguration.regulation, + authentication_methods: authenticationMethods }; } diff --git a/server/src/lib/configuration/adapters/ACLAdapter.ts b/server/src/lib/configuration/adapters/ACLAdapter.ts index 53e0801cc..d9fca60b8 100644 --- a/server/src/lib/configuration/adapters/ACLAdapter.ts +++ b/server/src/lib/configuration/adapters/ACLAdapter.ts @@ -1,8 +1,5 @@ import { ACLConfiguration } from "../Configuration"; - -function clone(obj: any): any { - return JSON.parse(JSON.stringify(obj)); -} +import { ObjectCloner } from "../../utils/ObjectCloner"; const DEFAULT_POLICY = "deny"; @@ -32,7 +29,7 @@ export class ACLAdapter { static adapt(configuration: ACLConfiguration): ACLConfiguration { if (!configuration) return; - const newConfiguration: ACLConfiguration = clone(configuration); + const newConfiguration: ACLConfiguration = ObjectCloner.clone(configuration); adaptDefaultPolicy(newConfiguration); adaptAny(newConfiguration); adaptGroups(newConfiguration); diff --git a/server/src/lib/configuration/adapters/AuthenticationMethodsAdapter.ts b/server/src/lib/configuration/adapters/AuthenticationMethodsAdapter.ts new file mode 100644 index 000000000..462d6bc65 --- /dev/null +++ b/server/src/lib/configuration/adapters/AuthenticationMethodsAdapter.ts @@ -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; + } +} diff --git a/server/src/lib/routes/firstfactor/get.ts b/server/src/lib/routes/firstfactor/get.ts index 249f9dc37..22972ae44 100644 --- a/server/src/lib/routes/firstfactor/get.ts +++ b/server/src/lib/routes/firstfactor/get.ts @@ -6,11 +6,52 @@ import Endpoints = require("../../../../../shared/api"); import AuthenticationValidator = require("../../AuthenticationValidator"); import { ServerVariablesHandler } from "../../ServerVariablesHandler"; 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 { +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", { first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST, reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET }); - return BluebirdPromise.resolve(); +} + +export default function (req: express.Request, res: express.Response): BluebirdPromise { + 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(); + }); } \ No newline at end of file diff --git a/server/src/lib/routes/firstfactor/post.ts b/server/src/lib/routes/firstfactor/post.ts index 072a55e13..b7ba04327 100644 --- a/server/src/lib/routes/firstfactor/post.ts +++ b/server/src/lib/routes/firstfactor/post.ts @@ -11,6 +11,7 @@ import ErrorReplies = require("../../ErrorReplies"); import { ServerVariablesHandler } from "../../ServerVariablesHandler"; import AuthenticationSession = require("../../AuthenticationSession"); import Constants = require("../../../../../shared/constants"); +import { DomainExtractor } from "../../utils/DomainExtractor"; export default function (req: express.Request, res: express.Response): BluebirdPromise { 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 accessController = ServerVariablesHandler.getAccessController(req.app); + const authenticationMethodsCalculator = + ServerVariablesHandler.getAuthenticationMethodCalculator(req.app); let authSession: AuthenticationSession.AuthenticationSession; 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)); authSession.userid = username; authSession.first_factor = true; - const redirectUrl = req.query[Constants.REDIRECT_QUERY_PARAM]; - const onlyBasicAuth = req.query[Constants.ONLY_BASIC_AUTH_QUERY_PARAM] === "true"; + const redirectUrl = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined" + // Fuck, don't know why it is a string! + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; const emails: string[] = groupsAndEmails.emails; 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) { 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."); regulator.mark(username, true); - if (onlyBasicAuth) { + if (authMethod == "basic_auth") { res.send({ redirect: redirectUrl }); logger.debug(req, "Redirect to '%s'", redirectUrl); } - else { + else if (authMethod == "two_factor") { let newRedirectUrl = Endpoint.SECOND_FACTOR_GET; - if (redirectUrl !== "undefined") { - newRedirectUrl += "?redirect=" + encodeURIComponent(redirectUrl); + if (redirectUrl) { + newRedirectUrl += "?" + Constants.REDIRECT_QUERY_PARAM + "=" + + encodeURIComponent(redirectUrl); } logger.debug(req, "Redirect to '%s'", newRedirectUrl, typeof redirectUrl); res.send({ redirect: newRedirectUrl }); } + else { + return BluebirdPromise.reject(new Error("Unknown authentication method for this domain.")); + } return BluebirdPromise.resolve(); }) .catch(exceptions.LdapSearchError, ErrorReplies.replyWithError500(req, res, logger)) diff --git a/server/src/lib/routes/loggedin/get.ts b/server/src/lib/routes/loggedin/get.ts new file mode 100644 index 000000000..0a9910a92 --- /dev/null +++ b/server/src/lib/routes/loggedin/get.ts @@ -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 + }); +} \ No newline at end of file diff --git a/server/src/lib/routes/secondfactor/get.ts b/server/src/lib/routes/secondfactor/get.ts index 172e1d6e6..b3cc003b0 100644 --- a/server/src/lib/routes/secondfactor/get.ts +++ b/server/src/lib/routes/secondfactor/get.ts @@ -4,15 +4,25 @@ import Endpoints = require("../../../../../shared/api"); import FirstFactorBlocker = require("../FirstFactorBlocker"); import BluebirdPromise = require("bluebird"); import { ServerVariablesHandler } from "../../ServerVariablesHandler"; +import AuthenticationSession = require("../../AuthenticationSession"); const TEMPLATE_NAME = "secondfactor"; export default FirstFactorBlocker.default(handler); function handler(req: Express.Request, res: Express.Response): BluebirdPromise { - res.render(TEMPLATE_NAME, { - totp_identity_start_endpoint: Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, - u2f_identity_start_endpoint: Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET - }); - return BluebirdPromise.resolve(); + 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, { + username: authSession.userid, + totp_identity_start_endpoint: Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, + u2f_identity_start_endpoint: Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET + }); + return BluebirdPromise.resolve(); + }); } \ No newline at end of file diff --git a/server/src/lib/routes/verify/get.ts b/server/src/lib/routes/verify/get.ts index d66cff648..fcbc73364 100644 --- a/server/src/lib/routes/verify/get.ts +++ b/server/src/lib/routes/verify/get.ts @@ -10,6 +10,7 @@ import { ServerVariablesHandler } from "../../ServerVariablesHandler"; import AuthenticationSession = require("../../AuthenticationSession"); import Constants = require("../../../../../shared/constants"); import Util = require("util"); +import { DomainExtractor } from "../../utils/DomainExtractor"; const FIRST_FACTOR_NOT_VALIDATED_MESSAGE = "First 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 { const logger = ServerVariablesHandler.getLogger(req.app); const accessController = ServerVariablesHandler.getAccessController(req.app); + const authenticationMethodsCalculator = ServerVariablesHandler.getAuthenticationMethodCalculator(req.app); return AuthenticationSession.get(req) .then(function (authSession) { @@ -29,12 +31,11 @@ function verify_filter(req: express.Request, res: express.Response): BluebirdPro return BluebirdPromise.reject( new exceptions.AccessDeniedError(FIRST_FACTOR_NOT_VALIDATED_MESSAGE)); - const onlyBasicAuth = req.query[Constants.ONLY_BASIC_AUTH_QUERY_PARAM] === "true"; - const host = objectPath.get(req, "headers.host"); const path = objectPath.get(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, 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 '%'", username, domain))); - if (!onlyBasicAuth && !authSession.second_factor) + if (authenticationMethod == "two_factor" && !authSession.second_factor) return BluebirdPromise.reject( new exceptions.AccessDeniedError(SECOND_FACTOR_NOT_VALIDATED_MESSAGE)); diff --git a/server/src/lib/utils/DomainExtractor.ts b/server/src/lib/utils/DomainExtractor.ts new file mode 100644 index 000000000..f2e8b8886 --- /dev/null +++ b/server/src/lib/utils/DomainExtractor.ts @@ -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]; + } +} \ No newline at end of file diff --git a/server/src/lib/utils/ObjectCloner.ts b/server/src/lib/utils/ObjectCloner.ts new file mode 100644 index 000000000..3e125d749 --- /dev/null +++ b/server/src/lib/utils/ObjectCloner.ts @@ -0,0 +1,6 @@ + +export class ObjectCloner { + static clone(obj: any): any { + return JSON.parse(JSON.stringify(obj)); + } +} \ No newline at end of file diff --git a/server/src/views/layout/layout.pug b/server/src/views/layout/layout.pug index 2c0246a0d..3f95cb9bc 100644 --- a/server/src/views/layout/layout.pug +++ b/server/src/views/layout/layout.pug @@ -26,5 +26,5 @@ html - script(src="/js/authelia.min.js") + script(src="/js/authelia.js") block entrypoint \ No newline at end of file diff --git a/server/src/views/secondfactor.pug b/server/src/views/secondfactor.pug index b53d53006..9c39fdb82 100644 --- a/server/src/views/secondfactor.pug +++ b/server/src/views/secondfactor.pug @@ -5,6 +5,7 @@ block form-header block content + p Hi #{username}, please complete second factor or logout.