Fix redirection after authentication and error page when accessing restricted pages

This commit is contained in:
Clement Michaud 2017-08-03 00:30:41 +02:00
parent 785182236c
commit 928209dc98
9 changed files with 89 additions and 51 deletions

View File

@ -61,6 +61,7 @@ http {
ssl_certificate_key /etc/ssl/server.key; ssl_certificate_key /etc/ssl/server.key;
error_page 401 = @error401; error_page 401 = @error401;
error_page 403 = https://auth.test.local:8080/error/403;
location @error401 { location @error401 {
return 302 https://auth.test.local:8080; return 302 https://auth.test.local:8080;
} }

View File

@ -39,13 +39,15 @@ function verify_filter(req: express.Request, res: express.Response): BluebirdPro
}); });
} }
export default function (req: express.Request, res: express.Response) { export default function (req: express.Request, res: express.Response): BluebirdPromise<void> {
const logger = ServerVariablesHandler.getLogger(req.app); const logger = ServerVariablesHandler.getLogger(req.app);
verify_filter(req, res) return verify_filter(req, res)
.then(function () { .then(function () {
res.status(204); res.status(204);
res.send(); res.send();
return BluebirdPromise.resolve();
}) })
.catch(exceptions.DomainAccessDenied, ErrorReplies.replyWithError403(res, logger))
.catch(ErrorReplies.replyWithError401(res, logger)); .catch(ErrorReplies.replyWithError401(res, logger));
} }

View File

@ -8,4 +8,4 @@ block form-header
<img class="header-img" src="/img/warning.png" alt=""> <img class="header-img" src="/img/warning.png" alt="">
block content block content
<p>You are either not authorized or not <a href="/">logged in</a>.</p> <p>Please <a href="/">log in</a> to access this resource.</p>

View File

@ -8,4 +8,4 @@ block form-header
<img class="header-img" src="/img/warning.png" alt=""> <img class="header-img" src="/img/warning.png" alt="">
block content block content
<p>You are either not authorized or not <a href="/">logged in</a>.</p> <p>You are not authorized to access this resource.</p>

View File

@ -1,7 +1,23 @@
Feature: User is redirected to authelia when he is not authenticated Feature: User is correctly redirected correctly
Scenario: User is redirected to authelia Scenario: User is redirected to authelia when he is not authenticated
Given I'm on https://home.test.local:8080 Given I'm on https://home.test.local:8080
When I click on the link to secret.test.local When I click on the link to secret.test.local
Then I'm redirected to "https://auth.test.local:8080/" Then I'm redirected to "https://auth.test.local:8080/"
Scenario: User is redirected to home page after several authentication tries
Given I'm on https://auth.test.local:8080/
And I login with user "john" and password "password"
And I register a TOTP secret called "Sec0"
And I visit "https://public.test.local:8080/secret.html"
When I login with user "john" and password "badpassword"
And I clear field "username"
And I login with user "john" and password "password"
And I use "Sec0" as TOTP token handle
And I click on "TOTP"
Then I'm redirected to "https://public.test.local:8080/secret.html"
Scenario: User Harry does not have access to https://secret.test.local:8080/secret.html and thus he must get an error 401
When I register TOTP and login with user "harry" and password "password"
And I visit "https://secret.test.local:8080/secret.html"
Then I get an error 403

View File

@ -14,6 +14,10 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
return this.setFieldTo(fieldName, content); return this.setFieldTo(fieldName, content);
}); });
When("I clear field {stringInDoubleQuotes}", function (fieldName: string) {
return this.clearField(fieldName);
});
When("I click on {stringInDoubleQuotes}", function (text: string) { When("I click on {stringInDoubleQuotes}", function (text: string) {
return this.clickOnButton(text); return this.clickOnButton(text);
}); });
@ -55,20 +59,20 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
return this.registerTotpAndSignin(username, password); return this.registerTotpAndSignin(username, password);
}); });
function hasAccessToSecret(link: string, driver: any) { function hasAccessToSecret(link: string, that: any) {
return driver.get(link) return that.driver.get(link)
.then(function () { .then(function () {
return driver.findElement(seleniumWebdriver.By.tagName("body")).getText() return that.driver.findElement(seleniumWebdriver.By.tagName("body")).getText()
.then(function (body: string) { .then(function (body: string) {
Assert(body.indexOf("This is a very important secret!") > -1); Assert(body.indexOf("This is a very important secret!") > -1, body);
}); });
}); });
} }
function hasNoAccessToSecret(link: string, driver: any) { function hasNoAccessToSecret(link: string, that: any) {
return driver.get(link) return that.driver.get(link)
.then(function () { .then(function () {
return driver.wait(seleniumWebdriver.until.urlIs("https://auth.test.local:8080/")); return that.getErrorPage(403);
}); });
} }
@ -76,7 +80,7 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
const promises = []; const promises = [];
for (let i = 0; i < dataTable.rows().length; i++) { for (let i = 0; i < dataTable.rows().length; i++) {
const url = (dataTable.hashes() as any)[i].url; const url = (dataTable.hashes() as any)[i].url;
promises.push(hasAccessToSecret(url, this.driver)); promises.push(hasAccessToSecret(url, this));
} }
return Promise.all(promises); return Promise.all(promises);
}); });
@ -85,7 +89,7 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
const promises = []; const promises = [];
for (let i = 0; i < dataTable.rows().length; i++) { for (let i = 0; i < dataTable.rows().length; i++) {
const url = (dataTable.hashes() as any)[i].url; const url = (dataTable.hashes() as any)[i].url;
promises.push(hasNoAccessToSecret(url, this.driver)); promises.push(hasNoAccessToSecret(url, this));
} }
return Promise.all(promises); return Promise.all(promises);
}); });

View File

@ -4,8 +4,6 @@ import Assert = require("assert");
Cucumber.defineSupportCode(function ({ Given, When, Then }) { Cucumber.defineSupportCode(function ({ Given, When, Then }) {
Then("I get an error {number}", function (code: number) { Then("I get an error {number}", function (code: number) {
return this.driver return this.getErrorPage(code);
.findElement(seleniumWebdriver.By.tagName("h1"))
.findElement(seleniumWebdriver.By.xpath("//h1[contains(.,'Error " + code + "')]"));
}); });
}); });

View File

@ -21,6 +21,16 @@ function CustomWorld() {
.sendKeys(content); .sendKeys(content);
}; };
this.clearField = function (fieldName: string) {
return this.driver.findElement(seleniumWebdriver.By.id(fieldName)).clear();
};
this.getErrorPage = function (code: number) {
return this.driver
.findElement(seleniumWebdriver.By.tagName("h1"))
.findElement(seleniumWebdriver.By.xpath("//h1[contains(.,'Error " + code + "')]"));
};
this.clickOnButton = function (buttonText: string) { this.clickOnButton = function (buttonText: string) {
return this.driver return this.driver
.findElement(seleniumWebdriver.By.tagName("button")) .findElement(seleniumWebdriver.By.tagName("button"))
@ -29,9 +39,11 @@ function CustomWorld() {
}; };
this.loginWithUserPassword = function (username: string, password: string) { this.loginWithUserPassword = function (username: string, password: string) {
return this.driver return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.id("username")), 4000)
.findElement(seleniumWebdriver.By.id("username")) .then(function () {
.sendKeys(username) return that.driver.findElement(seleniumWebdriver.By.id("username"))
.sendKeys(username);
})
.then(function () { .then(function () {
return that.driver.findElement(seleniumWebdriver.By.id("password")) return that.driver.findElement(seleniumWebdriver.By.id("password"))
.sendKeys(password); .sendKeys(password);
@ -39,14 +51,14 @@ function CustomWorld() {
.then(function () { .then(function () {
return that.driver.findElement(seleniumWebdriver.By.tagName("button")) return that.driver.findElement(seleniumWebdriver.By.tagName("button"))
.click(); .click();
})
.then(function () {
return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.className("register-totp")), 4000);
}); });
}; };
this.registerTotpSecret = function (totpSecretHandle: string) { this.registerTotpSecret = function (totpSecretHandle: string) {
return this.driver.findElement(seleniumWebdriver.By.className("register-totp")).click() return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.className("register-totp")), 4000)
.then(function () {
return that.driver.findElement(seleniumWebdriver.By.className("register-totp")).click();
})
.then(function () { .then(function () {
const notif = Fs.readFileSync("./notifications/notification.txt").toString(); const notif = Fs.readFileSync("./notifications/notification.txt").toString();
const regexp = new RegExp(/Link: (.+)/); const regexp = new RegExp(/Link: (.+)/);
@ -75,8 +87,11 @@ function CustomWorld() {
}; };
this.useTotpToken = function (totpSecret: string) { this.useTotpToken = function (totpSecret: string) {
return this.driver.findElement(seleniumWebdriver.By.id("token")) return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.className("register-totp")), 4000)
.sendKeys(totpSecret); .then(function () {
return that.driver.findElement(seleniumWebdriver.By.id("token"))
.sendKeys(totpSecret);
});
}; };
this.registerTotpAndSignin = function (username: string, password: string) { this.registerTotpAndSignin = function (username: string, password: string) {

View File

@ -24,6 +24,8 @@ describe("test authentication token verification", function () {
req = ExpressMock.RequestMock(); req = ExpressMock.RequestMock();
res = ExpressMock.ResponseMock(); res = ExpressMock.ResponseMock();
req.session = {};
AuthenticationSession.reset(req as any);
req.headers = {}; req.headers = {};
req.headers.host = "secret.example.com"; req.headers.host = "secret.example.com";
const mocks = ServerVariablesMock.mock(req.app); const mocks = ServerVariablesMock.mock(req.app);
@ -32,7 +34,7 @@ describe("test authentication token verification", function () {
mocks.accessController = accessController as any; mocks.accessController = accessController as any;
}); });
it("should be already authenticated", function (done) { it("should be already authenticated", function () {
req.session = {}; req.session = {};
AuthenticationSession.reset(req as any); AuthenticationSession.reset(req as any);
const authSession = AuthenticationSession.get(req as any); const authSession = AuthenticationSession.get(req as any);
@ -40,39 +42,34 @@ describe("test authentication token verification", function () {
authSession.second_factor = true; authSession.second_factor = true;
authSession.userid = "myuser"; authSession.userid = "myuser";
res.send = sinon.spy(function () { return VerifyGet.default(req as express.Request, res as any)
assert.equal(204, res.status.getCall(0).args[0]); .then(function () {
done(); assert.equal(204, res.status.getCall(0).args[0]);
}); });
VerifyGet.default(req as express.Request, res as any);
}); });
describe("given different cases of session", function () { describe("given different cases of session", function () {
function test_session(auth_session: AuthenticationSession.AuthenticationSession, status_code: number) { function test_session(auth_session: AuthenticationSession.AuthenticationSession, status_code: number) {
return new BluebirdPromise(function (resolve, reject) { return VerifyGet.default(req as express.Request, res as any)
req.session = {}; .then(function () {
req.session.auth_session = auth_session;
res.send = sinon.spy(function () {
assert.equal(status_code, res.status.getCall(0).args[0]); assert.equal(status_code, res.status.getCall(0).args[0]);
resolve();
}); });
VerifyGet.default(req as express.Request, res as any);
});
} }
function test_unauthorized(auth_session: AuthenticationSession.AuthenticationSession) { function test_non_authenticated_401(auth_session: AuthenticationSession.AuthenticationSession) {
return test_session(auth_session, 401); return test_session(auth_session, 401);
} }
function test_unauthorized_403(auth_session: AuthenticationSession.AuthenticationSession) {
return test_session(auth_session, 403);
}
function test_authorized(auth_session: AuthenticationSession.AuthenticationSession) { function test_authorized(auth_session: AuthenticationSession.AuthenticationSession) {
return test_session(auth_session, 204); return test_session(auth_session, 204);
} }
it("should not be authenticated when second factor is missing", function () { it("should not be authenticated when second factor is missing", function () {
return test_unauthorized({ return test_non_authenticated_401({
userid: "user", userid: "user",
first_factor: true, first_factor: true,
second_factor: false, second_factor: false,
@ -82,7 +79,7 @@ describe("test authentication token verification", function () {
}); });
it("should not be authenticated when first factor is missing", function () { it("should not be authenticated when first factor is missing", function () {
return test_unauthorized({ return test_non_authenticated_401({
userid: "user", userid: "user",
first_factor: false, first_factor: false,
second_factor: true, second_factor: true,
@ -92,7 +89,7 @@ describe("test authentication token verification", function () {
}); });
it("should not be authenticated when userid is missing", function () { it("should not be authenticated when userid is missing", function () {
return test_unauthorized({ return test_non_authenticated_401({
userid: undefined, userid: undefined,
first_factor: true, first_factor: true,
second_factor: false, second_factor: false,
@ -102,26 +99,31 @@ describe("test authentication token verification", function () {
}); });
it("should not be authenticated when first and second factor are missing", function () { it("should not be authenticated when first and second factor are missing", function () {
return test_unauthorized({ return test_non_authenticated_401({
userid: "user", userid: "user",
first_factor: false, first_factor: false,
second_factor: false, second_factor: false,
email: undefined, email: undefined,
groups: [], groups: [],
}); });
}); });
it("should not be authenticated when session has not be initiated", function () { it("should not be authenticated when session has not be initiated", function () {
return test_unauthorized(undefined); return test_non_authenticated_401(undefined);
}); });
it("should not be authenticated when domain is not allowed for user", function () { it("should not be authenticated when domain is not allowed for user", function () {
const authSession = AuthenticationSession.get(req as any);
authSession.first_factor = true;
authSession.second_factor = true;
authSession.userid = "myuser";
req.headers.host = "test.example.com"; req.headers.host = "test.example.com";
accessController.isDomainAllowedForUser.returns(false); accessController.isDomainAllowedForUser.returns(false);
accessController.isDomainAllowedForUser.withArgs("test.example.com", "user", ["group1", "group2"]).returns(true); accessController.isDomainAllowedForUser.withArgs("test.example.com", "user", ["group1", "group2"]).returns(true);
return test_unauthorized({ return test_unauthorized_403({
first_factor: true, first_factor: true,
second_factor: true, second_factor: true,
userid: "user", userid: "user",