diff --git a/client/src/lib/firstfactor/FirstFactorValidator.ts b/client/src/lib/firstfactor/FirstFactorValidator.ts index 60a4d17c..eaa496fd 100644 --- a/client/src/lib/firstfactor/FirstFactorValidator.ts +++ b/client/src/lib/firstfactor/FirstFactorValidator.ts @@ -6,7 +6,8 @@ import Util = require("util"); import UserMessages = require("../../../../shared/UserMessages"); export function validate(username: string, password: string, - redirectUrl: string, $: JQueryStatic): BluebirdPromise { + keepMeLoggedIn: boolean, redirectUrl: string, $: JQueryStatic) + : BluebirdPromise { return new BluebirdPromise(function (resolve, reject) { let url: string; if (redirectUrl != undefined) { @@ -17,13 +18,19 @@ export function validate(username: string, password: string, url = Util.format("%s", Endpoints.FIRST_FACTOR_POST); } + const data: any = { + username: username, + password: password, + }; + + if (keepMeLoggedIn) { + data.keepMeLoggedIn = "true"; + } + $.ajax({ method: "POST", url: url, - data: { - username: username, - password: password, - } + data: data }) .done(function (body: any) { if (body && body.error) { diff --git a/client/src/lib/firstfactor/UISelectors.ts b/client/src/lib/firstfactor/UISelectors.ts index 07591b4d..0e971b3c 100644 --- a/client/src/lib/firstfactor/UISelectors.ts +++ b/client/src/lib/firstfactor/UISelectors.ts @@ -2,3 +2,4 @@ export const USERNAME_FIELD_ID = "#username"; export const PASSWORD_FIELD_ID = "#password"; export const SIGN_IN_BUTTON_ID = "#signin"; +export const KEEP_ME_LOGGED_IN_ID = "#keep_me_logged_in"; diff --git a/client/src/lib/firstfactor/index.ts b/client/src/lib/firstfactor/index.ts index 0e07e7ed..a6e2e197 100644 --- a/client/src/lib/firstfactor/index.ts +++ b/client/src/lib/firstfactor/index.ts @@ -15,13 +15,14 @@ export default function (window: Window, $: JQueryStatic, function onFormSubmitted() { const username: string = $(UISelectors.USERNAME_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); $("input,button").attr("disabled", "true"); $(UISelectors.SIGN_IN_BUTTON_ID).text("Please wait..."); const redirectUrl = QueryParametersRetriever.get(Constants.REDIRECT_QUERY_PARAM); - firstFactorValidator.validate(username, password, redirectUrl, $) + firstFactorValidator.validate(username, password, keepMeLoggedIn, redirectUrl, $) .then(onFirstFactorSuccess, onFirstFactorFailure); return false; } diff --git a/client/test/firstfactor/FirstFactorValidator.test.ts b/client/test/firstfactor/FirstFactorValidator.test.ts index ac835327..027bc71d 100644 --- a/client/test/firstfactor/FirstFactorValidator.test.ts +++ b/client/test/firstfactor/FirstFactorValidator.test.ts @@ -13,7 +13,8 @@ describe("test FirstFactorValidator", function () { const jqueryMock = JQueryMock.JQueryMock(); 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) { @@ -27,7 +28,8 @@ describe("test FirstFactorValidator", function () { const jqueryMock = JQueryMock.JQueryMock(); 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 () { return BluebirdPromise.reject(new Error("First factor validation successfully finished while it should have not.")); }, function (err: Error) { diff --git a/config.minimal.yml b/config.minimal.yml index 385b05b4..1e5c9bc7 100644 --- a/config.minimal.yml +++ b/config.minimal.yml @@ -2,6 +2,8 @@ # Authelia minimal configuration # ############################################################### +logs_level: debug + authentication_backend: file: path: /etc/authelia/users_database.yml @@ -9,6 +11,7 @@ authentication_backend: session: secret: unsecure_session_secret domain: example.com + inactivity: 30000 # Configuration of the storage backend used to store data and secrets. i.e. totp data storage: diff --git a/server/src/lib/AuthenticationSessionHandler.ts b/server/src/lib/AuthenticationSessionHandler.ts index ae2ff7c5..f7ed752f 100644 --- a/server/src/lib/AuthenticationSessionHandler.ts +++ b/server/src/lib/AuthenticationSessionHandler.ts @@ -7,6 +7,7 @@ import { AuthenticationSession } from "../../types/AuthenticationSession"; import { IRequestLogger } from "./logging/IRequestLogger"; const INITIAL_AUTHENTICATION_SESSION: AuthenticationSession = { + keep_me_logged_in: false, first_factor: false, second_factor: false, last_activity_datetime: undefined, diff --git a/server/src/lib/routes/firstfactor/post.spec.ts b/server/src/lib/routes/firstfactor/post.spec.ts index ca014351..98479d63 100644 --- a/server/src/lib/routes/firstfactor/post.spec.ts +++ b/server/src/lib/routes/firstfactor/post.spec.ts @@ -43,6 +43,7 @@ describe("routes/firstfactor/post", function () { redirect: "http://redirect.url" }, session: { + cookie: {} }, headers: { 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 () { mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password") .returns(BluebirdPromise.resolve([{ mail: ["test@example.com"] }])); diff --git a/server/src/lib/routes/firstfactor/post.ts b/server/src/lib/routes/firstfactor/post.ts index 94136b44..20731fcd 100644 --- a/server/src/lib/routes/firstfactor/post.ts +++ b/server/src/lib/routes/firstfactor/post.ts @@ -21,8 +21,16 @@ export default function (vars: ServerVariables) { : BluebirdPromise { const username: string = req.body.username; const password: string = req.body.password; + const keepMeLoggedIn: boolean = req.body.keepMeLoggedIn && + req.body.keepMeLoggedIn === "true"; 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() .then(function () { if (!username || !password) { @@ -41,6 +49,7 @@ export default function (vars: ServerVariables) { "LDAP binding successful. Retrieved information about user are %s", JSON.stringify(groupsAndEmails)); authSession.userid = username; + authSession.keep_me_logged_in = keepMeLoggedIn; authSession.first_factor = true; const redirectUrl = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined" // Fuck, don't know why it is a string! diff --git a/server/src/lib/routes/verify/get.spec.ts b/server/src/lib/routes/verify/get.spec.ts index effa1309..3643dc02 100644 --- a/server/src/lib/routes/verify/get.spec.ts +++ b/server/src/lib/routes/verify/get.spec.ts @@ -80,6 +80,7 @@ describe("routes/verify/get", function () { describe("given different cases of session", function () { it("should not be authenticated when second factor is missing", function () { return test_non_authenticated_401({ + keep_me_logged_in: false, userid: "user", first_factor: true, second_factor: false, @@ -91,6 +92,7 @@ describe("routes/verify/get", function () { it("should not be authenticated when first factor is missing", function () { return test_non_authenticated_401({ + keep_me_logged_in: false, userid: "user", first_factor: false, second_factor: true, @@ -102,6 +104,7 @@ describe("routes/verify/get", function () { it("should not be authenticated when userid is missing", function () { return test_non_authenticated_401({ + keep_me_logged_in: false, userid: undefined, first_factor: true, 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 () { return test_non_authenticated_401({ + keep_me_logged_in: false, userid: "user", first_factor: false, second_factor: false, @@ -134,6 +138,7 @@ describe("routes/verify/get", function () { mocks.accessController.isAccessAllowedMock.returns(false); return test_unauthorized_403({ + keep_me_logged_in: false, first_factor: true, second_factor: true, userid: "user", diff --git a/server/src/lib/routes/verify/get_session_cookie.ts b/server/src/lib/routes/verify/get_session_cookie.ts index 8b144cd9..504c8707 100644 --- a/server/src/lib/routes/verify/get_session_cookie.ts +++ b/server/src/lib/routes/verify/get_session_cookie.ts @@ -25,7 +25,7 @@ function verify_inactivity(req: Express.Request, : BluebirdPromise { // 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(); } diff --git a/server/types/AuthenticationSession.ts b/server/types/AuthenticationSession.ts index 05aab533..e299bc43 100644 --- a/server/types/AuthenticationSession.ts +++ b/server/types/AuthenticationSession.ts @@ -4,6 +4,7 @@ export interface AuthenticationSession { userid: string; first_factor: boolean; second_factor: boolean; + keep_me_logged_in: boolean; last_activity_datetime: number; identity_check?: { challenge: string;