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
|
|
@ -587,8 +587,10 @@ def register(
|
|||
upsert_pending_invite(db, email) # admin-approval gate
|
||||
if email_ok:
|
||||
raw = _issue_token(db, user, "verify", VERIFY_TTL)
|
||||
# Fragment (#), not a query token: keeps the token out of proxy/access logs + Referer.
|
||||
# The SPA reads the fragment and POSTs it to /auth/verify (SB3).
|
||||
background.add_task(
|
||||
email_mod.send_verify_email, email, f"{_app_base()}/auth/verify?token={raw}"
|
||||
email_mod.send_verify_email, email, f"{_app_base()}/#verify={raw}"
|
||||
)
|
||||
if settings.admin_email_set:
|
||||
background.add_task(
|
||||
|
|
@ -598,9 +600,22 @@ def register(
|
|||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.post("/verify")
|
||||
def verify_email_confirm(payload: dict, db: Session = Depends(get_db)) -> dict:
|
||||
"""Confirm an email-verification token the SPA read from the URL fragment (SB3). Uniform 400
|
||||
on any invalid/expired/used token."""
|
||||
user = _consume_token(db, payload.get("token"), "verify")
|
||||
if user is None:
|
||||
raise HTTPException(status_code=400, detail="This verification link is invalid or has expired.")
|
||||
user.email_verified = True
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/verify")
|
||||
def verify_email(token: str, db: Session = Depends(get_db)):
|
||||
"""Confirm an email-verification link, then bounce to a friendly status on the app."""
|
||||
"""Legacy query-token verification link (pre-SB3 emails still in flight). New emails use the
|
||||
fragment + POST flow above; this bounces to a friendly status for older links until they expire."""
|
||||
user = _consume_token(db, token, "verify")
|
||||
if user is None:
|
||||
return RedirectResponse(url="/?verify=invalid")
|
||||
|
|
@ -662,7 +677,9 @@ def password_reset_request(
|
|||
user = db.execute(select(User).where(User.email == email)).scalar_one_or_none()
|
||||
if user is not None and user.password_hash and not user.is_demo:
|
||||
raw = _issue_token(db, user, "reset", RESET_TTL)
|
||||
background.add_task(email_mod.send_password_reset, email, f"{_app_base()}/?reset={raw}")
|
||||
# Token in the URL FRAGMENT (#), not the query (?): a fragment is never sent to the
|
||||
# server, so it can't leak into proxy/access logs or a Referer header (SB3).
|
||||
background.add_task(email_mod.send_password_reset, email, f"{_app_base()}/#reset={raw}")
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue