From 0c1d7fceeab843599534a35f2e57e3335dd165ae Mon Sep 17 00:00:00 2001 From: wood chen Date: Mon, 17 Feb 2025 05:56:15 +0800 Subject: [PATCH] refactor: Replace Discourse SSO with Q58 authentication flow --- src/actions/discourse-sso-url.ts | 2 +- .../{discourse => q58}/callback/page.tsx | 15 ++-- src/app/api/auth/{discourse => q58}/route.ts | 0 src/auth.config.ts | 4 +- src/components/auth/authorization-card.tsx | 32 ++++++-- src/components/auth/user-auth-form.tsx | 2 +- src/lib/discourse/verify.ts | 6 +- src/lib/q58/verify.ts | 74 +++++++++++++++++++ 8 files changed, 111 insertions(+), 24 deletions(-) rename src/app/(oauth)/{discourse => q58}/callback/page.tsx (76%) rename src/app/api/auth/{discourse => q58}/route.ts (100%) create mode 100644 src/lib/q58/verify.ts diff --git a/src/actions/discourse-sso-url.ts b/src/actions/discourse-sso-url.ts index f407607..64c4ab6 100644 --- a/src/actions/discourse-sso-url.ts +++ b/src/actions/discourse-sso-url.ts @@ -15,7 +15,7 @@ export async function getDiscourseSSOUrl(searchParams: string) { console.log(`searchParams: ${searchParams}`); const nonce = WordArray.random(16).toString(); - const return_url = `${appHost}/discourse/callback?oauth=${btoa(searchParams)}`; + const return_url = `${appHost}/q58/callback?oauth=${btoa(searchParams)}`; const sso = btoa(`nonce=${nonce}&return_sso_url=${encodeURI(return_url)}`); const sig = hmacSHA256(sso, oauthSecret).toString(Hex); cookies().set(AUTH_NONCE, nonce, { maxAge: 60 * 10 }); diff --git a/src/app/(oauth)/discourse/callback/page.tsx b/src/app/(oauth)/q58/callback/page.tsx similarity index 76% rename from src/app/(oauth)/discourse/callback/page.tsx rename to src/app/(oauth)/q58/callback/page.tsx index 3059649..39de7aa 100644 --- a/src/app/(oauth)/discourse/callback/page.tsx +++ b/src/app/(oauth)/q58/callback/page.tsx @@ -1,22 +1,22 @@ import { Suspense } from "react"; import { redirect } from "next/navigation"; -import { discourseCallbackVerify } from "@/lib/discourse/verify"; import { findAuthorization } from "@/lib/dto/authorization"; import { getClientByClientId } from "@/lib/dto/client"; import { getAuthorizeUrl } from "@/lib/oauth/authorize-url"; +import { q58CallbackVerify } from "@/lib/q58/verify"; import { AuthorizationCard } from "@/components/auth/authorization-card"; -export interface DiscourseCallbackParams extends Record { +export interface Q58CallbackParams extends Record { sig: string; sso: string; oauth: string; } -export default async function DiscourseCallbackPage({ +export default async function Q58CallbackPage({ searchParams, }: { - searchParams: DiscourseCallbackParams; + searchParams: Q58CallbackParams; }) { const oauthParams = new URLSearchParams(atob(searchParams.oauth)); // check client info @@ -27,11 +27,8 @@ export default async function DiscourseCallbackPage({ throw new Error("Client Id invalid (code: -1004)."); } - // verify discourse callback - const user = await discourseCallbackVerify( - searchParams.sso, - searchParams.sig, - ); + // verify q58 callback + const user = await q58CallbackVerify(searchParams.sso, searchParams.sig); // check authorization const authorization = await findAuthorization(user.id, client.id); diff --git a/src/app/api/auth/discourse/route.ts b/src/app/api/auth/q58/route.ts similarity index 100% rename from src/app/api/auth/discourse/route.ts rename to src/app/api/auth/q58/route.ts diff --git a/src/auth.config.ts b/src/auth.config.ts index b93490c..35cbbd4 100644 --- a/src/auth.config.ts +++ b/src/auth.config.ts @@ -1,7 +1,7 @@ import type { NextAuthConfig } from "next-auth"; import Credentials from "next-auth/providers/credentials"; -import { discourseCallbackVerify } from "./lib/discourse/verify"; +import { q58CallbackVerify } from "./lib/q58/verify"; // Notice this is only an object, not a full Auth.js instance export default { @@ -14,7 +14,7 @@ export default { authorize: async (credentials) => { const sso = credentials.sso as string; const sig = credentials.sig as string; - const user = await discourseCallbackVerify(sso, sig); + const user = await q58CallbackVerify(sso, sig); return user; }, }), diff --git a/src/components/auth/authorization-card.tsx b/src/components/auth/authorization-card.tsx index 7fea48f..be8b6de 100644 --- a/src/components/auth/authorization-card.tsx +++ b/src/components/auth/authorization-card.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import Image from "next/image"; import { useRouter } from "next/navigation"; import { handleAuthorizeAction } from "@/actions/authorizing"; import { Client } from "@prisma/client"; @@ -75,9 +76,19 @@ export function AuthorizationCard({
- - {client.name[0].toUpperCase()} - + {client.logo ? ( + {client.name} + ) : ( + + {client.name[0].toUpperCase()} + + )}
@@ -90,18 +101,23 @@ export function AuthorizationCard({ -
-

请求的权限

+
+

+ 请求的权限 +

{permissions.map((permission) => (
togglePermission(permission.id)} >
-
@@ -112,7 +128,7 @@ export function AuthorizationCard({ )}
{expandedPermission === permission.id && ( -
+
{permission.description}
)} diff --git a/src/components/auth/user-auth-form.tsx b/src/components/auth/user-auth-form.tsx index cd0aa0d..de3ad1d 100644 --- a/src/components/auth/user-auth-form.tsx +++ b/src/components/auth/user-auth-form.tsx @@ -22,7 +22,7 @@ export function UserAuthForm({ const signIn = () => { React.startTransition(async () => { - const response = await fetch("/api/auth/discourse", { method: "POST" }); + const response = await fetch("/api/auth/q58", { method: "POST" }); if (!response.ok || response.status !== 200) { setIsLoading(false); toast({ diff --git a/src/lib/discourse/verify.ts b/src/lib/discourse/verify.ts index 8c36d1b..607bcad 100644 --- a/src/lib/discourse/verify.ts +++ b/src/lib/discourse/verify.ts @@ -8,11 +8,11 @@ import hmacSHA256 from "crypto-js/hmac-sha256"; import { AUTH_NONCE } from "@/lib/constants"; import { createUser, getUserById, updateUser } from "@/lib/dto/user"; -const DISCOUSE_SECRET = process.env.DISCOURSE_SECRET as string; +const Q58_SECRET = process.env.DISCOURSE_SECRET as string; -export async function discourseCallbackVerify(sso: string, sig: string) { +export async function q58CallbackVerify(sso: string, sig: string) { // 校验数据正确性 - if (hmacSHA256(sso, DISCOUSE_SECRET).toString(Hex) != sig) { + if (hmacSHA256(sso, Q58_SECRET).toString(Hex) != sig) { throw new Error("Request params is invalid (code: -1001)."); } // 校验 nonce diff --git a/src/lib/q58/verify.ts b/src/lib/q58/verify.ts new file mode 100644 index 0000000..607bcad --- /dev/null +++ b/src/lib/q58/verify.ts @@ -0,0 +1,74 @@ +import "server-only"; + +import { cookies } from "next/headers"; +import { UserRole } from "@prisma/client"; +import Hex from "crypto-js/enc-hex"; +import hmacSHA256 from "crypto-js/hmac-sha256"; + +import { AUTH_NONCE } from "@/lib/constants"; +import { createUser, getUserById, updateUser } from "@/lib/dto/user"; + +const Q58_SECRET = process.env.DISCOURSE_SECRET as string; + +export async function q58CallbackVerify(sso: string, sig: string) { + // 校验数据正确性 + if (hmacSHA256(sso, Q58_SECRET).toString(Hex) != sig) { + throw new Error("Request params is invalid (code: -1001)."); + } + // 校验 nonce + const cookieStore = cookies(); + let searchParams = new URLSearchParams(atob(sso as string)); + const nonce = searchParams.get("nonce"); + if (!cookieStore.has(AUTH_NONCE) || !nonce) { + throw new Error("Request params is invalid (code: -1002)."); + } + if (cookieStore.get(AUTH_NONCE)?.value != nonce) { + throw new Error("Request params is invalid (code: -1003)."); + } + // cookieStore.delete(AUTH_NONCE); + + const id = searchParams.get("external_id"); + const email = searchParams.get("email"); + const username = searchParams.get("username"); + const name = searchParams.get("name"); + const avatarUrl = searchParams.get("avatar_url"); + const isAdmin = searchParams.get("admin") == "true"; + const moderator = searchParams.get("moderator") == "true"; + const groups = searchParams.get("groups")?.split(","); + if (!id || !email || !username) { + throw new Error("User not found."); + } + + // 查数据库是否有人 + let dbUser = await getUserById(id); + if (dbUser) { + // 更新 + dbUser = await updateUser(id, { + username, + email, + name, + avatarUrl, + role: isAdmin ? UserRole.ADMIN : UserRole.USER, + moderator, + groups, + }); + } else { + // 创建 + dbUser = await createUser({ + id, + username, + email, + name, + avatarUrl, + role: isAdmin ? UserRole.ADMIN : UserRole.USER, + moderator, + groups, + }); + } + + if (!dbUser) { + throw new Error("User not create success."); + } + + return dbUser; +}