Add basic authorization support for single-factor protected endpoints

One can now access a service using the basic authorization mechanism. Note the
service must not be protected by 2 factors.

The Remote-User and Remote-Groups are forwarded from Authelia like any browser
authentication.
pull/193/head
Clement Michaud 2017-11-01 19:23:45 +01:00
parent e3e1235755
commit 009e7c2b78
12 changed files with 560 additions and 248 deletions

View File

@ -22,7 +22,7 @@ used in production to secure internal services in a small docker swarm cluster.
3. [Second factor with U2F security keys](#second-factor-with-u2f-security-keys) 3. [Second factor with U2F security keys](#second-factor-with-u2f-security-keys)
4. [Password reset](#password-reset) 4. [Password reset](#password-reset)
5. [Access control](#access-control) 5. [Access control](#access-control)
6. [Basic authentication](#basic-authentication) 6. [Single factor authentication](#single-factor-authentication)
7. [Session management with Redis](#session-management-with-redis) 7. [Session management with Redis](#session-management-with-redis)
4. [Security](#security) 4. [Security](#security)
5. [Documentation](#documentation) 5. [Documentation](#documentation)
@ -37,12 +37,12 @@ used in production to secure internal services in a small docker swarm cluster.
* Two-factor authentication using either * Two-factor authentication using either
**[TOTP] - Time-Base One Time password -** or **[U2F] - Universal 2-Factor -** **[TOTP] - Time-Base One Time password -** or **[U2F] - Universal 2-Factor -**
as 2nd factor. as 2nd factor.
* Password reset with identity verification by sending links to user email * Password reset with identity verification using email.
address. * Single and two factors authentication methods available.
* Two-factor and basic authentication methods available.
* Access restriction after too many authentication attempts. * Access restriction after too many authentication attempts.
* Session management using Redis key/value store.
* User-defined access control per subdomain and resource. * User-defined access control per subdomain and resource.
* Support of [basic authentication] for endpoints protected by single factor.
* High-availability using a highly-available distributed database and KV store.
## Deployment ## Deployment
@ -190,11 +190,14 @@ user access to some resources and subdomains. Those rules are defined and fully
in the configuration file. They can apply to users, groups or everyone. in the configuration file. They can apply to users, groups or everyone.
Check out [config.template.yml] to see how they are defined. Check out [config.template.yml] to see how they are defined.
### Basic Authentication ### Single factor authentication
Authelia allows you to customize the authentication method to use for each sub-domain. Authelia allows you to customize the authentication method to use for each
The supported methods are either "single_factor" and "two_factor". sub-domain.The supported methods are either "single_factor" or "two_factor".
Please see [config.template.yml] to see an example of configuration. Please see [config.template.yml] to see an example of configuration.
It is also possible to use [basic authentication] to access a resource
protected by a single factor.
### Session management with Redis ### Session management with Redis
When your users authenticate against Authelia, sessions are stored in a Redis key/value store. You can specify your own Redis instance in [config.template.yml]. When your users authenticate against Authelia, sessions are stored in a Redis key/value store. You can specify your own Redis instance in [config.template.yml].
@ -293,3 +296,4 @@ Follow [contributing](CONTRIBUTORS.md) file.
[Google Authenticator]: https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en [Google Authenticator]: https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en
[config.template.yml]: https://github.com/clems4ever/authelia/blob/master/config.template.yml [config.template.yml]: https://github.com/clems4ever/authelia/blob/master/config.template.yml
[HSTS]: https://www.nginx.com/blog/http-strict-transport-security-hsts-and-nginx/ [HSTS]: https://www.nginx.com/blog/http-strict-transport-security-hsts-and-nginx/
[basic authentication]: https://en.wikipedia.org/wiki/Basic_access_authentication

View File

@ -119,7 +119,7 @@ http {
error_page 401 =302 https://auth.test.local:8080?redirect=$redirect; error_page 401 =302 https://auth.test.local:8080?redirect=$redirect;
error_page 403 = https://auth.test.local:8080/error/403; error_page 403 = https://auth.test.local:8080/error/403;
} }
} }
server { server {
@ -262,6 +262,7 @@ http {
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header Content-Length ""; proxy_set_header Content-Length "";
proxy_set_header Proxy-Authorization $http_authorization;
proxy_pass http://authelia/api/verify; proxy_pass http://authelia/api/verify;
} }
@ -280,6 +281,23 @@ http {
error_page 401 =302 https://auth.test.local:8080?redirect=$redirect; error_page 401 =302 https://auth.test.local:8080?redirect=$redirect;
error_page 403 = https://auth.test.local:8080/error/403; error_page 403 = https://auth.test.local:8080/error/403;
} }
location /headers {
auth_request /auth_verify;
auth_request_set $redirect $upstream_http_redirect;
auth_request_set $user $upstream_http_remote_user;
proxy_set_header Custom-Forwarded-User $user;
auth_request_set $groups $upstream_http_remote_groups;
proxy_set_header Custom-Forwarded-Groups $groups;
proxy_pass http://httpbin:8000/headers;
error_page 401 =302 https://auth.test.local:8080?redirect=$redirect;
error_page 403 = https://auth.test.local:8080/error/403;
}
} }
} }

View File

@ -0,0 +1,22 @@
import Express = require("express");
import BluebirdPromise = require("bluebird");
import Util = require("util");
import { ServerVariables } from "../../ServerVariables";
import Exceptions = require("../../Exceptions");
export default function (req: Express.Request, vars: ServerVariables,
domain: string, path: string, username: string, groups: string[]) {
return new BluebirdPromise(function (resolve, reject) {
const isAllowed = vars.accessController
.isAccessAllowed(domain, path, username, groups);
if (!isAllowed) {
reject(new Exceptions.DomainAccessDenied(Util.format(
"User '%s' does not have access to '%s'", username, domain)));
return;
}
resolve();
});
}

View File

@ -1,118 +1,69 @@
import objectPath = require("object-path");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import express = require("express"); import Express = require("express");
import exceptions = require("../../Exceptions"); import Exceptions = require("../../Exceptions");
import winston = require("winston");
import ErrorReplies = require("../../ErrorReplies"); import ErrorReplies = require("../../ErrorReplies");
import { AppConfiguration } from "../../configuration/Configuration";
import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler";
import { AuthenticationSession } from "../../../../types/AuthenticationSession";
import Constants = require("../../../../../shared/constants");
import Util = require("util");
import { DomainExtractor } from "../../utils/DomainExtractor";
import { ServerVariables } from "../../ServerVariables"; import { ServerVariables } from "../../ServerVariables";
import { MethodCalculator } from "../../authentication/MethodCalculator"; import GetWithSessionCookieMethod from "./get_session_cookie";
import { IRequestLogger } from "../../logging/IRequestLogger"; import GetWithBasicAuthMethod from "./get_basic_auth";
const FIRST_FACTOR_NOT_VALIDATED_MESSAGE = "First factor not yet validated"; import { AuthenticationSessionHandler }
const SECOND_FACTOR_NOT_VALIDATED_MESSAGE = "Second factor not yet validated"; from "../../AuthenticationSessionHandler";
import { AuthenticationSession }
from "../../../../types/AuthenticationSession";
const REMOTE_USER = "Remote-User"; const REMOTE_USER = "Remote-User";
const REMOTE_GROUPS = "Remote-Groups"; const REMOTE_GROUPS = "Remote-Groups";
function verify_inactivity(req: express.Request,
authSession: AuthenticationSession,
configuration: AppConfiguration, logger: IRequestLogger)
: BluebirdPromise<void> {
const lastActivityTime = authSession.last_activity_datetime; function verifyWithSelectedMethod(req: Express.Request, res: Express.Response,
const currentTime = new Date().getTime(); vars: ServerVariables, authSession: AuthenticationSession)
authSession.last_activity_datetime = currentTime; : () => BluebirdPromise<{ username: string, groups: string[] }> {
return function () {
const authorization: string = "" + req.headers["proxy-authorization"];
if (authorization && authorization.startsWith("Basic "))
return GetWithBasicAuthMethod(req, res, vars, authorization);
// If inactivity is not specified, then inactivity timeout does not apply return GetWithSessionCookieMethod(req, res, vars, authSession);
if (!configuration.session.inactivity) { };
return BluebirdPromise.resolve();
}
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."));
} }
function verify_filter(req: express.Request, res: express.Response, function setRedirectHeader(req: Express.Request, res: Express.Response) {
vars: ServerVariables): BluebirdPromise<void> { return function () {
let authSession: AuthenticationSession;
let username: string;
let groups: string[];
return new BluebirdPromise(function (resolve, reject) {
authSession = AuthenticationSessionHandler.get(req, vars.logger);
username = authSession.userid;
groups = authSession.groups;
res.set("Redirect", encodeURIComponent("https://" + req.headers["host"] + res.set("Redirect", encodeURIComponent("https://" + req.headers["host"] +
req.headers["x-original-uri"])); req.headers["x-original-uri"]));
return BluebirdPromise.resolve();
};
}
if (!authSession.userid) { function setUserAndGroupsHeaders(res: Express.Response) {
reject(new exceptions.AccessDeniedError( return function (u: { username: string, groups: string[] }) {
Util.format("%s: %s.", FIRST_FACTOR_NOT_VALIDATED_MESSAGE, "userid is missing"))); res.setHeader(REMOTE_USER, u.username);
return; res.setHeader(REMOTE_GROUPS, u.groups.join(","));
} return BluebirdPromise.resolve();
};
}
const host = objectPath.get<express.Request, string>(req, "headers.host"); function replyWith200(res: Express.Response) {
const path = objectPath.get<express.Request, string>(req, "headers.x-original-uri"); return function () {
res.status(204);
const domain = DomainExtractor.fromHostHeader(host); res.send();
const authenticationMethod = };
MethodCalculator.compute(vars.config.authentication_methods, domain);
vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", domain, path,
username, groups.join(","));
if (!authSession.first_factor)
return reject(new exceptions.AccessDeniedError(
Util.format("%s: %s.", FIRST_FACTOR_NOT_VALIDATED_MESSAGE, "first factor is false")));
if (authenticationMethod == "two_factor" && !authSession.second_factor)
return reject(new exceptions.AccessDeniedError(
Util.format("%s: %s.", SECOND_FACTOR_NOT_VALIDATED_MESSAGE, "second factor is false")));
const isAllowed = vars.accessController.isAccessAllowed(domain, path, username, groups);
if (!isAllowed) return reject(
new exceptions.DomainAccessDenied(Util.format("User '%s' does not have access to '%s'",
username, domain)));
resolve();
})
.then(function () {
return verify_inactivity(req, authSession,
vars.config, vars.logger);
})
.then(function () {
res.setHeader(REMOTE_USER, username);
res.setHeader(REMOTE_GROUPS, groups.join(","));
return BluebirdPromise.resolve();
});
} }
export default function (vars: ServerVariables) { export default function (vars: ServerVariables) {
return function (req: express.Request, res: express.Response) return function (req: Express.Request, res: Express.Response)
: BluebirdPromise<void> { : BluebirdPromise<void> {
return verify_filter(req, res, vars) let authSession: AuthenticationSession;
.then(function () { return new BluebirdPromise(function (resolve, reject) {
res.status(204); authSession = AuthenticationSessionHandler.get(req, vars.logger);
res.send(); resolve();
return BluebirdPromise.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 // The user is authenticated but has restricted access -> 403
.catch(exceptions.DomainAccessDenied, ErrorReplies .catch(Exceptions.DomainAccessDenied, ErrorReplies
.replyWithError403(req, res, vars.logger)) .replyWithError403(req, res, vars.logger))
// The user is not yet authenticated -> 401 // The user is not yet authenticated -> 401
.catch(ErrorReplies.replyWithError401(req, res, vars.logger)); .catch(ErrorReplies.replyWithError401(req, res, vars.logger));

View File

@ -0,0 +1,75 @@
import Express = require("express");
import BluebirdPromise = require("bluebird");
import ObjectPath = require("object-path");
import { ServerVariables } from "../../ServerVariables";
import { AuthenticationSession }
from "../../../../types/AuthenticationSession";
import { DomainExtractor } from "../../utils/DomainExtractor";
import { MethodCalculator } from "../../authentication/MethodCalculator";
import AccessControl from "./access_control";
export default function (req: Express.Request, res: Express.Response,
vars: ServerVariables, authorizationHeader: string)
: BluebirdPromise<{ username: string, groups: string[] }> {
let username: string;
let groups: string[];
let domain: string;
let path: string;
return new BluebirdPromise<[string, string]>(function (resolve, reject) {
const host = ObjectPath.get<Express.Request, string>(req, "headers.host");
domain = DomainExtractor.fromHostHeader(host);
path =
ObjectPath.get<Express.Request, string>(req, "headers.x-original-uri");
const authenticationMethod =
MethodCalculator.compute(vars.config.authentication_methods, domain);
if (authenticationMethod != "single_factor") {
reject(new Error("This domain is not protected with single factor. " +
"You cannot log in with basic authentication."));
return;
}
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) {
reject(new Error("No valid base64 token found in the header"));
return;
}
const tokenMatches = authorizationHeader.match(base64Re);
const base64Token = tokenMatches[1];
const decodedToken = Buffer.from(base64Token, "base64").toString();
const splittedToken = decodedToken.split(":");
if (splittedToken.length != 2) {
reject(new Error(
"The authorization token is invalid. Expecting 'userid:password'"));
return;
}
username = splittedToken[0];
const password = splittedToken[1];
resolve([username, password]);
})
.then(function ([userid, password]) {
return vars.ldapAuthenticator.authenticate(userid, password);
})
.then(function (groupsAndEmails) {
groups = groupsAndEmails.groups;
return AccessControl(req, vars, domain, path, username, groups);
})
.then(function () {
return BluebirdPromise.resolve({
username: username,
groups: groups
});
})
.catch(function (err: Error) {
return BluebirdPromise.reject(
new Error("Unable to authenticate the user with basic auth. Cause: "
+ err.message));
});
}

View File

@ -0,0 +1,102 @@
import Express = require("express");
import BluebirdPromise = require("bluebird");
import Util = require("util");
import ObjectPath = require("object-path");
import Exceptions = require("../../Exceptions");
import { AppConfiguration } from "../../configuration/Configuration";
import Constants = require("../../../../../shared/constants");
import { DomainExtractor } from "../../utils/DomainExtractor";
import { ServerVariables } from "../../ServerVariables";
import { MethodCalculator } from "../../authentication/MethodCalculator";
import { IRequestLogger } from "../../logging/IRequestLogger";
import { AuthenticationSession }
from "../../../../types/AuthenticationSession";
import { AuthenticationSessionHandler }
from "../../AuthenticationSessionHandler";
import AccessControl from "./access_control";
const FIRST_FACTOR_NOT_VALIDATED_MESSAGE = "First factor not yet validated";
const SECOND_FACTOR_NOT_VALIDATED_MESSAGE = "Second factor not yet validated";
function verify_inactivity(req: Express.Request,
authSession: AuthenticationSession,
configuration: AppConfiguration, logger: IRequestLogger)
: BluebirdPromise<void> {
// If inactivity is not specified, then inactivity timeout does not apply
if (!configuration.session.inactivity) {
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[] }> {
let username: string;
let groups: string[];
let domain: string;
let path: string;
return new BluebirdPromise(function (resolve, reject) {
username = authSession.userid;
groups = authSession.groups;
if (!authSession.userid) {
reject(new Exceptions.AccessDeniedError(
Util.format("%s: %s.", FIRST_FACTOR_NOT_VALIDATED_MESSAGE,
"userid is missing")));
return;
}
const host = ObjectPath.get<Express.Request, string>(req, "headers.host");
path =
ObjectPath.get<Express.Request, string>(req, "headers.x-original-uri");
domain = DomainExtractor.fromHostHeader(host);
const authenticationMethod =
MethodCalculator.compute(vars.config.authentication_methods, domain);
vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", domain,
path, username, groups.join(","));
if (!authSession.first_factor)
return reject(new Exceptions.AccessDeniedError(
Util.format("%s: %s.", FIRST_FACTOR_NOT_VALIDATED_MESSAGE,
"first factor is false")));
if (authenticationMethod == "two_factor" && !authSession.second_factor)
return reject(new Exceptions.AccessDeniedError(
Util.format("%s: %s.", SECOND_FACTOR_NOT_VALIDATED_MESSAGE,
"second factor is false")));
resolve();
})
.then(function () {
return AccessControl(req, vars, domain, path, username, groups);
})
.then(function () {
return verify_inactivity(req, authSession,
vars.config, vars.logger);
})
.then(function () {
return BluebirdPromise.resolve({
username: authSession.userid,
groups: authSession.groups
});
});
}

View File

@ -28,190 +28,284 @@ describe("test /api/verify endpoint", function () {
}; };
AuthenticationSessionHandler.reset(req as any); AuthenticationSessionHandler.reset(req as any);
req.headers.host = "secret.example.com"; req.headers.host = "secret.example.com";
const s = ServerVariablesMockBuilder.build(); const s = ServerVariablesMockBuilder.build(true);
mocks = s.mocks; mocks = s.mocks;
vars = s.variables; vars = s.variables;
vars.config.authentication_methods.default_method = "two_factor";
authSession = AuthenticationSessionHandler.get(req as any, vars.logger); authSession = AuthenticationSessionHandler.get(req as any, vars.logger);
}); });
it("should be already authenticated", function () { describe("with session cookie", function () {
mocks.accessController.isAccessAllowedMock.returns(true); beforeEach(function () {
authSession.first_factor = true; vars.config.authentication_methods.default_method = "two_factor";
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 be already authenticated", function () {
it("should not be authenticated when second factor is missing", function () { mocks.accessController.isAccessAllowedMock.returns(true);
return test_non_authenticated_401({ authSession.first_factor = true;
userid: "user", authSession.second_factor = true;
first_factor: true, authSession.userid = "myuser";
second_factor: false, authSession.groups = ["mygroup", "othergroup"];
email: undefined, return VerifyGet.default(vars)(req as express.Request, res as any)
groups: [], .then(function () {
last_activity_datetime: new Date().getTime() 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()
});
}); });
}); });
});
it("should not be authenticated when first factor is missing", function () { describe("given user tries to access a single factor endpoint", function () {
return test_non_authenticated_401({ beforeEach(function () {
userid: "user", req.query = {
first_factor: false, redirect: "http://redirect.url"
second_factor: true, };
email: undefined, req.headers["host"] = "redirect.url";
groups: [], mocks.config.authentication_methods.per_subdomain_methods = {
last_activity_datetime: new Date().getTime() "redirect.url": "single_factor"
}); };
}); });
it("should not be authenticated when userid is missing", function () { it("should be authenticated when first factor is validated and second factor is not", function () {
return test_non_authenticated_401({ mocks.accessController.isAccessAllowedMock.returns(true);
userid: undefined, authSession.first_factor = true;
first_factor: true, authSession.userid = "user1";
second_factor: false, return VerifyGet.default(vars)(req as express.Request, res as any)
email: undefined, .then(function () {
groups: [], Assert(res.status.calledWith(204));
last_activity_datetime: new Date().getTime() Assert(res.send.calledOnce);
}); });
}); });
it("should not be authenticated when first and second factor are missing", function () { it("should be rejected with 401 when first factor is not validated", function () {
return test_non_authenticated_401({ mocks.accessController.isAccessAllowedMock.returns(true);
userid: "user", authSession.first_factor = false;
first_factor: false, return VerifyGet.default(vars)(req as express.Request, res as any)
second_factor: false, .then(function () {
email: undefined, Assert(res.status.calledWith(401));
groups: [], });
last_activity_datetime: new Date().getTime()
});
}); });
});
it("should not be authenticated when session has not be initiated", function () { describe("inactivity period", function () {
return test_non_authenticated_401(undefined); it("should update last inactivity period on requests on /api/verify", function () {
}); mocks.config.session.inactivity = 200000;
mocks.accessController.isAccessAllowedMock.returns(true);
it("should not be authenticated when domain is not allowed for user", function () { const currentTime = new Date().getTime() - 1000;
AuthenticationSessionHandler.reset(req as any);
authSession.first_factor = true; authSession.first_factor = true;
authSession.second_factor = true; authSession.second_factor = true;
authSession.userid = "myuser"; authSession.userid = "myuser";
req.headers.host = "test.example.com"; authSession.groups = ["mygroup", "othergroup"];
mocks.accessController.isAccessAllowedMock.returns(false); 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);
});
});
return test_unauthorized_403({ it("should reset session when max inactivity period has been reached", function () {
first_factor: true, mocks.config.session.inactivity = 1;
second_factor: true, mocks.accessController.isAccessAllowedMock.returns(true);
userid: "user", const currentTime = new Date().getTime() - 1000;
groups: ["group1", "group2"], AuthenticationSessionHandler.reset(req as any);
email: undefined, authSession.first_factor = true;
last_activity_datetime: new Date().getTime() 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("given user tries to access a basic auth endpoint", function () { describe("with basic auth", function () {
beforeEach(function () { it("should authenticate correctly", function () {
req.query = { mocks.accessController.isAccessAllowedMock.returns(true);
redirect: "http://redirect.url" mocks.config.authentication_methods.default_method = "single_factor";
}; mocks.ldapAuthenticator.authenticateStub.returns({
req.headers["host"] = "redirect.url"; 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 = { mocks.config.authentication_methods.per_subdomain_methods = {
"redirect.url": "single_factor" "secret.example.com": "two_factor"
}; };
}); mocks.ldapAuthenticator.authenticateStub.resolves({
groups: ["mygroup", "othergroup"],
});
req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA==";
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) return VerifyGet.default(vars)(req as express.Request, res as any)
.then(function () { .then(function () {
Assert(res.status.calledWith(204)); Assert(res.status.calledWithExactly(401));
Assert(res.send.calledOnce);
}); });
}); });
it("should be rejected with 401 when first factor is not validated", function () { it("should fail when base64 token is not valid", function () {
mocks.accessController.isAccessAllowedMock.returns(true); mocks.accessController.isAccessAllowedMock.returns(true);
authSession.first_factor = false; mocks.config.authentication_methods.default_method = "single_factor";
return VerifyGet.default(vars)(req as express.Request, res as any) mocks.ldapAuthenticator.authenticateStub.resolves({
.then(function () { groups: ["mygroup", "othergroup"],
Assert(res.status.calledWith(401)); });
}); req.headers["proxy-authorization"] = "Basic i_m*not_a_base64*token";
});
});
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) return VerifyGet.default(vars)(req as express.Request, res as any)
.then(function () { .then(function () {
return AuthenticationSessionHandler.get(req as any, vars.logger); Assert(res.status.calledWithExactly(401));
})
.then(function (authSession) {
Assert(authSession.last_activity_datetime > currentTime);
}); });
}); });
it("should reset session when max inactivity period has been reached", function () { it("should fail when base64 token has not format user:psswd", function () {
mocks.config.session.inactivity = 1;
mocks.accessController.isAccessAllowedMock.returns(true); mocks.accessController.isAccessAllowedMock.returns(true);
const currentTime = new Date().getTime() - 1000; mocks.config.authentication_methods.default_method = "single_factor";
AuthenticationSessionHandler.reset(req as any); mocks.ldapAuthenticator.authenticateStub.resolves({
authSession.first_factor = true; groups: ["mygroup", "othergroup"],
authSession.second_factor = true; });
authSession.userid = "myuser"; req.headers["proxy-authorization"] = "Basic am9objpwYXNzOmJhZA==";
authSession.groups = ["mygroup", "othergroup"];
authSession.last_activity_datetime = currentTime;
return VerifyGet.default(vars)(req as express.Request, res as any) return VerifyGet.default(vars)(req as express.Request, res as any)
.then(function () { .then(function () {
return AuthenticationSessionHandler.get(req as any, vars.logger); Assert(res.status.calledWithExactly(401));
}) });
.then(function (authSession) { });
Assert.equal(authSession.first_factor, false);
Assert.equal(authSession.second_factor, false); it("should fail when bad user password is provided", function () {
Assert.equal(authSession.userid, undefined); 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));
}); });
}); });
}); });

View File

@ -4,3 +4,8 @@ Feature: User and groups headers are correctly forwarded to backend
When I visit "https://public.test.local:8080/headers" When I visit "https://public.test.local:8080/headers"
Then I see header "Custom-Forwarded-User" set to "john" Then I see header "Custom-Forwarded-User" set to "john"
Then I see header "Custom-Forwarded-Groups" set to "dev,admin" Then I see header "Custom-Forwarded-Groups" set to "dev,admin"
Scenario: Custom-Forwarded-User and Custom-Forwarded-Groups are correctly forwarded to protected backend when basic auth is used
When I request "https://single_factor.test.local:8080/headers" with username "john" and password "password" using basic authentication
Then I received header "Custom-Forwarded-User" set to "john"
And I received header "Custom-Forwarded-Groups" set to "dev,admin"

View File

@ -1,13 +1,15 @@
Feature: User can access certain subdomains with single factor Feature: User can access certain subdomains with single factor
@need-registered-user-john
Scenario: User is redirected to service after first factor if allowed Scenario: User is redirected to service after first factor if allowed
When I visit "https://auth.test.local:8080/?redirect=https%3A%2F%2Fsingle_factor.test.local%3A8080%2Fsecret.html" When I visit "https://auth.test.local:8080/?redirect=https%3A%2F%2Fsingle_factor.test.local%3A8080%2Fsecret.html"
And I login with user "john" and password "password" And I login with user "john" and password "password"
Then I'm redirected to "https://single_factor.test.local:8080/secret.html" Then I'm redirected to "https://single_factor.test.local:8080/secret.html"
@need-registered-user-john
Scenario: Redirection after first factor fails if single_factor not allowed. It redirects user to first factor. Scenario: Redirection after first factor fails if single_factor not allowed. It redirects user to first factor.
When I visit "https://auth.test.local:8080/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html" When I visit "https://auth.test.local:8080/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html"
And I login with user "john" and password "password" And I login with user "john" and password "password"
Then I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html" Then I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html"
Scenario: User can login using basic authentication
When I request "https://single_factor.test.local:8080/secret.html" with username "john" and password "password" using basic authentication
Then I receive the secret page

View File

@ -10,7 +10,7 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
function (expectedHeaderName: string, expectedValue: string) { function (expectedHeaderName: string, expectedValue: string) {
return this.driver.findElement(seleniumWebdriver.By.tagName("body")).getText() return this.driver.findElement(seleniumWebdriver.By.tagName("body")).getText()
.then(function (txt: string) { .then(function (txt: string) {
const expectedLine = Util.format("\"%s\": \"%s\"", expectedHeaderName, expectedValue); const expectedLine = Util.format("\"%s\": \"%s\"", expectedHeaderName, expectedValue);
if (txt.indexOf(expectedLine) > 0) if (txt.indexOf(expectedLine) > 0)
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
else else

View File

@ -0,0 +1,39 @@
import Cucumber = require("cucumber");
import seleniumWebdriver = require("selenium-webdriver");
import Request = require("request-promise");
import BluebirdPromise = require("bluebird");
import Util = require("util");
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
When("I request {stringInDoubleQuotes} with username {stringInDoubleQuotes}" +
" and password {stringInDoubleQuotes} using basic authentication",
function (url: string, username: string, password: string) {
const that = this;
return Request(url, {
auth: {
username: username,
password: password
},
resolveWithFullResponse: true
})
.then(function (response: any) {
that.response = response;
});
});
Then("I receive the secret page", function () {
if (this.response.body.match("This is a very important secret!"))
return BluebirdPromise.resolve();
return BluebirdPromise.reject(new Error("Secret page not received."));
});
Then("I received header {stringInDoubleQuotes} set to {stringInDoubleQuotes}",
function (expectedHeaderName: string, expectedValue: string) {
const expectedLine = Util.format("\"%s\": \"%s\"", expectedHeaderName,
expectedValue);
if (this.response.body.indexOf(expectedLine) > 0)
return BluebirdPromise.resolve();
return BluebirdPromise.reject(new Error(
Util.format("No such header or with unexpected value.")));
})
});