mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
Merge pull request #336 from clems4ever/fix-bypass-policy
Fix bypass policy
This commit is contained in:
commit
92eb897a03
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");
|
||||
|
|
|
@ -9,3 +9,6 @@ export const HEADER_PROXY_AUTHORIZATION = "proxy-authorization";
|
|||
export const HEADER_REDIRECT = "redirect";
|
||||
|
||||
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