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:
Clement Michaud 2019-03-22 16:54:27 +01:00
parent 55f423a6ae
commit 40574bc8ec
40 changed files with 933 additions and 613 deletions

56
package-lock.json generated
View File

@ -482,6 +482,12 @@
"integrity": "sha512-txsii9cwD2OUOPukfPu3Jpoi3CnznBAwRX3JF26EC4p5T6IA8AaL6PBilACyY2fJkk+ydDNo4BJrJOo/OmNaZw==", "integrity": "sha512-txsii9cwD2OUOPukfPu3Jpoi3CnznBAwRX3JF26EC4p5T6IA8AaL6PBilACyY2fJkk+ydDNo4BJrJOo/OmNaZw==",
"dev": true "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": { "@types/query-string": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "http://registry.npmjs.org/@types/query-string/-/query-string-5.1.0.tgz", "resolved": "http://registry.npmjs.org/@types/query-string/-/query-string-5.1.0.tgz",
@ -2524,6 +2530,16 @@
"integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=", "integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=",
"dev": true "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": { "fill-range": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
@ -4185,6 +4201,12 @@
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
"dev": true "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": { "is-path-cwd": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
@ -5141,6 +5163,12 @@
"integrity": "sha1-WuDA6vj+I+AJzQH5iJtCxPY0rxI=", "integrity": "sha1-WuDA6vj+I+AJzQH5iJtCxPY0rxI=",
"dev": true "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": { "moment": {
"version": "2.22.1", "version": "2.22.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz",
@ -7260,6 +7288,28 @@
"ipaddr.js": "1.6.0" "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": { "pseudomap": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
@ -8522,6 +8572,12 @@
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
"dev": true "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": { "ts-node": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-6.0.2.tgz", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-6.0.2.tgz",

View File

@ -113,6 +113,7 @@
"sinon": "^5.0.7", "sinon": "^5.0.7",
"tmp": "0.0.33", "tmp": "0.0.33",
"tree-kill": "^1.2.1", "tree-kill": "^1.2.1",
"ts-mock-imports": "^1.2.3",
"ts-node": "^6.0.1", "ts-node": "^6.0.1",
"tslint": "^5.2.0", "tslint": "^5.2.0",
"typescript": "^2.9.2", "typescript": "^2.9.2",

View File

@ -66,7 +66,7 @@ export class NotAuthenticatedError extends Error {
export class NotAuthorizedError extends Error { export class NotAuthorizedError extends Error {
constructor(message?: string) { constructor(message?: string) {
super(message); super(message);
this.name = "NotAuthanticatedError"; this.name = "NotAuthenticatedError";
(<any>Object).setPrototypeOf(this, NotAuthorizedError.prototype); (<any>Object).setPrototypeOf(this, NotAuthorizedError.prototype);
} }
} }

View File

@ -2,7 +2,7 @@ import { ServerVariables } from "./ServerVariables";
import { Configuration } from "./configuration/schema/Configuration"; import { Configuration } from "./configuration/schema/Configuration";
import { IUsersDatabaseStub } from "./authentication/backends/IUsersDatabaseStub.spec"; 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 { RequestLoggerStub } from "./logging/RequestLoggerStub.spec";
import { NotifierStub } from "./notifiers/NotifierStub.spec"; import { NotifierStub } from "./notifiers/NotifierStub.spec";
import { RegulatorStub } from "./regulation/RegulatorStub.spec"; import { RegulatorStub } from "./regulation/RegulatorStub.spec";

View File

@ -1,5 +1,5 @@
import { ACLConfiguration, ACLRule } from "../configuration/schema/AclConfiguration"; import { ACLConfiguration, ACLRule, ACLPolicy } from "../configuration/schema/AclConfiguration";
import { IAuthorizer } from "./IAuthorizer"; import { IAuthorizer } from "./IAuthorizer";
import { Winston } from "../../../types/Dependencies"; import { Winston } from "../../../types/Dependencies";
import { MultipleDomainMatcher } from "./MultipleDomainMatcher"; import { MultipleDomainMatcher } from "./MultipleDomainMatcher";
@ -60,7 +60,7 @@ export class Authorizer implements IAuthorizer {
.filter(MatchSubject(subject)); .filter(MatchSubject(subject));
} }
private ruleToLevel(policy: string): Level { private ruleToLevel(policy: ACLPolicy): Level {
if (policy == "bypass") { if (policy == "bypass") {
return Level.BYPASS; return Level.BYPASS;
} else if (policy == "one_factor") { } else if (policy == "one_factor") {

View File

@ -4,7 +4,7 @@ import { Level } from "./Level";
import { Object } from "./Object"; import { Object } from "./Object";
import { Subject } from "./Subject"; import { Subject } from "./Subject";
export class AuthorizerStub implements IAuthorizer { export default class AuthorizerStub implements IAuthorizer {
authorizationMock: Sinon.SinonStub; authorizationMock: Sinon.SinonStub;
constructor() { constructor() {

View 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);
});
});
});

View 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;
}

View 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();
});
});

View 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.");
}

View 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();
});
});
});

View 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);
}
}
};
}

View 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();
});
})

View 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);
}
}

View 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();
});
});

View 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);
}
}

View 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"));
})
})

View 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(","));
}

View File

@ -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();
});
}

View File

@ -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));
});
});
});
});

View File

@ -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);
}
});
};
}

View File

@ -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));
});
}

View File

@ -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
});
});
}

View 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);
});
}

View File

@ -3,7 +3,7 @@ import GetHeader from "./GetHeader";
import { RequestMock } from "../stubs/express.spec"; import { RequestMock } from "../stubs/express.spec";
import * as Assert from "assert"; import * as Assert from "assert";
describe('GetHeader', function() { describe('utils/GetHeader', function() {
let req: Express.Request; let req: Express.Request;
beforeEach(() => { beforeEach(() => {
req = RequestMock(); req = RequestMock();

View 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'));
});
});

View 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);
}

View File

@ -3,7 +3,7 @@ import Express = require("express");
import FirstFactorPost = require("../routes/firstfactor/post"); import FirstFactorPost = require("../routes/firstfactor/post");
import LogoutPost from "../routes/logout/post"; import LogoutPost from "../routes/logout/post";
import StateGet from "../routes/state/get"; 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 TOTPSignGet = require("../routes/secondfactor/totp/sign/post");
import IdentityCheckMiddleware = require("../IdentityCheckMiddleware"); import IdentityCheckMiddleware = require("../IdentityCheckMiddleware");

View File

@ -8,4 +8,7 @@ export const HEADER_X_ORIGINAL_URL = "x-original-url";
export const HEADER_PROXY_AUTHORIZATION = "proxy-authorization"; export const HEADER_PROXY_AUTHORIZATION = "proxy-authorization";
export const HEADER_REDIRECT = "redirect"; 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";

View 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.

View 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

View 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
};

View 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);
});
}

View 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);
});

View 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

View File

@ -42,6 +42,9 @@ access_control:
- domain: singlefactor.example.com - domain: singlefactor.example.com
policy: one_factor policy: one_factor
- domain: public.example.com
policy: bypass
- domain: '*.example.com' - domain: '*.example.com'
subject: "group:admins" subject: "group:admins"
policy: two_factor policy: two_factor

View 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);
});
}

View File

@ -4,6 +4,7 @@ import { StartDriver, StopDriver } from '../../../helpers/context/WithDriver';
import VerifyIsSecondFactorStage from '../../../helpers/assertions/VerifyIsSecondFactorStage'; import VerifyIsSecondFactorStage from '../../../helpers/assertions/VerifyIsSecondFactorStage';
import VisitPage from '../../../helpers/VisitPage'; import VisitPage from '../../../helpers/VisitPage';
import FillLoginPageAndClick from '../../../helpers/FillLoginPageAndClick'; import FillLoginPageAndClick from '../../../helpers/FillLoginPageAndClick';
import Logout from '../../../helpers/Logout';
export default function() { export default function() {
describe('User tries to access a page protected by second factor while he only passed first factor', 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() { after(async function() {
await Logout(this.driver);
await StopDriver(this.driver); await StopDriver(this.driver);
}); });

View File

@ -9,6 +9,7 @@ import RequiredTwoFactor from './scenarii/RequiredTwoFactor';
import LogoutRedirectToAlreadyLoggedIn from './scenarii/LogoutRedirectToAlreadyLoggedIn'; import LogoutRedirectToAlreadyLoggedIn from './scenarii/LogoutRedirectToAlreadyLoggedIn';
import { exec } from '../../helpers/utils/exec'; import { exec } from '../../helpers/utils/exec';
import TwoFactorAuthentication from "../../helpers/scenarii/TwoFactorAuthentication"; import TwoFactorAuthentication from "../../helpers/scenarii/TwoFactorAuthentication";
import BypassPolicy from "./scenarii/BypassPolicy";
AutheliaSuite(__dirname, function() { AutheliaSuite(__dirname, function() {
this.timeout(10000); this.timeout(10000);
@ -18,6 +19,7 @@ AutheliaSuite(__dirname, function() {
}); });
describe('Two-factor authentication', TwoFactorAuthentication()); describe('Two-factor authentication', TwoFactorAuthentication());
describe('Bypass policy', BypassPolicy)
describe('Backend protection', BackendProtection); describe('Backend protection', BackendProtection);
describe('Verify API endpoint', VerifyEndpoint); describe('Verify API endpoint', VerifyEndpoint);
describe('Bad password', BadPassword); describe('Bad password', BadPassword);

View File

@ -5,6 +5,7 @@ import VisitPageAndWaitUrlIs from "../../../helpers/behaviors/VisitPageAndWaitUr
import VisitPage from "../../../helpers/VisitPage"; import VisitPage from "../../../helpers/VisitPage";
import VerifyUrlIs from "../../../helpers/assertions/VerifyUrlIs"; import VerifyUrlIs from "../../../helpers/assertions/VerifyUrlIs";
import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver";
import Logout from "../../../helpers/Logout";
export default function(this: Mocha.ISuiteCallbackContext) { export default function(this: Mocha.ISuiteCallbackContext) {
this.timeout(20000); 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 VerifyUrlIs(this.driver, "https://admin.example.com:8080/secret.html");
await VisitPageAndWaitUrlIs(this.driver, "https://home.example.com:8080/"); await VisitPageAndWaitUrlIs(this.driver, "https://home.example.com:8080/");
await this.driver.sleep(6000); 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"); 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 FillLoginPageWithUserAndPasswordAndClick(this.driver, 'john', 'password', false);
await ValidateTotp(this.driver, this.secret); await ValidateTotp(this.driver, this.secret);
await VerifyUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); 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.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"); 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 ValidateTotp(this.driver, this.secret);
await VerifyUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); await VerifyUrlIs(this.driver, "https://admin.example.com:8080/secret.html");
await VisitPageAndWaitUrlIs(this.driver, "https://home.example.com:8080/"); 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 VisitPage(this.driver, "https://admin.example.com:8080/secret.html");
await VerifyUrlIs(this.driver, "https://admin.example.com:8080/secret.html"); await VerifyUrlIs(this.driver, "https://admin.example.com:8080/secret.html");
}); });