Implement Keep me logged in feature.

This commit is contained in:
Clement Michaud 2018-10-21 16:11:31 +02:00
parent 059c5936f5
commit 4c3b5cfbb3
11 changed files with 60 additions and 9 deletions

View File

@ -6,7 +6,8 @@ import Util = require("util");
import UserMessages = require("../../../../shared/UserMessages"); import UserMessages = require("../../../../shared/UserMessages");
export function validate(username: string, password: string, export function validate(username: string, password: string,
redirectUrl: string, $: JQueryStatic): BluebirdPromise<string> { keepMeLoggedIn: boolean, redirectUrl: string, $: JQueryStatic)
: BluebirdPromise<string> {
return new BluebirdPromise<string>(function (resolve, reject) { return new BluebirdPromise<string>(function (resolve, reject) {
let url: string; let url: string;
if (redirectUrl != undefined) { if (redirectUrl != undefined) {
@ -17,13 +18,19 @@ export function validate(username: string, password: string,
url = Util.format("%s", Endpoints.FIRST_FACTOR_POST); url = Util.format("%s", Endpoints.FIRST_FACTOR_POST);
} }
const data: any = {
username: username,
password: password,
};
if (keepMeLoggedIn) {
data.keepMeLoggedIn = "true";
}
$.ajax({ $.ajax({
method: "POST", method: "POST",
url: url, url: url,
data: { data: data
username: username,
password: password,
}
}) })
.done(function (body: any) { .done(function (body: any) {
if (body && body.error) { if (body && body.error) {

View File

@ -2,3 +2,4 @@
export const USERNAME_FIELD_ID = "#username"; export const USERNAME_FIELD_ID = "#username";
export const PASSWORD_FIELD_ID = "#password"; export const PASSWORD_FIELD_ID = "#password";
export const SIGN_IN_BUTTON_ID = "#signin"; export const SIGN_IN_BUTTON_ID = "#signin";
export const KEEP_ME_LOGGED_IN_ID = "#keep_me_logged_in";

View File

@ -15,13 +15,14 @@ export default function (window: Window, $: JQueryStatic,
function onFormSubmitted() { function onFormSubmitted() {
const username: string = $(UISelectors.USERNAME_FIELD_ID).val() as string; const username: string = $(UISelectors.USERNAME_FIELD_ID).val() as string;
const password: string = $(UISelectors.PASSWORD_FIELD_ID).val() as string; const password: string = $(UISelectors.PASSWORD_FIELD_ID).val() as string;
const keepMeLoggedIn: boolean = $(UISelectors.KEEP_ME_LOGGED_IN_ID).is(":checked");
$("form").css("opacity", 0.5); $("form").css("opacity", 0.5);
$("input,button").attr("disabled", "true"); $("input,button").attr("disabled", "true");
$(UISelectors.SIGN_IN_BUTTON_ID).text("Please wait..."); $(UISelectors.SIGN_IN_BUTTON_ID).text("Please wait...");
const redirectUrl = QueryParametersRetriever.get(Constants.REDIRECT_QUERY_PARAM); const redirectUrl = QueryParametersRetriever.get(Constants.REDIRECT_QUERY_PARAM);
firstFactorValidator.validate(username, password, redirectUrl, $) firstFactorValidator.validate(username, password, keepMeLoggedIn, redirectUrl, $)
.then(onFirstFactorSuccess, onFirstFactorFailure); .then(onFirstFactorSuccess, onFirstFactorFailure);
return false; return false;
} }

View File

@ -13,7 +13,8 @@ describe("test FirstFactorValidator", function () {
const jqueryMock = JQueryMock.JQueryMock(); const jqueryMock = JQueryMock.JQueryMock();
jqueryMock.jquery.ajax.returns(postPromise); jqueryMock.jquery.ajax.returns(postPromise);
return FirstFactorValidator.validate("username", "password", "http://redirect", jqueryMock.jquery as any); return FirstFactorValidator.validate("username", "password", false,
"http://redirect", jqueryMock.jquery as any);
}); });
function should_fail_first_factor_validation(errorMessage: string) { function should_fail_first_factor_validation(errorMessage: string) {
@ -27,7 +28,8 @@ describe("test FirstFactorValidator", function () {
const jqueryMock = JQueryMock.JQueryMock(); const jqueryMock = JQueryMock.JQueryMock();
jqueryMock.jquery.ajax.returns(postPromise); jqueryMock.jquery.ajax.returns(postPromise);
return FirstFactorValidator.validate("username", "password", "http://redirect", jqueryMock.jquery as any) return FirstFactorValidator.validate("username", "password", false,
"http://redirect", jqueryMock.jquery as any)
.then(function () { .then(function () {
return BluebirdPromise.reject(new Error("First factor validation successfully finished while it should have not.")); return BluebirdPromise.reject(new Error("First factor validation successfully finished while it should have not."));
}, function (err: Error) { }, function (err: Error) {

View File

@ -2,6 +2,8 @@
# Authelia minimal configuration # # Authelia minimal configuration #
############################################################### ###############################################################
logs_level: debug
authentication_backend: authentication_backend:
file: file:
path: /etc/authelia/users_database.yml path: /etc/authelia/users_database.yml
@ -9,6 +11,7 @@ authentication_backend:
session: session:
secret: unsecure_session_secret secret: unsecure_session_secret
domain: example.com domain: example.com
inactivity: 30000
# Configuration of the storage backend used to store data and secrets. i.e. totp data # Configuration of the storage backend used to store data and secrets. i.e. totp data
storage: storage:

View File

@ -7,6 +7,7 @@ import { AuthenticationSession } from "../../types/AuthenticationSession";
import { IRequestLogger } from "./logging/IRequestLogger"; import { IRequestLogger } from "./logging/IRequestLogger";
const INITIAL_AUTHENTICATION_SESSION: AuthenticationSession = { const INITIAL_AUTHENTICATION_SESSION: AuthenticationSession = {
keep_me_logged_in: false,
first_factor: false, first_factor: false,
second_factor: false, second_factor: false,
last_activity_datetime: undefined, last_activity_datetime: undefined,

View File

@ -43,6 +43,7 @@ describe("routes/firstfactor/post", function () {
redirect: "http://redirect.url" redirect: "http://redirect.url"
}, },
session: { session: {
cookie: {}
}, },
headers: { headers: {
host: "home.example.com" host: "home.example.com"
@ -66,6 +67,26 @@ describe("routes/firstfactor/post", function () {
}); });
}); });
describe("keep me logged in", () => {
beforeEach(() => {
mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password")
.returns(BluebirdPromise.resolve({
emails: emails,
groups: groups
}));
req.body.keepMeLoggedIn = "true";
return FirstFactorPost.default(vars)(req as any, res as any);
});
it("should set keep_me_logged_in session variable to true", function () {
Assert.equal(authSession.keep_me_logged_in, true);
});
it("should set cookie maxAge to one year", function () {
Assert.equal(req.session.cookie.maxAge, 365 * 24 * 60 * 60 * 1000);
});
});
it("should retrieve email from LDAP", function () { it("should retrieve email from LDAP", function () {
mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password")
.returns(BluebirdPromise.resolve([{ mail: ["test@example.com"] }])); .returns(BluebirdPromise.resolve([{ mail: ["test@example.com"] }]));

View File

@ -21,8 +21,16 @@ export default function (vars: ServerVariables) {
: BluebirdPromise<void> { : BluebirdPromise<void> {
const username: string = req.body.username; const username: string = req.body.username;
const password: string = req.body.password; const password: string = req.body.password;
const keepMeLoggedIn: boolean = req.body.keepMeLoggedIn &&
req.body.keepMeLoggedIn === "true";
let authSession: AuthenticationSession; let authSession: AuthenticationSession;
if (keepMeLoggedIn) {
// Stay connected for 1 year.
vars.logger.debug(req, "User requested to stay logged in for one year.");
req.session.cookie.maxAge = 365 * 24 * 60 * 60 * 1000;
}
return BluebirdPromise.resolve() return BluebirdPromise.resolve()
.then(function () { .then(function () {
if (!username || !password) { if (!username || !password) {
@ -41,6 +49,7 @@ export default function (vars: ServerVariables) {
"LDAP binding successful. Retrieved information about user are %s", "LDAP binding successful. Retrieved information about user are %s",
JSON.stringify(groupsAndEmails)); JSON.stringify(groupsAndEmails));
authSession.userid = username; authSession.userid = username;
authSession.keep_me_logged_in = keepMeLoggedIn;
authSession.first_factor = true; authSession.first_factor = true;
const redirectUrl = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined" const redirectUrl = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined"
// Fuck, don't know why it is a string! // Fuck, don't know why it is a string!

View File

@ -80,6 +80,7 @@ describe("routes/verify/get", function () {
describe("given different cases of session", function () { describe("given different cases of session", function () {
it("should not be authenticated when second factor is missing", function () { it("should not be authenticated when second factor is missing", function () {
return test_non_authenticated_401({ return test_non_authenticated_401({
keep_me_logged_in: false,
userid: "user", userid: "user",
first_factor: true, first_factor: true,
second_factor: false, second_factor: false,
@ -91,6 +92,7 @@ describe("routes/verify/get", 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_non_authenticated_401({ return test_non_authenticated_401({
keep_me_logged_in: false,
userid: "user", userid: "user",
first_factor: false, first_factor: false,
second_factor: true, second_factor: true,
@ -102,6 +104,7 @@ describe("routes/verify/get", function () {
it("should not be authenticated when userid is missing", function () { it("should not be authenticated when userid is missing", function () {
return test_non_authenticated_401({ return test_non_authenticated_401({
keep_me_logged_in: false,
userid: undefined, userid: undefined,
first_factor: true, first_factor: true,
second_factor: false, second_factor: false,
@ -113,6 +116,7 @@ describe("routes/verify/get", 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_non_authenticated_401({ return test_non_authenticated_401({
keep_me_logged_in: false,
userid: "user", userid: "user",
first_factor: false, first_factor: false,
second_factor: false, second_factor: false,
@ -134,6 +138,7 @@ describe("routes/verify/get", function () {
mocks.accessController.isAccessAllowedMock.returns(false); mocks.accessController.isAccessAllowedMock.returns(false);
return test_unauthorized_403({ return test_unauthorized_403({
keep_me_logged_in: false,
first_factor: true, first_factor: true,
second_factor: true, second_factor: true,
userid: "user", userid: "user",

View File

@ -25,7 +25,7 @@ function verify_inactivity(req: Express.Request,
: BluebirdPromise<void> { : BluebirdPromise<void> {
// If inactivity is not specified, then inactivity timeout does not apply // If inactivity is not specified, then inactivity timeout does not apply
if (!configuration.session.inactivity) { if (!configuration.session.inactivity || authSession.keep_me_logged_in) {
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
} }

View File

@ -4,6 +4,7 @@ export interface AuthenticationSession {
userid: string; userid: string;
first_factor: boolean; first_factor: boolean;
second_factor: boolean; second_factor: boolean;
keep_me_logged_in: boolean;
last_activity_datetime: number; last_activity_datetime: number;
identity_check?: { identity_check?: {
challenge: string; challenge: string;