-
Notifications
You must be signed in to change notification settings - Fork 152
fix: prevent 500 error when logging in with + in email (Fixes #548) #666
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b16400b
50cbeb8
5eafdef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| from __future__ import annotations | ||
|
|
||
| import os | ||
| import urllib.parse | ||
| from typing import Any | ||
|
|
||
| from fastapi import APIRouter, Depends, HTTPException, Request, Response, status | ||
|
|
@@ -112,10 +113,19 @@ class SignupBody(BaseModel): | |
|
|
||
| @router.post("/login") | ||
| async def auth_login(body: LoginBody, response: Response): | ||
| # Decode any URL-encoded characters (e.g. %2B → +) in the email. | ||
| # This prevents the `+` character in email sub-addressing (user+tag@...) | ||
| # from being corrupted by intermediate form/URL encoding layers. | ||
| raw_email = str(body.email) | ||
| try: | ||
| safe_email = urllib.parse.unquote(raw_email) | ||
| except Exception: | ||
| safe_email = raw_email | ||
|
|
||
| try: | ||
| client = _anon_supabase() | ||
| result = client.auth.sign_in_with_password( | ||
| {"email": str(body.email), "password": body.password} | ||
| {"email": safe_email, "password": body.password} | ||
| ) | ||
| except Exception as exc: | ||
| raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc | ||
|
|
@@ -132,6 +142,13 @@ async def auth_login(body: LoginBody, response: Response): | |
|
|
||
| @router.post("/signup") | ||
| async def auth_signup(body: SignupBody, response: Response): | ||
| # Decode URL-encoded characters in email (defense in depth) | ||
| raw_email = str(body.email) | ||
| try: | ||
| safe_email = urllib.parse.unquote(raw_email) | ||
| except Exception: | ||
| safe_email = raw_email | ||
|
|
||
| metadata: dict[str, str] = {} | ||
| if body.full_name: | ||
| metadata["full_name"] = body.full_name | ||
|
|
@@ -144,25 +161,21 @@ async def auth_signup(body: SignupBody, response: Response): | |
| client = _anon_supabase() | ||
| result = client.auth.sign_up( | ||
| { | ||
| "email": str(body.email), | ||
| "email": safe_email, | ||
| "password": body.password, | ||
| "options": {"data": metadata} if metadata else {}, | ||
| } | ||
| ) | ||
| except Exception as exc: | ||
| raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc | ||
|
|
||
| session = getattr(result, "session", None) | ||
| user = getattr(result, "user", None) | ||
| if session: | ||
| _set_session_cookies(response, session) | ||
| user_payload = user.model_dump() if user and hasattr(user, "model_dump") else None | ||
| return {"user": user_payload, "message": "Signup complete"} | ||
| response.status_code = status.HTTP_201_CREATED | ||
| return {"message": "Signup initiated, check your email for verification."} | ||
|
|
||
|
|
||
| @router.post("/logout") | ||
| async def auth_logout(request: Request, response: Response): | ||
| # Invalidate the session server-side before clearing cookies | ||
| # Revoke Supabase session server-side before clearing cookies | ||
| token = extract_token(request) | ||
| if token: | ||
| try: | ||
|
|
@@ -171,7 +184,7 @@ async def auth_logout(request: Request, response: Response): | |
| except Exception: | ||
| pass # Still clear cookies even if server-side invalidation fails | ||
| _clear_session_cookies(response) | ||
|
Comment on lines
176
to
186
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| return {"ok": True} | ||
| return {"message": "Logged out"} | ||
|
|
||
|
|
||
| @router.get("/me") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For addresses containing
+,normalizeEmailturnsuser+tag@example.comintouser%2Btag@example.com, and this value is passed directly to the Supabase JS client. These calls send JSON, so Supabase treats the percent sequence as part of the email rather than decoding it; after the backend mirror succeeds,signInWithPasswordstill looks up the wrong address and the login fails. The samesafeEmailis also used for OTP and signup, so only the backend transport should receive any encoded value while Supabase auth should receive the original email string.Useful? React with 👍 / 👎.