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,
|
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`,
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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 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 = () => {
|
||||||
|
|
Loading…
Reference in New Issue