diff --git a/navi/api/routes/auth.py b/navi/api/routes/auth.py index 9d1d1f6..870e1fb 100644 --- a/navi/api/routes/auth.py +++ b/navi/api/routes/auth.py @@ -74,7 +74,7 @@ safe_return_to = _sanitize_return_to(return_to) auth_request = client.build_authorization_request( return_to=safe_return_to, - scopes=["openid", "email", "profile", "roles", "permissions", "offline_access"], + scopes=["openid", "email", "profile", "roles", "permissions"], ) # Detect Android WebView via User-Agent (more reliable than JS navigator.userAgent) @@ -100,11 +100,35 @@ @router.get("/callback") -async def auth_callback(code: str, state: str, request: Request) -> Response: +async def auth_callback( + request: Request, + code: str | None = None, + state: str | None = None, + error: str | None = None, + error_description: str | None = None, +) -> Response: """Handle OAuth callback from gnexus-auth.""" if not _auth_configured(): raise HTTPException(status_code=503, detail="OAuth is not configured. Set GNAUTH_CLIENT_ID and GNAUTH_CLIENT_SECRET in .env") + # OAuth error response from the authorization server (e.g. user denied consent, + # or the requested scope is not allowed for this client). + if error: + log.warning( + "auth.oauth_error", + error=error, + error_description=error_description, + state=state[:8] if state else None, + ) + raise HTTPException( + status_code=400, + detail=f"OAuth authorization failed: {error_description or error}", + ) + + if not code: + log.warning("auth.missing_code", query=str(request.query_params)) + raise HTTPException(status_code=400, detail="Missing authorization code") + redirect_uri = _get_redirect_uri() client = get_gauth_client(redirect_uri=redirect_uri) encryptor = get_encryptor() @@ -113,7 +137,7 @@ try: token_set = await asyncio.to_thread(client.exchange_authorization_code, code, state) except (StateValidationException, PkceException) as e: - log.warning("auth.invalid_state", state=state[:8], error=str(e)) + log.warning("auth.invalid_state", state=state[:8] if state else None, error=str(e)) raise HTTPException(status_code=400, detail="Invalid or expired state") from e except TokenExchangeException as e: log.warning("auth.token_exchange_failed", error=str(e)) @@ -203,7 +227,7 @@ log.warning("auth.create_session_failed", user_id=auth_user.user_id, exc_info=True) # Retrieve platform/return_to info before state is forgotten - state_info = _mobile_auth_states.pop(state, {}) + state_info = _mobile_auth_states.pop(state, {}) if state else {} is_mobile = state_info.get("platform") == "android" return_to = state_info.get("return_to", "/") diff --git a/navi/api/routes/sessions.py b/navi/api/routes/sessions.py index beb32d1..ae9ba80 100644 --- a/navi/api/routes/sessions.py +++ b/navi/api/routes/sessions.py @@ -270,13 +270,15 @@ async def get_session_content( session_id: str, store: Annotated[SessionStore, Depends(get_session_store)], - user: Annotated[User, Depends(require_user)], ) -> dict: - """Return published inline content records for this session.""" + """Return published inline content records for this session. + + Public — no auth required. The session ID acts as an unguessable + capability token for accessing shared/published files. + """ session = await store.get(session_id) if session is None: raise HTTPException(status_code=404, detail="Session not found") - check_session_access(session, user) return {"session_id": session.id, "content": await list_for_session(session_id)} @@ -357,14 +359,16 @@ session_id: str, filename: str, store: Annotated[SessionStore, Depends(get_session_store)], - user: Annotated[User, Depends(require_user)], download: bool = False, ) -> FileResponse: - """Download a file from the session's file directory.""" + """Download a file from the session's file directory. + + Public — no auth required. The session ID acts as an unguessable + capability token for accessing shared/published files. + """ session = await store.get(session_id) if session is None: raise HTTPException(status_code=404, detail="Session not found") - check_session_access(session, user, permission="navi.files.read_all") # Resolve and verify the file is within the session directory (no path traversal) base = session_dir(session_id).resolve() diff --git a/tests/unit/api/test_session_files.py b/tests/unit/api/test_session_files.py index eb83297..526859d 100644 --- a/tests/unit/api/test_session_files.py +++ b/tests/unit/api/test_session_files.py @@ -85,7 +85,7 @@ session_id = await _session_id(store) with pytest.raises(HTTPException) as exc: - await sessions_mod.download_file(session_id, "../secret.txt", store, _FAKE_USER) + await sessions_mod.download_file(session_id, "../secret.txt", store) assert exc.value.status_code == 403 @@ -95,7 +95,7 @@ dest = session_files_mod.ensure_session_dir(session_id) / "page.html" dest.write_text("