Implement session inactivity timeout
This timeout will prevent an attacker from using a session that has been inactive for too long. This inactivity timeout combined with the timeout before expiration makes a good combination of security mechanisms to prevent session theft. If no activity timeout is provided, then the feature is disabled and only session expiration remains as a protection.pull/175/head
parent
b9fa786df6
commit
dacdce6c50
|
@ -29,6 +29,7 @@ dist/
|
||||||
|
|
||||||
# Specific files
|
# Specific files
|
||||||
/config.yml
|
/config.yml
|
||||||
|
/config.test.yml
|
||||||
|
|
||||||
example/ldap/private.ldif
|
example/ldap/private.ldif
|
||||||
|
|
||||||
|
|
|
@ -42,8 +42,8 @@ module.exports = function (grunt) {
|
||||||
args: ['--colors', '--compilers', 'ts:ts-node/register', '--recursive', 'client/test']
|
args: ['--colors', '--compilers', 'ts:ts-node/register', '--recursive', 'client/test']
|
||||||
},
|
},
|
||||||
"test-int": {
|
"test-int": {
|
||||||
cmd: "./node_modules/.bin/cucumber-js",
|
cmd: "./scripts/run-cucumber.sh",
|
||||||
args: ["--colors", "--compiler", "ts:ts-node/register", "./test/features"]
|
args: ["./test/features"]
|
||||||
},
|
},
|
||||||
"docker-build": {
|
"docker-build": {
|
||||||
cmd: "docker",
|
cmd: "docker",
|
||||||
|
|
|
@ -156,6 +156,9 @@ session:
|
||||||
# The time in ms before the cookie expires and session is reset.
|
# The time in ms before the cookie expires and session is reset.
|
||||||
expiration: 3600000 # 1 hour
|
expiration: 3600000 # 1 hour
|
||||||
|
|
||||||
|
# The inactivity time in ms before the session is reset.
|
||||||
|
inactivity: 300000 # 5 minutes
|
||||||
|
|
||||||
# The domain to protect.
|
# The domain to protect.
|
||||||
# Note: the authenticator must also be in that domain. If empty, the cookie
|
# Note: the authenticator must also be in that domain. If empty, the cookie
|
||||||
# is restricted to the subdomain of the issuer.
|
# is restricted to the subdomain of the issuer.
|
||||||
|
|
196
config.test.yml
196
config.test.yml
|
@ -1,196 +0,0 @@
|
||||||
###############################################################
|
|
||||||
# Authelia configuration #
|
|
||||||
###############################################################
|
|
||||||
|
|
||||||
# The port to listen on
|
|
||||||
port: 80
|
|
||||||
|
|
||||||
# Log level
|
|
||||||
#
|
|
||||||
# Level of verbosity for logs
|
|
||||||
logs_level: debug
|
|
||||||
|
|
||||||
# LDAP configuration
|
|
||||||
#
|
|
||||||
# Example: for user john, the DN will be cn=john,ou=users,dc=example,dc=com
|
|
||||||
ldap:
|
|
||||||
# The url of the ldap server
|
|
||||||
url: ldap://openldap
|
|
||||||
|
|
||||||
# The base dn for every entries
|
|
||||||
base_dn: dc=example,dc=com
|
|
||||||
|
|
||||||
# An additional dn to define the scope to all users
|
|
||||||
additional_users_dn: ou=users
|
|
||||||
|
|
||||||
# The users filter.
|
|
||||||
# {0} is the matcher replaced by username.
|
|
||||||
# 'cn={0}' by default.
|
|
||||||
users_filter: cn={0}
|
|
||||||
|
|
||||||
# An additional dn to define the scope of groups
|
|
||||||
additional_groups_dn: ou=groups
|
|
||||||
|
|
||||||
# The groups filter used for retrieving groups of a given user.
|
|
||||||
# {0} is a matcher replaced by username.
|
|
||||||
# {dn} is a matcher replaced by user DN.
|
|
||||||
# 'member={dn}' by default.
|
|
||||||
groups_filter: (&(member={dn})(objectclass=groupOfNames))
|
|
||||||
|
|
||||||
# The attribute holding the name of the group
|
|
||||||
group_name_attribute: cn
|
|
||||||
|
|
||||||
# The attribute holding the mail address of the user
|
|
||||||
mail_attribute: mail
|
|
||||||
|
|
||||||
# The username and password of the admin user.
|
|
||||||
user: cn=admin,dc=example,dc=com
|
|
||||||
password: password
|
|
||||||
|
|
||||||
# Authentication methods
|
|
||||||
#
|
|
||||||
# Authentication methods can be defined per subdomain.
|
|
||||||
# There are currently two available methods: "basic_auth" and "two_factor"
|
|
||||||
#
|
|
||||||
# Note: by default a domain uses "two_factor" method.
|
|
||||||
#
|
|
||||||
# Note: 'per_subdomain_methods' is a dictionary where keys must be subdomains and
|
|
||||||
# values must be one of the two possible methods.
|
|
||||||
#
|
|
||||||
# Note: 'per_subdomain_methods' is optional.
|
|
||||||
authentication_methods:
|
|
||||||
default_method: two_factor
|
|
||||||
per_subdomain_methods:
|
|
||||||
basicauth.test.local: basic_auth
|
|
||||||
|
|
||||||
# Access Control
|
|
||||||
#
|
|
||||||
# Access control is a set of rules you can use to restrict the user access.
|
|
||||||
# Default (anyone), per-user or per-group rules can be defined.
|
|
||||||
#
|
|
||||||
# If 'access_control' is not defined, ACL rules are disabled and a default policy
|
|
||||||
# is applied, i.e., access is allowed to anyone. Otherwise restrictions follow
|
|
||||||
# the rules defined below.
|
|
||||||
# If no rule is provided, all domains are denied.
|
|
||||||
#
|
|
||||||
# One can use the wildcard * to match any subdomain.
|
|
||||||
# Note 1: It must stand at the beginning of the pattern. (example: *.mydomain.com)
|
|
||||||
# Note 2: You must put the pattern in simple quotes when using the wildcard.
|
|
||||||
access_control:
|
|
||||||
# Default policy can either be `allow` or `deny`.
|
|
||||||
# It is the policy applied to any resource if it has not been overriden
|
|
||||||
# in the `any`, `groups` or `users` category.
|
|
||||||
default_policy: deny
|
|
||||||
|
|
||||||
# The rules that apply to anyone.
|
|
||||||
# The value is a list of rules.
|
|
||||||
any:
|
|
||||||
- domain: public.test.local
|
|
||||||
policy: allow
|
|
||||||
|
|
||||||
# Group-based rules. The key is a group name and the value
|
|
||||||
# is a list of rules.
|
|
||||||
groups:
|
|
||||||
admin:
|
|
||||||
# All resources in all domains
|
|
||||||
- domain: '*.test.local'
|
|
||||||
policy: allow
|
|
||||||
# Except mx2.mail.test.local (it restricts the first rule)
|
|
||||||
- domain: 'mx2.mail.test.local'
|
|
||||||
policy: deny
|
|
||||||
dev:
|
|
||||||
- domain: dev.test.local
|
|
||||||
policy: allow
|
|
||||||
resources:
|
|
||||||
- '^/groups/dev/.*$'
|
|
||||||
|
|
||||||
# User-based rules. The key is a user name and the value
|
|
||||||
# is a list of rules.
|
|
||||||
users:
|
|
||||||
john:
|
|
||||||
- domain: dev.test.local
|
|
||||||
policy: allow
|
|
||||||
resources:
|
|
||||||
- '^/users/john/.*$'
|
|
||||||
harry:
|
|
||||||
- domain: dev.test.local
|
|
||||||
policy: allow
|
|
||||||
resources:
|
|
||||||
- '^/users/harry/.*$'
|
|
||||||
bob:
|
|
||||||
- domain: '*.mail.test.local'
|
|
||||||
policy: allow
|
|
||||||
- domain: 'dev.test.local'
|
|
||||||
policy: allow
|
|
||||||
resources:
|
|
||||||
- '^/users/bob/.*$'
|
|
||||||
|
|
||||||
# Configuration of session cookies
|
|
||||||
#
|
|
||||||
# The session cookies identify the user once logged in.
|
|
||||||
session:
|
|
||||||
# The secret to encrypt the session cookie.
|
|
||||||
secret: unsecure_secret
|
|
||||||
|
|
||||||
# The time before the cookie expires.
|
|
||||||
expiration: 10000
|
|
||||||
|
|
||||||
# The domain to protect.
|
|
||||||
# Note: the authenticator must also be in that domain. If empty, the cookie
|
|
||||||
# is restricted to the subdomain of the issuer.
|
|
||||||
domain: test.local
|
|
||||||
|
|
||||||
# The redis connection details
|
|
||||||
redis:
|
|
||||||
host: redis
|
|
||||||
port: 6379
|
|
||||||
|
|
||||||
# Configuration of the authentication regulation mechanism.
|
|
||||||
#
|
|
||||||
# This mechanism prevents attackers from brute forcing the first factor.
|
|
||||||
# It bans the user if too many attempts are done in a short period of
|
|
||||||
# time.
|
|
||||||
regulation:
|
|
||||||
# The number of failed login attempts before user is banned.
|
|
||||||
# Set it to 0 for disabling regulation.
|
|
||||||
max_retries: 3
|
|
||||||
|
|
||||||
# The length of time between login attempts before user is banned.
|
|
||||||
find_time: 15
|
|
||||||
|
|
||||||
# The length of time before a banned user can login again.
|
|
||||||
ban_time: 4
|
|
||||||
|
|
||||||
# Configuration of the storage backend used to store data and secrets.
|
|
||||||
#
|
|
||||||
# You must use only an available configuration: local, mongo
|
|
||||||
storage:
|
|
||||||
# The directory where the DB files will be saved
|
|
||||||
# local: /var/lib/authelia/store
|
|
||||||
|
|
||||||
# Settings to connect to mongo server
|
|
||||||
mongo:
|
|
||||||
url: mongodb://mongo/authelia
|
|
||||||
|
|
||||||
# Configuration of the notification system.
|
|
||||||
#
|
|
||||||
# Notifications are sent to users when they require a password reset, a u2f
|
|
||||||
# registration or a TOTP registration.
|
|
||||||
# Use only an available configuration: filesystem, gmail
|
|
||||||
notifier:
|
|
||||||
# Use your email account to send the notifications. You can use an app password.
|
|
||||||
# List of valid services can be found here: https://nodemailer.com/smtp/well-known/
|
|
||||||
# email:
|
|
||||||
# username: user@example.com
|
|
||||||
# password: yourpassword
|
|
||||||
# sender: admin@example.com
|
|
||||||
# service: gmail
|
|
||||||
|
|
||||||
# Use a SMTP server for sending notifications
|
|
||||||
smtp:
|
|
||||||
username: test
|
|
||||||
password: test
|
|
||||||
secure: false
|
|
||||||
host: 'smtp'
|
|
||||||
port: 1025
|
|
||||||
sender: admin@example.com
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
./node_modules/.bin/cucumber-js --colors --compiler ts:ts-node/register $*
|
|
@ -9,7 +9,7 @@ export interface AuthenticationSession {
|
||||||
userid: string;
|
userid: string;
|
||||||
first_factor: boolean;
|
first_factor: boolean;
|
||||||
second_factor: boolean;
|
second_factor: boolean;
|
||||||
last_activity_datetime: Date;
|
last_activity_datetime: number;
|
||||||
identity_check?: {
|
identity_check?: {
|
||||||
challenge: string;
|
challenge: string;
|
||||||
userid: string;
|
userid: string;
|
||||||
|
@ -40,7 +40,7 @@ export function reset(req: express.Request): void {
|
||||||
req.session.auth = Object.assign({}, INITIAL_AUTHENTICATION_SESSION, {});
|
req.session.auth = Object.assign({}, INITIAL_AUTHENTICATION_SESSION, {});
|
||||||
|
|
||||||
// Initialize last activity with current time
|
// Initialize last activity with current time
|
||||||
req.session.auth.last_activity_datetime = new Date();
|
req.session.auth.last_activity_datetime = new Date().getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function get(req: express.Request): BluebirdPromise<AuthenticationSession> {
|
export function get(req: express.Request): BluebirdPromise<AuthenticationSession> {
|
||||||
|
|
|
@ -62,6 +62,7 @@ export interface SessionRedisOptions {
|
||||||
interface SessionCookieConfiguration {
|
interface SessionCookieConfiguration {
|
||||||
secret: string;
|
secret: string;
|
||||||
expiration?: number;
|
expiration?: number;
|
||||||
|
inactivity?: number;
|
||||||
domain?: string;
|
domain?: string;
|
||||||
redis?: SessionRedisOptions;
|
redis?: SessionRedisOptions;
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,7 @@ function adaptFromUserConfiguration(userConfiguration: UserConfiguration)
|
||||||
domain: ObjectPath.get<object, string>(userConfiguration, "session.domain"),
|
domain: ObjectPath.get<object, string>(userConfiguration, "session.domain"),
|
||||||
secret: ObjectPath.get<object, string>(userConfiguration, "session.secret"),
|
secret: ObjectPath.get<object, string>(userConfiguration, "session.secret"),
|
||||||
expiration: get_optional<number>(userConfiguration, "session.expiration", 3600000), // in ms
|
expiration: get_optional<number>(userConfiguration, "session.expiration", 3600000), // in ms
|
||||||
|
inactivity: get_optional<number>(userConfiguration, "session.inactivity", undefined),
|
||||||
redis: ObjectPath.get<object, SessionRedisOptions>(userConfiguration, "session.redis")
|
redis: ObjectPath.get<object, SessionRedisOptions>(userConfiguration, "session.redis")
|
||||||
},
|
},
|
||||||
storage: {
|
storage: {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import Util = require("util");
|
||||||
import { DomainExtractor } from "../../utils/DomainExtractor";
|
import { DomainExtractor } from "../../utils/DomainExtractor";
|
||||||
import { ServerVariables } from "../../ServerVariables";
|
import { ServerVariables } from "../../ServerVariables";
|
||||||
import { AuthenticationMethodCalculator } from "../../AuthenticationMethodCalculator";
|
import { AuthenticationMethodCalculator } from "../../AuthenticationMethodCalculator";
|
||||||
|
import { IRequestLogger } from "../../logging/IRequestLogger";
|
||||||
|
|
||||||
const FIRST_FACTOR_NOT_VALIDATED_MESSAGE = "First factor not yet validated";
|
const FIRST_FACTOR_NOT_VALIDATED_MESSAGE = "First factor not yet validated";
|
||||||
const SECOND_FACTOR_NOT_VALIDATED_MESSAGE = "Second factor not yet validated";
|
const SECOND_FACTOR_NOT_VALIDATED_MESSAGE = "Second factor not yet validated";
|
||||||
|
@ -20,17 +21,48 @@ const SECOND_FACTOR_NOT_VALIDATED_MESSAGE = "Second factor not yet validated";
|
||||||
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.AuthenticationSession,
|
||||||
|
configuration: AppConfiguration, logger: IRequestLogger)
|
||||||
|
: BluebirdPromise<void> {
|
||||||
|
|
||||||
|
const lastActivityTime = authSession.last_activity_datetime;
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
authSession.last_activity_datetime = currentTime;
|
||||||
|
|
||||||
|
// If inactivity is not specified, then inactivity timeout does not apply
|
||||||
|
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.");
|
||||||
|
AuthenticationSession.reset(req);
|
||||||
|
return BluebirdPromise.reject(new Error("Inactivity period exceeded."));
|
||||||
|
}
|
||||||
|
|
||||||
function verify_filter(req: express.Request, res: express.Response,
|
function verify_filter(req: express.Request, res: express.Response,
|
||||||
vars: ServerVariables): BluebirdPromise<void> {
|
vars: ServerVariables): BluebirdPromise<void> {
|
||||||
|
let _authSession: AuthenticationSession.AuthenticationSession;
|
||||||
|
let username: string;
|
||||||
|
let groups: string[];
|
||||||
|
|
||||||
return AuthenticationSession.get(req)
|
return AuthenticationSession.get(req)
|
||||||
.then(function (authSession) {
|
.then(function (authSession) {
|
||||||
|
_authSession = authSession;
|
||||||
|
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"]));
|
||||||
|
|
||||||
const username = authSession.userid;
|
if (!_authSession.userid)
|
||||||
const groups = authSession.groups;
|
|
||||||
if (!authSession.userid)
|
|
||||||
return BluebirdPromise.reject(
|
return BluebirdPromise.reject(
|
||||||
new exceptions.AccessDeniedError(FIRST_FACTOR_NOT_VALIDATED_MESSAGE));
|
new exceptions.AccessDeniedError(FIRST_FACTOR_NOT_VALIDATED_MESSAGE));
|
||||||
|
|
||||||
|
@ -44,23 +76,27 @@ function verify_filter(req: express.Request, res: express.Response,
|
||||||
vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", domain, path,
|
vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", domain, path,
|
||||||
username, groups.join(","));
|
username, groups.join(","));
|
||||||
|
|
||||||
if (!authSession.first_factor)
|
if (!_authSession.first_factor)
|
||||||
return BluebirdPromise.reject(
|
return BluebirdPromise.reject(
|
||||||
new exceptions.AccessDeniedError(FIRST_FACTOR_NOT_VALIDATED_MESSAGE));
|
new exceptions.AccessDeniedError(FIRST_FACTOR_NOT_VALIDATED_MESSAGE));
|
||||||
|
|
||||||
|
if (authenticationMethod == "two_factor" && !_authSession.second_factor)
|
||||||
|
return BluebirdPromise.reject(
|
||||||
|
new exceptions.AccessDeniedError(SECOND_FACTOR_NOT_VALIDATED_MESSAGE));
|
||||||
|
|
||||||
const isAllowed = vars.accessController.isAccessAllowed(domain, path, username, groups);
|
const isAllowed = vars.accessController.isAccessAllowed(domain, path, username, groups);
|
||||||
if (!isAllowed) return BluebirdPromise.reject(
|
if (!isAllowed) return BluebirdPromise.reject(
|
||||||
new exceptions.DomainAccessDenied(Util.format("User '%s' does not have access to '%s'",
|
new exceptions.DomainAccessDenied(Util.format("User '%s' does not have access to '%s'",
|
||||||
username, domain)));
|
username, domain)));
|
||||||
|
return BluebirdPromise.resolve();
|
||||||
if (authenticationMethod == "two_factor" && !authSession.second_factor)
|
})
|
||||||
return BluebirdPromise.reject(
|
.then(function () {
|
||||||
new exceptions.AccessDeniedError(SECOND_FACTOR_NOT_VALIDATED_MESSAGE));
|
return verify_inactivity(req, _authSession,
|
||||||
|
vars.config, vars.logger);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
res.setHeader(REMOTE_USER, username);
|
res.setHeader(REMOTE_USER, username);
|
||||||
res.setHeader(REMOTE_GROUPS, groups.join(","));
|
res.setHeader(REMOTE_GROUPS, groups.join(","));
|
||||||
|
|
||||||
return BluebirdPromise.resolve();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -113,6 +113,7 @@ describe("test session configuration builder", function () {
|
||||||
domain: "example.com",
|
domain: "example.com",
|
||||||
expiration: 3600,
|
expiration: 3600,
|
||||||
secret: "secret",
|
secret: "secret",
|
||||||
|
inactivity: 4000,
|
||||||
redis: {
|
redis: {
|
||||||
host: "redis.example.com",
|
host: "redis.example.com",
|
||||||
port: 6379
|
port: 6379
|
||||||
|
|
|
@ -61,7 +61,23 @@ describe("test config parser", function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("test session configuration", function() {
|
||||||
it("should get the session attributes", function () {
|
it("should get the session attributes", function () {
|
||||||
|
const yaml_config = buildYamlConfig();
|
||||||
|
yaml_config.session = {
|
||||||
|
domain: "example.com",
|
||||||
|
secret: "secret",
|
||||||
|
expiration: 3600,
|
||||||
|
inactivity: 4000
|
||||||
|
};
|
||||||
|
const config = ConfigurationParser.parse(yaml_config);
|
||||||
|
Assert.equal(config.session.domain, "example.com");
|
||||||
|
Assert.equal(config.session.secret, "secret");
|
||||||
|
Assert.equal(config.session.expiration, 3600);
|
||||||
|
Assert.equal(config.session.inactivity, 4000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be ok not specifying inactivity", function () {
|
||||||
const yaml_config = buildYamlConfig();
|
const yaml_config = buildYamlConfig();
|
||||||
yaml_config.session = {
|
yaml_config.session = {
|
||||||
domain: "example.com",
|
domain: "example.com",
|
||||||
|
@ -72,6 +88,8 @@ describe("test config parser", function () {
|
||||||
Assert.equal(config.session.domain, "example.com");
|
Assert.equal(config.session.domain, "example.com");
|
||||||
Assert.equal(config.session.secret, "secret");
|
Assert.equal(config.session.secret, "secret");
|
||||||
Assert.equal(config.session.expiration, 3600);
|
Assert.equal(config.session.expiration, 3600);
|
||||||
|
Assert.equal(config.session.inactivity, undefined);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get the log level", function () {
|
it("should get the log level", function () {
|
||||||
|
|
|
@ -91,7 +91,7 @@ describe("test /verify endpoint", function () {
|
||||||
second_factor: false,
|
second_factor: false,
|
||||||
email: undefined,
|
email: undefined,
|
||||||
groups: [],
|
groups: [],
|
||||||
last_activity_datetime: new Date()
|
last_activity_datetime: new Date().getTime()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ describe("test /verify endpoint", function () {
|
||||||
second_factor: true,
|
second_factor: true,
|
||||||
email: undefined,
|
email: undefined,
|
||||||
groups: [],
|
groups: [],
|
||||||
last_activity_datetime: new Date()
|
last_activity_datetime: new Date().getTime()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ describe("test /verify endpoint", function () {
|
||||||
second_factor: false,
|
second_factor: false,
|
||||||
email: undefined,
|
email: undefined,
|
||||||
groups: [],
|
groups: [],
|
||||||
last_activity_datetime: new Date()
|
last_activity_datetime: new Date().getTime()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ describe("test /verify endpoint", function () {
|
||||||
second_factor: false,
|
second_factor: false,
|
||||||
email: undefined,
|
email: undefined,
|
||||||
groups: [],
|
groups: [],
|
||||||
last_activity_datetime: new Date()
|
last_activity_datetime: new Date().getTime()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -147,7 +147,7 @@ describe("test /verify endpoint", function () {
|
||||||
userid: "user",
|
userid: "user",
|
||||||
groups: ["group1", "group2"],
|
groups: ["group1", "group2"],
|
||||||
email: undefined,
|
email: undefined,
|
||||||
last_activity_datetime: new Date()
|
last_activity_datetime: new Date().getTime()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -191,5 +191,53 @@ describe("test /verify endpoint", function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("inactivity period", function () {
|
||||||
|
it("should update last inactivity period on requests on /verify", function () {
|
||||||
|
mocks.config.session.inactivity = 200000;
|
||||||
|
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||||
|
const currentTime = new Date().getTime() - 1000;
|
||||||
|
AuthenticationSession.reset(req as any);
|
||||||
|
return AuthenticationSession.get(req as any)
|
||||||
|
.then(function (authSession: AuthenticationSession.AuthenticationSession) {
|
||||||
|
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 AuthenticationSession.get(req as any);
|
||||||
|
})
|
||||||
|
.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;
|
||||||
|
AuthenticationSession.reset(req as any);
|
||||||
|
return AuthenticationSession.get(req as any)
|
||||||
|
.then(function (authSession: AuthenticationSession.AuthenticationSession) {
|
||||||
|
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 AuthenticationSession.get(req as any);
|
||||||
|
})
|
||||||
|
.then(function (authSession) {
|
||||||
|
Assert.equal(authSession.first_factor, false);
|
||||||
|
Assert.equal(authSession.second_factor, false);
|
||||||
|
Assert.equal(authSession.userid, undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,6 @@ Feature: User is correctly redirected
|
||||||
And I visit "https://admin.test.local:8080/secret.html"
|
And I visit "https://admin.test.local:8080/secret.html"
|
||||||
Then I get an error 403
|
Then I get an error 403
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Scenario: Redirection URL is propagated from restricted page to first factor
|
Scenario: Redirection URL is propagated from restricted page to first factor
|
||||||
When I visit "https://public.test.local:8080/secret.html"
|
When I visit "https://public.test.local:8080/secret.html"
|
||||||
Then I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fpublic.test.local%3A8080%2Fsecret.html"
|
Then I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fpublic.test.local%3A8080%2Fsecret.html"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
@needs-regulation-config
|
||||||
Feature: Authelia regulates authentication to avoid brute force
|
Feature: Authelia regulates authentication to avoid brute force
|
||||||
|
|
||||||
@needs-test-config
|
|
||||||
@need-registered-user-blackhat
|
@need-registered-user-blackhat
|
||||||
Scenario: Attacker tries too many authentication in a short period of time and get banned
|
Scenario: Attacker tries too many authentication in a short period of time and get banned
|
||||||
Given I visit "https://auth.test.local:8080/"
|
Given I visit "https://auth.test.local:8080/"
|
||||||
|
@ -18,7 +18,6 @@ Feature: Authelia regulates authentication to avoid brute force
|
||||||
And I click on "Sign in"
|
And I click on "Sign in"
|
||||||
Then I get a notification of type "error" with message "Authentication failed. Please check your credentials."
|
Then I get a notification of type "error" with message "Authentication failed. Please check your credentials."
|
||||||
|
|
||||||
@needs-test-config
|
|
||||||
@need-registered-user-blackhat
|
@need-registered-user-blackhat
|
||||||
Scenario: User is unbanned after a configured amount of time
|
Scenario: User is unbanned after a configured amount of time
|
||||||
Given I visit "https://auth.test.local:8080/?redirect=https%3A%2F%2Fpublic.test.local%3A8080%2Fsecret.html"
|
Given I visit "https://auth.test.local:8080/?redirect=https%3A%2F%2Fpublic.test.local%3A8080%2Fsecret.html"
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
@needs-inactivity-config
|
||||||
|
Feature: Session is closed after a certain amount of time
|
||||||
|
|
||||||
|
@need-authenticated-user-john
|
||||||
|
Scenario: An authenticated user is disconnected after a certain inactivity period
|
||||||
|
Given I have access to:
|
||||||
|
| url |
|
||||||
|
| https://public.test.local:8080/secret.html |
|
||||||
|
When I sleep for 6 seconds
|
||||||
|
And I visit "https://public.test.local:8080/secret.html"
|
||||||
|
Then I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fpublic.test.local%3A8080%2Fsecret.html"
|
||||||
|
|
||||||
|
@need-authenticated-user-john
|
||||||
|
Scenario: An authenticated user is disconnected after session expiration period
|
||||||
|
Given I have access to:
|
||||||
|
| url |
|
||||||
|
| https://public.test.local:8080/secret.html |
|
||||||
|
When I sleep for 4 seconds
|
||||||
|
And I visit "https://public.test.local:8080/secret.html"
|
||||||
|
And I sleep for 4 seconds
|
||||||
|
And I visit "https://public.test.local:8080/secret.html"
|
||||||
|
And I sleep for 4 seconds
|
||||||
|
And I visit "https://public.test.local:8080/secret.html"
|
||||||
|
Then I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fpublic.test.local%3A8080%2Fsecret.html"
|
|
@ -6,7 +6,7 @@ import { UserDataStore } from "../../../server/src/lib/storage/UserDataStore";
|
||||||
import { CollectionFactoryFactory } from "../../../server/src/lib/storage/CollectionFactoryFactory";
|
import { CollectionFactoryFactory } from "../../../server/src/lib/storage/CollectionFactoryFactory";
|
||||||
import { MongoConnector } from "../../../server/src/lib/connectors/mongo/MongoConnector";
|
import { MongoConnector } from "../../../server/src/lib/connectors/mongo/MongoConnector";
|
||||||
import { IMongoClient } from "../../../server/src/lib/connectors/mongo/IMongoClient";
|
import { IMongoClient } from "../../../server/src/lib/connectors/mongo/IMongoClient";
|
||||||
import { TOTPGenerator } from "../../../server/src/lib/TOTPGenerator";
|
import { TotpHandler } from "../../../server/src/lib/authentication/totp/TotpHandler";
|
||||||
import Speakeasy = require("speakeasy");
|
import Speakeasy = require("speakeasy");
|
||||||
|
|
||||||
Cucumber.defineSupportCode(function ({ setDefaultTimeout }) {
|
Cucumber.defineSupportCode(function ({ setDefaultTimeout }) {
|
||||||
|
@ -14,19 +14,46 @@ Cucumber.defineSupportCode(function ({ setDefaultTimeout }) {
|
||||||
});
|
});
|
||||||
|
|
||||||
Cucumber.defineSupportCode(function ({ After, Before }) {
|
Cucumber.defineSupportCode(function ({ After, Before }) {
|
||||||
const exec = BluebirdPromise.promisify(ChildProcess.exec);
|
const exec = BluebirdPromise.promisify<any, any>(ChildProcess.exec);
|
||||||
|
|
||||||
After(function () {
|
After(function () {
|
||||||
return this.driver.quit();
|
return this.driver.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
Before({ tags: "@needs-test-config", timeout: 20 * 1000 }, function () {
|
function createRegulationConfiguration(): BluebirdPromise<void> {
|
||||||
return exec("./scripts/example-commit/dc-example.sh -f docker-compose.test.yml up -d authelia && sleep 2");
|
return exec("\
|
||||||
|
cat config.template.yml | \
|
||||||
|
sed 's/find_time: [0-9]\\+/find_time: 15/' | \
|
||||||
|
sed 's/ban_time: [0-9]\\+/ban_time: 4/' > config.test.yml \
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInactivityConfiguration(): BluebirdPromise<void> {
|
||||||
|
return exec("\
|
||||||
|
cat config.template.yml | \
|
||||||
|
sed 's/expiration: [0-9]\\+/expiration: 10000/' | \
|
||||||
|
sed 's/inactivity: [0-9]\\+/inactivity: 5000/' > config.test.yml \
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
function declareNeedsConfiguration(tag: string, cb: () => BluebirdPromise<void>) {
|
||||||
|
Before({ tags: "@needs-" + tag + "-config", timeout: 20 * 1000 }, function () {
|
||||||
|
return cb()
|
||||||
|
.then(function () {
|
||||||
|
return exec("./scripts/example-commit/dc-example.sh -f docker-compose.test.yml up -d authelia && sleep 1");
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
After({ tags: "@needs-test-config", timeout: 20 * 1000 }, function () {
|
After({ tags: "@needs-" + tag + "-config", timeout: 20 * 1000 }, function () {
|
||||||
return exec("./scripts/example-commit/dc-example.sh up -d authelia && sleep 2");
|
return exec("rm config.test.yml")
|
||||||
|
.then(function () {
|
||||||
|
return exec("./scripts/example-commit/dc-example.sh up -d authelia && sleep 1");
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
declareNeedsConfiguration("regulation", createRegulationConfiguration);
|
||||||
|
declareNeedsConfiguration("inactivity", createInactivityConfiguration);
|
||||||
|
|
||||||
function registerUser(context: any, username: string) {
|
function registerUser(context: any, username: string) {
|
||||||
let secret: Speakeasy.Key;
|
let secret: Speakeasy.Key;
|
||||||
|
@ -36,7 +63,7 @@ Cucumber.defineSupportCode(function ({ After, Before }) {
|
||||||
const collectionFactory = CollectionFactoryFactory.createMongo(mongoClient);
|
const collectionFactory = CollectionFactoryFactory.createMongo(mongoClient);
|
||||||
const userDataStore = new UserDataStore(collectionFactory);
|
const userDataStore = new UserDataStore(collectionFactory);
|
||||||
|
|
||||||
const generator = new TOTPGenerator(Speakeasy);
|
const generator = new TotpHandler(Speakeasy);
|
||||||
secret = generator.generate();
|
secret = generator.generate();
|
||||||
return userDataStore.saveTOTPSecret(username, secret);
|
return userDataStore.saveTOTPSecret(username, secret);
|
||||||
})
|
})
|
||||||
|
@ -56,7 +83,10 @@ Cucumber.defineSupportCode(function ({ After, Before }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function needAuthenticatedUser(context: any, username: string): BluebirdPromise<void> {
|
function needAuthenticatedUser(context: any, username: string): BluebirdPromise<void> {
|
||||||
return context.visit("https://auth.test.local:8080/")
|
return context.visit("https://auth.test.local:8080/logout")
|
||||||
|
.then(function () {
|
||||||
|
return context.visit("https://auth.test.local:8080/");
|
||||||
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
return registerUser(context, username);
|
return registerUser(context, username);
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,6 +7,6 @@ import BluebirdPromise = require("bluebird");
|
||||||
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
|
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
|
||||||
When(/^the application restarts$/, {timeout: 15 * 1000}, function () {
|
When(/^the application restarts$/, {timeout: 15 * 1000}, function () {
|
||||||
const exec = BluebirdPromise.promisify(ChildProcess.exec);
|
const exec = BluebirdPromise.promisify(ChildProcess.exec);
|
||||||
return exec("./scripts/example-commit/dc-example.sh restart authelia && sleep 2");
|
return exec("./scripts/example-commit/dc-example.sh restart authelia && sleep 1");
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -0,0 +1,8 @@
|
||||||
|
import Cucumber = require("cucumber");
|
||||||
|
import seleniumWebdriver = require("selenium-webdriver");
|
||||||
|
|
||||||
|
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
|
||||||
|
When("I sleep for {number} seconds", function (seconds: number) {
|
||||||
|
return this.driver.sleep(seconds * 1000);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue