fix(auth): SB3 — keep reset/verify tokens out of URL query strings
Secret email tokens now ride the URL fragment (#reset=/#verify=), never the query (?reset=/?token=): a fragment isn't sent to the server, so the token can't leak into proxy/access logs or a Referer header. - Reset: link → /#reset=; the SPA reads the token from location.hash and POSTs it (unchanged /password-reset/confirm). - Verify: link → /#verify=; new POST /auth/verify (token in body). The legacy GET /auth/verify?token= is kept so pre-deploy emails in flight still work until they expire. The SPA reads the fragment token, POSTs it, shows ok/invalid. - Welcome: read secret tokens from the fragment, status flags from the query; strip both after capture so nothing lingers in history.
This commit is contained in:
parent
9375b46bc8
commit
a65915ea11
3 changed files with 45 additions and 9 deletions
|
|
@ -259,22 +259,38 @@ function Lightbox({ src, label, onClose }: { src: string; label: string; onClose
|
|||
|
||||
function AuthCard() {
|
||||
const { t } = useTranslation();
|
||||
// Snapshot the redirect params ONCE so we can clean them from the address bar below without
|
||||
// dismissing the banners/reset-mode they drive (those read this stable snapshot, not the URL).
|
||||
// Snapshot the pre-login params ONCE (query + fragment) so we can clean the address bar below
|
||||
// without dismissing the banners/reset-mode they drive. Secret tokens (reset, verify) ride the
|
||||
// URL FRAGMENT so they never reach the server / proxy logs / Referer (SB3); plain status flags
|
||||
// stay in the query string.
|
||||
const [params] = useState(() => new URLSearchParams(window.location.search));
|
||||
const resetToken = params.get("reset");
|
||||
const [hashParams] = useState(() => new URLSearchParams(window.location.hash.replace(/^#/, "")));
|
||||
const resetToken = hashParams.get("reset");
|
||||
const verifyToken = hashParams.get("verify"); // raw token → POST to confirm (new flow)
|
||||
const bounced = params.get("access"); // Google denied → "requested" | "denied"
|
||||
const verify = params.get("verify"); // "ok" | "invalid"
|
||||
const login = params.get("login"); // Google login blocked → "suspended"
|
||||
const deleted = params.get("deleted"); // self-service account deletion just completed
|
||||
// Verification outcome: from POSTing the fragment token (new flow), or the legacy
|
||||
// ?verify=ok|invalid redirect (pre-SB3 emails still in flight).
|
||||
const [verify, setVerify] = useState<string | null>(() => params.get("verify"));
|
||||
|
||||
// Strip the query string so the address bar stays clean (http://host/) after we've captured it.
|
||||
// Strip BOTH the query string and the fragment so the address bar stays clean and the token
|
||||
// doesn't linger in history.
|
||||
useEffect(() => {
|
||||
if (window.location.search) {
|
||||
if (window.location.search || window.location.hash) {
|
||||
window.history.replaceState(null, "", window.location.pathname);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Confirm a fragment verification token by POSTing it (new SB3 flow).
|
||||
useEffect(() => {
|
||||
if (!verifyToken) return;
|
||||
api
|
||||
.verifyEmail(verifyToken)
|
||||
.then(() => setVerify("ok"))
|
||||
.catch(() => setVerify("invalid"));
|
||||
}, [verifyToken]);
|
||||
|
||||
const [mode, setMode] = useState<Mode>(resetToken ? "reset" : "signin");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
|
|
|||
|
|
@ -1391,6 +1391,9 @@ export const api = {
|
|||
req("/auth/password-reset/request", { method: "POST", body: JSON.stringify({ email }) }, { quiet: true }),
|
||||
confirmPasswordReset: (token: string, password: string): Promise<{ ok: boolean }> =>
|
||||
req("/auth/password-reset/confirm", { method: "POST", body: JSON.stringify({ token, password }) }, { quiet: true }),
|
||||
// Confirm an email-verification token the SPA read from the URL fragment (SB3).
|
||||
verifyEmail: (token: string): Promise<{ ok: boolean }> =>
|
||||
req("/auth/verify", { method: "POST", body: JSON.stringify({ token }) }, { quiet: true }),
|
||||
// Set or change the signed-in account's password (errors shown inline via quiet).
|
||||
setPassword: (password: string, currentPassword?: string): Promise<{ ok: boolean }> =>
|
||||
req(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue