frontend(auth): persist refresh_token and auto-refresh access token for long-lived sessions

This commit is contained in:
Your Name 2025-12-11 19:00:07 -05:00
parent eb81f038bf
commit 8348d48b82
1 changed files with 139 additions and 83 deletions

View File

@ -7,12 +7,15 @@ import React, {
const AuthContext = createContext(null);
// Config Keycloak pour le login inline
/* ===== Keycloak config ===== */
const KC_BASE = "https://web.sociowire.com:8443";
const KC_REALM = "sociowire";
const KC_CLIENT_ID = "sociowire-frontend";
/* ===== Storage keys ===== */
const TOKEN_KEY = "sociowire:access_token";
const REFRESH_KEY = "sociowire:refresh_token";
const EXPIRES_AT_KEY = "sociowire:expires_at";
const USERNAME_KEY = "sociowire:username";
export function AuthProvider({ children }) {
@ -22,38 +25,126 @@ export function AuthProvider({ children }) {
const [token, setToken] = useState(null);
const [lastError, setLastError] = useState("");
// Charge token/username depuis localStorage au démarrage
useEffect(() => {
/* ==============================
REFRESH TOKEN (helper)
============================== */
async function refreshWithToken(refreshToken) {
const params = new URLSearchParams();
params.set("grant_type", "refresh_token");
params.set("client_id", KC_CLIENT_ID);
params.set("refresh_token", refreshToken);
try {
const storedToken = localStorage.getItem(TOKEN_KEY);
const storedUser = localStorage.getItem(USERNAME_KEY) || "";
if (storedToken) {
setToken(storedToken);
setUsername(storedUser);
setAuthenticated(true);
} else {
setAuthenticated(false);
const res = await fetch(
`${KC_BASE}/realms/${KC_REALM}/protocol/openid-connect/token`,
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: params.toString(),
}
);
if (!res.ok) {
console.error("[AUTH] refresh failed", res.status);
logout();
return false;
}
const data = await res.json();
const accessToken = data.access_token;
const newRefresh = data.refresh_token || refreshToken;
const expiresIn = Number(data.expires_in || 0);
if (!accessToken) {
logout();
return false;
}
setToken(accessToken);
setAuthenticated(true);
localStorage.setItem(TOKEN_KEY, accessToken);
localStorage.setItem(REFRESH_KEY, newRefresh);
localStorage.setItem(
EXPIRES_AT_KEY,
String(Date.now() + expiresIn * 1000)
);
return true;
} catch (e) {
console.warn("[AUTH] localStorage error", e);
} finally {
setLoading(false);
console.error("[AUTH] refresh error", e);
return false;
}
}
/* ==============================
INIT AUTH (APP LOAD)
============================== */
useEffect(() => {
(async () => {
try {
const storedToken = localStorage.getItem(TOKEN_KEY);
const storedRefresh = localStorage.getItem(REFRESH_KEY);
const storedUser = localStorage.getItem(USERNAME_KEY) || "";
const expiresAt = Number(localStorage.getItem(EXPIRES_AT_KEY) || "0");
const now = Date.now();
const needsRefresh = !storedToken || now > expiresAt - 60_000;
if (storedRefresh && needsRefresh) {
const ok = await refreshWithToken(storedRefresh);
if (ok) {
setUsername(storedUser);
return;
}
}
if (storedToken) {
setToken(storedToken);
setUsername(storedUser);
setAuthenticated(true);
}
} catch (e) {
console.warn("[AUTH] init error", e);
} finally {
setLoading(false);
}
})();
}, []);
// Login via password grant sur Keycloak
/* ==============================
BACKGROUND REFRESH
============================== */
useEffect(() => {
if (!authenticated) return;
const t = setInterval(async () => {
try {
const refresh = localStorage.getItem(REFRESH_KEY);
const expiresAt = Number(localStorage.getItem(EXPIRES_AT_KEY) || "0");
if (!refresh) return;
if (Date.now() > expiresAt - 60_000) {
await refreshWithToken(refresh);
}
} catch {
/* ignore */
}
}, 30_000);
return () => clearInterval(t);
}, [authenticated]);
/* ==============================
LOGIN
============================== */
async function login(user, pass) {
setLastError("");
const u = (user || "").trim();
if (!u || !pass) {
setLastError("Username and password required.");
return;
}
const params = new URLSearchParams();
params.set("grant_type", "password");
params.set("client_id", KC_CLIENT_ID);
params.set("username", u);
params.set("username", user);
params.set("password", pass);
try {
@ -61,89 +152,56 @@ export function AuthProvider({ children }) {
`${KC_BASE}/realms/${KC_REALM}/protocol/openid-connect/token`,
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: params.toString(),
}
);
if (!res.ok) {
const text = await res.text();
console.error("[AUTH] login failed", res.status, text);
let msg = "Invalid credentials or auth error.";
try {
const j = JSON.parse(text);
if (j.error_description) {
msg = j.error_description;
} else if (j.error) {
msg = j.error;
}
} catch (e) {
// ignore parse error
}
setLastError(`${res.status} ${msg}`);
setAuthenticated(false);
setToken(null);
setUsername("");
try {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USERNAME_KEY);
} catch (e) {
// ignore
}
setLastError("Invalid credentials");
return;
}
const data = await res.json();
const accessToken = data.access_token;
if (!accessToken) {
setLastError("No access token in response.");
setAuthenticated(false);
setToken(null);
setUsername("");
const refreshToken = data.refresh_token;
const expiresIn = Number(data.expires_in || 0);
if (!accessToken || !refreshToken) {
setLastError("Invalid token response");
return;
}
setToken(accessToken);
setAuthenticated(true);
setUsername(u);
setLastError("");
setUsername(user);
try {
localStorage.setItem(TOKEN_KEY, accessToken);
localStorage.setItem(USERNAME_KEY, u);
} catch (e) {
console.warn("[AUTH] failed to persist token", e);
}
localStorage.setItem(TOKEN_KEY, accessToken);
localStorage.setItem(REFRESH_KEY, refreshToken);
localStorage.setItem(
EXPIRES_AT_KEY,
String(Date.now() + expiresIn * 1000)
);
localStorage.setItem(USERNAME_KEY, user);
} catch (e) {
console.error("[AUTH] network error", e);
setLastError("Network error during login.");
setAuthenticated(false);
setToken(null);
setUsername("");
try {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USERNAME_KEY);
} catch (e2) {
// ignore
}
console.error("[AUTH] login error", e);
setLastError("Network error");
}
}
/* ==============================
LOGOUT
============================== */
function logout() {
setAuthenticated(false);
setToken(null);
setUsername("");
setLastError("");
try {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USERNAME_KEY);
} catch (e) {
// ignore
}
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(REFRESH_KEY);
localStorage.removeItem(EXPIRES_AT_KEY);
localStorage.removeItem(USERNAME_KEY);
}
return (
@ -165,8 +223,6 @@ export function AuthProvider({ children }) {
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) {
throw new Error("useAuth must be used inside <AuthProvider>");
}
if (!ctx) throw new Error("useAuth must be used inside AuthProvider");
return ctx;
}