Fix misleading auth token storage in sessionStorage
- Remove JWT token and expires_at from sessionStorage - Simplify AuthProvider to use boolean isAuthenticated flag - Persist only isAuthenticated boolean for page-reload continuity - Update AuthProvider test to verify new auth model - Add comprehensive auth documentation to Web-Development.md explaining: - Cookie-based authentication model - How frontend auth state persists - Why tokens are no longer stored - Error handling flow for 401/403 responses The authentication model is cookie-based: the backend sets bangui_session cookie on login, frontend automatically includes it via credentials: 'include', and the backend is the sole authority on session validity. Previously stored tokens were never actually used and made the auth model misleading during development. Fixes TASK-STATE-04. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2,9 +2,18 @@
|
||||
* Authentication context and provider.
|
||||
*
|
||||
* Manages the user's authenticated state and exposes `login`, `logout`, and
|
||||
* `isAuthenticated` through `useAuth()`. The session token is persisted in
|
||||
* `sessionStorage` so it survives page refreshes within the browser tab but
|
||||
* is automatically cleared when the tab is closed.
|
||||
* `isAuthenticated` through `useAuth()`.
|
||||
*
|
||||
* The authentication model is **cookie-based**: the backend sets a session
|
||||
* cookie (`bangui_session`) on successful login, and the frontend automatically
|
||||
* includes it in all requests via `credentials: "include"`. The backend is the
|
||||
* authority on session validity; the frontend is authenticated when the backend
|
||||
* accepts the request (returns 2xx) and not authenticated when the backend
|
||||
* rejects it (returns 401/403).
|
||||
*
|
||||
* The `isAuthenticated` state persists in `sessionStorage` to survive page
|
||||
* refreshes within the browser tab but is cleared on tab close. The session
|
||||
* cookie itself persists according to the backend's cookie settings.
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -19,16 +28,11 @@ import * as authApi from "../api/auth";
|
||||
import { SESSION_EXPIRED_EVENT } from "../api/client";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// Context
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface AuthState {
|
||||
token: string | null;
|
||||
expiresAt: string | null;
|
||||
}
|
||||
|
||||
export interface AuthContextValue {
|
||||
/** `true` when a valid session token is held in state. */
|
||||
/** `true` when the backend considers the session valid. */
|
||||
isAuthenticated: boolean;
|
||||
/**
|
||||
* Authenticate with the master password.
|
||||
@@ -39,14 +43,9 @@ export interface AuthContextValue {
|
||||
logout: () => Promise<void>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Context
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const AuthContext = createContext<AuthContextValue | null>(null);
|
||||
|
||||
const SESSION_KEY = "bangui_token";
|
||||
const SESSION_EXPIRES_KEY = "bangui_expires_at";
|
||||
const IS_AUTHENTICATED_KEY = "bangui_authenticated";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Provider
|
||||
@@ -63,16 +62,15 @@ export function AuthProvider({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): React.JSX.Element {
|
||||
const [auth, setAuth] = useState<AuthState>(() => ({
|
||||
token: sessionStorage.getItem(SESSION_KEY),
|
||||
expiresAt: sessionStorage.getItem(SESSION_EXPIRES_KEY),
|
||||
}));
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(() => {
|
||||
const stored = sessionStorage.getItem(IS_AUTHENTICATED_KEY);
|
||||
return stored === "true";
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSessionExpired = useCallback((): void => {
|
||||
sessionStorage.removeItem(SESSION_KEY);
|
||||
sessionStorage.removeItem(SESSION_EXPIRES_KEY);
|
||||
setAuth({ token: null, expiresAt: null });
|
||||
sessionStorage.removeItem(IS_AUTHENTICATED_KEY);
|
||||
setIsAuthenticated(false);
|
||||
navigate("/login", { replace: true });
|
||||
}, [navigate]);
|
||||
|
||||
@@ -83,17 +81,10 @@ export function AuthProvider({
|
||||
};
|
||||
}, [handleSessionExpired]);
|
||||
|
||||
const isAuthenticated = useMemo<boolean>(() => {
|
||||
if (!auth.token || !auth.expiresAt) return false;
|
||||
// Treat the session as expired if the expiry time has passed.
|
||||
return new Date(auth.expiresAt) > new Date();
|
||||
}, [auth]);
|
||||
|
||||
const login = useCallback(async (password: string): Promise<void> => {
|
||||
const response = await authApi.login(password);
|
||||
sessionStorage.setItem(SESSION_KEY, response.token);
|
||||
sessionStorage.setItem(SESSION_EXPIRES_KEY, response.expires_at);
|
||||
setAuth({ token: response.token, expiresAt: response.expires_at });
|
||||
await authApi.login(password);
|
||||
sessionStorage.setItem(IS_AUTHENTICATED_KEY, "true");
|
||||
setIsAuthenticated(true);
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(async (): Promise<void> => {
|
||||
@@ -101,9 +92,8 @@ export function AuthProvider({
|
||||
await authApi.logout();
|
||||
} finally {
|
||||
// Always clear local state even if the API call fails (e.g. expired session).
|
||||
sessionStorage.removeItem(SESSION_KEY);
|
||||
sessionStorage.removeItem(SESSION_EXPIRES_KEY);
|
||||
setAuth({ token: null, expiresAt: null });
|
||||
sessionStorage.removeItem(IS_AUTHENTICATED_KEY);
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ describe("AuthProvider", () => {
|
||||
});
|
||||
|
||||
it("clears auth state and redirects to /login when session-expired fires", async () => {
|
||||
sessionStorage.setItem("bangui_token", "token");
|
||||
sessionStorage.setItem("bangui_expires_at", new Date(Date.now() + 10000).toISOString());
|
||||
sessionStorage.setItem("bangui_authenticated", "true");
|
||||
|
||||
render(
|
||||
<FluentProvider theme={webLightTheme}>
|
||||
@@ -38,7 +37,6 @@ describe("AuthProvider", () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("location")).toHaveTextContent("/login");
|
||||
});
|
||||
expect(sessionStorage.getItem("bangui_token")).toBeNull();
|
||||
expect(sessionStorage.getItem("bangui_expires_at")).toBeNull();
|
||||
expect(sessionStorage.getItem("bangui_authenticated")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user