🐛 fix: fix desktop auth redirect url error (#8597)

* try to fix cloudflare issue

* fix 0.0.0.0

* fix route

* fix desktop callback

* update pkg

* try to fix again

* try to fix route again
This commit is contained in:
Arvin Xu 2025-07-30 00:17:23 +08:00 committed by GitHub
parent f92b86b194
commit 0ed73685dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 130 additions and 21 deletions

View file

@ -257,7 +257,7 @@
"semver": "^7.7.2",
"sharp": "^0.34.3",
"shiki": "^3.8.1",
"stripe": "^16.12.0",
"stripe": "^17.7.0",
"superjson": "^2.2.2",
"svix": "^1.69.0",
"swr": "^2.3.4",

View file

@ -3,9 +3,50 @@ import { NextRequest, NextResponse, after } from 'next/server';
import { OAuthHandoffModel } from '@/database/models/oauthHandoff';
import { serverDB } from '@/database/server';
import { correctOIDCUrl } from '@/utils/server/correctOIDCUrl';
const log = debug('lobe-oidc:callback:desktop');
const errorPathname = '/oauth/callback/error';
/**
* URL
*/
const buildRedirectUrl = (req: NextRequest, pathname: string): URL => {
const forwardedHost = req.headers.get('x-forwarded-host');
const requestHost = req.headers.get('host');
const forwardedProto =
req.headers.get('x-forwarded-proto') || req.headers.get('x-forwarded-protocol');
// 确定实际的主机名,提供后备值
const actualHost = forwardedHost || requestHost;
const actualProto = forwardedProto || 'https';
log(
'Building redirect URL - host: %s, proto: %s, pathname: %s',
actualHost,
actualProto,
pathname,
);
// 如果主机名仍然无效使用req.nextUrl作为后备
if (!actualHost) {
log('Warning: Invalid host detected, using req.nextUrl as fallback');
const fallbackUrl = req.nextUrl.clone();
fallbackUrl.pathname = pathname;
return fallbackUrl;
}
try {
return new URL(`${actualProto}://${actualHost}${pathname}`);
} catch (error) {
log('Error constructing URL, using req.nextUrl as fallback: %O', error);
const fallbackUrl = req.nextUrl.clone();
fallbackUrl.pathname = pathname;
return fallbackUrl;
}
};
export const GET = async (req: NextRequest) => {
try {
const searchParams = req.nextUrl.searchParams;
@ -14,9 +55,11 @@ export const GET = async (req: NextRequest) => {
if (!code || !state || typeof code !== 'string' || typeof state !== 'string') {
log('Missing code or state in form data');
const errorUrl = req.nextUrl.clone();
errorUrl.pathname = '/oauth/callback/error';
const errorUrl = buildRedirectUrl(req, errorPathname);
errorUrl.searchParams.set('reason', 'invalid_request');
log('Redirecting to error URL: %s', errorUrl.toString());
return NextResponse.redirect(errorUrl);
}
@ -31,9 +74,16 @@ export const GET = async (req: NextRequest) => {
await authHandoffModel.create({ client, id, payload });
log('Handoff record created successfully for id: %s', id);
// Redirect to a generic success page. The desktop app will poll for the result.
const successUrl = req.nextUrl.clone();
successUrl.pathname = '/oauth/callback/success';
const successUrl = buildRedirectUrl(req, '/oauth/callback/success');
// 添加调试日志
log('Request host header: %s', req.headers.get('host'));
log('Request x-forwarded-host: %s', req.headers.get('x-forwarded-host'));
log('Request x-forwarded-proto: %s', req.headers.get('x-forwarded-proto'));
log('Constructed success URL: %s', successUrl.toString());
const correctedUrl = correctOIDCUrl(req, successUrl);
log('Final redirect URL: %s', correctedUrl.toString());
// cleanup expired
after(async () => {
@ -42,17 +92,18 @@ export const GET = async (req: NextRequest) => {
log('Cleaned up %d expired handoff records', cleanedCount);
});
return NextResponse.redirect(successUrl);
return NextResponse.redirect(correctedUrl);
} catch (error) {
log('Error in OIDC callback: %O', error);
const errorUrl = req.nextUrl.clone();
errorUrl.pathname = '/oauth/callback/error';
const errorUrl = buildRedirectUrl(req, errorPathname);
errorUrl.searchParams.set('reason', 'internal_error');
if (error instanceof Error) {
errorUrl.searchParams.set('errorMessage', error.message);
}
log('Redirecting to error URL: %s', errorUrl.toString());
return NextResponse.redirect(errorUrl);
}
};

View file

@ -3,6 +3,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { OIDCService } from '@/server/services/oidc';
import { getUserAuth } from '@/utils/server/auth';
import { correctOIDCUrl } from '@/utils/server/correctOIDCUrl';
const log = debug('lobe-oidc:consent');
@ -113,19 +114,15 @@ export async function POST(request: NextRequest) {
const internalRedirectUrlString = await oidcService.getInteractionResult(uid, result);
log('OIDC Provider internal redirect URL string: %s', internalRedirectUrlString);
// // Construct the handoff URL
// const handoffUrl = new URL('/oauth/handoff', request.nextUrl.origin);
// // Set the original redirect URL as the 'target' query parameter (URL encoded)
// handoffUrl.searchParams.set('target', internalRedirectUrlString);
//
// log('Redirecting to handoff page: %s', handoffUrl.toString());
// // Redirect to the handoff page
// return NextResponse.redirect(handoffUrl.toString(), {
// headers: request.headers, // Keep original headers if necessary
// status: 303,
// });
let finalRedirectUrl;
try {
finalRedirectUrl = correctOIDCUrl(request, new URL(internalRedirectUrlString));
} catch {
finalRedirectUrl = new URL(internalRedirectUrlString);
log('Warning: Could not parse redirect URL, using as-is: %s', internalRedirectUrlString);
}
return NextResponse.redirect(internalRedirectUrlString, {
return NextResponse.redirect(finalRedirectUrl, {
headers: request.headers,
status: 303,
});

View file

@ -0,0 +1,61 @@
import debug from 'debug';
import { NextRequest } from 'next/server';
const log = debug('lobe-oidc:correctOIDCUrl');
/**
* OIDC URL
* @param req - Next.js
* @param url - URL
* @returns URL
*/
export const correctOIDCUrl = (req: NextRequest, url: URL): URL => {
const requestHost = req.headers.get('host');
const forwardedHost = req.headers.get('x-forwarded-host');
const forwardedProto =
req.headers.get('x-forwarded-proto') || req.headers.get('x-forwarded-protocol');
log('Input URL: %s', url.toString());
log(
'Request headers - host: %s, x-forwarded-host: %s, x-forwarded-proto: %s',
requestHost,
forwardedHost,
forwardedProto,
);
// 确定实际的主机名和协议,提供后备值
const actualHost = forwardedHost || requestHost;
const actualProto = forwardedProto || (url.protocol === 'https:' ? 'https' : 'http');
// 如果无法确定有效的主机名直接返回原URL
if (!actualHost || actualHost === 'null') {
log('Warning: Cannot determine valid host, returning original URL');
return url;
}
// 如果 URL 指向本地地址,或者主机名与实际请求主机不匹配,则修正 URL
const needsCorrection =
url.hostname === 'localhost' ||
url.hostname === '127.0.0.1' ||
url.hostname === '0.0.0.0' ||
url.hostname !== actualHost;
if (needsCorrection) {
log('URL needs correction. Original hostname: %s, correcting to: %s', url.hostname, actualHost);
try {
const correctedUrl = new URL(url.toString());
correctedUrl.protocol = actualProto + ':';
correctedUrl.host = actualHost;
log('Corrected URL: %s', correctedUrl.toString());
return correctedUrl;
} catch (error) {
log('Error creating corrected URL, returning original: %O', error);
return url;
}
}
log('URL does not need correction, returning original: %s', url.toString());
return url;
};