feat(dev): OAuth redirect returns to the origin that started it
A Google login started on the Vite dev server (:5173) always came back to the fixed :8080 callback, because login/link/upgrade passed the static settings.oauth_redirect_url. Derive the redirect_uri from the request origin instead, but only in local dev and only for a known dev-host allowlist — production still uses the fixed configured URL and never trusts the Host header (host-injection guard). Set the Vite proxy to changeOrigin:false so it forwards the real Host:localhost:5173 (it was rewriting it to the 127.0.0.1:8080 target). Both callbacks must be registered in the Google console.
This commit is contained in:
+25
-3
@@ -246,6 +246,28 @@ def upsert_pending_invite(db: Session, email: str) -> Invite | None:
|
||||
return inv
|
||||
|
||||
|
||||
# Local-dev origins allowed to drive the OAuth redirect_uri from the request Host. The Vite dev
|
||||
# server (:5173) proxies /auth to the backend (:8080) preserving the Host header, and the session
|
||||
# cookie is port-agnostic — so a flow started on :5173 can come back to :5173 instead of always
|
||||
# landing on the fixed :8080 callback. Both must also be registered in the Google console.
|
||||
_DEV_OAUTH_HOSTS = frozenset(
|
||||
{"localhost:5173", "localhost:8080", "127.0.0.1:5173", "127.0.0.1:8080"}
|
||||
)
|
||||
|
||||
|
||||
def _oauth_redirect_uri(request: Request) -> str:
|
||||
"""The redirect_uri handed to Google. Production always uses the fixed configured URL and never
|
||||
trusts the request Host (a forged Host header must not be able to redirect the OAuth code). In
|
||||
local dev we honor the request's own origin when it is one of the known dev hosts, so a login
|
||||
started on the Vite server (:5173) returns to :5173 and one on :8080 returns to :8080."""
|
||||
if settings.session_https_only: # https redirect URL == real deployment
|
||||
return settings.oauth_redirect_url
|
||||
host = (request.headers.get("host") or "").lower()
|
||||
if host in _DEV_OAUTH_HOSTS:
|
||||
return f"http://{host}/auth/callback"
|
||||
return settings.oauth_redirect_url
|
||||
|
||||
|
||||
@router.get("/login")
|
||||
async def login(request: Request, db: Session = Depends(get_db)):
|
||||
# access_type=offline ensures a refresh_token on first authorization. We avoid
|
||||
@@ -258,7 +280,7 @@ async def login(request: Request, db: Session = Depends(get_db)):
|
||||
return RedirectResponse(url="/")
|
||||
return await client.authorize_redirect(
|
||||
request,
|
||||
settings.oauth_redirect_url,
|
||||
_oauth_redirect_uri(request),
|
||||
access_type="offline",
|
||||
prompt="select_account",
|
||||
include_granted_scopes="true",
|
||||
@@ -1003,7 +1025,7 @@ async def link_google(
|
||||
request.session["oauth_link_explicit"] = True
|
||||
return await client.authorize_redirect(
|
||||
request,
|
||||
settings.oauth_redirect_url,
|
||||
_oauth_redirect_uri(request),
|
||||
access_type="offline",
|
||||
prompt="select_account",
|
||||
include_granted_scopes="true",
|
||||
@@ -1068,7 +1090,7 @@ async def upgrade(
|
||||
scope = WRITE_SCOPES if access == "write" else READ_SCOPES
|
||||
return await client.authorize_redirect(
|
||||
request,
|
||||
settings.oauth_redirect_url,
|
||||
_oauth_redirect_uri(request),
|
||||
access_type="offline",
|
||||
prompt="consent",
|
||||
include_granted_scopes="true",
|
||||
|
||||
Reference in New Issue
Block a user