authelia/server/test/routes/verify/get.test.ts
Clement Michaud 6586402114 Support 'redirect' in /api/verify endpoint to support Traefik
Traefik handles auth forwarding but does not manage redirections like Nginx.
Therefore, Authelia must redirect the user and Traefik will forward this
request.

To support both Nginx and Traefik, /api/verify is now configurable with the
'redirect' get parameter. If the verification fails and 'redirect' is not
provided the response will be a 401 error as before.
If the parameter is provided and set to any URL, the response will be a
redirection (302) to this URL.
2017-12-04 22:52:33 +01:00

341 lines
13 KiB
TypeScript

import Assert = require("assert");
import VerifyGet = require("../../../src/lib/routes/verify/get");
import { AuthenticationSessionHandler } from "../../../src/lib/AuthenticationSessionHandler";
import { AuthenticationSession } from "../../../types/AuthenticationSession";
import { AuthenticationMethodsConfiguration } from "../../../src/lib/configuration/Configuration";
import Sinon = require("sinon");
import winston = require("winston");
import BluebirdPromise = require("bluebird");
import express = require("express");
import ExpressMock = require("../../mocks/express");
import { ServerVariables } from "../../../src/lib/ServerVariables";
import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../mocks/ServerVariablesMockBuilder";
describe("test /api/verify endpoint", function () {
let req: ExpressMock.RequestMock;
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.host = "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 () {
beforeEach(function () {
vars.config.authentication_methods.default_method = "two_factor";
});
it("should be already authenticated", function () {
mocks.accessController.isAccessAllowedMock.returns(true);
authSession.first_factor = true;
authSession.second_factor = true;
authSession.userid = "myuser";
authSession.groups = ["mygroup", "othergroup"];
return VerifyGet.default(vars)(req as express.Request, res as any)
.then(function () {
Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser");
Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup");
Assert.equal(204, res.status.getCall(0).args[0]);
});
});
function test_session(_authSession: AuthenticationSession, status_code: number) {
return VerifyGet.default(vars)(req as express.Request, res as any)
.then(function () {
Assert.equal(status_code, res.status.getCall(0).args[0]);
});
}
function test_non_authenticated_401(authSession: AuthenticationSession) {
return test_session(authSession, 401);
}
function test_unauthorized_403(authSession: AuthenticationSession) {
return test_session(authSession, 403);
}
function test_authorized(authSession: AuthenticationSession) {
return test_session(authSession, 204);
}
describe("given user tries to access a 2-factor endpoint", function () {
before(function () {
mocks.accessController.isAccessAllowedMock.returns(true);
});
describe("given different cases of session", function () {
it("should not be authenticated when second factor is missing", function () {
return test_non_authenticated_401({
userid: "user",
first_factor: true,
second_factor: false,
email: undefined,
groups: [],
last_activity_datetime: new Date().getTime()
});
});
it("should not be authenticated when first factor is missing", function () {
return test_non_authenticated_401({
userid: "user",
first_factor: false,
second_factor: true,
email: undefined,
groups: [],
last_activity_datetime: new Date().getTime()
});
});
it("should not be authenticated when userid is missing", function () {
return test_non_authenticated_401({
userid: undefined,
first_factor: true,
second_factor: false,
email: undefined,
groups: [],
last_activity_datetime: new Date().getTime()
});
});
it("should not be authenticated when first and second factor are missing", function () {
return test_non_authenticated_401({
userid: "user",
first_factor: false,
second_factor: false,
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.first_factor = true;
authSession.second_factor = true;
authSession.userid = "myuser";
req.headers.host = "test.example.com";
mocks.accessController.isAccessAllowedMock.returns(false);
return test_unauthorized_403({
first_factor: true,
second_factor: true,
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["host"] = "redirect.url";
mocks.config.authentication_methods.per_subdomain_methods = {
"redirect.url": "single_factor"
};
});
it("should be authenticated when first factor is validated and second factor is not", function () {
mocks.accessController.isAccessAllowedMock.returns(true);
authSession.first_factor = true;
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 first factor is not validated", function () {
mocks.accessController.isAccessAllowedMock.returns(true);
authSession.first_factor = false;
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.accessController.isAccessAllowedMock.returns(true);
const currentTime = new Date().getTime() - 1000;
AuthenticationSessionHandler.reset(req as any);
authSession.first_factor = true;
authSession.second_factor = true;
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.accessController.isAccessAllowedMock.returns(true);
const currentTime = new Date().getTime() - 1000;
AuthenticationSessionHandler.reset(req as any);
authSession.first_factor = true;
authSession.second_factor = true;
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.first_factor, false);
Assert.equal(authSession.second_factor, false);
Assert.equal(authSession.userid, undefined);
});
});
});
});
describe("response type 401 | 302", function() {
it("should return error code 401", function() {
mocks.accessController.isAccessAllowedMock.returns(true);
mocks.config.authentication_methods.default_method = "single_factor";
mocks.ldapAuthenticator.authenticateStub.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.accessController.isAccessAllowedMock.returns(true);
mocks.config.authentication_methods.default_method = "single_factor";
mocks.ldapAuthenticator.authenticateStub.rejects(new Error(
"Invalid credentials"));
req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA==";
req.query["redirect"] = 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", function () {
mocks.accessController.isAccessAllowedMock.returns(true);
mocks.config.authentication_methods.default_method = "single_factor";
mocks.ldapAuthenticator.authenticateStub.returns({
groups: ["mygroup", "othergroup"],
});
req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA==";
return VerifyGet.default(vars)(req as express.Request, res as any)
.then(function () {
Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "john");
Sinon.assert.calledWithExactly(res.setHeader, "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.accessController.isAccessAllowedMock.returns(true);
mocks.config.authentication_methods.default_method = "single_factor";
mocks.config.authentication_methods.per_subdomain_methods = {
"secret.example.com": "two_factor"
};
mocks.ldapAuthenticator.authenticateStub.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.accessController.isAccessAllowedMock.returns(true);
mocks.config.authentication_methods.default_method = "single_factor";
mocks.ldapAuthenticator.authenticateStub.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.accessController.isAccessAllowedMock.returns(true);
mocks.config.authentication_methods.default_method = "single_factor";
mocks.ldapAuthenticator.authenticateStub.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.accessController.isAccessAllowedMock.returns(true);
mocks.config.authentication_methods.default_method = "single_factor";
mocks.ldapAuthenticator.authenticateStub.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.accessController.isAccessAllowedMock.returns(false);
mocks.config.authentication_methods.default_method = "single_factor";
mocks.ldapAuthenticator.authenticateStub.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));
});
});
});
});