diff --git a/package-lock.json b/package-lock.json index 2a435d61..ba64b8be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -482,6 +482,12 @@ "integrity": "sha512-txsii9cwD2OUOPukfPu3Jpoi3CnznBAwRX3JF26EC4p5T6IA8AaL6PBilACyY2fJkk+ydDNo4BJrJOo/OmNaZw==", "dev": true }, + "@types/proxyquire": { + "version": "1.3.28", + "resolved": "https://registry.npmjs.org/@types/proxyquire/-/proxyquire-1.3.28.tgz", + "integrity": "sha512-SQaNzWQ2YZSr7FqAyPPiA3FYpux2Lqh3HWMZQk47x3xbMCqgC/w0dY3dw9rGqlweDDkrySQBcaScXWeR+Yb11Q==", + "dev": true + }, "@types/query-string": { "version": "5.1.0", "resolved": "http://registry.npmjs.org/@types/query-string/-/query-string-5.1.0.tgz", @@ -2524,6 +2530,16 @@ "integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=", "dev": true }, + "fill-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", + "integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=", + "dev": true, + "requires": { + "is-object": "1.0.1", + "merge-descriptors": "1.0.1" + } + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -4185,6 +4201,12 @@ "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, + "is-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", + "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", + "dev": true + }, "is-path-cwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", @@ -5141,6 +5163,12 @@ "integrity": "sha1-WuDA6vj+I+AJzQH5iJtCxPY0rxI=", "dev": true }, + "module-not-found-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", + "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=", + "dev": true + }, "moment": { "version": "2.22.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz", @@ -7260,6 +7288,28 @@ "ipaddr.js": "1.6.0" } }, + "proxyquire": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.0.tgz", + "integrity": "sha512-kptdFArCfGRtQFv3Qwjr10lwbEV0TBJYvfqzhwucyfEXqVgmnAkyEw/S3FYzR5HI9i5QOq4rcqQjZ6AlknlCDQ==", + "dev": true, + "requires": { + "fill-keys": "1.0.2", + "module-not-found-error": "1.0.1", + "resolve": "1.8.1" + }, + "dependencies": { + "resolve": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", + "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + } + } + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -8522,6 +8572,12 @@ "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "dev": true }, + "ts-mock-imports": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/ts-mock-imports/-/ts-mock-imports-1.2.3.tgz", + "integrity": "sha512-pKeHFhlM4s4LvAPiixTsBTzJ65SY0pcXYFQ6nAmDOHl3lYZk4zi2zZFC3et6xX6tKhCCkt2NaYAY+vciPJlo8Q==", + "dev": true + }, "ts-node": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-6.0.2.tgz", diff --git a/package.json b/package.json index eebb268f..589edd3d 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "sinon": "^5.0.7", "tmp": "0.0.33", "tree-kill": "^1.2.1", + "ts-mock-imports": "^1.2.3", "ts-node": "^6.0.1", "tslint": "^5.2.0", "typescript": "^2.9.2", diff --git a/server/src/lib/Exceptions.ts b/server/src/lib/Exceptions.ts index 83fa4eb6..8db02826 100644 --- a/server/src/lib/Exceptions.ts +++ b/server/src/lib/Exceptions.ts @@ -66,7 +66,7 @@ export class NotAuthenticatedError extends Error { export class NotAuthorizedError extends Error { constructor(message?: string) { super(message); - this.name = "NotAuthanticatedError"; + this.name = "NotAuthenticatedError"; (Object).setPrototypeOf(this, NotAuthorizedError.prototype); } } diff --git a/server/src/lib/ServerVariablesMockBuilder.spec.ts b/server/src/lib/ServerVariablesMockBuilder.spec.ts index 7874702a..8e0eae06 100644 --- a/server/src/lib/ServerVariablesMockBuilder.spec.ts +++ b/server/src/lib/ServerVariablesMockBuilder.spec.ts @@ -2,7 +2,7 @@ import { ServerVariables } from "./ServerVariables"; import { Configuration } from "./configuration/schema/Configuration"; import { IUsersDatabaseStub } from "./authentication/backends/IUsersDatabaseStub.spec"; -import { AuthorizerStub } from "./authorization/AuthorizerStub.spec"; +import AuthorizerStub from "./authorization/AuthorizerStub.spec"; import { RequestLoggerStub } from "./logging/RequestLoggerStub.spec"; import { NotifierStub } from "./notifiers/NotifierStub.spec"; import { RegulatorStub } from "./regulation/RegulatorStub.spec"; diff --git a/server/src/lib/authorization/Authorizer.ts b/server/src/lib/authorization/Authorizer.ts index f6f9cf41..64332724 100644 --- a/server/src/lib/authorization/Authorizer.ts +++ b/server/src/lib/authorization/Authorizer.ts @@ -1,5 +1,5 @@ -import { ACLConfiguration, ACLRule } from "../configuration/schema/AclConfiguration"; +import { ACLConfiguration, ACLRule, ACLPolicy } from "../configuration/schema/AclConfiguration"; import { IAuthorizer } from "./IAuthorizer"; import { Winston } from "../../../types/Dependencies"; import { MultipleDomainMatcher } from "./MultipleDomainMatcher"; @@ -60,7 +60,7 @@ export class Authorizer implements IAuthorizer { .filter(MatchSubject(subject)); } - private ruleToLevel(policy: string): Level { + private ruleToLevel(policy: ACLPolicy): Level { if (policy == "bypass") { return Level.BYPASS; } else if (policy == "one_factor") { diff --git a/server/src/lib/authorization/AuthorizerStub.spec.ts b/server/src/lib/authorization/AuthorizerStub.spec.ts index 9bd6f4a8..957e0b38 100644 --- a/server/src/lib/authorization/AuthorizerStub.spec.ts +++ b/server/src/lib/authorization/AuthorizerStub.spec.ts @@ -4,7 +4,7 @@ import { Level } from "./Level"; import { Object } from "./Object"; import { Subject } from "./Subject"; -export class AuthorizerStub implements IAuthorizer { +export default class AuthorizerStub implements IAuthorizer { authorizationMock: Sinon.SinonStub; constructor() { diff --git a/server/src/lib/routes/verify/CheckAuthorizations.spec.ts b/server/src/lib/routes/verify/CheckAuthorizations.spec.ts new file mode 100644 index 00000000..75f8b25e --- /dev/null +++ b/server/src/lib/routes/verify/CheckAuthorizations.spec.ts @@ -0,0 +1,100 @@ +import CheckAuthorizations from "./CheckAuthorizations"; +import AuthorizerStub from "../../authorization/AuthorizerStub.spec"; +import { Level } from "../../authentication/Level"; +import { Level as AuthorizationLevel } from "../../authorization/Level"; +import * as Assert from "assert"; +import { NotAuthenticatedError, NotAuthorizedError } from "../../Exceptions"; + +describe('routes/verify/CheckAuthorizations', function() { + describe('bypass policy', function() { + it('should allow an anonymous user', function() { + const authorizer = new AuthorizerStub(); + authorizer.authorizationMock.returns(AuthorizationLevel.BYPASS); + CheckAuthorizations(authorizer, "public.example.com", "/index.html", undefined, + undefined, Level.NOT_AUTHENTICATED); + }); + + it('should allow an authenticated user (1FA)', function() { + const authorizer = new AuthorizerStub(); + authorizer.authorizationMock.returns(AuthorizationLevel.BYPASS); + CheckAuthorizations(authorizer, "public.example.com", "/index.html", "john", + ["group1", "group2"], Level.ONE_FACTOR); + }); + + it('should allow an authenticated user (2FA)', function() { + const authorizer = new AuthorizerStub(); + authorizer.authorizationMock.returns(AuthorizationLevel.BYPASS); + CheckAuthorizations(authorizer, "public.example.com", "/index.html", "john", + ["group1", "group2"], Level.TWO_FACTOR); + }); + }); + + describe('one_factor policy', function() { + it('should not allow an anonymous user', function() { + const authorizer = new AuthorizerStub(); + authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); + Assert.throws(() => { CheckAuthorizations(authorizer, "public.example.com", "/index.html", undefined, + undefined, Level.NOT_AUTHENTICATED) }, NotAuthenticatedError); + }); + + it('should allow an authenticated user (1FA)', function() { + const authorizer = new AuthorizerStub(); + authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); + CheckAuthorizations(authorizer, "public.example.com", "/index.html", "john", + ["group1", "group2"], Level.ONE_FACTOR); + }); + + it('should allow an authenticated user (2FA)', function() { + const authorizer = new AuthorizerStub(); + authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); + CheckAuthorizations(authorizer, "public.example.com", "/index.html", "john", + ["group1", "group2"], Level.TWO_FACTOR); + }); + }); + + describe('two_factor policy', function() { + it('should not allow an anonymous user', function() { + const authorizer = new AuthorizerStub(); + authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + Assert.throws(() => CheckAuthorizations(authorizer, "public.example.com", "/index.html", undefined, + undefined, Level.NOT_AUTHENTICATED), NotAuthenticatedError); + }); + + it('should not allow an authenticated user (1FA)', function() { + const authorizer = new AuthorizerStub(); + authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + Assert.throws(() => CheckAuthorizations(authorizer, "public.example.com", "/index.html", "john", + ["group1", "group2"], Level.ONE_FACTOR), NotAuthenticatedError); + }); + + it('should allow an authenticated user (2FA)', function() { + const authorizer = new AuthorizerStub(); + authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); + CheckAuthorizations(authorizer, "public.example.com", "/index.html", "john", + ["group1", "group2"], Level.TWO_FACTOR); + }); + }); + + describe('deny policy', function() { + it('should not allow an anonymous user', function() { + const authorizer = new AuthorizerStub(); + authorizer.authorizationMock.returns(AuthorizationLevel.DENY); + Assert.throws(() => CheckAuthorizations(authorizer, "public.example.com", "/index.html", undefined, + undefined, Level.NOT_AUTHENTICATED), NotAuthenticatedError); + }); + + it('should not allow an authenticated user (1FA)', function() { + const authorizer = new AuthorizerStub(); + authorizer.authorizationMock.returns(AuthorizationLevel.DENY); + Assert.throws(() => CheckAuthorizations(authorizer, "public.example.com", "/index.html", "john", + ["group1", "group2"], Level.ONE_FACTOR), NotAuthorizedError); + }); + + it('should not allow an authenticated user (2FA)', function() { + const authorizer = new AuthorizerStub(); + authorizer.authorizationMock.returns(AuthorizationLevel.DENY); + Assert.throws(() => CheckAuthorizations(authorizer, "public.example.com", "/index.html", "john", + ["group1", "group2"], Level.TWO_FACTOR), NotAuthorizedError); + }); + }); +}); \ No newline at end of file diff --git a/server/src/lib/routes/verify/CheckAuthorizations.ts b/server/src/lib/routes/verify/CheckAuthorizations.ts new file mode 100644 index 00000000..0374a1f4 --- /dev/null +++ b/server/src/lib/routes/verify/CheckAuthorizations.ts @@ -0,0 +1,46 @@ +import * as Util from "util"; + +import Exceptions = require("../../Exceptions"); + +import { Level as AuthorizationLevel } from "../../authorization/Level"; +import { Level as AuthenticationLevel } from "../../authentication/Level"; +import { IAuthorizer } from "../../authorization/IAuthorizer"; + +function isAuthorized( + authorization: AuthorizationLevel, + authentication: AuthenticationLevel): boolean { + + if (authorization == AuthorizationLevel.BYPASS) { + return true; + } else if (authorization == AuthorizationLevel.ONE_FACTOR && + authentication >= AuthenticationLevel.ONE_FACTOR) { + return true; + } else if (authorization == AuthorizationLevel.TWO_FACTOR && + authentication >= AuthenticationLevel.TWO_FACTOR) { + return true; + } + return false; +} + +export default function ( + authorizer: IAuthorizer, + domain: string, resource: string, + user: string, groups: string[], + authenticationLevel: AuthenticationLevel): AuthorizationLevel { + + const authorizationLevel = authorizer + .authorization({domain, resource}, {user, groups}); + + if (authorizationLevel == AuthorizationLevel.BYPASS) { + return authorizationLevel; + } + else if (user && authorizationLevel == AuthorizationLevel.DENY) { + throw new Exceptions.NotAuthorizedError( + Util.format("User %s is not authorized to access %s%s", user, domain, resource)); + } + else if (!isAuthorized(authorizationLevel, authenticationLevel)) { + throw new Exceptions.NotAuthenticatedError(Util.format( + "User '%s' is not sufficiently authorized to access %s%s.", user, domain, resource)); + } + return authorizationLevel; +} \ No newline at end of file diff --git a/server/src/lib/routes/verify/CheckInactivity.spec.ts b/server/src/lib/routes/verify/CheckInactivity.spec.ts new file mode 100644 index 00000000..29656991 --- /dev/null +++ b/server/src/lib/routes/verify/CheckInactivity.spec.ts @@ -0,0 +1,60 @@ +import * as Express from "express"; +import * as ExpressMock from "../../stubs/express.spec"; +import * as Sinon from "sinon"; +import * as Assert from "assert"; +import CheckInactivity from "./CheckInactivity"; +import { AuthenticationSession } from "../../../../types/AuthenticationSession"; +import { Configuration } from "../../configuration/schema/Configuration"; +import { RequestLoggerStub } from "../../logging/RequestLoggerStub.spec"; + + +describe('routes/verify/VerifyInactivity', function() { + let req: Express.Request; + let authSession: AuthenticationSession; + let configuration: Configuration; + let logger: RequestLoggerStub; + + beforeEach(function() { + req = ExpressMock.RequestMock(); + authSession = {} as any; + configuration = { + session: { + domain: 'example.com', + secret: 'abc', + inactivity: 1000, + }, + authentication_backend: { + file: { + path: 'abc' + } + } + } + logger = new RequestLoggerStub(); + }); + + it('should not throw if inactivity timeout is disabled', function() { + delete configuration.session.inactivity; + CheckInactivity(req, authSession, configuration, logger); + }); + + it('should not throw if keep me logged in has been checked', function() { + authSession.keep_me_logged_in = true; + CheckInactivity(req, authSession, configuration, logger); + }); + + it('should not throw if the inactivity timeout has not timed out', function() { + this.clock = Sinon.useFakeTimers(); + authSession.last_activity_datetime = new Date().getTime(); + this.clock.tick(200); + CheckInactivity(req, authSession, configuration, logger); + this.clock.restore(); + }); + + it('should throw if the inactivity timeout has timed out', function() { + this.clock = Sinon.useFakeTimers(); + authSession.last_activity_datetime = new Date().getTime(); + this.clock.tick(2000); + Assert.throws(() => CheckInactivity(req, authSession, configuration, logger)); + this.clock.restore(); + }); +}); \ No newline at end of file diff --git a/server/src/lib/routes/verify/CheckInactivity.ts b/server/src/lib/routes/verify/CheckInactivity.ts new file mode 100644 index 00000000..db61a6fb --- /dev/null +++ b/server/src/lib/routes/verify/CheckInactivity.ts @@ -0,0 +1,31 @@ +import * as Express from "express"; +import { AuthenticationSession } from "AuthenticationSession"; +import { Configuration } from "../../configuration/schema/Configuration"; +import { IRequestLogger } from "../../logging/IRequestLogger"; +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; + +export default function(req: Express.Request, + authSession: AuthenticationSession, + configuration: Configuration, logger: IRequestLogger): void { + + // If inactivity is not specified, then inactivity timeout does not apply + if (!configuration.session.inactivity || authSession.keep_me_logged_in) { + return; + } + + const lastActivityTime = authSession.last_activity_datetime; + const currentTime = new Date().getTime(); + authSession.last_activity_datetime = currentTime; + + const inactivityPeriodMs = currentTime - lastActivityTime; + logger.debug(req, "Inactivity period was %s sec and max period was %s sec.", + inactivityPeriodMs / 1000, configuration.session.inactivity / 1000); + + if (inactivityPeriodMs < configuration.session.inactivity) { + return; + } + + logger.debug(req, "Session has been reset after too long inactivity period."); + AuthenticationSessionHandler.reset(req); + throw new Error("Inactivity period exceeded."); +} diff --git a/server/src/lib/routes/verify/Get.spec.ts b/server/src/lib/routes/verify/Get.spec.ts new file mode 100644 index 00000000..d5d5d5b4 --- /dev/null +++ b/server/src/lib/routes/verify/Get.spec.ts @@ -0,0 +1,96 @@ + +import * as Assert from "assert"; +import * as Express from "express"; +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../types/AuthenticationSession"; +import ExpressMock = require("../../stubs/express.spec"); +import { ServerVariables } from "../../ServerVariables"; +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../ServerVariablesMockBuilder.spec"; +import { HEADER_X_ORIGINAL_URL } from "../../../../../shared/constants"; +import Get from "./Get"; +import { ImportMock } from 'ts-mock-imports'; +import * as GetBasicAuth from "./GetBasicAuth"; +import * as GetSessionCookie from "./GetSessionCookie"; +import { NotAuthorizedError, NotAuthenticatedError } from "../../Exceptions"; + + +describe("routes/verify/get", function () { + let req: Express.Request; + let res: ExpressMock.ResponseMock; + let mocks: ServerVariablesMock; + let vars: ServerVariables; + let authSession: AuthenticationSession; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + req.query = { + redirect: "undefined" + }; + AuthenticationSessionHandler.reset(req as any); + req.headers[HEADER_X_ORIGINAL_URL] = "https://secret.example.com/"; + const s = ServerVariablesMockBuilder.build(false); + mocks = s.mocks; + vars = s.variables; + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + }); + + describe("with basic auth", function () { + it('should allow access to user', async function() { + req.headers['proxy-authorization'] = 'zglfzeljfzelmkj'; + const mock = ImportMock.mockOther(GetBasicAuth, "default", () => Promise.resolve()); + await Get(vars)(req, res as any); + Assert(res.send.calledWithExactly()); + Assert(res.status.calledWithExactly(204)) + mock.restore(); + }); + }); + + describe("with session cookie", function () { + it('should allow access to user', async function() { + const mock = ImportMock.mockOther(GetSessionCookie, "default", () => Promise.resolve()); + await Get(vars)(req, res as any); + Assert(res.send.calledWithExactly()); + Assert(res.status.calledWithExactly(204)) + mock.restore(); + }); + }); + + describe('Deny access', function() { + it('should deny access to user on NotAuthorizedError', async function() { + req.headers['proxy-authorization'] = 'zglfzeljfzelmkj'; + const mock = ImportMock.mockOther(GetBasicAuth, "default", () => Promise.reject(new NotAuthorizedError('No!'))); + await Get(vars)(req, res as any); + Assert(res.status.calledWith(403)); + mock.restore(); + }); + + it('should deny access to user on NotAuthenticatedError', async function() { + req.headers['proxy-authorization'] = 'zglfzeljfzelmkj'; + const mock = ImportMock.mockOther(GetBasicAuth, "default", () => Promise.reject(new NotAuthenticatedError('No!'))); + await Get(vars)(req, res as any); + Assert(res.status.calledWith(401)); + mock.restore(); + }); + + it('should deny access to user on any exception', async function() { + req.headers['proxy-authorization'] = 'zglfzeljfzelmkj'; + const mock = ImportMock.mockOther(GetBasicAuth, "default", () => Promise.reject(new Error('No!'))); + await Get(vars)(req, res as any); + Assert(res.status.calledWith(401)); + mock.restore(); + }); + }) + + describe('Kubernetes ingress controller', function() { + it('should redirect user to login portal', async function() { + req.headers['proxy-authorization'] = 'zglfzeljfzelmkj'; + req.query.rd = 'https://login.example.com/'; + const mock = ImportMock.mockOther(GetBasicAuth, "default", () => Promise.reject(new NotAuthenticatedError('No!'))); + await Get(vars)(req, res as any); + Assert(res.redirect.calledWith('https://login.example.com/')); + mock.restore(); + }); + }); +}); + diff --git a/server/src/lib/routes/verify/Get.ts b/server/src/lib/routes/verify/Get.ts new file mode 100644 index 00000000..b647334d --- /dev/null +++ b/server/src/lib/routes/verify/Get.ts @@ -0,0 +1,71 @@ +import Express = require("express"); +import Exceptions = require("../../Exceptions"); +import ErrorReplies = require("../../ErrorReplies"); +import { ServerVariables } from "../../ServerVariables"; +import GetSessionCookie from "./GetSessionCookie"; +import GetBasicAuth from "./GetBasicAuth"; +import Constants = require("../../../../../shared/constants"); +import { AuthenticationSessionHandler } + from "../../AuthenticationSessionHandler"; +import { AuthenticationSession } + from "../../../../types/AuthenticationSession"; +import GetHeader from "../../utils/GetHeader"; +import HasHeader from "../..//utils/HasHeader"; + + +async function verifyWithSelectedMethod(req: Express.Request, res: Express.Response, + vars: ServerVariables, authSession: AuthenticationSession | undefined) + : Promise { + if (HasHeader(req, Constants.HEADER_PROXY_AUTHORIZATION)) { + await GetBasicAuth(req, res, vars); + } + else { + await GetSessionCookie(req, res, vars, authSession); + } +} + +/** + * The Redirect header is used to set the target URL in the login portal. + * + * @param req The request to extract X-Original-Url from. + * @param res The response to write Redirect header to. + */ +function setRedirectHeader(req: Express.Request, res: Express.Response) { + const originalUrl = GetHeader(req, Constants.HEADER_X_ORIGINAL_URL); + res.set(Constants.HEADER_REDIRECT, originalUrl); +} + +function getRedirectParam(req: Express.Request) { + return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; +} + +export default function (vars: ServerVariables) { + return async function (req: Express.Request, res: Express.Response) + : Promise { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + setRedirectHeader(req, res); + + try { + await verifyWithSelectedMethod(req, res, vars, authSession); + res.status(204); + res.send(); + } catch (err) { + // This redirect parameter is used in Kubernetes to annotate the ingress with + // the url to the authentication portal. + const redirectUrl = getRedirectParam(req); + if (redirectUrl) { + ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err); + return; + } + + if (err instanceof Exceptions.NotAuthorizedError) { + ErrorReplies.replyWithError403(req, res, vars.logger)(err); + } else { + ErrorReplies.replyWithError401(req, res, vars.logger)(err); + } + } + }; +} + diff --git a/server/src/lib/routes/verify/GetBasicAuth.spec.ts b/server/src/lib/routes/verify/GetBasicAuth.spec.ts new file mode 100644 index 00000000..06ad59ee --- /dev/null +++ b/server/src/lib/routes/verify/GetBasicAuth.spec.ts @@ -0,0 +1,65 @@ +import * as Express from "express"; +import { ServerVariables } from "../../ServerVariables"; +import * as ExpressMock from "../../stubs/express.spec"; +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../ServerVariablesMockBuilder.spec"; +import { HEADER_X_ORIGINAL_URL } from "../../../../../shared/constants"; +import { Level } from "../../authorization/Level"; +import GetBasicAuthModule from "./GetBasicAuth"; +import * as CheckAuthorizations from "./CheckAuthorizations"; +import { ImportMock } from 'ts-mock-imports'; +import AssertRejects from "../../utils/AssertRejects"; + +describe('routes/verify/GetBasicAuth', function() { + let req: Express.Request; + let res: ExpressMock.ResponseMock; + let vars: ServerVariables; + let mocks: ServerVariablesMock; + + beforeEach(function() { + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + req.headers[HEADER_X_ORIGINAL_URL] = 'https://secure.example.com'; + const sv = ServerVariablesMockBuilder.build(); + vars = sv.variables; + mocks = sv.mocks; + }) + + it('should fail on invalid format of token', async function() { + req.headers['proxy-authorization'] = 'Basic abc'; + AssertRejects(async () => GetBasicAuthModule(req, res as any, vars)); + }); + + it('should fail decoded token is not of form user:pass', function() { + req.headers['proxy-authorization'] = 'Basic aGVsbG93b3JsZAo='; + AssertRejects(async () => GetBasicAuthModule(req, res as any, vars)); + }); + + it('should fail when credentials are wrong', function() { + req.headers['proxy-authorization'] = 'Basic aGVsbG8xOndvcmxkCg=='; + mocks.usersDatabase.checkUserPasswordStub.rejects(new Error('Bad credentials')); + AssertRejects(async () => await GetBasicAuthModule(req, res as any, vars)); + }); + + it('should fail when authorizations are not sufficient', function() { + req.headers['proxy-authorization'] = 'Basic aGVsbG8xOndvcmxkCg=='; + const mock = ImportMock.mockOther(CheckAuthorizations, 'default', () => { throw new Error('Not enough permissions.')}); + mocks.usersDatabase.checkUserPasswordStub.resolves({ + email: 'john@example.com', + groups: ['group1', 'group2'], + }); + AssertRejects(async () => await GetBasicAuthModule(req, res as any, vars)); + mock.restore(); + }); + + it('should succeed when user is authenticated and authorizations are sufficient', async function() { + req.headers['proxy-authorization'] = 'Basic aGVsbG8xOndvcmxkCg=='; + const mock = ImportMock.mockOther(CheckAuthorizations, 'default', () => Level.TWO_FACTOR); + mocks.usersDatabase.checkUserPasswordStub.resolves({ + email: 'john@example.com', + groups: ['group1', 'group2'], + }); + await GetBasicAuthModule(req, res as any, vars); + mock.restore(); + }); + +}) \ No newline at end of file diff --git a/server/src/lib/routes/verify/GetBasicAuth.ts b/server/src/lib/routes/verify/GetBasicAuth.ts new file mode 100644 index 00000000..f28f1911 --- /dev/null +++ b/server/src/lib/routes/verify/GetBasicAuth.ts @@ -0,0 +1,49 @@ +import Express = require("express"); +import { ServerVariables } from "../../ServerVariables"; +import { URLDecomposer } from "../../utils/URLDecomposer"; +import { Level } from "../../authentication/Level"; +import GetHeader from "../../utils/GetHeader"; +import { HEADER_X_ORIGINAL_URL, HEADER_PROXY_AUTHORIZATION } from "../../../../../shared/constants"; +import setUserAndGroupsHeaders from "./SetUserAndGroupsHeaders"; +import CheckAuthorizations from "./CheckAuthorizations"; +import { Level as AuthorizationLevel } from "../../authorization/Level"; + +export default async function(req: Express.Request, res: Express.Response, + vars: ServerVariables) + : Promise { + const authorizationValue = GetHeader(req, HEADER_PROXY_AUTHORIZATION); + + if (!authorizationValue.startsWith("Basic ")) { + throw new Error("The authorization header should be of the form 'Basic XXXXXX'"); + } + + const base64Re = new RegExp("^Basic ((?:[A-Za-z0-9+/]{4})*" + + "(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)$"); + const isTokenValidBase64 = base64Re.test(authorizationValue); + + if (!isTokenValidBase64) { + throw new Error("No valid base64 token found in the header"); + } + + const tokenMatches = authorizationValue.match(base64Re); + const base64Token = tokenMatches[1]; + const decodedToken = Buffer.from(base64Token, "base64").toString(); + const splittedToken = decodedToken.split(":"); + + if (splittedToken.length != 2) { + throw new Error("The authorization token is invalid. Expecting 'userid:password'"); + } + + const username = splittedToken[0]; + const password = splittedToken[1]; + const groupsAndEmails = await vars.usersDatabase.checkUserPassword(username, password); + + const uri = GetHeader(req, HEADER_X_ORIGINAL_URL); + const urlDecomposition = URLDecomposer.fromUrl(uri); + const authorizationLevel = CheckAuthorizations(vars.authorizer, urlDecomposition.domain, urlDecomposition.path, + username, groupsAndEmails.groups, Level.ONE_FACTOR); + + if (authorizationLevel > AuthorizationLevel.BYPASS) { + setUserAndGroupsHeaders(res, username, groupsAndEmails.groups); + } +} \ No newline at end of file diff --git a/server/src/lib/routes/verify/GetSessionCookie.spec.ts b/server/src/lib/routes/verify/GetSessionCookie.spec.ts new file mode 100644 index 00000000..374e836c --- /dev/null +++ b/server/src/lib/routes/verify/GetSessionCookie.spec.ts @@ -0,0 +1,57 @@ +import * as Express from "express"; +import * as ExpressMock from "../../stubs/express.spec"; +import { ImportMock } from 'ts-mock-imports'; +import * as CheckAuthorizations from "./CheckAuthorizations"; +import * as CheckInactivity from "./CheckInactivity"; +import GetSessionCookie from "./GetSessionCookie"; +import { ServerVariables } from "../../ServerVariables"; +import { ServerVariablesMockBuilder } from "../../ServerVariablesMockBuilder.spec"; +import { AuthenticationSession } from "../../../../types/AuthenticationSession"; +import AssertRejects from "../../utils/AssertRejects"; +import { Level } from "../../authorization/Level"; + + +describe('routes/verify/GetSessionCookie', function() { + let req: Express.Request; + let res: ExpressMock.ResponseMock; + let vars: ServerVariables; + let authSession: AuthenticationSession; + + beforeEach(function() { + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + const sv = ServerVariablesMockBuilder.build(); + vars = sv.variables; + authSession = {} as any; + }); + + it("should fail when target url is not provided", async function() { + AssertRejects(async () => await GetSessionCookie(req, res as any, vars, authSession)); + }); + + it("should not let unauthorized users in", async function() { + req.originalUrl = "https://public.example.com"; + const mock = ImportMock.mockOther(CheckAuthorizations, "default", () => { throw new Error('Not authorized')}); + AssertRejects(async () => await GetSessionCookie(req, res as any, vars, authSession)); + mock.restore(); + }); + + it("should not let authorize user after a long period of inactivity", async function() { + req.originalUrl = "https://public.example.com"; + const checkAuthorizationsMock = ImportMock.mockOther(CheckAuthorizations, "default", () => Level.ONE_FACTOR); + const checkInactivityMock = ImportMock.mockOther(CheckInactivity, "default", () => { throw new Error('Timed out')}); + AssertRejects(async () => await GetSessionCookie(req, res as any, vars, authSession)); + checkInactivityMock.restore(); + checkAuthorizationsMock.restore(); + }); + + it("should let the user in", async function() { + req.headers['x-original-url'] = "https://public.example.com"; + + const checkAuthorizationsMock = ImportMock.mockOther(CheckAuthorizations, "default", () => Level.ONE_FACTOR); + const checkInactivityMock = ImportMock.mockOther(CheckInactivity, "default", () => {}); + await GetSessionCookie(req, res as any, vars, authSession); + checkInactivityMock.restore(); + checkAuthorizationsMock.restore(); + }); +}); \ No newline at end of file diff --git a/server/src/lib/routes/verify/GetSessionCookie.ts b/server/src/lib/routes/verify/GetSessionCookie.ts new file mode 100644 index 00000000..47bc3c27 --- /dev/null +++ b/server/src/lib/routes/verify/GetSessionCookie.ts @@ -0,0 +1,43 @@ +import Express = require("express"); +import { ServerVariables } from "../../ServerVariables"; +import { AuthenticationSession } + from "../../../../types/AuthenticationSession"; +import { URLDecomposer } from "../../utils/URLDecomposer"; +import GetHeader from "../../utils/GetHeader"; +import { + HEADER_X_ORIGINAL_URL, +} from "../../../../../shared/constants"; +import { Level as AuthorizationLevel } from "../../authorization/Level"; +import setUserAndGroupsHeaders from "./SetUserAndGroupsHeaders"; +import CheckAuthorizations from "./CheckAuthorizations"; +import CheckInactivity from "./CheckInactivity"; + + +export default async function (req: Express.Request, res: Express.Response, + vars: ServerVariables, authSession: AuthenticationSession | undefined) + : Promise { + if (!authSession) { + throw new Error("No cookie detected."); + } + + const originalUrl = GetHeader(req, HEADER_X_ORIGINAL_URL); + + if (!originalUrl) { + throw new Error("Cannot detect the original URL from headers."); + } + + const d = URLDecomposer.fromUrl(originalUrl); + + const username = authSession.userid; + const groups = authSession.groups; + + vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", d.domain, + d.path, (username) ? username : "unknown", (groups instanceof Array && groups.length > 0) ? groups.join(",") : "unknown"); + const authorizationLevel = CheckAuthorizations(vars.authorizer, d.domain, d.path, username, groups, + authSession.authentication_level); + + if (authorizationLevel > AuthorizationLevel.BYPASS) { + CheckInactivity(req, authSession, vars.config, vars.logger); + setUserAndGroupsHeaders(res, username, groups); + } +} \ No newline at end of file diff --git a/server/src/lib/routes/verify/SetUserAndGroupsHeaders.spec.ts b/server/src/lib/routes/verify/SetUserAndGroupsHeaders.spec.ts new file mode 100644 index 00000000..f9dd3585 --- /dev/null +++ b/server/src/lib/routes/verify/SetUserAndGroupsHeaders.spec.ts @@ -0,0 +1,13 @@ +import * as ExpressMock from "../../stubs/express.spec"; +import * as Assert from "assert"; +import { HEADER_REMOTE_USER, HEADER_REMOTE_GROUPS } from "../../../../../shared/constants"; +import SetUserAndGroupsHeaders from "./SetUserAndGroupsHeaders"; + +describe("routes/verify/SetUserAndGroupsHeaders", function() { + it('should set the correct headers', function() { + const res = ExpressMock.ResponseMock(); + SetUserAndGroupsHeaders(res as any, "john", ["group1", "group2"]); + Assert(res.setHeader.calledWith(HEADER_REMOTE_USER, "john")); + Assert(res.setHeader.calledWith(HEADER_REMOTE_GROUPS, "group1,group2")); + }) +}) \ No newline at end of file diff --git a/server/src/lib/routes/verify/SetUserAndGroupsHeaders.ts b/server/src/lib/routes/verify/SetUserAndGroupsHeaders.ts new file mode 100644 index 00000000..167ec88d --- /dev/null +++ b/server/src/lib/routes/verify/SetUserAndGroupsHeaders.ts @@ -0,0 +1,7 @@ +import * as Express from "express"; +import { HEADER_REMOTE_USER, HEADER_REMOTE_GROUPS } from "../../../../../shared/constants"; + +export default function(res: Express.Response, username: string | undefined, groups: string[] | undefined) { + if (username) res.setHeader(HEADER_REMOTE_USER, username); + if (groups instanceof Array) res.setHeader(HEADER_REMOTE_GROUPS, groups.join(",")); +} \ No newline at end of file diff --git a/server/src/lib/routes/verify/access_control.ts b/server/src/lib/routes/verify/access_control.ts deleted file mode 100644 index cbf97ab3..00000000 --- a/server/src/lib/routes/verify/access_control.ts +++ /dev/null @@ -1,50 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import Util = require("util"); - -import Exceptions = require("../../Exceptions"); - -import { Level as AuthorizationLevel } from "../../authorization/Level"; -import { Level as AuthenticationLevel } from "../../authentication/Level"; -import { ServerVariables } from "../../ServerVariables"; - -function isAuthorized( - authorization: AuthorizationLevel, - authentication: AuthenticationLevel): boolean { - - if (authorization == AuthorizationLevel.BYPASS) { - return true; - } else if (authorization == AuthorizationLevel.ONE_FACTOR && - authentication >= AuthenticationLevel.ONE_FACTOR) { - return true; - } else if (authorization == AuthorizationLevel.TWO_FACTOR && - authentication >= AuthenticationLevel.TWO_FACTOR) { - return true; - } - return false; -} - -export default function ( - req: Express.Request, - vars: ServerVariables, - domain: string, resource: string, - user: string, groups: string[], - authenticationLevel: AuthenticationLevel) { - - return new BluebirdPromise(function (resolve, reject) { - const authorizationLevel = vars.authorizer - .authorization({domain, resource}, {user, groups}); - - if (!isAuthorized(authorizationLevel, authenticationLevel)) { - if (authorizationLevel == AuthorizationLevel.DENY) { - reject(new Exceptions.NotAuthorizedError( - Util.format("User %s is not authorized to access %s%s", user, domain, resource))); - return; - } - reject(new Exceptions.NotAuthenticatedError(Util.format( - "User '%s' is not sufficiently authorized to access %s%s.", user, domain, resource))); - return; - } - resolve(); - }); -} \ No newline at end of file diff --git a/server/src/lib/routes/verify/get.spec.ts b/server/src/lib/routes/verify/get.spec.ts deleted file mode 100644 index ec6751d3..00000000 --- a/server/src/lib/routes/verify/get.spec.ts +++ /dev/null @@ -1,314 +0,0 @@ - -import * as Assert from "assert"; -import * as Express from "express"; -import * as Sinon from "sinon"; - -import VerifyGet = require("./get"); -import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; -import { AuthenticationSession } from "../../../../types/AuthenticationSession"; -import ExpressMock = require("../../stubs/express.spec"); -import { ServerVariables } from "../../ServerVariables"; -import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../ServerVariablesMockBuilder.spec"; -import { Level } from "../../authentication/Level"; -import { Level as AuthorizationLevel } from "../../authorization/Level"; -import { HEADER_X_ORIGINAL_URL } from "../../../../../shared/constants"; - -describe("routes/verify/get", function () { - let req: Express.Request; - let res: ExpressMock.ResponseMock; - let mocks: ServerVariablesMock; - let vars: ServerVariables; - let authSession: AuthenticationSession; - - beforeEach(function () { - req = ExpressMock.RequestMock(); - res = ExpressMock.ResponseMock(); - req.originalUrl = "/api/xxxx"; - req.query = { - redirect: "undefined" - }; - AuthenticationSessionHandler.reset(req as any); - req.headers[HEADER_X_ORIGINAL_URL] = "https://secret.example.com/"; - const s = ServerVariablesMockBuilder.build(false); - mocks = s.mocks; - vars = s.variables; - authSession = AuthenticationSessionHandler.get(req as any, vars.logger); - }); - - describe("with session cookie", function () { - it("should be already authenticated", async function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - await VerifyGet.default(vars)(req as Express.Request, res as any); - res.setHeader.calledWith("Remote-User", "myuser"); - res.setHeader.calledWith("Remote-Groups", "mygroup,othergroup"); - Assert.equal(204, res.status.getCall(0).args[0]); - }); - - function test_session(_authSession: AuthenticationSession, status_code: number) { - const GetMock = Sinon.stub(AuthenticationSessionHandler, 'get'); - GetMock.returns(_authSession); - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert.equal(status_code, res.status.getCall(0).args[0]); - GetMock.restore(); - }) - } - - function test_non_authenticated_401(_authSession: AuthenticationSession) { - return test_session(_authSession, 401); - } - - function test_unauthorized_403(_authSession: AuthenticationSession) { - return test_session(_authSession, 403); - } - - describe("given user tries to access a 2-factor endpoint", function () { - beforeEach(function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - }); - - describe("given different cases of session", function () { - it("should not be authenticated when second factor is missing", function () { - return test_non_authenticated_401({ - keep_me_logged_in: false, - userid: "user", - authentication_level: Level.ONE_FACTOR, - email: undefined, - groups: [], - last_activity_datetime: new Date().getTime() - }); - }); - - it("should not be authenticated when userid is missing", function () { - return test_non_authenticated_401({ - keep_me_logged_in: false, - userid: undefined, - authentication_level: Level.TWO_FACTOR, - email: undefined, - groups: [], - last_activity_datetime: new Date().getTime() - }); - }); - - it("should not be authenticated when level is insufficient", function () { - return test_non_authenticated_401({ - keep_me_logged_in: false, - userid: "user", - authentication_level: Level.NOT_AUTHENTICATED, - email: undefined, - groups: [], - last_activity_datetime: new Date().getTime() - }); - }); - - it("should not be authenticated when session has not be initiated", function () { - return test_non_authenticated_401(undefined); - }); - - it("should not be authenticated when domain is not allowed for user", function () { - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - req.headers[HEADER_X_ORIGINAL_URL] = "https://test.example.com/"; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.DENY); - - return test_unauthorized_403({ - keep_me_logged_in: false, - authentication_level: Level.TWO_FACTOR, - userid: "user", - groups: ["group1", "group2"], - email: undefined, - last_activity_datetime: new Date().getTime() - }); - }); - }); - }); - - describe("given user tries to access a single factor endpoint", function () { - beforeEach(function () { - req.headers[HEADER_X_ORIGINAL_URL] = "https://redirect.url/"; - }); - - it("should be authenticated when first factor is validated", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); - authSession.authentication_level = Level.ONE_FACTOR; - authSession.userid = "user1"; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWith(204)); - Assert(res.send.calledOnce); - }); - }); - - it("should be rejected with 401 when not authenticated", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); - authSession.authentication_level = Level.NOT_AUTHENTICATED; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWith(401)); - }); - }); - }); - - describe("inactivity period", function () { - it("should update last inactivity period on requests on /api/verify", function () { - mocks.config.session.inactivity = 200000; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - const currentTime = new Date().getTime() - 1000; - AuthenticationSessionHandler.reset(req as any); - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - authSession.last_activity_datetime = currentTime; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - return AuthenticationSessionHandler.get(req as any, vars.logger); - }) - .then(function (authSession) { - Assert(authSession.last_activity_datetime > currentTime); - }); - }); - - it("should reset session when max inactivity period has been reached", function () { - mocks.config.session.inactivity = 1; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - const currentTime = new Date().getTime() - 1000; - AuthenticationSessionHandler.reset(req as any); - authSession.authentication_level = Level.TWO_FACTOR; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - authSession.last_activity_datetime = currentTime; - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - return AuthenticationSessionHandler.get(req as any, vars.logger); - }) - .then(function (authSession) { - Assert.equal(authSession.authentication_level, Level.NOT_AUTHENTICATED); - Assert.equal(authSession.userid, undefined); - }); - }); - }); - }); - - describe("response type 401 | 302", function() { - it("should return error code 401", function() { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( - "Invalid credentials")); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should redirect to provided redirection url", function() { - const REDIRECT_URL = "http://redirection_url.com"; - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( - "Invalid credentials")); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - req.query["rd"] = REDIRECT_URL; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.redirect.calledWithExactly(REDIRECT_URL)); - }); - }); - }); - - describe("with basic auth", function () { - it("should authenticate correctly", async function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.returns({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - await VerifyGet.default(vars)(req as Express.Request, res as any) - res.setHeader.calledWith("Remote-User", "john"); - res.setHeader.calledWith("Remote-Groups", "mygroup,othergroup"); - Assert.equal(204, res.status.getCall(0).args[0]); - }); - - it("should fail when endpoint is protected by two factors", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.config.access_control.rules = [{ - domain: "secret.example.com", - policy: "two_factor" - }]; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when base64 token is not valid", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic i_m*not_a_base64*token"; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when base64 token has not format user:psswd", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzOmJhZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when bad user password is provided", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.rejects(new Error( - "Invalid credentials")); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - - it("should fail when resource is restricted", function () { - mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); - mocks.config.access_control.default_policy = "one_factor"; - mocks.usersDatabase.checkUserPasswordStub.resolves({ - groups: ["mygroup", "othergroup"], - }); - req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; - - return VerifyGet.default(vars)(req as Express.Request, res as any) - .then(function () { - Assert(res.status.calledWithExactly(401)); - }); - }); - }); -}); - diff --git a/server/src/lib/routes/verify/get.ts b/server/src/lib/routes/verify/get.ts deleted file mode 100644 index 40e1b940..00000000 --- a/server/src/lib/routes/verify/get.ts +++ /dev/null @@ -1,104 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import Express = require("express"); -import Exceptions = require("../../Exceptions"); -import ErrorReplies = require("../../ErrorReplies"); -import { ServerVariables } from "../../ServerVariables"; -import GetWithSessionCookieMethod from "./get_session_cookie"; -import GetWithBasicAuthMethod from "./get_basic_auth"; -import Constants = require("../../../../../shared/constants"); -import { AuthenticationSessionHandler } - from "../../AuthenticationSessionHandler"; -import { AuthenticationSession } - from "../../../../types/AuthenticationSession"; -import GetHeader from "../../utils/GetHeader"; - -const REMOTE_USER = "Remote-User"; -const REMOTE_GROUPS = "Remote-Groups"; - - -function verifyWithSelectedMethod(req: Express.Request, res: Express.Response, - vars: ServerVariables, authSession: AuthenticationSession | undefined) - : () => BluebirdPromise<{ username: string, groups: string[] }> { - return function () { - const authorization = GetHeader(req, Constants.HEADER_PROXY_AUTHORIZATION); - if (authorization) { - if (authorization.startsWith("Basic ")) { - return GetWithBasicAuthMethod(req, res, vars, authorization); - } - else { - throw new Error("The authorization header should be of the form 'Basic XXXXXX'"); - } - } - else { - if (authSession) { - return GetWithSessionCookieMethod(req, res, vars, authSession); - } - else { - throw new Error("No cookie detected."); - } - } - }; -} - -function setRedirectHeader(req: Express.Request, res: Express.Response) { - return function () { - const originalUrl = GetHeader(req, Constants.HEADER_X_ORIGINAL_URL); - res.set(Constants.HEADER_REDIRECT, originalUrl); - return BluebirdPromise.resolve(); - }; -} - -function setUserAndGroupsHeaders(res: Express.Response) { - return function (u: { username: string, groups: string[] }) { - res.setHeader(REMOTE_USER, u.username); - res.setHeader(REMOTE_GROUPS, u.groups.join(",")); - return BluebirdPromise.resolve(); - }; -} - -function replyWith200(res: Express.Response) { - return function () { - res.status(204); - res.send(); - }; -} - -function getRedirectParam(req: Express.Request) { - return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : undefined; -} - -export default function (vars: ServerVariables) { - return function (req: Express.Request, res: Express.Response) - : BluebirdPromise { - let authSession: AuthenticationSession | undefined; - return new BluebirdPromise(function (resolve, reject) { - authSession = AuthenticationSessionHandler.get(req, vars.logger); - resolve(); - }) - .then(setRedirectHeader(req, res)) - .then(verifyWithSelectedMethod(req, res, vars, authSession)) - .then(setUserAndGroupsHeaders(res)) - .then(replyWith200(res)) - // The user is authenticated but has restricted access -> 403 - .catch(Exceptions.NotAuthorizedError, - ErrorReplies.replyWithError403(req, res, vars.logger)) - .catch(Exceptions.NotAuthenticatedError, - ErrorReplies.replyWithError401(req, res, vars.logger)) - // The user is not yet authenticated -> 401 - .catch((err) => { - console.error(err); - // This redirect parameter is used in Kubernetes to annotate the ingress with - // the url to the authentication portal. - const redirectUrl = getRedirectParam(req); - if (redirectUrl) { - ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err); - } - else { - ErrorReplies.replyWithError401(req, res, vars.logger)(err); - } - }); - }; -} - diff --git a/server/src/lib/routes/verify/get_basic_auth.ts b/server/src/lib/routes/verify/get_basic_auth.ts deleted file mode 100644 index 73e81063..00000000 --- a/server/src/lib/routes/verify/get_basic_auth.ts +++ /dev/null @@ -1,54 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import { ServerVariables } from "../../ServerVariables"; -import AccessControl from "./access_control"; -import { URLDecomposer } from "../../utils/URLDecomposer"; -import { Level } from "../../authentication/Level"; -import GetHeader from "../../utils/GetHeader"; -import { HEADER_X_ORIGINAL_URL } from "../../../../../shared/constants"; - -export default function (req: Express.Request, res: Express.Response, - vars: ServerVariables, authorizationHeader: string) - : BluebirdPromise<{ username: string, groups: string[] }> { - let username: string; - const uri = GetHeader(req, HEADER_X_ORIGINAL_URL); - const urlDecomposition = URLDecomposer.fromUrl(uri); - - return BluebirdPromise.resolve() - .then(() => { - const base64Re = new RegExp("^Basic ((?:[A-Za-z0-9+/]{4})*" + - "(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)$"); - const isTokenValidBase64 = base64Re.test(authorizationHeader); - - if (!isTokenValidBase64) { - return BluebirdPromise.reject(new Error("No valid base64 token found in the header")); - } - - const tokenMatches = authorizationHeader.match(base64Re); - const base64Token = tokenMatches[1]; - const decodedToken = Buffer.from(base64Token, "base64").toString(); - const splittedToken = decodedToken.split(":"); - - if (splittedToken.length != 2) { - return BluebirdPromise.reject(new Error( - "The authorization token is invalid. Expecting 'userid:password'")); - } - - username = splittedToken[0]; - const password = splittedToken[1]; - return vars.usersDatabase.checkUserPassword(username, password); - }) - .then(function (groupsAndEmails) { - return AccessControl(req, vars, urlDecomposition.domain, urlDecomposition.path, - username, groupsAndEmails.groups, Level.ONE_FACTOR) - .then(() => BluebirdPromise.resolve({ - username: username, - groups: groupsAndEmails.groups - })); - }) - .catch(function (err: Error) { - return BluebirdPromise.reject( - new Error("Unable to authenticate the user with basic auth. Cause: " - + err.message)); - }); -} \ No newline at end of file diff --git a/server/src/lib/routes/verify/get_session_cookie.ts b/server/src/lib/routes/verify/get_session_cookie.ts deleted file mode 100644 index d7bfc04d..00000000 --- a/server/src/lib/routes/verify/get_session_cookie.ts +++ /dev/null @@ -1,74 +0,0 @@ -import Express = require("express"); -import BluebirdPromise = require("bluebird"); -import Exceptions = require("../../Exceptions"); -import { Configuration } from "../../configuration/schema/Configuration"; -import { ServerVariables } from "../../ServerVariables"; -import { IRequestLogger } from "../../logging/IRequestLogger"; -import { AuthenticationSession } - from "../../../../types/AuthenticationSession"; -import { AuthenticationSessionHandler } - from "../../AuthenticationSessionHandler"; -import AccessControl from "./access_control"; -import { URLDecomposer } from "../../utils/URLDecomposer"; -import GetHeader from "../../utils/GetHeader"; -import { HEADER_X_ORIGINAL_URL } from "../../../../../shared/constants"; - -function verify_inactivity(req: Express.Request, - authSession: AuthenticationSession, - configuration: Configuration, logger: IRequestLogger) - : BluebirdPromise { - - // If inactivity is not specified, then inactivity timeout does not apply - if (!configuration.session.inactivity || authSession.keep_me_logged_in) { - return BluebirdPromise.resolve(); - } - - const lastActivityTime = authSession.last_activity_datetime; - const currentTime = new Date().getTime(); - authSession.last_activity_datetime = currentTime; - - const inactivityPeriodMs = currentTime - lastActivityTime; - logger.debug(req, "Inactivity period was %s s and max period was %s.", - inactivityPeriodMs / 1000, configuration.session.inactivity / 1000); - if (inactivityPeriodMs < configuration.session.inactivity) { - return BluebirdPromise.resolve(); - } - - logger.debug(req, "Session has been reset after too long inactivity period."); - AuthenticationSessionHandler.reset(req); - return BluebirdPromise.reject(new Error("Inactivity period exceeded.")); -} - -export default function (req: Express.Request, res: Express.Response, - vars: ServerVariables, authSession: AuthenticationSession) - : BluebirdPromise<{ username: string, groups: string[] }> { - - return BluebirdPromise.resolve() - .then(() => { - const username = authSession.userid; - const groups = authSession.groups; - - if (!authSession.userid) { - return BluebirdPromise.reject(new Exceptions.AccessDeniedError( - "userid is missing")); - } - - const originalUrl = GetHeader(req, HEADER_X_ORIGINAL_URL); - - const d = URLDecomposer.fromUrl(originalUrl); - vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", d.domain, - d.path, username, groups.join(",")); - return AccessControl(req, vars, d.domain, d.path, username, groups, - authSession.authentication_level); - }) - .then(() => { - return verify_inactivity(req, authSession, - vars.config, vars.logger); - }) - .then(() => { - return BluebirdPromise.resolve({ - username: authSession.userid, - groups: authSession.groups - }); - }); -} \ No newline at end of file diff --git a/server/src/lib/utils/AssertRejects.ts b/server/src/lib/utils/AssertRejects.ts new file mode 100644 index 00000000..cca93b63 --- /dev/null +++ b/server/src/lib/utils/AssertRejects.ts @@ -0,0 +1,11 @@ + +export default function(fn: () => Promise, expectedErrorType: any = Error, logs = false): void { + fn().then(() => { + throw new Error("Should reject"); + }, (err: Error) => { + if (!(err instanceof expectedErrorType)) { + throw new Error(`Received error ${typeof err} != Expected error ${expectedErrorType}`); + } + if (logs) console.error(err); + }); +} \ No newline at end of file diff --git a/server/src/lib/utils/GetHeader.spec.ts b/server/src/lib/utils/GetHeader.spec.ts index d2543719..0377f173 100644 --- a/server/src/lib/utils/GetHeader.spec.ts +++ b/server/src/lib/utils/GetHeader.spec.ts @@ -3,7 +3,7 @@ import GetHeader from "./GetHeader"; import { RequestMock } from "../stubs/express.spec"; import * as Assert from "assert"; -describe('GetHeader', function() { +describe('utils/GetHeader', function() { let req: Express.Request; beforeEach(() => { req = RequestMock(); diff --git a/server/src/lib/utils/HasHeader.spec.ts b/server/src/lib/utils/HasHeader.spec.ts new file mode 100644 index 00000000..f435117d --- /dev/null +++ b/server/src/lib/utils/HasHeader.spec.ts @@ -0,0 +1,20 @@ +import * as Express from "express"; +import HasHeader from "./HasHeader"; +import { RequestMock } from "../stubs/express.spec"; +import * as Assert from "assert"; + +describe('utils/HasHeader', function() { + let req: Express.Request; + beforeEach(() => { + req = RequestMock(); + }); + + it('should return the header if it exists', function() { + req.headers["x-target-url"] = 'www.example.com'; + Assert(HasHeader(req, 'x-target-url')); + }); + + it('should return undefined if header does not exist', function() { + Assert(!HasHeader(req, 'x-target-url')); + }); +}); \ No newline at end of file diff --git a/server/src/lib/utils/HasHeader.ts b/server/src/lib/utils/HasHeader.ts new file mode 100644 index 00000000..74ce354d --- /dev/null +++ b/server/src/lib/utils/HasHeader.ts @@ -0,0 +1,12 @@ +import * as Express from "express"; +import * as ObjectPath from "object-path"; + +/** + * + * @param req The express request to extract headers from + * @param header The name of the header to check the existence of. + * @returns true if the header is found, otherwise false. + */ +export default function(req: Express.Request, header: string): boolean { + return ObjectPath.has(req, "headers." + header); +} \ No newline at end of file diff --git a/server/src/lib/web_server/RestApi.ts b/server/src/lib/web_server/RestApi.ts index 1a6ab725..467f40c5 100644 --- a/server/src/lib/web_server/RestApi.ts +++ b/server/src/lib/web_server/RestApi.ts @@ -3,7 +3,7 @@ import Express = require("express"); import FirstFactorPost = require("../routes/firstfactor/post"); import LogoutPost from "../routes/logout/post"; import StateGet from "../routes/state/get"; -import VerifyGet = require("../routes/verify/get"); +import VerifyGet = require("../routes/verify/Get"); import TOTPSignGet = require("../routes/secondfactor/totp/sign/post"); import IdentityCheckMiddleware = require("../IdentityCheckMiddleware"); diff --git a/shared/constants.ts b/shared/constants.ts index 8a1ca26d..72ce3511 100644 --- a/shared/constants.ts +++ b/shared/constants.ts @@ -8,4 +8,7 @@ export const HEADER_X_ORIGINAL_URL = "x-original-url"; export const HEADER_PROXY_AUTHORIZATION = "proxy-authorization"; export const HEADER_REDIRECT = "redirect"; -export const GET_VARIABLE_KEY = "variables"; \ No newline at end of file +export const GET_VARIABLE_KEY = "variables"; + +export const HEADER_REMOTE_USER = "Remote-User"; +export const HEADER_REMOTE_GROUPS = "Remote-Groups"; \ No newline at end of file diff --git a/test/suites/acl-full-bypass/README.md b/test/suites/acl-full-bypass/README.md new file mode 100644 index 00000000..22e53301 --- /dev/null +++ b/test/suites/acl-full-bypass/README.md @@ -0,0 +1,11 @@ +# ACL full bypass suite + +This suite has been created to test Authelia with a bypass policy on all resources + +## Components + +Authelia, nginx, fake webmail for registering devices. + +## Tests + +Check access to secret of multiple domains. \ No newline at end of file diff --git a/test/suites/acl-full-bypass/config.yml b/test/suites/acl-full-bypass/config.yml new file mode 100644 index 00000000..5adfe2f8 --- /dev/null +++ b/test/suites/acl-full-bypass/config.yml @@ -0,0 +1,37 @@ +############################################################### +# Authelia minimal configuration # +############################################################### + +port: 9091 + +logs_level: debug + +authentication_backend: + file: + path: ./test/suites/basic/users_database.test.yml + +session: + secret: unsecure_session_secret + domain: example.com + expiration: 3600000 # 1 hour + inactivity: 300000 # 5 minutes + +storage: + local: + path: /tmp/authelia/db + +access_control: + default_policy: bypass + rules: + - domain: 'public.example.com' + policy: bypass + +notifier: + smtp: + username: test + password: password + secure: false + host: 127.0.0.1 + port: 1025 + sender: admin@example.com + diff --git a/test/suites/acl-full-bypass/environment.ts b/test/suites/acl-full-bypass/environment.ts new file mode 100644 index 00000000..8fa6b2f2 --- /dev/null +++ b/test/suites/acl-full-bypass/environment.ts @@ -0,0 +1,36 @@ +import fs from 'fs'; +import { exec } from "../../helpers/utils/exec"; +import AutheliaServer from "../../helpers/context/AutheliaServer"; +import DockerEnvironment from "../../helpers/context/DockerEnvironment"; + +const autheliaServer = new AutheliaServer(__dirname + '/config.yml'); +const dockerEnv = new DockerEnvironment([ + 'docker-compose.yml', + 'example/compose/nginx/backend/docker-compose.yml', + 'example/compose/nginx/portal/docker-compose.yml', + 'example/compose/smtp/docker-compose.yml', +]) + +async function setup() { + await exec(`cp ${__dirname}/users_database.yml ${__dirname}/users_database.test.yml`); + await exec('mkdir -p /tmp/authelia/db'); + await exec('./example/compose/nginx/portal/render.js ' + (fs.existsSync('.suite') ? '': '--production')); + await dockerEnv.start(); + await autheliaServer.start(); +} + +async function teardown() { + await autheliaServer.stop(); + await dockerEnv.stop(); + await exec('rm -rf /tmp/authelia/db'); +} + +const setup_timeout = 30000; +const teardown_timeout = 30000; + +export { + setup, + setup_timeout, + teardown, + teardown_timeout +}; \ No newline at end of file diff --git a/test/suites/acl-full-bypass/scenarii/BypassPolicy.ts b/test/suites/acl-full-bypass/scenarii/BypassPolicy.ts new file mode 100644 index 00000000..c556ffd0 --- /dev/null +++ b/test/suites/acl-full-bypass/scenarii/BypassPolicy.ts @@ -0,0 +1,23 @@ +import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; +import VerifySecretObserved from "../../../helpers/assertions/VerifySecretObserved"; +import VisitPageAndWaitUrlIs from "../../../helpers/behaviors/VisitPageAndWaitUrlIs"; + +export default function() { + before(async function() { + this.driver = await StartDriver(); + }); + + after(async function () { + await StopDriver(this.driver); + }); + + it('should have access to admin.example.com/secret.html', async function () { + await VisitPageAndWaitUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); + await VerifySecretObserved(this.driver); + }); + + it('should have access to public.example.com/secret.html', async function () { + await VisitPageAndWaitUrlIs(this.driver, "https://public.example.com:8080/secret.html"); + await VerifySecretObserved(this.driver); + }); +} \ No newline at end of file diff --git a/test/suites/acl-full-bypass/test.ts b/test/suites/acl-full-bypass/test.ts new file mode 100644 index 00000000..167a323b --- /dev/null +++ b/test/suites/acl-full-bypass/test.ts @@ -0,0 +1,13 @@ +import AutheliaSuite from "../../helpers/context/AutheliaSuite"; +import { exec } from '../../helpers/utils/exec'; +import BypassPolicy from "./scenarii/BypassPolicy"; + +AutheliaSuite(__dirname, function() { + this.timeout(10000); + + beforeEach(async function() { + await exec(`cp ${__dirname}/users_database.yml ${__dirname}/users_database.test.yml`); + }); + + describe('Bypass policy', BypassPolicy); +}); \ No newline at end of file diff --git a/test/suites/acl-full-bypass/users_database.yml b/test/suites/acl-full-bypass/users_database.yml new file mode 100644 index 00000000..7832e85b --- /dev/null +++ b/test/suites/acl-full-bypass/users_database.yml @@ -0,0 +1,29 @@ +############################################################### +# Users Database # +############################################################### + +# This file can be used if you do not have an LDAP set up. + +# List of users +users: + john: + password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + email: john.doe@authelia.com + groups: + - admins + - dev + + harry: + password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + emails: harry.potter@authelia.com + groups: [] + + bob: + password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + email: bob.dylan@authelia.com + groups: + - dev + + james: + password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + email: james.dean@authelia.com \ No newline at end of file diff --git a/test/suites/basic/config.yml b/test/suites/basic/config.yml index d1516034..9fcd9e81 100644 --- a/test/suites/basic/config.yml +++ b/test/suites/basic/config.yml @@ -42,6 +42,9 @@ access_control: - domain: singlefactor.example.com policy: one_factor + - domain: public.example.com + policy: bypass + - domain: '*.example.com' subject: "group:admins" policy: two_factor diff --git a/test/suites/basic/scenarii/BypassPolicy.ts b/test/suites/basic/scenarii/BypassPolicy.ts new file mode 100644 index 00000000..f63dbe75 --- /dev/null +++ b/test/suites/basic/scenarii/BypassPolicy.ts @@ -0,0 +1,18 @@ +import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; +import VerifySecretObserved from "../../../helpers/assertions/VerifySecretObserved"; +import VisitPageAndWaitUrlIs from "../../../helpers/behaviors/VisitPageAndWaitUrlIs"; + +export default function() { + before(async function() { + this.driver = await StartDriver(); + }); + + after(async function () { + await StopDriver(this.driver); + }); + + it('should have access to public.example.com/secret.html', async function () { + await VisitPageAndWaitUrlIs(this.driver, "https://public.example.com:8080/secret.html"); + await VerifySecretObserved(this.driver); + }); +} \ No newline at end of file diff --git a/test/suites/basic/scenarii/RequiredTwoFactor.ts b/test/suites/basic/scenarii/RequiredTwoFactor.ts index 76b83f6f..a6f831dc 100644 --- a/test/suites/basic/scenarii/RequiredTwoFactor.ts +++ b/test/suites/basic/scenarii/RequiredTwoFactor.ts @@ -4,6 +4,7 @@ import { StartDriver, StopDriver } from '../../../helpers/context/WithDriver'; import VerifyIsSecondFactorStage from '../../../helpers/assertions/VerifyIsSecondFactorStage'; import VisitPage from '../../../helpers/VisitPage'; import FillLoginPageAndClick from '../../../helpers/FillLoginPageAndClick'; +import Logout from '../../../helpers/Logout'; export default function() { describe('User tries to access a page protected by second factor while he only passed first factor', function() { @@ -19,6 +20,7 @@ export default function() { }); after(async function() { + await Logout(this.driver); await StopDriver(this.driver); }); diff --git a/test/suites/basic/test.ts b/test/suites/basic/test.ts index e6f5c870..1ef52c98 100644 --- a/test/suites/basic/test.ts +++ b/test/suites/basic/test.ts @@ -9,6 +9,7 @@ import RequiredTwoFactor from './scenarii/RequiredTwoFactor'; import LogoutRedirectToAlreadyLoggedIn from './scenarii/LogoutRedirectToAlreadyLoggedIn'; import { exec } from '../../helpers/utils/exec'; import TwoFactorAuthentication from "../../helpers/scenarii/TwoFactorAuthentication"; +import BypassPolicy from "./scenarii/BypassPolicy"; AutheliaSuite(__dirname, function() { this.timeout(10000); @@ -18,6 +19,7 @@ AutheliaSuite(__dirname, function() { }); describe('Two-factor authentication', TwoFactorAuthentication()); + describe('Bypass policy', BypassPolicy) describe('Backend protection', BackendProtection); describe('Verify API endpoint', VerifyEndpoint); describe('Bad password', BadPassword); diff --git a/test/suites/short-timeouts/scenarii/Inactivity.ts b/test/suites/short-timeouts/scenarii/Inactivity.ts index a3c1c717..9a0f03a3 100644 --- a/test/suites/short-timeouts/scenarii/Inactivity.ts +++ b/test/suites/short-timeouts/scenarii/Inactivity.ts @@ -5,6 +5,7 @@ import VisitPageAndWaitUrlIs from "../../../helpers/behaviors/VisitPageAndWaitUr import VisitPage from "../../../helpers/VisitPage"; import VerifyUrlIs from "../../../helpers/assertions/VerifyUrlIs"; import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; +import Logout from "../../../helpers/Logout"; export default function(this: Mocha.ISuiteCallbackContext) { this.timeout(20000); @@ -25,7 +26,7 @@ export default function(this: Mocha.ISuiteCallbackContext) { await VerifyUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); await VisitPageAndWaitUrlIs(this.driver, "https://home.example.com:8080/"); await this.driver.sleep(6000); - await this.driver.get("https://admin.example.com:8080/secret.html"); + await VisitPage(this.driver, "https://admin.example.com:8080/secret.html"); await VerifyUrlIs(this.driver, "https://login.example.com:8080/#/?rd=https://admin.example.com:8080/secret.html"); }); @@ -34,15 +35,15 @@ export default function(this: Mocha.ISuiteCallbackContext) { await FillLoginPageWithUserAndPasswordAndClick(this.driver, 'john', 'password', false); await ValidateTotp(this.driver, this.secret); await VerifyUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); - await VisitPageAndWaitUrlIs(this.driver, "https://home.example.com:8080/"); - - await this.driver.sleep(4000); - await this.driver.get("https://admin.example.com:8080/secret.html"); - await this.driver.sleep(2000); - await this.driver.get("https://admin.example.com:8080/secret.html"); await this.driver.sleep(2000); - await this.driver.get("https://admin.example.com:8080/secret.html"); + await VisitPageAndWaitUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); + await this.driver.sleep(2000); + await VisitPageAndWaitUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); + await this.driver.sleep(2000); + await VisitPageAndWaitUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); + await this.driver.sleep(2000); + await VisitPage(this.driver, "https://admin.example.com:8080/secret.html"); await VerifyUrlIs(this.driver, "https://login.example.com:8080/#/?rd=https://admin.example.com:8080/secret.html"); }); @@ -53,7 +54,7 @@ export default function(this: Mocha.ISuiteCallbackContext) { await ValidateTotp(this.driver, this.secret); await VerifyUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); await VisitPageAndWaitUrlIs(this.driver, "https://home.example.com:8080/"); - await this.driver.sleep(6000); + await this.driver.sleep(9000); await VisitPage(this.driver, "https://admin.example.com:8080/secret.html"); await VerifyUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); });