Spaces:
Runtime error
Runtime error
File size: 6,587 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | "use client";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
/**
* OAuth Callback Page
*
* Reads URL params via window.location.search (not useSearchParams) to avoid
* the Next.js Suspense boundary requirement, which can delay hydration in popup
* windows that navigate back from a cross-origin OAuth page (e.g. Google).
* Sends the callback data back via three methods in order of reliability:
* 1. postMessage to window.opener (may be null after COOP cross-origin nav)
* 2. BroadcastChannel (same-origin, works across browsing context groups)
* 3. localStorage storage event (works across browsing context groups)
*/
export default function CallbackPage() {
const [status, setStatus] = useState<"processing" | "success" | "done" | "manual">("processing");
const [currentUrl] = useState(() =>
typeof window === "undefined" ? "" : window.location.href
);
const t = useTranslations("auth");
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const code = params.get("code");
const state = params.get("state");
const error = params.get("error");
const errorDescription = params.get("error_description");
const callbackData = {
code,
state,
error,
errorDescription,
fullUrl: window.location.href,
};
let sent = false;
let openerSameOrigin = false;
const queueStatusUpdate = (nextStatus: "processing" | "success" | "done" | "manual") => {
queueMicrotask(() => setStatus(nextStatus));
};
if (window.opener) {
try {
openerSameOrigin = window.opener.location.origin === window.location.origin;
} catch {
openerSameOrigin = false;
}
}
// Method 1: postMessage to opener (popup mode).
// May be null when Google OAuth's COOP header severs the opener reference.
//
// Only relay {code, state} to a known-trusted target origin. A wildcard "*"
// here would leak the OAuth code/state to a hostile opener — e.g. a page
// that opened this callback URL in a popup to phish the code. The browser
// delivers postMessage only when the opener's origin matches `targetOrigin`,
// so iterating over an allowlist lets the same-origin parent and Codex's
// fixed loopback helper receive it while silently dropping it for any other
// origin. Methods 2 (BroadcastChannel) and 3 (localStorage) cover the
// same-origin fallback when the opener was severed by COOP.
const trustedTargetOrigins = [
window.location.origin, // Same origin (dashboard popup mode).
"http://localhost:1455", // Codex helper (fixed loopback port).
"http://127.0.0.1:1455", // Same Codex helper, IPv4 literal form.
];
if (window.opener) {
for (const origin of trustedTargetOrigins) {
try {
window.opener.postMessage(
{ type: "oauth_callback", data: callbackData },
origin
);
sent = true;
} catch (e) {
console.log("postMessage failed:", e);
}
}
}
// Method 2: BroadcastChannel — works across browsing context groups for same origin.
try {
const channel = new BroadcastChannel("oauth_callback");
channel.postMessage(callbackData);
channel.close();
sent = true;
} catch (e) {
console.log("BroadcastChannel failed:", e);
}
// Method 3: localStorage — triggers storage event in all same-origin windows,
// regardless of browsing context group isolation from COOP.
try {
localStorage.setItem(
"oauth_callback",
JSON.stringify({ ...callbackData, timestamp: Date.now() })
);
sent = true;
} catch (e) {
console.log("localStorage failed:", e);
}
if (sent && (code || error)) {
if (window.opener && openerSameOrigin) {
queueStatusUpdate("success");
setTimeout(() => {
window.close();
// If close is prevented (browser policy), fall through to manual close prompt.
setTimeout(() => setStatus("done"), 500);
}, 1500);
} else {
// Opened as a tab, opener severed by COOP, or remote dashboard using a
// loopback/tunnel callback. Keep the full URL visible as a manual fallback
// in case the opener cannot receive the cross-origin postMessage.
queueStatusUpdate("manual");
}
} else {
// No code/error in URL or all send methods failed — show URL for manual copy.
// Batch the URL and status update so they render together (React 18 auto-batching).
queueStatusUpdate("manual");
}
}, []);
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center p-8 max-w-md">
{status === "processing" && (
<>
<div className="size-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center">
<span className="material-symbols-outlined text-3xl text-primary animate-spin">
progress_activity
</span>
</div>
<h1 className="text-xl font-semibold mb-2">{t("processing")}</h1>
<p className="text-text-muted">{t("pleaseWait")}</p>
</>
)}
{(status === "success" || status === "done") && (
<>
<div className="size-16 mx-auto mb-4 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
<span className="material-symbols-outlined text-3xl text-green-600">
check_circle
</span>
</div>
<h1 className="text-xl font-semibold mb-2">{t("authSuccess")}</h1>
<p className="text-text-muted">
{status === "success" ? t("windowWillClose") : t("closeTabNow")}
</p>
</>
)}
{status === "manual" && (
<>
<div className="size-16 mx-auto mb-4 rounded-full bg-yellow-100 dark:bg-yellow-900/30 flex items-center justify-center">
<span className="material-symbols-outlined text-3xl text-yellow-600">info</span>
</div>
<h1 className="text-xl font-semibold mb-2">{t("copyUrl")}</h1>
<p className="text-text-muted mb-4">{t("copyUrlManual")}</p>
<div className="bg-surface border border-border rounded-lg p-3 text-left">
<code className="text-xs break-all">{currentUrl}</code>
</div>
</>
)}
</div>
</div>
);
}
|