diff --git a/config.template.yml b/config.template.yml index 3fb5438fb..2058b7cb4 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/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..49bf5c325 100644 --- a/server/src/lib/configuration/ConfigurationAdapter.ts +++ b/server/src/lib/configuration/ConfigurationAdapter.ts @@ -4,7 +4,7 @@ import { AppConfiguration, UserConfiguration, NotifierConfiguration, ACLConfiguration, LdapConfiguration, SessionRedisOptions, MongoStorageConfiguration, LocalStorageConfiguration, - UserLdapConfiguration + UserLdapConfiguration, AuthenticationMethodsConfiguration } from "./Configuration"; import Util = require("util"); 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 { 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 = adaptAuthenticationMethods(userConfiguration.authentication_methods); return { port: port, @@ -81,7 +91,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/routes/firstfactor/post.ts b/server/src/lib/routes/firstfactor/post.ts index 072a55e13..d1b82bb90 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); @@ -46,10 +49,12 @@ export default function (req: express.Request, res: express.Response): BluebirdP 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 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,13 +68,13 @@ 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); @@ -79,6 +84,9 @@ export default function (req: express.Request, res: express.Response): BluebirdP 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/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..2aa55b61b --- /dev/null +++ b/server/src/lib/utils/DomainExtractor.ts @@ -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]; + } +} \ No newline at end of file diff --git a/server/test/AuthenticationMethodCalculator.test.ts b/server/test/AuthenticationMethodCalculator.test.ts new file mode 100644 index 000000000..3fa470e4d --- /dev/null +++ b/server/test/AuthenticationMethodCalculator.test.ts @@ -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"); + }); +}); \ No newline at end of file diff --git a/server/test/SessionConfigurationBuilder.test.ts b/server/test/SessionConfigurationBuilder.test.ts index 67fcb58e6..bae332347 100644 --- a/server/test/SessionConfigurationBuilder.test.ts +++ b/server/test/SessionConfigurationBuilder.test.ts @@ -8,155 +8,163 @@ import Sinon = require("sinon"); import Assert = require("assert"); describe("test session configuration builder", function () { - it("should return session options without redis options", function () { - const configuration: AppConfiguration = { - access_control: { - default_policy: "deny", - any: [], - users: {}, - groups: {} - }, - ldap: { - url: "ldap://ldap", - user: "user", - password: "password", - groups_dn: "ou=groups,dc=example,dc=com", - users_dn: "ou=users,dc=example,dc=com", - group_name_attribute: "", - groups_filter: "", - mail_attribute: "", - users_filter: "" - }, - logs_level: "debug", - notifier: { - filesystem: { - filename: "/test" - } - }, - port: 8080, - session: { - domain: "example.com", - expiration: 3600, - secret: "secret" - }, - regulation: { - max_retries: 3, - ban_time: 5 * 60, - find_time: 5 * 60 - }, - storage: { - local: { - in_memory: true - } - } - }; + it("should return session options without redis options", function () { + const configuration: AppConfiguration = { + access_control: { + default_policy: "deny", + any: [], + users: {}, + groups: {} + }, + ldap: { + url: "ldap://ldap", + user: "user", + password: "password", + groups_dn: "ou=groups,dc=example,dc=com", + users_dn: "ou=users,dc=example,dc=com", + group_name_attribute: "", + groups_filter: "", + mail_attribute: "", + users_filter: "" + }, + logs_level: "debug", + notifier: { + filesystem: { + filename: "/test" + } + }, + port: 8080, + session: { + domain: "example.com", + expiration: 3600, + secret: "secret" + }, + regulation: { + max_retries: 3, + ban_time: 5 * 60, + find_time: 5 * 60 + }, + storage: { + local: { + in_memory: true + } + }, + authentication_methods: { + default_method: "two_factor", + per_subdomain_methods: {} + } + }; - const deps: GlobalDependencies = { - ConnectRedis: Sinon.spy() as any, - ldapjs: Sinon.spy() as any, - nedb: Sinon.spy() as any, - session: Sinon.spy() as any, - speakeasy: Sinon.spy() as any, - u2f: Sinon.spy() as any, - winston: Sinon.spy() as any, - dovehash: Sinon.spy() as any - }; + const deps: GlobalDependencies = { + ConnectRedis: Sinon.spy() as any, + ldapjs: Sinon.spy() as any, + nedb: Sinon.spy() as any, + session: Sinon.spy() as any, + speakeasy: Sinon.spy() as any, + u2f: Sinon.spy() as any, + winston: Sinon.spy() as any, + dovehash: Sinon.spy() as any + }; - const options = SessionConfigurationBuilder.build(configuration, deps); + const options = SessionConfigurationBuilder.build(configuration, deps); - const expectedOptions = { - secret: "secret", - resave: false, - saveUninitialized: true, - cookie: { - secure: false, - maxAge: 3600, - domain: "example.com" - } - }; + const expectedOptions = { + secret: "secret", + resave: false, + saveUninitialized: true, + cookie: { + secure: false, + maxAge: 3600, + domain: "example.com" + } + }; - Assert.deepEqual(expectedOptions, options); - }); + Assert.deepEqual(expectedOptions, options); + }); - it("should return session options with redis options", function () { - const configuration: AppConfiguration = { - access_control: { - default_policy: "deny", - any: [], - users: {}, - groups: {} - }, - ldap: { - url: "ldap://ldap", - user: "user", - password: "password", - groups_dn: "ou=groups,dc=example,dc=com", - users_dn: "ou=users,dc=example,dc=com", - group_name_attribute: "", - groups_filter: "", - mail_attribute: "", - users_filter: "" - }, - logs_level: "debug", - notifier: { - filesystem: { - filename: "/test" - } - }, - port: 8080, - session: { - domain: "example.com", - expiration: 3600, - secret: "secret", - redis: { - host: "redis.example.com", - port: 6379 - } - }, - regulation: { - max_retries: 3, - ban_time: 5 * 60, - find_time: 5 * 60 - }, - storage: { - local: { - in_memory: true - } - } - }; + it("should return session options with redis options", function () { + const configuration: AppConfiguration = { + access_control: { + default_policy: "deny", + any: [], + users: {}, + groups: {} + }, + ldap: { + url: "ldap://ldap", + user: "user", + password: "password", + groups_dn: "ou=groups,dc=example,dc=com", + users_dn: "ou=users,dc=example,dc=com", + group_name_attribute: "", + groups_filter: "", + mail_attribute: "", + users_filter: "" + }, + logs_level: "debug", + notifier: { + filesystem: { + filename: "/test" + } + }, + port: 8080, + session: { + domain: "example.com", + expiration: 3600, + secret: "secret", + redis: { + host: "redis.example.com", + port: 6379 + } + }, + regulation: { + max_retries: 3, + ban_time: 5 * 60, + find_time: 5 * 60 + }, + storage: { + local: { + in_memory: true + } + }, + authentication_methods: { + default_method: "two_factor", + per_subdomain_methods: {} + } + }; - const RedisStoreMock = Sinon.spy(); + const RedisStoreMock = Sinon.spy(); - const deps: GlobalDependencies = { - ConnectRedis: Sinon.stub().returns(RedisStoreMock) as any, - ldapjs: Sinon.spy() as any, - nedb: Sinon.spy() as any, - session: Sinon.spy() as any, - speakeasy: Sinon.spy() as any, - u2f: Sinon.spy() as any, - winston: Sinon.spy() as any, - dovehash: Sinon.spy() as any - }; + const deps: GlobalDependencies = { + ConnectRedis: Sinon.stub().returns(RedisStoreMock) as any, + ldapjs: Sinon.spy() as any, + nedb: Sinon.spy() as any, + session: Sinon.spy() as any, + speakeasy: Sinon.spy() as any, + u2f: Sinon.spy() as any, + winston: Sinon.spy() as any, + dovehash: Sinon.spy() as any + }; - const options = SessionConfigurationBuilder.build(configuration, deps); + const options = SessionConfigurationBuilder.build(configuration, deps); - const expectedOptions: ExpressSession.SessionOptions = { - secret: "secret", - resave: false, - saveUninitialized: true, - cookie: { - secure: false, - maxAge: 3600, - domain: "example.com" - }, - store: Sinon.match.object as any - }; + const expectedOptions: ExpressSession.SessionOptions = { + secret: "secret", + resave: false, + saveUninitialized: true, + cookie: { + secure: false, + maxAge: 3600, + domain: "example.com" + }, + store: Sinon.match.object as any + }; - Assert((deps.ConnectRedis as Sinon.SinonStub).calledWith(deps.session)); - Assert.equal(options.secret, expectedOptions.secret); - Assert.equal(options.resave, expectedOptions.resave); - Assert.equal(options.saveUninitialized, expectedOptions.saveUninitialized); - Assert.deepEqual(options.cookie, expectedOptions.cookie); - Assert(options.store != undefined); - }); + Assert((deps.ConnectRedis as Sinon.SinonStub).calledWith(deps.session)); + Assert.equal(options.secret, expectedOptions.secret); + Assert.equal(options.resave, expectedOptions.resave); + Assert.equal(options.saveUninitialized, expectedOptions.saveUninitialized); + Assert.deepEqual(options.cookie, expectedOptions.cookie); + Assert(options.store != undefined); + }); }); \ No newline at end of file diff --git a/server/test/mocks/ServerVariablesMock.ts b/server/test/mocks/ServerVariablesMock.ts index a56676251..997b99de3 100644 --- a/server/test/mocks/ServerVariablesMock.ts +++ b/server/test/mocks/ServerVariablesMock.ts @@ -2,6 +2,7 @@ import Sinon = require("sinon"); import express = require("express"); import { RequestLoggerStub } from "./RequestLoggerStub"; import { UserDataStoreStub } from "./storage/UserDataStoreStub"; +import { AuthenticationMethodCalculator } from "../../src/lib/AuthenticationMethodCalculator"; import { VARIABLES_KEY } from "../../src/lib/ServerVariablesHandler"; export interface ServerVariablesMock { @@ -17,6 +18,7 @@ export interface ServerVariablesMock { regulator: any; config: any; accessController: any; + authenticationMethodsCalculator: any; } @@ -33,7 +35,11 @@ export function mock(app: express.Application): ServerVariablesMock { totpGenerator: Sinon.stub(), totpValidator: 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); return mocks; diff --git a/server/test/routes/verify/get.test.ts b/server/test/routes/verify/get.test.ts index b2083dd9c..7167199b1 100644 --- a/server/test/routes/verify/get.test.ts +++ b/server/test/routes/verify/get.test.ts @@ -2,6 +2,8 @@ import Assert = require("assert"); import VerifyGet = require("../../../src/lib/routes/verify/get"); import AuthenticationSession = require("../../../src/lib/AuthenticationSession"); +import { AuthenticationMethodCalculator } from "../../../src/lib/AuthenticationMethodCalculator"; +import { AuthenticationMethodsConfiguration } from "../../../src/lib/configuration/Configuration"; import Sinon = require("sinon"); import winston = require("winston"); @@ -17,6 +19,7 @@ describe("test authentication token verification", function () { let req: ExpressMock.RequestMock; let res: ExpressMock.ResponseMock; let accessController: AccessControllerStub; + let mocks: any; beforeEach(function () { accessController = new AccessControllerStub(); @@ -34,9 +37,16 @@ describe("test authentication token verification", function () { AuthenticationSession.reset(req as any); req.headers = {}; req.headers.host = "secret.example.com"; - const mocks = ServerVariablesMock.mock(req.app); + mocks = ServerVariablesMock.mock(req.app); mocks.config = {} 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 () { @@ -153,9 +163,9 @@ describe("test authentication token verification", function () { describe("given user tries to access a basic auth endpoint", function () { beforeEach(function () { req.query = { - redirect: "http://redirect.url", - only_basic_auth: "true" + redirect: "http://redirect.url" }; + req.headers["host"] = "redirect.url"; }); it("should be authenticated when first factor is validated and not second factor", function () { diff --git a/server/test/utils/DomainExtractor.test.ts b/server/test/utils/DomainExtractor.test.ts new file mode 100644 index 000000000..f179ba9e7 --- /dev/null +++ b/server/test/utils/DomainExtractor.test.ts @@ -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"); + }); + }); +}); \ No newline at end of file