feat(05-10): oauth_initiate returns 200 JSON {url} instead of 302 redirect
- Remove response_class=RedirectResponse from @router.get decorator
- Replace both RedirectResponse(status_code=302) returns with JSONResponse({url})
- Frontend can now inject Bearer header before navigating to OAuth URL (T-05-10-01)
- Update test_connect_google_drive to expect 200 JSON (regression fix)
This commit is contained in:
+11
-5
@@ -311,22 +311,28 @@ async def _upsert_cloud_connection(
|
|||||||
# ── GET /api/cloud/oauth/initiate/{provider} ──────────────────────────────────
|
# ── GET /api/cloud/oauth/initiate/{provider} ──────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@router.get("/oauth/initiate/{provider}", response_class=RedirectResponse)
|
@router.get("/oauth/initiate/{provider}")
|
||||||
async def oauth_initiate(
|
async def oauth_initiate(
|
||||||
provider: str,
|
provider: str,
|
||||||
request: Request,
|
request: Request,
|
||||||
current_user: User = Depends(get_regular_user),
|
current_user: User = Depends(get_regular_user),
|
||||||
):
|
) -> dict:
|
||||||
"""Start the OAuth flow for Google Drive or OneDrive.
|
"""Start the OAuth flow for Google Drive or OneDrive.
|
||||||
|
|
||||||
Generates a CSRF state token, stores it in Redis with TTL 1800 (30 min),
|
Generates a CSRF state token, stores it in Redis with TTL 1800 (30 min),
|
||||||
and redirects the browser to the provider's authorization URL.
|
and returns the provider's authorization URL as JSON so the frontend can
|
||||||
|
navigate using fetch() with the Bearer header (plan 05-10 fix).
|
||||||
|
|
||||||
|
Returns: {"url": "<authorization_url>"}
|
||||||
|
|
||||||
Security:
|
Security:
|
||||||
- state token is secrets.token_urlsafe(32) — 256 bits of entropy (T-05-05-01)
|
- state token is secrets.token_urlsafe(32) — 256 bits of entropy (T-05-05-01)
|
||||||
- Redis key is single-use: deleted in the callback handler (T-05-05-02)
|
- Redis key is single-use: deleted in the callback handler (T-05-05-02)
|
||||||
- Only google_drive and onedrive are accepted (T-05-05-06)
|
- Only google_drive and onedrive are accepted (T-05-05-06)
|
||||||
|
- Endpoint requires get_regular_user — no unauthenticated access (T-05-10-01)
|
||||||
"""
|
"""
|
||||||
|
from fastapi.responses import JSONResponse # already available via fastapi
|
||||||
|
|
||||||
if provider not in VALID_OAUTH_PROVIDERS:
|
if provider not in VALID_OAUTH_PROVIDERS:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
@@ -359,7 +365,7 @@ async def oauth_initiate(
|
|||||||
prompt="consent",
|
prompt="consent",
|
||||||
state=state_token,
|
state=state_token,
|
||||||
)
|
)
|
||||||
return RedirectResponse(url=authorization_url, status_code=302)
|
return JSONResponse({"url": authorization_url})
|
||||||
|
|
||||||
elif provider == "onedrive":
|
elif provider == "onedrive":
|
||||||
import msal # lazy import
|
import msal # lazy import
|
||||||
@@ -375,7 +381,7 @@ async def oauth_initiate(
|
|||||||
redirect_uri=redirect_uri,
|
redirect_uri=redirect_uri,
|
||||||
state=state_token,
|
state=state_token,
|
||||||
)
|
)
|
||||||
return RedirectResponse(url=auth_url, status_code=302)
|
return JSONResponse({"url": auth_url})
|
||||||
|
|
||||||
|
|
||||||
# ── GET /api/cloud/oauth/callback/{provider} ──────────────────────────────────
|
# ── GET /api/cloud/oauth/callback/{provider} ──────────────────────────────────
|
||||||
|
|||||||
@@ -178,7 +178,11 @@ async def test_factory_returns_correct_backend():
|
|||||||
# ── CLOUD-01: OAuth connect / WebDAV connect ──────────────────────────────────
|
# ── CLOUD-01: OAuth connect / WebDAV connect ──────────────────────────────────
|
||||||
|
|
||||||
async def test_connect_google_drive(async_client, db_session, monkeypatch):
|
async def test_connect_google_drive(async_client, db_session, monkeypatch):
|
||||||
"""GET /api/cloud/oauth/initiate/google_drive redirects to Google's OAuth URL."""
|
"""GET /api/cloud/oauth/initiate/google_drive returns 200 JSON {url} pointing to Google OAuth.
|
||||||
|
|
||||||
|
Updated in plan 05-10: endpoint now returns JSON instead of 302 redirect
|
||||||
|
so the frontend can inject the Bearer Authorization header before navigating.
|
||||||
|
"""
|
||||||
from main import app
|
from main import app
|
||||||
|
|
||||||
auth = await _create_user_and_token(db_session, role="user")
|
auth = await _create_user_and_token(db_session, role="user")
|
||||||
@@ -187,15 +191,23 @@ async def test_connect_google_drive(async_client, db_session, monkeypatch):
|
|||||||
fake_redis = FakeRedis()
|
fake_redis = FakeRedis()
|
||||||
app.state.redis = fake_redis
|
app.state.redis = fake_redis
|
||||||
|
|
||||||
|
mock_flow = MagicMock()
|
||||||
|
mock_flow.authorization_url.return_value = (
|
||||||
|
"https://accounts.google.com/o/oauth2/auth?scope=drive&state=test",
|
||||||
|
"test",
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("google_auth_oauthlib.flow.Flow.from_client_config", return_value=mock_flow):
|
||||||
resp = await async_client.get(
|
resp = await async_client.get(
|
||||||
"/api/cloud/oauth/initiate/google_drive",
|
"/api/cloud/oauth/initiate/google_drive",
|
||||||
headers=auth["headers"],
|
headers=auth["headers"],
|
||||||
follow_redirects=False,
|
follow_redirects=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert resp.status_code == 302
|
assert resp.status_code == 200
|
||||||
location = resp.headers.get("location", "")
|
data = resp.json()
|
||||||
assert "accounts.google.com" in location
|
assert "url" in data
|
||||||
|
assert "accounts.google.com" in data["url"]
|
||||||
|
|
||||||
# Clean up
|
# Clean up
|
||||||
app.state.redis = None
|
app.state.redis = None
|
||||||
|
|||||||
Reference in New Issue
Block a user