mirror of
https://github.com/woodchen-ink/Q58Connect.git
synced 2025-07-18 14:01:55 +08:00
feat: Improve OAuth authorization error handling and client validation
- Add comprehensive error handling for OAuth authorization requests - Implement detailed error redirects for invalid client, redirect URI, and disabled applications - Update Authorizing component to handle OAuth parameters more robustly - Refactor client retrieval to select specific fields and improve security - Enhance error messaging for OAuth authorization flow
This commit is contained in:
parent
bd6b2b747d
commit
1d0fe64fdb
@ -1,3 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getClientByClientId } from "@/lib/dto/client";
|
||||
import { Authorizing } from "@/components/auth/authorizing";
|
||||
|
||||
@ -8,30 +10,56 @@ export interface AuthorizeParams extends Record<string, string> {
|
||||
redirect_uri: string;
|
||||
}
|
||||
|
||||
export default async function OAuthAuthorization({
|
||||
export default async function AuthorizePage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: AuthorizeParams;
|
||||
}) {
|
||||
// params invalid
|
||||
// 检查必要的参数
|
||||
if (
|
||||
!searchParams.response_type ||
|
||||
!searchParams.client_id ||
|
||||
!searchParams.redirect_uri
|
||||
) {
|
||||
throw new Error("Params invalid");
|
||||
const errorParams = new URLSearchParams({
|
||||
error: "invalid_request",
|
||||
error_description: "缺少必要的参数",
|
||||
});
|
||||
redirect(`${searchParams.redirect_uri}?${errorParams.toString()}`);
|
||||
}
|
||||
|
||||
// client invalid
|
||||
const client = await getClientByClientId(searchParams.client_id);
|
||||
if (!client || client.redirectUri !== searchParams.redirect_uri) {
|
||||
throw new Error("Client not found");
|
||||
|
||||
// 检查应用是否存在
|
||||
if (!client) {
|
||||
const errorParams = new URLSearchParams({
|
||||
error: "invalid_client",
|
||||
error_description: "应用不存在",
|
||||
});
|
||||
redirect(`${searchParams.redirect_uri}?${errorParams.toString()}`);
|
||||
}
|
||||
|
||||
// 检查回调地址是否匹配
|
||||
if (client.redirectUri !== searchParams.redirect_uri) {
|
||||
const errorParams = new URLSearchParams({
|
||||
error: "invalid_request",
|
||||
error_description: "回调地址不匹配",
|
||||
});
|
||||
redirect(`${searchParams.redirect_uri}?${errorParams.toString()}`);
|
||||
}
|
||||
|
||||
// 检查应用是否被禁用
|
||||
if (!client.enabled) {
|
||||
const errorParams = new URLSearchParams({
|
||||
error: "access_denied",
|
||||
error_description: "此应用已被禁用",
|
||||
});
|
||||
redirect(`${searchParams.redirect_uri}?${errorParams.toString()}`);
|
||||
}
|
||||
|
||||
// Authorizing ...
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gray-50 p-4">
|
||||
<Authorizing />
|
||||
<Authorizing searchParams={searchParams} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -27,6 +27,15 @@ export default async function Q58CallbackPage({
|
||||
throw new Error("Client Id invalid (code: -1004).");
|
||||
}
|
||||
|
||||
// 检查应用是否被禁用
|
||||
if (!client.enabled) {
|
||||
const redirectUri = client.redirectUri;
|
||||
const redirectUrl = new URL(redirectUri);
|
||||
redirectUrl.searchParams.set("error", "access_denied");
|
||||
redirectUrl.searchParams.set("error_description", "该应用已被禁用");
|
||||
return redirect(redirectUrl.toString());
|
||||
}
|
||||
|
||||
// verify q58 callback
|
||||
const user = await q58CallbackVerify(searchParams.sso, searchParams.sig);
|
||||
|
||||
@ -34,6 +43,14 @@ export default async function Q58CallbackPage({
|
||||
const authorization = await findAuthorization(user.id, client.id);
|
||||
|
||||
if (authorization) {
|
||||
// 如果授权被禁用,也返回错误
|
||||
if (!authorization.enabled) {
|
||||
const redirectUrl = new URL(client.redirectUri);
|
||||
redirectUrl.searchParams.set("error", "access_denied");
|
||||
redirectUrl.searchParams.set("error_description", "您的授权已被禁用");
|
||||
return redirect(redirectUrl.toString());
|
||||
}
|
||||
|
||||
const redirectUrl = await getAuthorizeUrl(oauthParams);
|
||||
return redirect(redirectUrl);
|
||||
}
|
||||
|
@ -1,38 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { getDiscourseSSOUrl } from "@/actions/discourse-sso-url";
|
||||
|
||||
export function Authorizing() {
|
||||
import type { AuthorizeParams } from "@/app/(oauth)/oauth/authorize/page";
|
||||
|
||||
interface AuthorizingProps {
|
||||
searchParams: AuthorizeParams;
|
||||
}
|
||||
|
||||
export function Authorizing({ searchParams }: AuthorizingProps) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [error, setError] = useState<unknown | null>(null);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const signInCallback = useCallback(async () => {
|
||||
try {
|
||||
const url = await getDiscourseSSOUrl(searchParams.toString());
|
||||
const url = await getDiscourseSSOUrl(
|
||||
new URLSearchParams(searchParams).toString(),
|
||||
);
|
||||
router.push(url);
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
}
|
||||
}, []);
|
||||
}, [searchParams, router]);
|
||||
|
||||
useEffect(() => {
|
||||
// 模拟进度条
|
||||
const progressInterval = setInterval(() => {
|
||||
setProgress((prev) => (prev >= 90 ? 90 : prev + 10));
|
||||
}, 300);
|
||||
// 启动进度条动画
|
||||
const progressTimer = setInterval(() => {
|
||||
setProgress((prev) => Math.min(prev + 10, 90));
|
||||
}, 500);
|
||||
|
||||
// Delay 3s get sso url go to ...
|
||||
const timer = setTimeout(signInCallback, 3000);
|
||||
// 立即开始授权
|
||||
signInCallback();
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
clearInterval(progressInterval);
|
||||
clearInterval(progressTimer);
|
||||
};
|
||||
}, []);
|
||||
}, [signInCallback]);
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
|
||||
|
@ -3,7 +3,24 @@ import { Prisma as PrismaType } from "@prisma/client";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function getClientByClientId(clientId: string) {
|
||||
return prisma.client.findUnique({ where: { clientId } });
|
||||
return prisma.client.findUnique({
|
||||
where: { clientId },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
clientId: true,
|
||||
clientSecret: true,
|
||||
redirectUri: true,
|
||||
home: true,
|
||||
logo: true,
|
||||
description: true,
|
||||
enabled: true,
|
||||
allowedUsers: true,
|
||||
userId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function createClient(
|
||||
@ -31,5 +48,20 @@ export async function getClientById(id: string) {
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
clientId: true,
|
||||
clientSecret: true,
|
||||
redirectUri: true,
|
||||
home: true,
|
||||
logo: true,
|
||||
description: true,
|
||||
enabled: true,
|
||||
allowedUsers: true,
|
||||
userId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user