From f53146af3eda0ea7e9f7d9ce47156cdcc6f4e8b5 Mon Sep 17 00:00:00 2001 From: TheAchiever6823 <61914223+ShawnMadadha@users.noreply.github.com> Date: Mon, 18 May 2026 15:04:08 -0700 Subject: [PATCH] =?UTF-8?q?[shawn]=20fix:=20refuse=20Spotify=20token=20if?= =?UTF-8?q?=20scopes=20missing=20=E2=80=94=20show=20exact=20missing=20list?= =?UTF-8?q?=20inline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/apps/tools_lib/tools_lib.py | 41 ++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/backend/apps/tools_lib/tools_lib.py b/backend/apps/tools_lib/tools_lib.py index 7113aa32..3782e6fd 100644 --- a/backend/apps/tools_lib/tools_lib.py +++ b/backend/apps/tools_lib/tools_lib.py @@ -1752,15 +1752,36 @@ async def spotify_callback(code: str = "", state: str = "", error: str = "") -> if not refresh_token: return HTMLResponse("

Spotify did not return a refresh_token

Retry from OpenSwarm.

", status_code=400) - # Log what Spotify ACTUALLY granted vs what we requested. If a user - # complains about "scope not granted" later, this is the first place - # to check — the granted set should equal _SPOTIFY_SCOPES. + # Spotify returns the actual granted scopes in the token response. + # Compare against what we asked for and bail HARD if any are missing — + # otherwise the user thinks they're connected but every playlist / + # library mutation will 403 later with a cryptic error. granted_scopes = data.get("scope", "") requested_set = set(_SPOTIFY_SCOPES.split()) granted_set = set(granted_scopes.split()) missing = requested_set - granted_set if missing: - logger.warning(f"spotify: Spotify granted fewer scopes than requested. Missing: {missing}") + logger.warning(f"spotify: Spotify granted fewer scopes than requested. Missing: {sorted(missing)}") + missing_pretty = ", ".join(sorted(missing)) + return HTMLResponse( + "" + "
" + "
" + "

Spotify didn't grant all required scopes

" + f"

Spotify only granted {len(granted_set)} of {len(requested_set)} scopes. " + f"Missing:

" + f"
{html.escape(missing_pretty)}
" + "

To fix:

" + "
    " + "
  1. Open spotify.com/account/apps in your browser
  2. " + "
  3. Click REMOVE ACCESS next to the OpenSwarm app
  4. " + "
  5. Come back to OpenSwarm and click Connect Spotify again
  6. " + "
  7. This time the consent screen will show every permission — tick all of them and click Agree
  8. " + "
" + "

Your token was NOT saved. The Spotify tile in OpenSwarm is still disconnected.

" + "
", + status_code=400, + ) else: logger.info(f"spotify: all {len(granted_set)} scopes granted") @@ -1776,7 +1797,12 @@ async def spotify_callback(code: str = "", state: str = "", error: str = "") -> except Exception: # noqa: BLE001 pass - tool.credentials = {"SPOTIFY_REFRESH_TOKEN": refresh_token} + tool.credentials = { + "SPOTIFY_REFRESH_TOKEN": refresh_token, + # Stash the granted scopes so we can introspect later without + # round-tripping to Spotify. Used by the diagnostic display below. + "SPOTIFY_GRANTED_SCOPES": granted_scopes, + } tool.auth_type = "env_vars" tool.auth_status = "connected" tool.connected_account_email = display_name or "Spotify" @@ -1784,10 +1810,11 @@ async def spotify_callback(code: str = "", state: str = "", error: str = "") -> return HTMLResponse( "" - "
" + "
" "
" f"

Spotify connected{(' as ' + html.escape(display_name)) if display_name else ''}

" - "

You can close this tab and return to OpenSwarm.

" + f"

All {len(granted_set)} permissions granted, including playlist write access.

" + "

You can close this tab and return to OpenSwarm.

" "
" )