mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 07:12:12 +03:00
fix(oauth): target specific connection by id on re-auth token exchange (#1702)
Integrated into release/v3.7.2 — fixes OAuth re-auth targeting specific connection by ID
This commit is contained in:
@@ -969,7 +969,8 @@ export default function ProviderDetailPage() {
|
||||
const [connections, setConnections] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [providerNode, setProviderNode] = useState(null);
|
||||
const [showOAuthModal, setShowOAuthModal] = useState(false);
|
||||
const [showOAuthModal, _setShowOAuthModal] = useState(false);
|
||||
const [reauthConnection, setReauthConnection] = useState<ConnectionRowConnection | null>(null);
|
||||
const [showAddApiKeyModal, setShowAddApiKeyModal] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [showEditNodeModal, setShowEditNodeModal] = useState(false);
|
||||
@@ -1021,6 +1022,11 @@ export default function ProviderDetailPage() {
|
||||
const isCompatible = isOpenAICompatible || isAnthropicCompatible || isCcCompatible;
|
||||
const isAnthropicProtocolCompatible = isAnthropicCompatible || isCcCompatible;
|
||||
|
||||
const setShowOAuthModal = (show: boolean, connectionRow?: ConnectionRowConnection) => {
|
||||
_setShowOAuthModal(show);
|
||||
setReauthConnection(show && connectionRow ? connectionRow : null);
|
||||
};
|
||||
|
||||
const providerInfo = resolveDashboardProviderInfo(providerId, {
|
||||
providerNode,
|
||||
compatibleLabels: {
|
||||
@@ -2928,7 +2934,9 @@ export default function ProviderDetailPage() {
|
||||
}}
|
||||
onDelete={() => handleDelete(conn.id)}
|
||||
onReauth={
|
||||
conn.authType === "oauth" ? () => setShowOAuthModal(true) : undefined
|
||||
conn.authType === "oauth"
|
||||
? () => setShowOAuthModal(true, conn)
|
||||
: undefined
|
||||
}
|
||||
onRefreshToken={
|
||||
conn.authType === "oauth" ? () => handleRefreshToken(conn.id) : undefined
|
||||
@@ -3048,7 +3056,7 @@ export default function ProviderDetailPage() {
|
||||
onDelete={() => handleDelete(conn.id)}
|
||||
onReauth={
|
||||
conn.authType === "oauth"
|
||||
? () => setShowOAuthModal(true)
|
||||
? () => setShowOAuthModal(true, conn)
|
||||
: undefined
|
||||
}
|
||||
onRefreshToken={
|
||||
@@ -3183,6 +3191,7 @@ export default function ProviderDetailPage() {
|
||||
(providerId === "kiro" || providerId === "amazon-q" ? (
|
||||
<KiroOAuthWrapper
|
||||
isOpen={showOAuthModal}
|
||||
reauthConnection={reauthConnection}
|
||||
providerInfo={{ ...providerInfo, id: providerId }}
|
||||
onSuccess={handleOAuthSuccess}
|
||||
onClose={() => {
|
||||
@@ -3192,6 +3201,7 @@ export default function ProviderDetailPage() {
|
||||
) : providerId === "cursor" ? (
|
||||
<CursorAuthModal
|
||||
isOpen={showOAuthModal}
|
||||
reauthConnection={reauthConnection}
|
||||
onSuccess={handleOAuthSuccess}
|
||||
onClose={() => {
|
||||
setShowOAuthModal(false);
|
||||
@@ -3200,6 +3210,7 @@ export default function ProviderDetailPage() {
|
||||
) : (
|
||||
<OAuthModal
|
||||
isOpen={showOAuthModal}
|
||||
reauthConnection={reauthConnection}
|
||||
provider={providerId}
|
||||
providerInfo={providerInfo}
|
||||
onSuccess={handleOAuthSuccess}
|
||||
|
||||
@@ -234,7 +234,7 @@ export async function POST(
|
||||
}
|
||||
|
||||
if (action === "exchange") {
|
||||
const { code, redirectUri, codeVerifier, state } = body;
|
||||
const { code, redirectUri, connectionId, codeVerifier, state } = body;
|
||||
const normalizedState = typeof state === "string" && state.length > 0 ? state : undefined;
|
||||
const providerData = getProvider(provider);
|
||||
|
||||
@@ -278,6 +278,7 @@ export async function POST(
|
||||
if (tokenData.email) {
|
||||
const existing = await getProviderConnections({ provider });
|
||||
const match = existing.find((c: any) => {
|
||||
if (c.id && safeEqual(connectionId, c.id)) return true;
|
||||
// safeEqual: constant-time comparison to prevent timing attacks (CWE-208, finding #258-6/7)
|
||||
if (!safeEqual(c.email, tokenData.email) || c.authType !== "oauth") return false;
|
||||
// For Codex, also check workspaceId to avoid overwriting different workspace connections
|
||||
@@ -322,7 +323,7 @@ export async function POST(
|
||||
}
|
||||
|
||||
if (action === "poll") {
|
||||
const { deviceCode, codeVerifier, extraData } = body;
|
||||
const { deviceCode, connectionId, codeVerifier, extraData } = body;
|
||||
|
||||
// Resolve proxy for this provider (provider-level → global → direct)
|
||||
const proxy = await resolveProxyForProvider(provider);
|
||||
@@ -364,6 +365,7 @@ export async function POST(
|
||||
if (result.tokens.email) {
|
||||
const existing = await getProviderConnections({ provider });
|
||||
const match = existing.find((c: any) => {
|
||||
if (c.id && safeEqual(connectionId, c.id)) return true;
|
||||
// safeEqual: constant-time comparison to prevent timing attacks (CWE-208, finding #258-8/9)
|
||||
if (!safeEqual(c.email, result.tokens.email) || c.authType !== "oauth") return false;
|
||||
// For Codex, also check workspaceId to avoid overwriting different workspace connections
|
||||
@@ -418,6 +420,8 @@ export async function POST(
|
||||
}
|
||||
|
||||
if (action === "poll-callback") {
|
||||
const { connectionId } = body;
|
||||
|
||||
// Poll for Codex callback server result
|
||||
if (provider !== "codex") {
|
||||
return NextResponse.json(
|
||||
@@ -489,6 +493,7 @@ export async function POST(
|
||||
if (tokenData.email) {
|
||||
const existing = await getProviderConnections({ provider });
|
||||
const match = existing.find((c: any) => {
|
||||
if (c.id && safeEqual(connectionId, c.id)) return true;
|
||||
// safeEqual: constant-time comparison to prevent timing attacks (CWE-208, finding #258-6/7)
|
||||
if (!safeEqual(c.email, tokenData.email) || c.authType !== "oauth") return false;
|
||||
// For Codex, also check workspaceId to avoid overwriting different workspace connections
|
||||
|
||||
@@ -11,7 +11,7 @@ import Input from "./Input";
|
||||
* Cursor Auth Modal
|
||||
* Auto-detect and import token from Cursor IDE's local SQLite database
|
||||
*/
|
||||
export default function CursorAuthModal({ isOpen, onSuccess, onClose }) {
|
||||
export default function CursorAuthModal({ isOpen, onSuccess, onClose, reauthConnection: _ }) {
|
||||
const t = useTranslations("cursorAuthModal");
|
||||
const [accessToken, setAccessToken] = useState("");
|
||||
const [machineId, setMachineId] = useState("");
|
||||
|
||||
@@ -10,7 +10,13 @@ import KiroSocialOAuthModal from "./KiroSocialOAuthModal";
|
||||
* Kiro OAuth Wrapper
|
||||
* Orchestrates between method selection, device code flow, and social login flow
|
||||
*/
|
||||
export default function KiroOAuthWrapper({ isOpen, providerInfo, onSuccess, onClose }) {
|
||||
export default function KiroOAuthWrapper({
|
||||
isOpen,
|
||||
providerInfo,
|
||||
onSuccess,
|
||||
onClose,
|
||||
reauthConnection,
|
||||
}) {
|
||||
const [authMethod, setAuthMethod] = useState(null); // null | "builder-id" | "idc" | "social" | "import"
|
||||
const [socialProvider, setSocialProvider] = useState(null); // "google" | "github"
|
||||
const [idcConfig, setIdcConfig] = useState(null);
|
||||
@@ -78,6 +84,7 @@ export default function KiroOAuthWrapper({ isOpen, providerInfo, onSuccess, onCl
|
||||
provider={oauthProviderId}
|
||||
providerInfo={providerInfo}
|
||||
onSuccess={handleDeviceSuccess}
|
||||
reauthConnection={reauthConnection}
|
||||
onClose={handleBack}
|
||||
idcConfig={idcConfig}
|
||||
/>
|
||||
|
||||
@@ -17,6 +17,7 @@ type OAuthModalProps = {
|
||||
onSuccess?: () => void;
|
||||
onClose: () => void;
|
||||
idcConfig?: unknown;
|
||||
reauthConnection?: null | { id?: string };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -31,6 +32,7 @@ export default function OAuthModal({
|
||||
onSuccess,
|
||||
onClose,
|
||||
idcConfig,
|
||||
reauthConnection,
|
||||
}: OAuthModalProps) {
|
||||
const t = useTranslations("oauthModal");
|
||||
const [step, setStep] = useState("waiting"); // waiting | input | success | error
|
||||
@@ -93,6 +95,7 @@ export default function OAuthModal({
|
||||
body: JSON.stringify({
|
||||
code,
|
||||
redirectUri: authData.redirectUri,
|
||||
connectionId: reauthConnection?.id,
|
||||
codeVerifier: authData.codeVerifier,
|
||||
...(normalizedState ? { state: normalizedState } : {}),
|
||||
}),
|
||||
@@ -141,7 +144,7 @@ export default function OAuthModal({
|
||||
setStep("error");
|
||||
}
|
||||
},
|
||||
[authData, provider, onSuccess]
|
||||
[authData, provider, onSuccess, reauthConnection]
|
||||
);
|
||||
|
||||
// Poll for device code token
|
||||
@@ -157,7 +160,12 @@ export default function OAuthModal({
|
||||
const res = await fetch(`/api/oauth/${provider}/poll`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ deviceCode, codeVerifier, extraData }),
|
||||
body: JSON.stringify({
|
||||
deviceCode,
|
||||
connectionId: reauthConnection?.id,
|
||||
codeVerifier,
|
||||
extraData,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
@@ -188,7 +196,7 @@ export default function OAuthModal({
|
||||
setStep("error");
|
||||
setPolling(false);
|
||||
},
|
||||
[provider, onSuccess]
|
||||
[provider, onSuccess, reauthConnection]
|
||||
);
|
||||
|
||||
// Start OAuth flow
|
||||
@@ -271,7 +279,7 @@ export default function OAuthModal({
|
||||
const pollRes = await fetch(`/api/oauth/codex/poll-callback`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
body: JSON.stringify({ connectionId: reauthConnection?.id }),
|
||||
});
|
||||
const pollData = await pollRes.json();
|
||||
|
||||
@@ -371,7 +379,7 @@ export default function OAuthModal({
|
||||
setError(err.message);
|
||||
setStep("error");
|
||||
}
|
||||
}, [provider, isLocalhost, isTrueLocalhost, startPolling, onSuccess]);
|
||||
}, [provider, isLocalhost, isTrueLocalhost, startPolling, onSuccess, reauthConnection]);
|
||||
|
||||
// Reset guard when modal closes
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user