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>pull/3549/head^2
parent
245d422a29
commit
1991c443ba
|
@ -62,7 +62,7 @@ func init() {
|
|||
OnError: displayAutheliaLogs,
|
||||
OnSetupTimeout: displayAutheliaLogs,
|
||||
TearDown: teardown,
|
||||
TestTimeout: 3 * time.Minute,
|
||||
TestTimeout: 4 * time.Minute,
|
||||
TearDownTimeout: 2 * time.Minute,
|
||||
Description: `This suite is used to test Authelia in a standalone
|
||||
configuration with in-memory sessions and a local sqlite db stored on disk`,
|
||||
|
|
|
@ -71,6 +71,31 @@ func (s *StandaloneWebDriverSuite) TestShouldLetUserKnowHeIsAlreadyAuthenticated
|
|||
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() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer func() {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -8,10 +8,13 @@ import { useNavigate } from "react-router-dom";
|
|||
import FixedTextField from "@components/FixedTextField";
|
||||
import { ResetPasswordStep1Route } from "@constants/Routes";
|
||||
import { useNotifications } from "@hooks/NotificationsContext";
|
||||
import { usePageVisibility } from "@hooks/PageVisibility";
|
||||
import { useRedirectionURL } from "@hooks/RedirectionURL";
|
||||
import { useRequestMethod } from "@hooks/RequestMethod";
|
||||
import { useAutheliaState } from "@hooks/State";
|
||||
import LoginLayout from "@layouts/LoginLayout";
|
||||
import { postFirstFactor } from "@services/FirstFactor";
|
||||
import { AuthenticationLevel } from "@services/State";
|
||||
|
||||
export interface Props {
|
||||
disabled: boolean;
|
||||
|
@ -31,6 +34,7 @@ const FirstFactorForm = function (props: Props) {
|
|||
const redirectionURL = useRedirectionURL();
|
||||
const requestMethod = useRequestMethod();
|
||||
|
||||
const [state, fetchState, ,] = useAutheliaState();
|
||||
const [rememberMe, setRememberMe] = useState(false);
|
||||
const [username, setUsername] = useState("");
|
||||
const [usernameError, setUsernameError] = useState(false);
|
||||
|
@ -40,12 +44,28 @@ const FirstFactorForm = function (props: Props) {
|
|||
// TODO (PR: #806, Issue: #511) potentially refactor
|
||||
const usernameRef = useRef() as MutableRefObject<HTMLInputElement>;
|
||||
const passwordRef = useRef() as MutableRefObject<HTMLInputElement>;
|
||||
const visible = usePageVisibility();
|
||||
const { t: translate } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => usernameRef.current.focus(), 10);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [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 handleRememberMeChange = () => {
|
||||
|
|
Loading…
Reference in New Issue