feat(web): auto-redirect on appropriate authentication state changes (#3187)

This PR checks the authentication state of the Authelia portal on either a focus event or 1-second timer and if a state change has occurred will redirect accordingly.

Closes #3000.

Co-authored-by: Amir Zarrinkafsh <nightah@me.com>
This commit is contained in:
Manuel Nuñez 2022-06-19 09:43:19 -03:00 committed by GitHub
parent 245d422a29
commit 1991c443ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 89 additions and 1 deletions

View File

@ -62,7 +62,7 @@ func init() {
OnError: displayAutheliaLogs, OnError: displayAutheliaLogs,
OnSetupTimeout: displayAutheliaLogs, OnSetupTimeout: displayAutheliaLogs,
TearDown: teardown, TearDown: teardown,
TestTimeout: 3 * time.Minute, TestTimeout: 4 * time.Minute,
TearDownTimeout: 2 * time.Minute, TearDownTimeout: 2 * time.Minute,
Description: `This suite is used to test Authelia in a standalone Description: `This suite is used to test Authelia in a standalone
configuration with in-memory sessions and a local sqlite db stored on disk`, configuration with in-memory sessions and a local sqlite db stored on disk`,

View File

@ -71,6 +71,31 @@ func (s *StandaloneWebDriverSuite) TestShouldLetUserKnowHeIsAlreadyAuthenticated
s.verifyIsAuthenticatedPage(s.T(), s.Context(ctx)) s.verifyIsAuthenticatedPage(s.T(), s.Context(ctx))
} }
func (s *StandaloneWebDriverSuite) TestShouldRedirectAfterOneFactorOnAnotherTab() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
targetURL := fmt.Sprintf("%s/secret.html", SingleFactorBaseURL)
page2 := s.Browser().MustPage(targetURL)
defer func() {
cancel()
s.collectScreenshot(ctx.Err(), s.Page)
s.collectScreenshot(ctx.Err(), page2)
page2.MustClose()
}()
// Open second tab with secret page.
page2.MustWaitLoad()
// Switch to first, visit the login page and wait for redirection to secret page with secret displayed.
s.Page.MustActivate()
s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, targetURL)
s.verifySecretAuthorized(s.T(), s.Page)
// Switch to second tab and wait for redirection to secret page with secret displayed.
page2.MustActivate()
s.verifySecretAuthorized(s.T(), page2.Context(ctx))
}
func (s *StandaloneWebDriverSuite) TestShouldRedirectAlreadyAuthenticatedUser() { func (s *StandaloneWebDriverSuite) TestShouldRedirectAlreadyAuthenticatedUser() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer func() { defer func() {

View File

@ -0,0 +1,43 @@
import { useEffect, useState } from "react";
function getBrowserVisibilityProp() {
if (typeof document.hidden !== "undefined") {
// Opera 12.10 and Firefox 18 and later support
return "visibilitychange";
} else if (typeof document.msHidden !== "undefined") {
return "msvisibilitychange";
} else if (typeof document.webkitHidden !== "undefined") {
return "webkitvisibilitychange";
}
}
function getBrowserDocumentHiddenProp() {
if (typeof document.hidden !== "undefined") {
return "hidden";
} else if (typeof document.msHidden !== "undefined") {
return "msHidden";
} else if (typeof document.webkitHidden !== "undefined") {
return "webkitHidden";
}
}
function getIsDocumentHidden() {
return !document[getBrowserDocumentHiddenProp()];
}
export function usePageVisibility() {
const [isVisible, setIsVisible] = useState(getIsDocumentHidden());
const onVisibilityChange = () => setIsVisible(getIsDocumentHidden());
useEffect(() => {
const visibilityChange = getBrowserVisibilityProp();
document.addEventListener(visibilityChange, onVisibilityChange, false);
return () => {
document.removeEventListener(visibilityChange, onVisibilityChange);
};
});
return isVisible;
}

View File

@ -8,10 +8,13 @@ import { useNavigate } from "react-router-dom";
import FixedTextField from "@components/FixedTextField"; import FixedTextField from "@components/FixedTextField";
import { ResetPasswordStep1Route } from "@constants/Routes"; import { ResetPasswordStep1Route } from "@constants/Routes";
import { useNotifications } from "@hooks/NotificationsContext"; import { useNotifications } from "@hooks/NotificationsContext";
import { usePageVisibility } from "@hooks/PageVisibility";
import { useRedirectionURL } from "@hooks/RedirectionURL"; import { useRedirectionURL } from "@hooks/RedirectionURL";
import { useRequestMethod } from "@hooks/RequestMethod"; import { useRequestMethod } from "@hooks/RequestMethod";
import { useAutheliaState } from "@hooks/State";
import LoginLayout from "@layouts/LoginLayout"; import LoginLayout from "@layouts/LoginLayout";
import { postFirstFactor } from "@services/FirstFactor"; import { postFirstFactor } from "@services/FirstFactor";
import { AuthenticationLevel } from "@services/State";
export interface Props { export interface Props {
disabled: boolean; disabled: boolean;
@ -31,6 +34,7 @@ const FirstFactorForm = function (props: Props) {
const redirectionURL = useRedirectionURL(); const redirectionURL = useRedirectionURL();
const requestMethod = useRequestMethod(); const requestMethod = useRequestMethod();
const [state, fetchState, ,] = useAutheliaState();
const [rememberMe, setRememberMe] = useState(false); const [rememberMe, setRememberMe] = useState(false);
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [usernameError, setUsernameError] = useState(false); const [usernameError, setUsernameError] = useState(false);
@ -40,12 +44,28 @@ const FirstFactorForm = function (props: Props) {
// TODO (PR: #806, Issue: #511) potentially refactor // TODO (PR: #806, Issue: #511) potentially refactor
const usernameRef = useRef() as MutableRefObject<HTMLInputElement>; const usernameRef = useRef() as MutableRefObject<HTMLInputElement>;
const passwordRef = useRef() as MutableRefObject<HTMLInputElement>; const passwordRef = useRef() as MutableRefObject<HTMLInputElement>;
const visible = usePageVisibility();
const { t: translate } = useTranslation(); const { t: translate } = useTranslation();
useEffect(() => { useEffect(() => {
const timeout = setTimeout(() => usernameRef.current.focus(), 10); const timeout = setTimeout(() => usernameRef.current.focus(), 10);
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
}, [usernameRef]); }, [usernameRef]);
useEffect(() => {
if (visible) {
fetchState();
}
const timer = setInterval(() => fetchState(), 1000);
return () => clearInterval(timer);
}, [visible, fetchState]);
useEffect(() => {
if (state && state.authentication_level >= AuthenticationLevel.OneFactor) {
props.onAuthenticationSuccess(redirectionURL);
}
}, [state, redirectionURL, props]);
const disabled = props.disabled; const disabled = props.disabled;
const handleRememberMeChange = () => { const handleRememberMeChange = () => {