diff --git a/CHANGELOG.md b/CHANGELOG.md index 36af2298a4..5f411dac86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to - ✨(frontend) link to create new doc #1574 - ♿(frontend) improve accessibility: - ♿(frontend) add skip to content button for keyboard accessibility #1624 +- ✨(auth) add silent login #1690 ### Fixed diff --git a/src/backend/impress/settings.py b/src/backend/impress/settings.py index 2229036c8a..f7771fca96 100755 --- a/src/backend/impress/settings.py +++ b/src/backend/impress/settings.py @@ -525,6 +525,8 @@ class Base(Configuration): SESSION_COOKIE_NAME = "docs_sessionid" # OIDC - Authorization Code Flow + OIDC_AUTHENTICATE_CLASS = "lasuite.oidc_login.views.OIDCAuthenticationRequestView" + OIDC_CALLBACK_CLASS = "lasuite.oidc_login.views.OIDCAuthenticationCallbackView" OIDC_CREATE_USER = values.BooleanValue( default=True, environ_name="OIDC_CREATE_USER", diff --git a/src/frontend/apps/impress/src/features/auth/api/useAuthQuery.tsx b/src/frontend/apps/impress/src/features/auth/api/useAuthQuery.tsx index 592ce4c780..7e44974eb4 100644 --- a/src/frontend/apps/impress/src/features/auth/api/useAuthQuery.tsx +++ b/src/frontend/apps/impress/src/features/auth/api/useAuthQuery.tsx @@ -3,6 +3,8 @@ import { UseQueryOptions, useQuery } from '@tanstack/react-query'; import { APIError, errorCauses, fetchAPI } from '@/api'; import { DEFAULT_QUERY_RETRY } from '@/core'; +import { attemptSilentLogin, canAttemptSilentLogin } from '../utils'; + import { User } from './types'; type UserResponse = User | null; @@ -11,16 +13,20 @@ type UserResponse = User | null; * Asynchronously retrieves the current user's data from the API. * This function is called during frontend initialization to check * the user's authentication status through a session cookie. + * If a 401 is received, it will attempt silent login if allowed by retry logic. * * @async * @function getMe * @throws {Error} Throws an error if the API request fails. - * @returns {Promise} A promise that resolves to the user data. + * @returns {Promise} A promise that resolves to the user data or null if not authenticated. */ export const getMe = async (): Promise => { const response = await fetchAPI(`users/me/`); if (response.status === 401) { + if (canAttemptSilentLogin()) { + attemptSilentLogin(30); // 30 second retry interval + } return null; } diff --git a/src/frontend/apps/impress/src/features/auth/utils.ts b/src/frontend/apps/impress/src/features/auth/utils.ts index 2e765b2429..ae7d593a2a 100644 --- a/src/frontend/apps/impress/src/features/auth/utils.ts +++ b/src/frontend/apps/impress/src/features/auth/utils.ts @@ -43,3 +43,49 @@ export const gotoLogout = () => { terminateCrispSession(); window.location.replace(LOGOUT_URL); }; + +// Silent login utilities +const SILENT_LOGIN_RETRY_KEY = 'silent-login-retry'; + +const isRetryAllowed = () => { + const lastRetryDate = localStorage.getItem(SILENT_LOGIN_RETRY_KEY); + if (!lastRetryDate) { + return true; + } + const now = new Date(); + return now.getTime() > Number(lastRetryDate); +}; + +const setNextRetryTime = (retryIntervalInSeconds: number) => { + const now = new Date(); + const nextRetryTime = now.getTime() + retryIntervalInSeconds * 1000; + localStorage.setItem(SILENT_LOGIN_RETRY_KEY, String(nextRetryTime)); +}; + +const initiateSilentLogin = () => { + const returnTo = window.location.href; + window.location.replace( + `${LOGIN_URL}?silent=true&returnTo=${encodeURIComponent(returnTo)}`, + ); +}; + +/** + * Check if silent login retry is allowed based on the last attempt timestamp. + */ +export const canAttemptSilentLogin = () => { + return isRetryAllowed(); +}; + +/** + * Attempt silent login if retry is allowed. + * Sets a retry interval to prevent infinite loops. + * + * @param retryIntervalInSeconds - Minimum seconds between retry attempts (default: 30) + */ +export const attemptSilentLogin = (retryIntervalInSeconds: number) => { + if (!isRetryAllowed()) { + return; + } + setNextRetryTime(retryIntervalInSeconds); + initiateSilentLogin(); +};