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 { getClientByClientId } from "@/lib/dto/client";
|
||||||
import { Authorizing } from "@/components/auth/authorizing";
|
import { Authorizing } from "@/components/auth/authorizing";
|
||||||
|
|
||||||
@ -8,30 +10,56 @@ export interface AuthorizeParams extends Record<string, string> {
|
|||||||
redirect_uri: string;
|
redirect_uri: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function OAuthAuthorization({
|
export default async function AuthorizePage({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: AuthorizeParams;
|
searchParams: AuthorizeParams;
|
||||||
}) {
|
}) {
|
||||||
// params invalid
|
// 检查必要的参数
|
||||||
if (
|
if (
|
||||||
!searchParams.response_type ||
|
!searchParams.response_type ||
|
||||||
!searchParams.client_id ||
|
!searchParams.client_id ||
|
||||||
!searchParams.redirect_uri
|
!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);
|
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 (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-gray-50 p-4">
|
<div className="flex min-h-screen items-center justify-center bg-gray-50 p-4">
|
||||||
<Authorizing />
|
<Authorizing searchParams={searchParams} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,15 @@ export default async function Q58CallbackPage({
|
|||||||
throw new Error("Client Id invalid (code: -1004).");
|
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
|
// verify q58 callback
|
||||||
const user = await q58CallbackVerify(searchParams.sso, searchParams.sig);
|
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);
|
const authorization = await findAuthorization(user.id, client.id);
|
||||||
|
|
||||||
if (authorization) {
|
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);
|
const redirectUrl = await getAuthorizeUrl(oauthParams);
|
||||||
return redirect(redirectUrl);
|
return redirect(redirectUrl);
|
||||||
}
|
}
|
||||||
|
@ -1,38 +1,44 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { getDiscourseSSOUrl } from "@/actions/discourse-sso-url";
|
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 router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const [error, setError] = useState<unknown | null>(null);
|
const [error, setError] = useState<unknown | null>(null);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
const signInCallback = useCallback(async () => {
|
const signInCallback = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const url = await getDiscourseSSOUrl(searchParams.toString());
|
const url = await getDiscourseSSOUrl(
|
||||||
|
new URLSearchParams(searchParams).toString(),
|
||||||
|
);
|
||||||
router.push(url);
|
router.push(url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(error);
|
setError(error);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [searchParams, router]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 模拟进度条
|
// 启动进度条动画
|
||||||
const progressInterval = setInterval(() => {
|
const progressTimer = setInterval(() => {
|
||||||
setProgress((prev) => (prev >= 90 ? 90 : prev + 10));
|
setProgress((prev) => Math.min(prev + 10, 90));
|
||||||
}, 300);
|
}, 500);
|
||||||
|
|
||||||
// Delay 3s get sso url go to ...
|
// 立即开始授权
|
||||||
const timer = setTimeout(signInCallback, 3000);
|
signInCallback();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(timer);
|
clearInterval(progressTimer);
|
||||||
clearInterval(progressInterval);
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, [signInCallback]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
|
<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";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
export async function getClientByClientId(clientId: string) {
|
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(
|
export async function createClient(
|
||||||
@ -31,5 +48,20 @@ export async function getClientById(id: string) {
|
|||||||
where: {
|
where: {
|
||||||
id,
|
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