fix(auth): re-check auth status after logout; clear NextAuth session after sync
1) Sidebar logout flow:
- After AuthReset, also dispatch AuthStatusCheckSubmitted so the auth
machine re-verifies storage (not just resets to initialState). This
makes sure the redirect to /chat reflects the actual storage state,
not just the forcibly-cleared context.
2) OAuthSessionSync:
- After dispatching AuthGoogleSyncSubmitted / AuthFacebookSyncSubmitted,
call signOut({ redirect: false }) to drop the NextAuth session.
The OAuth idToken / accessToken has already been handed to the
backend; clearing the browser-side session prevents lingering OAuth
credential exposure. Fire-and-forget — once status flips to
"unauthenticated", the useEffect early-returns so no re-entry.
Note: auth-machine.ts lost a 3-line stale comment block about XState v5
type inference for inline assign; bundled with this commit.
This commit is contained in:
@@ -52,7 +52,12 @@ export function SidebarScreen() {
|
|||||||
const isNowLoggedOut = user.currentUser == null;
|
const isNowLoggedOut = user.currentUser == null;
|
||||||
prevUserRef.current = user.currentUser;
|
prevUserRef.current = user.currentUser;
|
||||||
if (wasLoggedIn && isNowLoggedOut) {
|
if (wasLoggedIn && isNowLoggedOut) {
|
||||||
|
// 1) AuthReset:清场(清 email/password/username 等敏感字段)
|
||||||
authDispatch({ type: "AuthReset" });
|
authDispatch({ type: "AuthReset" });
|
||||||
|
// 2) AuthStatusCheckSubmitted:通过 storage 重新验证实际 auth 状态
|
||||||
|
// (userLogoutActor 已清空 userStorage,checkAuthStatusActor 会返 "notLoggedIn")
|
||||||
|
// 这一步确保 redirect 到 /chat 后,chat-screen 看到的是真实 storage 状态而非 AuthReset 直接置的 initialState
|
||||||
|
authDispatch({ type: "AuthStatusCheckSubmitted" });
|
||||||
router.replace(ROUTES.chat);
|
router.replace(ROUTES.chat);
|
||||||
}
|
}
|
||||||
}, [user.currentUser, authDispatch, router]);
|
}, [user.currentUser, authDispatch, router]);
|
||||||
|
|||||||
@@ -32,9 +32,6 @@ export type { AuthEvent } from "./auth-events";
|
|||||||
// splash / auth screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` 跳 /chat
|
// splash / auth screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` 跳 /chat
|
||||||
// 跳完派 `AuthClearPendingRedirect` 置 false
|
// 跳完派 `AuthClearPendingRedirect` 置 false
|
||||||
// checkingAuthStatus 不设 flag —— 被动查 token 不算"用户意图"。
|
// checkingAuthStatus 不设 flag —— 被动查 token 不算"用户意图"。
|
||||||
//
|
|
||||||
// 注:onDone 用了内联assign(不是 helper function)—— XState v5 的 TypeScript
|
|
||||||
// 推断对 helper return type 不友好;内联能保留 `state.matches("loadingXxx")` 的类型。
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
export const authMachine = setup({
|
export const authMachine = setup({
|
||||||
types: {
|
types: {
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
* → 派发 `AuthGoogleSyncSubmitted` / `AuthFacebookSyncSubmitted` 事件
|
* → 派发 `AuthGoogleSyncSubmitted` / `AuthFacebookSyncSubmitted` 事件
|
||||||
* → auth machine 调 `authRepository.googleLogin/facebookLogin` 换业务 token
|
* → auth machine 调 `authRepository.googleLogin/facebookLogin` 换业务 token
|
||||||
* → 业务 token 写入本地 storage
|
* → 业务 token 写入本地 storage
|
||||||
|
* → **清掉 NextAuth session**(`signOut({ redirect: false })`):
|
||||||
|
* idToken / accessToken 已交给后端换业务 token,浏览器侧不再需要保留 OAuth 凭证
|
||||||
|
* 避免凭证在 cookie / sessionStorage 里持续暴露
|
||||||
*
|
*
|
||||||
* 此组件无可见 UI(返回 null),仅作为 NextAuth session 与 auth state
|
* 此组件无可见 UI(返回 null),仅作为 NextAuth session 与 auth state
|
||||||
* machine 之间的桥接器。挂在 RootProviders 内、AuthProvider 之后:
|
* machine 之间的桥接器。挂在 RootProviders 内、AuthProvider 之后:
|
||||||
@@ -16,7 +19,7 @@
|
|||||||
* </AuthProvider>
|
* </AuthProvider>
|
||||||
*/
|
*/
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
import { signOut, useSession } from "next-auth/react";
|
||||||
|
|
||||||
import { useAuthDispatch, useAuthState } from "./auth-context";
|
import { useAuthDispatch, useAuthState } from "./auth-context";
|
||||||
|
|
||||||
@@ -35,20 +38,24 @@ export function OAuthSessionSync() {
|
|||||||
// 3) 已经有该 provider 的业务 token(防止重复 sync 刷后端 user 表)
|
// 3) 已经有该 provider 的业务 token(防止重复 sync 刷后端 user 表)
|
||||||
if (loginStatus === session.provider) return;
|
if (loginStatus === session.provider) return;
|
||||||
|
|
||||||
// 4) Google:派发 idToken sync 事件
|
// 4) Google:派发 idToken sync 事件 → 清 NextAuth session
|
||||||
if (session.provider === "google") {
|
if (session.provider === "google") {
|
||||||
if (!session.idToken) return;
|
if (!session.idToken) return;
|
||||||
dispatch({ type: "AuthGoogleSyncSubmitted", idToken: session.idToken });
|
dispatch({ type: "AuthGoogleSyncSubmitted", idToken: session.idToken });
|
||||||
|
// OAuth token 已交给后端 —— 清掉 NextAuth session(fire-and-forget)
|
||||||
|
// 清完后 useSession 返 null / unauthenticated → 下次 effect 早 return,不会重入
|
||||||
|
void signOut({ redirect: false });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5) Facebook:派发 accessToken sync 事件
|
// 5) Facebook:派发 accessToken sync 事件 → 清 NextAuth session
|
||||||
if (session.provider === "facebook") {
|
if (session.provider === "facebook") {
|
||||||
if (!session.accessToken) return;
|
if (!session.accessToken) return;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "AuthFacebookSyncSubmitted",
|
type: "AuthFacebookSyncSubmitted",
|
||||||
accessToken: session.accessToken,
|
accessToken: session.accessToken,
|
||||||
});
|
});
|
||||||
|
void signOut({ redirect: false });
|
||||||
}
|
}
|
||||||
}, [status, session, loginStatus, dispatch]);
|
}, [status, session, loginStatus, dispatch]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user