Merge pull request #336 from clems4ever/fix-bypass-policy

Fix bypass policy
pull/340/head
Clément Michaud 2019-03-23 09:20:41 +01:00 committed by GitHub
commit 92eb897a03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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==",
"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",

View File

@ -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",

View File

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

View File

@ -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";

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 { 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") {

View File

@ -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() {

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 * as Assert from "assert";
describe('GetHeader', function() {
describe('utils/GetHeader', function() {
let req: Express.Request;
beforeEach(() => {
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 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");

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_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
policy: one_factor
- domain: public.example.com
policy: bypass
- domain: '*.example.com'
subject: "group:admins"
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 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);
});

View File

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

View File

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