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:
curo1305
2026-05-30 11:24:33 +02:00
parent 9b6d3f91d4
commit e2e499b8b1
2 changed files with 31 additions and 13 deletions
+11 -5
View File
@@ -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} ──────────────────────────────────
+16 -4
View File
@@ -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