Fix endpoints redirection on errors

From this commit on, api endpoints reply with a 401 error code and non api
endpoints redirect to /error/40X.

This commit also fixes missing restrictions on /loggedin (the "already logged
in page). This was not a security issue, though.

The change also makes error pages automatically redirect the user after few
seconds based on the referrer or the default_redirection_url if provided in the
configuration.

Warning: The old /verify endpoint of the REST API has moved to /api/verify.
You will need to update your nginx configuration to take this change into
account.
pull/187/head
Clement Michaud 2017-11-01 14:24:18 +01:00
parent 837884ef0d
commit 6b78240d39
25 changed files with 364 additions and 144 deletions

View File

@ -86,7 +86,7 @@ http {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header Content-Length ""; proxy_set_header Content-Length "";
proxy_pass http://authelia/verify; proxy_pass http://authelia/api/verify;
} }
location / { location / {
@ -143,7 +143,7 @@ http {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header Content-Length ""; proxy_set_header Content-Length "";
proxy_pass http://authelia/verify; proxy_pass http://authelia/api/verify;
} }
location / { location / {
@ -183,7 +183,7 @@ http {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header Content-Length ""; proxy_set_header Content-Length "";
proxy_pass http://authelia/verify; proxy_pass http://authelia/api/verify;
} }
location / { location / {
@ -223,7 +223,7 @@ http {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header Content-Length ""; proxy_set_header Content-Length "";
proxy_pass http://authelia/verify; proxy_pass http://authelia/api/verify;
} }
location / { location / {
@ -263,7 +263,7 @@ http {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header Content-Length ""; proxy_set_header Content-Length "";
proxy_pass http://authelia/verify; proxy_pass http://authelia/api/verify;
} }
location / { location / {

View File

@ -5,10 +5,17 @@ import { IRequestLogger } from "./logging/IRequestLogger";
function replyWithError(req: express.Request, res: express.Response, function replyWithError(req: express.Request, res: express.Response,
code: number, logger: IRequestLogger, body?: Object): (err: Error) => void { code: number, logger: IRequestLogger, body?: Object): (err: Error) => void {
return function (err: Error): void { return function (err: Error): void {
logger.error(req, "Reply with error %d: %s", code, err.message); if (req.originalUrl.startsWith("/api/") || code == 200) {
logger.debug(req, "%s", err.stack); logger.error(req, "Reply with error %d: %s", code, err.message);
res.status(code); logger.debug(req, "%s", err.stack);
res.send(body); res.status(code);
res.send(body);
}
else {
logger.error(req, "Redirect to error %d: %s", code, err.message);
logger.debug(req, "%s", err.stack);
res.redirect("/error/" + code);
}
}; };
} }

View File

@ -7,69 +7,84 @@ import Exceptions = require("./Exceptions");
import fs = require("fs"); import fs = require("fs");
import ejs = require("ejs"); import ejs = require("ejs");
import { IUserDataStore } from "./storage/IUserDataStore"; import { IUserDataStore } from "./storage/IUserDataStore";
import express = require("express"); import Express = require("express");
import ErrorReplies = require("./ErrorReplies"); import ErrorReplies = require("./ErrorReplies");
import { AuthenticationSessionHandler } from "./AuthenticationSessionHandler"; import { AuthenticationSessionHandler } from "./AuthenticationSessionHandler";
import { AuthenticationSession } from "../../types/AuthenticationSession"; import { AuthenticationSession } from "../../types/AuthenticationSession";
import { ServerVariables } from "./ServerVariables"; import { ServerVariables } from "./ServerVariables";
import Identity = require("../../types/Identity"); import Identity = require("../../types/Identity");
import { IdentityValidationDocument } from "./storage/IdentityValidationDocument"; import { IdentityValidationDocument }
from "./storage/IdentityValidationDocument";
const filePath = __dirname + "/../resources/email-template.ejs"; const filePath = __dirname + "/../resources/email-template.ejs";
const email_template = fs.readFileSync(filePath, "utf8"); const email_template = fs.readFileSync(filePath, "utf8");
// IdentityValidator allows user to go through a identity validation process in two steps: // IdentityValidator allows user to go through a identity validation process
// in two steps:
// - Request an operation to be performed (password reset, registration). // - Request an operation to be performed (password reset, registration).
// - Confirm operation with email. // - Confirm operation with email.
export interface IdentityValidable { export interface IdentityValidable {
challenge(): string; challenge(): string;
preValidationInit(req: express.Request): BluebirdPromise<Identity.Identity>; preValidationInit(req: Express.Request): BluebirdPromise<Identity.Identity>;
postValidationInit(req: express.Request): BluebirdPromise<void>; postValidationInit(req: Express.Request): BluebirdPromise<void>;
// Serves a page after identity check request // Serves a page after identity check request
preValidationResponse(req: express.Request, res: express.Response): void; preValidationResponse(req: Express.Request, res: Express.Response): void;
// Serves the page if identity validated // Serves the page if identity validated
postValidationResponse(req: express.Request, res: express.Response): void; postValidationResponse(req: Express.Request, res: Express.Response): void;
mailSubject(): string; mailSubject(): string;
} }
function createAndSaveToken(userid: string, challenge: string, userDataStore: IUserDataStore) function createAndSaveToken(userid: string, challenge: string,
userDataStore: IUserDataStore)
: BluebirdPromise<string> { : BluebirdPromise<string> {
const five_minutes = 4 * 60 * 1000; const five_minutes = 4 * 60 * 1000;
const token = randomstring.generate({ length: 64 }); const token = randomstring.generate({ length: 64 });
const that = this; const that = this;
return userDataStore.produceIdentityValidationToken(userid, token, challenge, five_minutes) return userDataStore.produceIdentityValidationToken(userid, token, challenge,
five_minutes)
.then(function () { .then(function () {
return BluebirdPromise.resolve(token); return BluebirdPromise.resolve(token);
}); });
} }
function consumeToken(token: string, challenge: string, userDataStore: IUserDataStore) function consumeToken(token: string, challenge: string,
userDataStore: IUserDataStore)
: BluebirdPromise<IdentityValidationDocument> { : BluebirdPromise<IdentityValidationDocument> {
return userDataStore.consumeIdentityValidationToken(token, challenge); return userDataStore.consumeIdentityValidationToken(token, challenge);
} }
export function register(app: express.Application, pre_validation_endpoint: string, export function register(app: Express.Application,
post_validation_endpoint: string, handler: IdentityValidable, vars: ServerVariables) { pre_validation_endpoint: string,
app.get(pre_validation_endpoint, get_start_validation(handler, post_validation_endpoint, vars)); post_validation_endpoint: string,
app.get(post_validation_endpoint, get_finish_validation(handler, vars)); handler: IdentityValidable,
vars: ServerVariables) {
app.get(pre_validation_endpoint,
get_start_validation(handler, post_validation_endpoint, vars));
app.get(post_validation_endpoint,
get_finish_validation(handler, vars));
} }
function checkIdentityToken(req: express.Request, identityToken: string): BluebirdPromise<void> { function checkIdentityToken(req: Express.Request, identityToken: string)
: BluebirdPromise<void> {
if (!identityToken) if (!identityToken)
return BluebirdPromise.reject(new Exceptions.AccessDeniedError("No identity token provided")); return BluebirdPromise.reject(
new Exceptions.AccessDeniedError("No identity token provided"));
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
} }
export function get_finish_validation(handler: IdentityValidable, export function get_finish_validation(handler: IdentityValidable,
vars: ServerVariables) vars: ServerVariables)
: express.RequestHandler { : Express.RequestHandler {
return function (req: express.Request, res: express.Response): BluebirdPromise<void> { return function (req: Express.Request, res: Express.Response)
: BluebirdPromise<void> {
let authSession: AuthenticationSession; let authSession: AuthenticationSession;
const identityToken = objectPath.get<express.Request, string>(req, "query.identity_token"); const identityToken = objectPath.get<Express.Request, string>(
req, "query.identity_token");
vars.logger.debug(req, "Identity token provided is %s", identityToken); vars.logger.debug(req, "Identity token provided is %s", identityToken);
return checkIdentityToken(req, identityToken) return checkIdentityToken(req, identityToken)
@ -78,7 +93,8 @@ export function get_finish_validation(handler: IdentityValidable,
return handler.postValidationInit(req); return handler.postValidationInit(req);
}) })
.then(function () { .then(function () {
return consumeToken(identityToken, handler.challenge(), vars.userDataStore); return consumeToken(identityToken, handler.challenge(),
vars.userDataStore);
}) })
.then(function (doc: IdentityValidationDocument) { .then(function (doc: IdentityValidationDocument) {
authSession.identity_check = { authSession.identity_check = {
@ -95,8 +111,9 @@ export function get_finish_validation(handler: IdentityValidable,
export function get_start_validation(handler: IdentityValidable, export function get_start_validation(handler: IdentityValidable,
postValidationEndpoint: string, postValidationEndpoint: string,
vars: ServerVariables) vars: ServerVariables)
: express.RequestHandler { : Express.RequestHandler {
return function (req: express.Request, res: express.Response): BluebirdPromise<void> { return function (req: Express.Request, res: Express.Response)
: BluebirdPromise<void> {
let identity: Identity.Identity; let identity: Identity.Identity;
return handler.preValidationInit(req) return handler.preValidationInit(req)
@ -104,20 +121,24 @@ export function get_start_validation(handler: IdentityValidable,
identity = id; identity = id;
const email = identity.email; const email = identity.email;
const userid = identity.userid; const userid = identity.userid;
vars.logger.info(req, "Start identity validation of user \"%s\"", userid); vars.logger.info(req, "Start identity validation of user \"%s\"",
userid);
if (!(email && userid)) if (!(email && userid))
return BluebirdPromise.reject(new Exceptions.IdentityError( return BluebirdPromise.reject(new Exceptions.IdentityError(
"Missing user id or email address")); "Missing user id or email address"));
return createAndSaveToken(userid, handler.challenge(), vars.userDataStore); return createAndSaveToken(userid, handler.challenge(),
vars.userDataStore);
}) })
.then(function (token: string) { .then(function (token: string) {
const host = req.get("Host"); const host = req.get("Host");
const link_url = util.format("https://%s%s?identity_token=%s", host, const link_url = util.format("https://%s%s?identity_token=%s", host,
postValidationEndpoint, token); postValidationEndpoint, token);
vars.logger.info(req, "Notification sent to user \"%s\"", identity.userid); vars.logger.info(req, "Notification sent to user \"%s\"",
return vars.notifier.notify(identity.email, handler.mailSubject(), link_url); identity.userid);
return vars.notifier.notify(identity.email, handler.mailSubject(),
link_url);
}) })
.then(function () { .then(function () {
handler.preValidationResponse(req, res); handler.preValidationResponse(req, res);

View File

@ -1,8 +1,15 @@
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import express = require("express"); import express = require("express");
import redirector from "../redirector";
import { ServerVariables } from "../../../ServerVariables";
export default function (req: express.Request, res: express.Response): BluebirdPromise<void> { export default function (vars: ServerVariables) {
res.render("errors/401"); return function (req: express.Request, res: express.Response): BluebirdPromise<void> {
const redirectionUrl = redirector(req, vars);
res.render("errors/401", {
redirection_url: redirectionUrl
});
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
};
} }

View File

@ -1,8 +1,15 @@
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import express = require("express"); import express = require("express");
import redirector from "../redirector";
import { ServerVariables } from "../../../ServerVariables";
export default function (req: express.Request, res: express.Response): BluebirdPromise<void> { export default function (vars: ServerVariables) {
res.render("errors/403"); return function (req: express.Request, res: express.Response): BluebirdPromise<void> {
const redirectionUrl = redirector(req, vars);
res.render("errors/403", {
redirection_url: redirectionUrl
});
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
};
} }

View File

@ -0,0 +1,13 @@
import Express = require("express");
import { ServerVariables } from "../../ServerVariables";
export default function (req: Express.Request, vars: ServerVariables): string {
let redirectionUrl: string;
if (req.headers && req.headers["referer"])
redirectionUrl = "" + req.headers["referer"];
else if (vars.config.default_redirection_url)
redirectionUrl = vars.config.default_redirection_url;
return redirectionUrl;
}

View File

@ -101,17 +101,21 @@ function setupU2f(app: Express.Application, vars: ServerVariables) {
} }
function setupResetPassword(app: Express.Application, vars: ServerVariables) { function setupResetPassword(app: Express.Application, vars: ServerVariables) {
IdentityCheckMiddleware.register(app, Endpoints.RESET_PASSWORD_IDENTITY_START_GET, IdentityCheckMiddleware.register(app,
Endpoints.RESET_PASSWORD_IDENTITY_START_GET,
Endpoints.RESET_PASSWORD_IDENTITY_FINISH_GET, Endpoints.RESET_PASSWORD_IDENTITY_FINISH_GET,
new ResetPasswordIdentityHandler(vars.logger, vars.ldapEmailsRetriever), vars); new ResetPasswordIdentityHandler(vars.logger, vars.ldapEmailsRetriever),
vars);
app.get(Endpoints.RESET_PASSWORD_REQUEST_GET, ResetPasswordRequestPost.default); app.get(Endpoints.RESET_PASSWORD_REQUEST_GET,
app.post(Endpoints.RESET_PASSWORD_FORM_POST, ResetPasswordFormPost.default(vars)); ResetPasswordRequestPost.default);
app.post(Endpoints.RESET_PASSWORD_FORM_POST,
ResetPasswordFormPost.default(vars));
} }
function setupErrors(app: Express.Application) { function setupErrors(app: Express.Application, vars: ServerVariables) {
app.get(Endpoints.ERROR_401_GET, Error401Get.default); app.get(Endpoints.ERROR_401_GET, Error401Get.default(vars));
app.get(Endpoints.ERROR_403_GET, Error403Get.default); app.get(Endpoints.ERROR_403_GET, Error403Get.default(vars));
app.get(Endpoints.ERROR_404_GET, Error404Get.default); app.get(Endpoints.ERROR_404_GET, Error404Get.default);
} }
@ -124,6 +128,7 @@ export class RestApi {
vars.config.authentication_methods), vars.config.authentication_methods),
RequireValidatedFirstFactor.middleware(vars.logger), RequireValidatedFirstFactor.middleware(vars.logger),
SecondFactorGet.default(vars)); SecondFactorGet.default(vars));
app.get(Endpoints.LOGOUT_GET, LogoutGet.default); app.get(Endpoints.LOGOUT_GET, LogoutGet.default);
app.get(Endpoints.VERIFY_GET, VerifyGet.default(vars)); app.get(Endpoints.VERIFY_GET, VerifyGet.default(vars));
@ -132,8 +137,10 @@ export class RestApi {
setupTotp(app, vars); setupTotp(app, vars);
setupU2f(app, vars); setupU2f(app, vars);
setupResetPassword(app, vars); setupResetPassword(app, vars);
setupErrors(app); setupErrors(app, vars);
app.get(Endpoints.LOGGED_IN, LoggedIn.default(vars)); app.get(Endpoints.LOGGED_IN,
RequireValidatedFirstFactor.middleware(vars.logger),
LoggedIn.default(vars));
} }
} }

View File

@ -8,4 +8,9 @@ block form-header
block content block content
img(class="header-img" src="/img/warning.png" alt="warning") img(class="header-img" src="/img/warning.png" alt="warning")
p Please <a href="/">log in</a> to access this resource. if redirection_url
p You are not authorized to access this resource.<br/><br/>
| Please click <a href=#{redirection_url}>here</a> if you are not
| redirected in few seconds.
else
p You are not authorized to access this resource.

View File

@ -8,4 +8,9 @@ block form-header
block content block content
img(class="header-img" src="/img/warning.png" alt="warning") img(class="header-img" src="/img/warning.png" alt="warning")
p You are not authorized to access this resource. if redirection_url
p You don't have enough privileges to access this resource.<br/><br/>
| Please click <a href=#{redirection_url}>here</a> if you are not
| redirected in few seconds.
else
p You don't have enough privileges to access this resource.

View File

@ -1,7 +1,8 @@
import sinon = require("sinon"); import sinon = require("sinon");
import IdentityValidator = require("../src/lib/IdentityCheckMiddleware"); import IdentityValidator = require("../src/lib/IdentityCheckMiddleware");
import { AuthenticationSessionHandler } from "../src/lib/AuthenticationSessionHandler"; import { AuthenticationSessionHandler }
from "../src/lib/AuthenticationSessionHandler";
import { AuthenticationSession } from "../types/AuthenticationSession"; import { AuthenticationSession } from "../types/AuthenticationSession";
import { UserDataStore } from "../src/lib/storage/UserDataStore"; import { UserDataStore } from "../src/lib/storage/UserDataStore";
import exceptions = require("../src/lib/Exceptions"); import exceptions = require("../src/lib/Exceptions");
@ -13,7 +14,8 @@ import ExpressMock = require("./mocks/express");
import NotifierMock = require("./mocks/Notifier"); import NotifierMock = require("./mocks/Notifier");
import IdentityValidatorMock = require("./mocks/IdentityValidator"); import IdentityValidatorMock = require("./mocks/IdentityValidator");
import { RequestLoggerStub } from "./mocks/RequestLoggerStub"; import { RequestLoggerStub } from "./mocks/RequestLoggerStub";
import { ServerVariablesMock, ServerVariablesMockBuilder } from "./mocks/ServerVariablesMockBuilder"; import { ServerVariablesMock, ServerVariablesMockBuilder }
from "./mocks/ServerVariablesMockBuilder";
describe("test identity check process", function () { describe("test identity check process", function () {
@ -36,17 +38,18 @@ describe("test identity check process", function () {
identityValidable = IdentityValidatorMock.IdentityValidableMock(); identityValidable = IdentityValidatorMock.IdentityValidableMock();
req.headers = {}; req.headers = {};
req.session = {}; req.originalUrl = "/non-api/xxx";
req.session = {}; req.session = {};
req.query = {}; req.query = {};
req.app = {}; req.app = {};
mocks.notifier.notifyStub.returns(BluebirdPromise.resolve()); mocks.notifier.notifyStub.returns(BluebirdPromise.resolve());
mocks.userDataStore.produceIdentityValidationTokenStub.returns(Promise.resolve()); mocks.userDataStore.produceIdentityValidationTokenStub
mocks.userDataStore.consumeIdentityValidationTokenStub.returns(Promise.resolve({ userId: "user" })); .returns(Promise.resolve());
mocks.userDataStore.consumeIdentityValidationTokenStub
.returns(Promise.resolve({ userId: "user" }));
app = express(); app = express();
app_get = sinon.stub(app, "get"); app_get = sinon.stub(app, "get");
@ -58,112 +61,142 @@ describe("test identity check process", function () {
app_post.restore(); app_post.restore();
}); });
describe("test start GET", test_start_get_handler); describe("test start GET", function () {
describe("test finish GET", test_finish_get_handler); it("should redirect to error 401 if pre validation initialization \
throws a first factor error", function () {
identityValidable.preValidationInit.returns(BluebirdPromise.reject(
new exceptions.FirstFactorValidationError(
"Error during prevalidation")));
const callback = IdentityValidator.get_start_validation(
identityValidable, "/endpoint", vars);
function test_start_get_handler() { return callback(req as any, res as any, undefined)
it("should send 401 if pre validation initialization throws a first factor error", function () { .then(function () { return BluebirdPromise.reject("Should fail"); })
identityValidable.preValidationInit.returns(BluebirdPromise.reject(new exceptions.FirstFactorValidationError("Error during prevalidation"))); .catch(function () {
const callback = IdentityValidator.get_start_validation(identityValidable, "/endpoint", vars); Assert(res.redirect.calledWith("/error/401"));
});
return callback(req as any, res as any, undefined) });
.then(function () { return BluebirdPromise.reject("Should fail"); })
.catch(function () {
Assert.equal(res.status.getCall(0).args[0], 401);
});
});
it("should send 401 if email is missing in provided identity", function () { it("should send 401 if email is missing in provided identity", function () {
const identity = { userid: "abc" }; const identity = { userid: "abc" };
identityValidable.preValidationInit.returns(BluebirdPromise.resolve(identity)); identityValidable.preValidationInit
const callback = IdentityValidator.get_start_validation(identityValidable, "/endpoint", vars); .returns(BluebirdPromise.resolve(identity));
const callback = IdentityValidator
.get_start_validation(identityValidable, "/endpoint", vars);
return callback(req as any, res as any, undefined) return callback(req as any, res as any, undefined)
.then(function () { return BluebirdPromise.reject("Should fail"); }) .then(function () {
return BluebirdPromise.reject("Should fail");
})
.catch(function () { .catch(function () {
Assert.equal(res.status.getCall(0).args[0], 401); Assert(res.redirect.calledWith("/error/401"));
}); });
}); });
it("should send 401 if userid is missing in provided identity", function () { it("should send 401 if userid is missing in provided identity",
const endpoint = "/protected"; function () {
const identity = { email: "abc@example.com" }; const endpoint = "/protected";
const identity = { email: "abc@example.com" };
identityValidable.preValidationInit.returns(BluebirdPromise.resolve(identity)); identityValidable.preValidationInit
const callback = IdentityValidator.get_start_validation(identityValidable, "/endpoint", vars); .returns(BluebirdPromise.resolve(identity));
const callback = IdentityValidator
.get_start_validation(identityValidable, "/endpoint", vars);
return callback(req as any, res as any, undefined) return callback(req as any, res as any, undefined)
.then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) .then(function () {
.catch(function (err: Error) { return BluebirdPromise.reject(new Error("It should fail"));
Assert.equal(res.status.getCall(0).args[0], 401); })
return BluebirdPromise.resolve(); .catch(function (err: Error) {
}); Assert(res.redirect.calledWith("/error/401"));
}); });
});
it("should issue a token, send an email and return 204", function () { it("should issue a token, send an email and return 204", function () {
const endpoint = "/protected"; const endpoint = "/protected";
const identity = { userid: "user", email: "abc@example.com" }; const identity = { userid: "user", email: "abc@example.com" };
req.get = sinon.stub().withArgs("Host").returns("localhost"); req.get = sinon.stub().withArgs("Host").returns("localhost");
identityValidable.preValidationInit.returns(BluebirdPromise.resolve(identity)); identityValidable.preValidationInit
const callback = IdentityValidator.get_start_validation(identityValidable, "/finish_endpoint", vars); .returns(BluebirdPromise.resolve(identity));
const callback = IdentityValidator
.get_start_validation(identityValidable, "/finish_endpoint", vars);
return callback(req as any, res as any, undefined) return callback(req as any, res as any, undefined)
.then(function () { .then(function () {
Assert(mocks.notifier.notifyStub.calledOnce); Assert(mocks.notifier.notifyStub.calledOnce);
Assert(mocks.userDataStore.produceIdentityValidationTokenStub.calledOnce); Assert(mocks.userDataStore.produceIdentityValidationTokenStub
Assert.equal(mocks.userDataStore.produceIdentityValidationTokenStub.getCall(0).args[0], "user"); .calledOnce);
Assert.equal(mocks.userDataStore.produceIdentityValidationTokenStub.getCall(0).args[3], 240000); Assert.equal(mocks.userDataStore.produceIdentityValidationTokenStub
.getCall(0).args[0], "user");
Assert.equal(mocks.userDataStore.produceIdentityValidationTokenStub
.getCall(0).args[3], 240000);
}); });
}); });
} });
function test_finish_get_handler() {
describe("test finish GET", function () {
it("should send 401 if no identity_token is provided", function () { it("should send 401 if no identity_token is provided", function () {
const callback = IdentityValidator.get_finish_validation(identityValidable, vars); const callback = IdentityValidator
.get_finish_validation(identityValidable, vars);
return callback(req as any, res as any, undefined) return callback(req as any, res as any, undefined)
.then(function () { return BluebirdPromise.reject("Should fail"); }) .then(function () {
return BluebirdPromise.reject("Should fail");
})
.catch(function () { .catch(function () {
Assert.equal(res.status.getCall(0).args[0], 401); Assert(res.redirect.calledWith("/error/401"));
}); });
}); });
it("should call postValidation if identity_token is provided and still valid", function () { it("should call postValidation if identity_token is provided and still \
req.query.identity_token = "token"; valid", function () {
req.query.identity_token = "token";
const callback = IdentityValidator.get_finish_validation(identityValidable, vars); const callback = IdentityValidator
return callback(req as any, res as any, undefined); .get_finish_validation(identityValidable, vars);
}); return callback(req as any, res as any, undefined);
});
it("should return 401 if identity_token is provided but invalid", function () { it("should return 401 if identity_token is provided but invalid",
req.query.identity_token = "token"; function () {
req.query.identity_token = "token";
mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.reject(new Error("Invalid token"))); mocks.userDataStore.consumeIdentityValidationTokenStub
.returns(BluebirdPromise.reject(new Error("Invalid token")));
const callback = IdentityValidator.get_finish_validation(identityValidable, vars); const callback = IdentityValidator
return callback(req as any, res as any, undefined) .get_finish_validation(identityValidable, vars);
.then(function () { return BluebirdPromise.reject("Should fail"); }) return callback(req as any, res as any, undefined)
.catch(function () { .then(function () {
Assert.equal(res.status.getCall(0).args[0], 401); return BluebirdPromise.reject("Should fail");
}); })
}); .catch(function () {
Assert(res.redirect.calledWith("/error/401"));
});
});
it("should set the identity_check session object even if session does not exist yet", function () { it("should set the identity_check session object even if session does \
req.query.identity_token = "token"; not exist yet", function () {
req.query.identity_token = "token";
req.session = {}; req.session = {};
const authSession: AuthenticationSession = AuthenticationSessionHandler.get(req as any, vars.logger); const authSession =
const callback = IdentityValidator.get_finish_validation(identityValidable, vars); AuthenticationSessionHandler.get(req as any, vars.logger);
const callback = IdentityValidator
.get_finish_validation(identityValidable, vars);
return callback(req as any, res as any, undefined) return callback(req as any, res as any, undefined)
.then(function () { return BluebirdPromise.reject("Should fail"); }) .then(function () {
.catch(function () { return BluebirdPromise.reject("Should fail");
Assert.equal(authSession.identity_check.userid, "user"); })
return BluebirdPromise.resolve(); .catch(function () {
}); Assert.equal(authSession.identity_check.userid, "user");
}); });
} });
});
}); });

View File

@ -9,6 +9,7 @@ export interface RequestMock {
headers?: any; headers?: any;
get?: any; get?: any;
query?: any; query?: any;
originalUrl: string;
} }
export interface ResponseMock { export interface ResponseMock {
@ -51,6 +52,7 @@ export interface ResponseMock {
export function RequestMock(): RequestMock { export function RequestMock(): RequestMock {
return { return {
originalUrl: "/non-api/xxx",
app: { app: {
get: sinon.stub() get: sinon.stub()
}, },

View File

@ -2,18 +2,60 @@ import Sinon = require("sinon");
import Express = require("express"); import Express = require("express");
import Assert = require("assert"); import Assert = require("assert");
import Get401 from "../../../../src/lib/routes/error/401/get"; import Get401 from "../../../../src/lib/routes/error/401/get";
import { ServerVariables } from "../../../../src/lib/ServerVariables";
import { ServerVariablesMockBuilder, ServerVariablesMock }
from "../../../mocks/ServerVariablesMockBuilder";
describe("Server error 401", function () { describe("Server error 401", function () {
it("should render the page", function () { let vars: ServerVariables;
const req = {} as Express.Request; let mocks: ServerVariablesMock;
const res = { let req: any;
render: Sinon.stub() let res: any;
}; let renderSpy: Sinon.SinonSpy;
return Get401(req, res as any) beforeEach(function () {
const s = ServerVariablesMockBuilder.build();
vars = s.variables;
mocks = s.mocks;
renderSpy = Sinon.spy();
req = {
headers: {}
};
res = {
render: renderSpy
};
});
it("should set redirection url to the default redirection url", function () {
vars.config.default_redirection_url = "http://default-redirection";
return Get401(vars)(req, res as any)
.then(function () { .then(function () {
Assert(res.render.calledOnce); Assert(renderSpy.calledOnce);
Assert(res.render.calledWith("errors/401")); Assert(renderSpy.calledWithExactly("errors/401", {
redirection_url: "http://default-redirection"
}));
});
});
it("should set redirection url to the referer", function () {
req.headers["referer"] = "http://redirection";
return Get401(vars)(req, res as any)
.then(function () {
Assert(renderSpy.calledOnce);
Assert(renderSpy.calledWithExactly("errors/401", {
redirection_url: "http://redirection"
}));
});
});
it("should render without redirecting the user", function () {
return Get401(vars)(req, res as any)
.then(function () {
Assert(renderSpy.calledOnce);
Assert(renderSpy.calledWithExactly("errors/401", {
redirection_url: undefined
}));
}); });
}); });
}); });

View File

@ -2,18 +2,60 @@ import Sinon = require("sinon");
import Express = require("express"); import Express = require("express");
import Assert = require("assert"); import Assert = require("assert");
import Get403 from "../../../../src/lib/routes/error/403/get"; import Get403 from "../../../../src/lib/routes/error/403/get";
import { ServerVariables } from "../../../../src/lib/ServerVariables";
import { ServerVariablesMockBuilder, ServerVariablesMock }
from "../../../mocks/ServerVariablesMockBuilder";
describe("Server error 403", function () { describe("Server error 403", function () {
it("should render the page", function () { let vars: ServerVariables;
const req = {} as Express.Request; let mocks: ServerVariablesMock;
const res = { let req: any;
render: Sinon.stub() let res: any;
}; let renderSpy: Sinon.SinonSpy;
return Get403(req, res as any) beforeEach(function () {
const s = ServerVariablesMockBuilder.build();
vars = s.variables;
mocks = s.mocks;
renderSpy = Sinon.spy();
req = {
headers: {}
};
res = {
render: renderSpy
};
});
it("should set redirection url to the default redirection url", function () {
vars.config.default_redirection_url = "http://default-redirection";
return Get403(vars)(req, res as any)
.then(function () { .then(function () {
Assert(res.render.calledOnce); Assert(renderSpy.calledOnce);
Assert(res.render.calledWith("errors/403")); Assert(renderSpy.calledWithExactly("errors/403", {
redirection_url: "http://default-redirection"
}));
});
});
it("should set redirection url to the referer", function () {
req.headers["referer"] = "http://redirection";
return Get403(vars)(req, res as any)
.then(function () {
Assert(renderSpy.calledOnce);
Assert(renderSpy.calledWithExactly("errors/403", {
redirection_url: "http://redirection"
}));
});
});
it("should render without redirecting the user", function () {
return Get403(vars)(req, res as any)
.then(function () {
Assert(renderSpy.calledOnce);
Assert(renderSpy.calledWithExactly("errors/403", {
redirection_url: undefined
}));
}); });
}); });
}); });

View File

@ -34,6 +34,7 @@ describe("test the first factor validation route", function () {
mocks.regulator.markStub.returns(BluebirdPromise.resolve()); mocks.regulator.markStub.returns(BluebirdPromise.resolve());
req = { req = {
originalUrl: "/api/firstfactor",
body: { body: {
username: "username", username: "username",
password: "password" password: "password"

View File

@ -18,6 +18,7 @@ describe("test reset password identity check", function () {
beforeEach(function () { beforeEach(function () {
req = { req = {
originalUrl: "/non-api/xxx",
query: { query: {
userid: "user" userid: "user"
}, },

View File

@ -20,6 +20,7 @@ describe("test reset password route", function () {
beforeEach(function () { beforeEach(function () {
req = { req = {
originalUrl: "/api/password-reset",
body: { body: {
userid: "user" userid: "user"
}, },

View File

@ -25,6 +25,7 @@ describe("test totp route", function () {
mocks = s.mocks; mocks = s.mocks;
const app_get = Sinon.stub(); const app_get = Sinon.stub();
req = { req = {
originalUrl: "/api/totp-register",
app: {}, app: {},
body: { body: {
token: "abc" token: "abc"

View File

@ -20,6 +20,7 @@ describe("test u2f routes: register", function () {
beforeEach(function () { beforeEach(function () {
req = ExpressMock.RequestMock(); req = ExpressMock.RequestMock();
req.originalUrl = "/api/xxxx";
req.app = {}; req.app = {};
req.session = { req.session = {
auth: { auth: {

View File

@ -16,6 +16,7 @@ describe("test u2f routes: register_request", function () {
beforeEach(function () { beforeEach(function () {
req = ExpressMock.RequestMock(); req = ExpressMock.RequestMock();
req.originalUrl = "/api/xxxx";
req.app = {}; req.app = {};
req.session = { req.session = {
auth: { auth: {

View File

@ -20,6 +20,7 @@ describe("test u2f routes: sign", function () {
beforeEach(function () { beforeEach(function () {
req = ExpressMock.RequestMock(); req = ExpressMock.RequestMock();
req.app = {}; req.app = {};
req.originalUrl = "/api/xxxx";
const s = ServerVariablesMockBuilder.build(); const s = ServerVariablesMockBuilder.build();
mocks = s.mocks; mocks = s.mocks;

View File

@ -20,6 +20,7 @@ describe("test u2f routes: sign_request", function () {
beforeEach(function () { beforeEach(function () {
req = ExpressMock.RequestMock(); req = ExpressMock.RequestMock();
req.originalUrl = "/api/xxxx";
req.app = {}; req.app = {};
req.session = { req.session = {
auth: { auth: {

View File

@ -12,7 +12,7 @@ import ExpressMock = require("../../mocks/express");
import { ServerVariables } from "../../../src/lib/ServerVariables"; import { ServerVariables } from "../../../src/lib/ServerVariables";
import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../mocks/ServerVariablesMockBuilder"; import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../mocks/ServerVariablesMockBuilder";
describe("test /verify endpoint", function () { describe("test /api/verify endpoint", function () {
let req: ExpressMock.RequestMock; let req: ExpressMock.RequestMock;
let res: ExpressMock.ResponseMock; let res: ExpressMock.ResponseMock;
let mocks: ServerVariablesMock; let mocks: ServerVariablesMock;
@ -22,6 +22,7 @@ describe("test /verify endpoint", function () {
beforeEach(function () { beforeEach(function () {
req = ExpressMock.RequestMock(); req = ExpressMock.RequestMock();
res = ExpressMock.ResponseMock(); res = ExpressMock.ResponseMock();
req.originalUrl = "/api/xxxx";
req.query = { req.query = {
redirect: "http://redirect.url" redirect: "http://redirect.url"
}; };
@ -174,7 +175,7 @@ describe("test /verify endpoint", function () {
}); });
describe("inactivity period", function () { describe("inactivity period", function () {
it("should update last inactivity period on requests on /verify", function () { it("should update last inactivity period on requests on /api/verify", function () {
mocks.config.session.inactivity = 200000; mocks.config.session.inactivity = 200000;
mocks.accessController.isAccessAllowedMock.returns(true); mocks.accessController.isAccessAllowedMock.returns(true);
const currentTime = new Date().getTime() - 1000; const currentTime = new Date().getTime() - 1000;

View File

@ -264,7 +264,7 @@ export const FIRST_FACTOR_GET = "/";
export const SECOND_FACTOR_GET = "/secondfactor"; export const SECOND_FACTOR_GET = "/secondfactor";
/** /**
* @api {get} /verify Verify user authentication * @api {get} /api/verify Verify user authentication
* @apiName VerifyAuthentication * @apiName VerifyAuthentication
* @apiGroup Verification * @apiGroup Verification
* @apiVersion 1.0.0 * @apiVersion 1.0.0
@ -279,7 +279,7 @@ export const SECOND_FACTOR_GET = "/secondfactor";
* are set. Remote-User contains the user id of the currently logged in user and Remote-Groups * are set. Remote-User contains the user id of the currently logged in user and Remote-Groups
* a comma separated list of assigned groups. * a comma separated list of assigned groups.
*/ */
export const VERIFY_GET = "/verify"; export const VERIFY_GET = "/api/verify";
/** /**
* @api {get} /logout Serves logout page * @api {get} /logout Serves logout page

View File

@ -48,4 +48,23 @@ Feature: User is correctly redirected
And I login with user "john" and password "password" And I login with user "john" and password "password"
And I use "REGISTERED" as TOTP token handle And I use "REGISTERED" as TOTP token handle
And I click on "Sign in" And I click on "Sign in"
Then I'm redirected to "https://home.test.local:8080/" Then I'm redirected to "https://home.test.local:8080/"
Scenario: User is redirected when hitting an error 401
When I visit "https://auth.test.local:8080/secondfactor/u2f/identity/finish"
Then I'm redirected to "https://auth.test.local:8080/error/401"
And I sleep for 5 seconds
And I'm redirected to "https://home.test.local:8080/"
@need-registered-user-harry
Scenario: User is redirected when hitting an error 403
When I visit "https://auth.test.local:8080"
And I login with user "harry" and password "password"
And I use "REGISTERED" as TOTP token handle
And I click on "Sign in"
And I'm redirected to "https://home.test.local:8080/"
When I visit "https://admin.test.local:8080/secret.html"
Then I'm redirected to "https://auth.test.local:8080/error/403"
And I sleep for 5 seconds
And I'm redirected to "https://home.test.local:8080/"

View File

@ -8,6 +8,7 @@ Feature: Non authenticated users have no access to certain pages
| https://auth.test.local:8080/secondfactor/u2f/identity/finish | 401 | GET | | https://auth.test.local:8080/secondfactor/u2f/identity/finish | 401 | GET |
| https://auth.test.local:8080/secondfactor/totp/identity/start | 401 | GET | | https://auth.test.local:8080/secondfactor/totp/identity/start | 401 | GET |
| https://auth.test.local:8080/secondfactor/totp/identity/finish | 401 | GET | | https://auth.test.local:8080/secondfactor/totp/identity/finish | 401 | GET |
| https://auth.test.local:8080/loggedin | 401 | GET |
| https://auth.test.local:8080/api/totp | 401 | POST | | https://auth.test.local:8080/api/totp | 401 | POST |
| https://auth.test.local:8080/api/u2f/sign_request | 401 | GET | | https://auth.test.local:8080/api/u2f/sign_request | 401 | GET |
| https://auth.test.local:8080/api/u2f/sign | 401 | POST | | https://auth.test.local:8080/api/u2f/sign | 401 | POST |