mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
Fix the bypass strategy.
Before this fix an anonymous user was not able to access a resource that were configured with a bypass policy. This was due to a useless check of the userid in the auth session. Moreover, in the case of an anonymous user, we should not check the inactivity period since there is no session. Also refactor /verify endpoint for better testability and add tests in a new suite.
This commit is contained in:
parent
55f423a6ae
commit
40574bc8ec
56
package-lock.json
generated
56
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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";
|
||||
(<any>Object).setPrototypeOf(this, NotAuthorizedError.prototype);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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() {
|
||||
|
|
100
server/src/lib/routes/verify/CheckAuthorizations.spec.ts
Normal file
100
server/src/lib/routes/verify/CheckAuthorizations.spec.ts
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
46
server/src/lib/routes/verify/CheckAuthorizations.ts
Normal file
46
server/src/lib/routes/verify/CheckAuthorizations.ts
Normal file
|
@ -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;
|
||||
}
|
60
server/src/lib/routes/verify/CheckInactivity.spec.ts
Normal file
60
server/src/lib/routes/verify/CheckInactivity.spec.ts
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
31
server/src/lib/routes/verify/CheckInactivity.ts
Normal file
31
server/src/lib/routes/verify/CheckInactivity.ts
Normal file
|
@ -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.");
|
||||
}
|
96
server/src/lib/routes/verify/Get.spec.ts
Normal file
96
server/src/lib/routes/verify/Get.spec.ts
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
71
server/src/lib/routes/verify/Get.ts
Normal file
71
server/src/lib/routes/verify/Get.ts
Normal file
|
@ -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<void> {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
65
server/src/lib/routes/verify/GetBasicAuth.spec.ts
Normal file
65
server/src/lib/routes/verify/GetBasicAuth.spec.ts
Normal file
|
@ -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();
|
||||
});
|
||||
|
||||
})
|
49
server/src/lib/routes/verify/GetBasicAuth.ts
Normal file
49
server/src/lib/routes/verify/GetBasicAuth.ts
Normal file
|
@ -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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
57
server/src/lib/routes/verify/GetSessionCookie.spec.ts
Normal file
57
server/src/lib/routes/verify/GetSessionCookie.spec.ts
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
43
server/src/lib/routes/verify/GetSessionCookie.ts
Normal file
43
server/src/lib/routes/verify/GetSessionCookie.ts
Normal file
|
@ -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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
13
server/src/lib/routes/verify/SetUserAndGroupsHeaders.spec.ts
Normal file
13
server/src/lib/routes/verify/SetUserAndGroupsHeaders.spec.ts
Normal file
|
@ -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"));
|
||||
})
|
||||
})
|
7
server/src/lib/routes/verify/SetUserAndGroupsHeaders.ts
Normal file
7
server/src/lib/routes/verify/SetUserAndGroupsHeaders.ts
Normal file
|
@ -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(","));
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
}
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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<void> {
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
@ -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));
|
||||
});
|
||||
}
|
|
@ -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<void> {
|
||||
|
||||
// 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
|
||||
});
|
||||
});
|
||||
}
|
11
server/src/lib/utils/AssertRejects.ts
Normal file
11
server/src/lib/utils/AssertRejects.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
|
||||
export default function<T>(fn: () => Promise<T>, 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);
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
|
|
20
server/src/lib/utils/HasHeader.spec.ts
Normal file
20
server/src/lib/utils/HasHeader.spec.ts
Normal file
|
@ -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'));
|
||||
});
|
||||
});
|
12
server/src/lib/utils/HasHeader.ts
Normal file
12
server/src/lib/utils/HasHeader.ts
Normal file
|
@ -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<Express.Request>(req, "headers." + header);
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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";
|
||||
export const GET_VARIABLE_KEY = "variables";
|
||||
|
||||
export const HEADER_REMOTE_USER = "Remote-User";
|
||||
export const HEADER_REMOTE_GROUPS = "Remote-Groups";
|
11
test/suites/acl-full-bypass/README.md
Normal file
11
test/suites/acl-full-bypass/README.md
Normal file
|
@ -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.
|
37
test/suites/acl-full-bypass/config.yml
Normal file
37
test/suites/acl-full-bypass/config.yml
Normal file
|
@ -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
|
||||
|
36
test/suites/acl-full-bypass/environment.ts
Normal file
36
test/suites/acl-full-bypass/environment.ts
Normal file
|
@ -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
|
||||
};
|
23
test/suites/acl-full-bypass/scenarii/BypassPolicy.ts
Normal file
23
test/suites/acl-full-bypass/scenarii/BypassPolicy.ts
Normal file
|
@ -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);
|
||||
});
|
||||
}
|
13
test/suites/acl-full-bypass/test.ts
Normal file
13
test/suites/acl-full-bypass/test.ts
Normal file
|
@ -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);
|
||||
});
|
29
test/suites/acl-full-bypass/users_database.yml
Normal file
29
test/suites/acl-full-bypass/users_database.yml
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
18
test/suites/basic/scenarii/BypassPolicy.ts
Normal file
18
test/suites/basic/scenarii/BypassPolicy.ts
Normal file
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user