feat: Replace localStorage with secure cookies for OAuth state management

This commit is contained in:
wood chen 2025-02-21 20:04:26 +08:00
parent a40234e6c8
commit 67b434e692
5 changed files with 37 additions and 33 deletions

View File

@ -29,10 +29,12 @@
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.1",
"@types/js-cookie": "^3.0.6",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"crypto-js": "^4.2.0",
"framer-motion": "^11.5.4",
"js-cookie": "^3.0.5",
"lucide-react": "^0.437.0",
"next": "14.2.7",
"next-auth": "5.0.0-beta.20",

15
pnpm-lock.yaml generated
View File

@ -44,6 +44,9 @@ dependencies:
'@radix-ui/react-toast':
specifier: ^1.2.1
version: 1.2.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@types/js-cookie':
specifier: ^3.0.6
version: 3.0.6
class-variance-authority:
specifier: ^0.7.0
version: 0.7.1
@ -56,6 +59,9 @@ dependencies:
framer-motion:
specifier: ^11.5.4
version: 11.18.2(react-dom@18.3.1)(react@18.3.1)
js-cookie:
specifier: ^3.0.5
version: 3.0.5
lucide-react:
specifier: ^0.437.0
version: 0.437.0(react@18.3.1)
@ -1295,6 +1301,10 @@ packages:
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
dev: true
/@types/js-cookie@3.0.6:
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
dev: false
/@types/json5@0.0.29:
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
dev: true
@ -2962,6 +2972,11 @@ packages:
resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==}
dev: false
/js-cookie@3.0.5:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
dev: false
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}

View File

@ -3,6 +3,7 @@
import { useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import Cookies from "js-cookie";
import { MessageCircleCode } from "lucide-react";
import { UserAuthorize } from "@/components/auth/user-authorize";
@ -15,18 +16,19 @@ export default function AuthPage({ searchParams }: Props) {
const router = useRouter();
useEffect(() => {
// 检查是否有待处理的 OAuth 请求
const pendingOAuth = localStorage.getItem("oauth_pending");
if (pendingOAuth) {
// 检查是否有 OAuth 状态参数
const oauthState = Cookies.get("oauth_state");
if (oauthState) {
try {
const params = JSON.parse(pendingOAuth);
// 清除存储的 OAuth 参数
localStorage.removeItem("oauth_pending");
// 重定向到 OAuth 授权页面
// 解码 OAuth 参数
const params = JSON.parse(atob(oauthState));
// 删除 cookie
Cookies.remove("oauth_state", { path: "/" });
// 构建重定向 URL
const searchParams = new URLSearchParams(params);
router.push(`/oauth/authorize?${searchParams.toString()}`);
} catch (error) {
console.error("Failed to process OAuth params:", error);
console.error("Failed to process OAuth state:", error);
}
}
}, [router]);

View File

@ -1,10 +1,10 @@
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getClientByClientId } from "@/lib/dto/client";
import { getCurrentUser } from "@/lib/session";
import { Authorizing } from "@/components/auth/authorizing";
import { ErrorCard } from "@/components/auth/error-card";
import { SaveOAuthParams } from "@/components/auth/save-oauth-params";
export interface AuthorizeParams {
scope?: string;
@ -22,9 +22,15 @@ export default async function OAuthAuthorization({
// 检查用户是否已登录
const user = await getCurrentUser();
if (!user?.id) {
return (
<SaveOAuthParams searchParams={searchParams} redirectTo="/sign-in" />
);
// 将 OAuth 参数保存到 cookie
const encodedParams = btoa(JSON.stringify(searchParams));
cookies().set("oauth_state", encodedParams, {
maxAge: 60 * 10, // 10分钟过期
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
});
redirect("/sign-in");
}
// 验证必要的参数

View File

@ -1,21 +0,0 @@
"use client";
import { useEffect } from "react";
import { redirect } from "next/navigation";
import { AuthorizeParams } from "@/app/(oauth)/oauth/authorize/page";
export function SaveOAuthParams({
searchParams,
redirectTo,
}: {
searchParams: AuthorizeParams;
redirectTo: string;
}) {
useEffect(() => {
localStorage.setItem("oauth_pending", JSON.stringify(searchParams));
window.location.href = redirectTo;
}, [searchParams, redirectTo]);
return null;
}