forked from baron/baron-sso
fix: resolve admin session infinite reload loop and sync auth state
- Prevent infinite redirection loop by clearing oidc-client user state on 401 errors. - Sync apiClient request interceptor to use userManager.getUser() for reliable token retrieval. - Add extensive console logs for better session issue diagnosis. - Fix TS error in LoginPage by updating button variant. - Revert 'ae03fe1' (updated playwright fixtures to real domain) as requested.
This commit is contained in:
@@ -80,9 +80,19 @@ function AppLayout() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const { data: profile } = useQuery({
|
||||
const { data: profile, isLoading: isProfileLoading, error: profileError } = useQuery({
|
||||
queryKey: ["me"],
|
||||
queryFn: fetchMe,
|
||||
queryFn: async () => {
|
||||
console.debug("[AppLayout] Fetching profile...");
|
||||
try {
|
||||
const data = await fetchMe();
|
||||
console.debug("[AppLayout] Profile fetched successfully:", data.email);
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error("[AppLayout] Failed to fetch profile:", err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
enabled:
|
||||
(auth.isAuthenticated && !auth.isLoading) ||
|
||||
import.meta.env.MODE === "development" ||
|
||||
@@ -170,7 +180,15 @@ function AppLayout() {
|
||||
const isTest =
|
||||
(window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean })
|
||||
._IS_TEST_MODE === true;
|
||||
|
||||
console.debug("[AppLayout] Auth state check:", {
|
||||
isLoading: auth.isLoading,
|
||||
isAuthenticated: auth.isAuthenticated,
|
||||
isTest
|
||||
});
|
||||
|
||||
if (!auth.isLoading && !auth.isAuthenticated && !isTest) {
|
||||
console.warn("[AppLayout] Not authenticated, redirecting to /login");
|
||||
navigate("/login");
|
||||
}
|
||||
}, [auth.isLoading, auth.isAuthenticated, navigate]);
|
||||
|
||||
@@ -8,6 +8,11 @@ function AuthCallbackPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
console.debug("[AuthCallbackPage] State:", {
|
||||
isAuthenticated: auth.isAuthenticated,
|
||||
isLoading: auth.isLoading,
|
||||
error: auth.error
|
||||
});
|
||||
if (auth.isAuthenticated) {
|
||||
// Save token to localStorage for existing API clients that might still use it
|
||||
const user = auth.user;
|
||||
@@ -21,12 +26,13 @@ function AuthCallbackPage() {
|
||||
typeof auth.user.state.returnTo === "string"
|
||||
? auth.user.state.returnTo
|
||||
: "/";
|
||||
console.info("[AuthCallbackPage] Auth successful, navigating to", returnTo);
|
||||
navigate(returnTo, { replace: true });
|
||||
} else if (auth.error) {
|
||||
console.error("Auth Error:", auth.error);
|
||||
console.error("[AuthCallbackPage] Auth Error:", auth.error);
|
||||
navigate("/login", { replace: true });
|
||||
}
|
||||
}, [auth.isAuthenticated, auth.error, navigate, auth.user]);
|
||||
}, [auth.isAuthenticated, auth.error, navigate, auth.user, auth.isLoading]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
||||
|
||||
@@ -20,10 +20,16 @@ function LoginPage() {
|
||||
const shouldAutoLogin = searchParams.get("auto") === "1";
|
||||
|
||||
useEffect(() => {
|
||||
console.debug("[LoginPage] Auth state check:", {
|
||||
isAuthenticated: auth.isAuthenticated,
|
||||
isLoading: auth.isLoading,
|
||||
returnTo
|
||||
});
|
||||
if (auth.isAuthenticated) {
|
||||
console.info("[LoginPage] User is authenticated, redirecting to", returnTo);
|
||||
navigate(returnTo, { replace: true });
|
||||
}
|
||||
}, [auth.isAuthenticated, navigate, returnTo]);
|
||||
}, [auth.isAuthenticated, navigate, returnTo, auth.isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldAutoLogin) {
|
||||
@@ -72,8 +78,8 @@ function LoginPage() {
|
||||
</div>
|
||||
<p className="opacity-90">{auth.error.message}</p>
|
||||
<Button
|
||||
variant="link"
|
||||
className="p-0 h-auto text-destructive underline mt-2"
|
||||
variant="ghost"
|
||||
className="p-0 h-auto text-destructive underline mt-2 hover:bg-transparent"
|
||||
onClick={() => {
|
||||
window.location.href =
|
||||
window.location.origin + window.location.pathname;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from "axios";
|
||||
import { userManager } from "./auth";
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: (window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean })
|
||||
@@ -7,14 +8,16 @@ const apiClient = axios.create({
|
||||
: (import.meta.env.VITE_ADMIN_API_BASE ?? "/api"),
|
||||
});
|
||||
|
||||
apiClient.interceptors.request.use((config) => {
|
||||
// TODO: IdP 중립 Auth 레이어 연동 시 세션 토큰을 주입한다.
|
||||
const sessionToken = window.localStorage.getItem("admin_session");
|
||||
apiClient.interceptors.request.use(async (config) => {
|
||||
// IdP 중립 Auth 레이어 연동: oidc-client의 userManager에서 최신 토큰을 가져옵니다.
|
||||
const user = await userManager.getUser();
|
||||
const sessionToken = user?.access_token || window.localStorage.getItem("admin_session");
|
||||
|
||||
if (sessionToken) {
|
||||
config.headers.Authorization = `Bearer ${sessionToken}`;
|
||||
}
|
||||
|
||||
// TODO: 테넌트 선택 값을 보관하고 헤더로 전달한다.
|
||||
// 테넌트 선택 값을 보관하고 헤더로 전달한다.
|
||||
const tenantId = window.localStorage.getItem("admin_tenant");
|
||||
if (tenantId) {
|
||||
config.headers["X-Tenant-ID"] = tenantId;
|
||||
@@ -33,10 +36,24 @@ apiClient.interceptors.request.use((config) => {
|
||||
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
async (error) => {
|
||||
if (error.response?.status === 401) {
|
||||
console.warn("[apiClient] 401 Unauthorized detected. Clearing session state.");
|
||||
|
||||
// 로컬 스토리지의 세션 키 제거
|
||||
window.localStorage.removeItem("admin_session");
|
||||
window.location.href = "/login";
|
||||
|
||||
// oidc-client의 유저 상태도 제거하여 isAuthenticated를 false로 만듭니다.
|
||||
// 이를 통해 LoginPage에서의 무한 리다이렉션 루프를 방지합니다.
|
||||
await userManager.removeUser();
|
||||
|
||||
const isAuthPath = window.location.pathname.startsWith("/auth/callback");
|
||||
const isLoginPath = window.location.pathname === "/login";
|
||||
|
||||
if (!isAuthPath && !isLoginPath) {
|
||||
console.info("[apiClient] Redirecting to /login from", window.location.pathname);
|
||||
window.location.href = "/login";
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
},
|
||||
|
||||
@@ -29,18 +29,28 @@ export function shouldAttemptSlidingSessionRenew({
|
||||
}
|
||||
|
||||
if (typeof expiresAtSec !== "number") {
|
||||
console.debug("[sessionSliding] expiresAtSec is not a number, skipping renew");
|
||||
return false;
|
||||
}
|
||||
|
||||
const remainingMs = expiresAtSec * 1000 - nowMs;
|
||||
if (remainingMs <= 0 || remainingMs > thresholdMs) {
|
||||
const remainingMin = Math.floor(remainingMs / 1000 / 60);
|
||||
|
||||
if (remainingMs <= 0) {
|
||||
console.debug("[sessionSliding] Session already expired, skipping renew");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (remainingMs > thresholdMs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nowMs - lastAttemptAtMs < throttleMs) {
|
||||
console.debug("[sessionSliding] Throttling renewal attempt");
|
||||
return false;
|
||||
}
|
||||
|
||||
console.info(`[sessionSliding] Attempting sliding session renewal. Remaining: ${remainingMin}m`);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -60,17 +70,27 @@ export function shouldAttemptUnlimitedSessionRenew({
|
||||
}
|
||||
|
||||
if (typeof expiresAtSec !== "number") {
|
||||
console.debug("[sessionSliding] expiresAtSec is not a number, skipping unlimited renew");
|
||||
return false;
|
||||
}
|
||||
|
||||
const remainingMs = expiresAtSec * 1000 - nowMs;
|
||||
if (remainingMs <= 0 || remainingMs > thresholdMs) {
|
||||
const remainingMin = Math.floor(remainingMs / 1000 / 60);
|
||||
|
||||
if (remainingMs <= 0) {
|
||||
console.debug("[sessionSliding] Session already expired, skipping unlimited renew");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (remainingMs > thresholdMs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nowMs - lastAttemptAtMs < throttleMs) {
|
||||
console.debug("[sessionSliding] Throttling unlimited renewal attempt");
|
||||
return false;
|
||||
}
|
||||
|
||||
console.info(`[sessionSliding] Attempting unlimited session renewal. Remaining: ${remainingMin}m`);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ test.describe("Authentication", () => {
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
|
||||
const authority = "https://sso.hmac.kr/oidc";
|
||||
const authority = "http://localhost:5000/oidc";
|
||||
const client_id = "adminfront";
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
const authData = {
|
||||
@@ -45,12 +45,12 @@ test.describe("Authentication", () => {
|
||||
if (url.includes(".well-known/openid-configuration")) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
issuer: "https://sso.hmac.kr/oidc",
|
||||
authorization_endpoint: "https://sso.hmac.kr/oidc/auth",
|
||||
token_endpoint: "https://sso.hmac.kr/oidc/token",
|
||||
jwks_uri: "https://sso.hmac.kr/oidc/jwks",
|
||||
userinfo_endpoint: "https://sso.hmac.kr/oidc/userinfo",
|
||||
end_session_endpoint: "https://sso.hmac.kr/oidc/session/end",
|
||||
issuer: "http://localhost:5000/oidc",
|
||||
authorization_endpoint: "http://localhost:5000/oidc/auth",
|
||||
token_endpoint: "http://localhost:5000/oidc/token",
|
||||
jwks_uri: "http://localhost:5000/oidc/jwks",
|
||||
userinfo_endpoint: "http://localhost:5000/oidc/userinfo",
|
||||
end_session_endpoint: "http://localhost:5000/oidc/session/end",
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ test.describe("Bulk Actions and Tree Search", () => {
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
|
||||
const authority = "https://sso.hmac.kr/oidc";
|
||||
const authority = "http://localhost:5000/oidc";
|
||||
const client_id = "adminfront";
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
const authData = {
|
||||
@@ -114,7 +114,7 @@ test.describe("Bulk Actions and Tree Search", () => {
|
||||
});
|
||||
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
await route.fulfill({ json: { issuer: "https://sso.hmac.kr/oidc" } });
|
||||
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test";
|
||||
test.describe("Tenant Owners Management", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
const authority = "https://sso.hmac.kr/oidc";
|
||||
const authority = "http://localhost:5000/oidc";
|
||||
const client_id = "adminfront";
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
const authData = {
|
||||
@@ -26,7 +26,7 @@ test.describe("Tenant Owners Management", () => {
|
||||
});
|
||||
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
await route.fulfill({ json: { issuer: "https://sso.hmac.kr/oidc" } });
|
||||
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
|
||||
});
|
||||
|
||||
await page.route(/.*\/api\/v1\/.*/, async (route) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ test.describe("Tenants Management", () => {
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
|
||||
const authority = "https://sso.hmac.kr/oidc";
|
||||
const authority = "http://localhost:5000/oidc";
|
||||
const client_id = "adminfront";
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
const authData = {
|
||||
@@ -52,7 +52,7 @@ test.describe("Tenants Management", () => {
|
||||
});
|
||||
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
await route.fulfill({ json: { issuer: "https://sso.hmac.kr/oidc" } });
|
||||
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test";
|
||||
test.describe("User Management", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
const authority = "https://sso.hmac.kr/oidc";
|
||||
const authority = "http://localhost:5000/oidc";
|
||||
const client_id = "adminfront";
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
const authData = {
|
||||
@@ -33,15 +33,15 @@ test.describe("User Management", () => {
|
||||
if (route.request().url().includes("/.well-known/openid-configuration")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
issuer: "https://sso.hmac.kr/oidc",
|
||||
authorization_endpoint: "https://sso.hmac.kr/oidc/auth",
|
||||
token_endpoint: "https://sso.hmac.kr/oidc/token",
|
||||
userinfo_endpoint: "https://sso.hmac.kr/oidc/userinfo",
|
||||
jwks_uri: "https://sso.hmac.kr/oidc/jwks",
|
||||
issuer: "http://localhost:5000/oidc",
|
||||
authorization_endpoint: "http://localhost:5000/oidc/auth",
|
||||
token_endpoint: "http://localhost:5000/oidc/token",
|
||||
userinfo_endpoint: "http://localhost:5000/oidc/userinfo",
|
||||
jwks_uri: "http://localhost:5000/oidc/jwks",
|
||||
},
|
||||
});
|
||||
}
|
||||
await route.fulfill({ json: { issuer: "https://sso.hmac.kr/oidc" } });
|
||||
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
|
||||
});
|
||||
|
||||
await page.route(/.*\/api\/v1\/.*/, async (route) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ test.describe("Users Bulk Upload", () => {
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
|
||||
const authority = "https://sso.hmac.kr/oidc";
|
||||
const authority = "http://localhost:5000/oidc";
|
||||
const client_id = "adminfront";
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
const authData = {
|
||||
@@ -54,7 +54,7 @@ test.describe("Users Bulk Upload", () => {
|
||||
});
|
||||
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
await route.fulfill({ json: { issuer: "https://sso.hmac.kr/oidc" } });
|
||||
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test";
|
||||
test.describe("User Schema Dynamic Form", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
const authority = "https://sso.hmac.kr/oidc";
|
||||
const authority = "http://localhost:5000/oidc";
|
||||
const client_id = "adminfront";
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
const authData = {
|
||||
@@ -35,15 +35,15 @@ test.describe("User Schema Dynamic Form", () => {
|
||||
if (route.request().url().includes("/.well-known/openid-configuration")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
issuer: "https://sso.hmac.kr/oidc",
|
||||
authorization_endpoint: "https://sso.hmac.kr/oidc/auth",
|
||||
token_endpoint: "https://sso.hmac.kr/oidc/token",
|
||||
userinfo_endpoint: "https://sso.hmac.kr/oidc/userinfo",
|
||||
jwks_uri: "https://sso.hmac.kr/oidc/jwks",
|
||||
issuer: "http://localhost:5000/oidc",
|
||||
authorization_endpoint: "http://localhost:5000/oidc/auth",
|
||||
token_endpoint: "http://localhost:5000/oidc/token",
|
||||
userinfo_endpoint: "http://localhost:5000/oidc/userinfo",
|
||||
jwks_uri: "http://localhost:5000/oidc/jwks",
|
||||
},
|
||||
});
|
||||
}
|
||||
await route.fulfill({ json: { issuer: "https://sso.hmac.kr/oidc" } });
|
||||
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
|
||||
});
|
||||
|
||||
await page.route(/.*\/api\/v1\/.*/, async (route) => {
|
||||
|
||||
@@ -137,11 +137,11 @@ export async function seedAuth(page: Page, role?: string) {
|
||||
};
|
||||
|
||||
window.localStorage.setItem(
|
||||
"oidc.user:https://sso.hmac.kr/oidc:devfront",
|
||||
"oidc.user:http://localhost:5000/oidc:devfront",
|
||||
JSON.stringify(mockOidcUser),
|
||||
);
|
||||
window.localStorage.setItem(
|
||||
"oidc.user:https://sso.hmac.kr/oidc/:devfront",
|
||||
"oidc.user:http://localhost:5000/oidc/:devfront",
|
||||
JSON.stringify(mockOidcUser),
|
||||
);
|
||||
window.localStorage.setItem("dev_role", injectedRole || "rp_admin");
|
||||
@@ -155,12 +155,12 @@ export async function seedAuth(page: Page, role?: string) {
|
||||
if (url.includes(".well-known/openid-configuration")) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
issuer: "https://sso.hmac.kr/oidc",
|
||||
authorization_endpoint: "https://sso.hmac.kr/oidc/auth",
|
||||
token_endpoint: "https://sso.hmac.kr/oidc/token",
|
||||
jwks_uri: "https://sso.hmac.kr/oidc/jwks",
|
||||
userinfo_endpoint: "https://sso.hmac.kr/oidc/userinfo",
|
||||
end_session_endpoint: "https://sso.hmac.kr/oidc/session/end",
|
||||
issuer: "http://localhost:5000/oidc",
|
||||
authorization_endpoint: "http://localhost:5000/oidc/auth",
|
||||
token_endpoint: "http://localhost:5000/oidc/token",
|
||||
jwks_uri: "http://localhost:5000/oidc/jwks",
|
||||
userinfo_endpoint: "http://localhost:5000/oidc/userinfo",
|
||||
end_session_endpoint: "http://localhost:5000/oidc/session/end",
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user