feat: allow redirect_uri param to spotify login, for Lockstep Demo app flow

This commit is contained in:
2026-05-14 01:37:23 +02:00
parent 378009f8b0
commit 71f55ab20d

59
api.py
View File

@@ -21,8 +21,9 @@ import sqlite3
from base64 import b64encode from base64 import b64encode
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import json import json
from urllib.parse import urlencode
from flask import Flask, request, session, jsonify, make_response from flask import Flask, request, session, jsonify, make_response, redirect, url_for
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
from authomatic import Authomatic from authomatic import Authomatic
from authomatic.adapters import WerkzeugAdapter from authomatic.adapters import WerkzeugAdapter
@@ -93,13 +94,22 @@ PROVIDER_ID_MAP = list(AUTHOMATIC_PROVIDER_ID_MAP) + [Spotify]
SPOTIFY_CLIENT_ID = os.environ["SPOTIFY_CLIENT_ID"] SPOTIFY_CLIENT_ID = os.environ["SPOTIFY_CLIENT_ID"]
SPOTIFY_CLIENT_SECRET = os.environ["SPOTIFY_CLIENT_SECRET"] SPOTIFY_CLIENT_SECRET = os.environ["SPOTIFY_CLIENT_SECRET"]
# Must exactly match a Redirect URI configured in your Spotify app settings. # OAuth redirect registered in the Spotify Developer Dashboard (Authorization Code
# flow). Spotify sends the browser here with ?code=&state= — not to the Android app scheme.
REDIRECT_URI = os.environ.get( REDIRECT_URI = os.environ.get(
"SPOTIFY_REDIRECT_URI", "SPOTIFY_REDIRECT_URI",
#"https://api.lockstep.at/spotify/callback" #"https://api.lockstep.at/spotify/callback"
"https://api.lockstep.at/login/spotify/" "https://api.lockstep.at/login/spotify/"
) )
# After server-side token exchange, the browser may be redirected to this URL with
# tokens in the query string (mobile / Custom Tab). This is NOT registered with Spotify;
# only REDIRECT_URI above goes in the dashboard for this flow.
ALLOWED_SPOTIFY_APP_POST_LOGIN_REDIRECT = os.environ.get(
"SPOTIFY_APP_POST_LOGIN_REDIRECT_URI",
"at.lockstep.player://spotify/callback",
)
DB_PATH = os.environ.get("TOKEN_DB_PATH", "spotify_tokens.db") DB_PATH = os.environ.get("TOKEN_DB_PATH", "spotify_tokens.db")
app = Flask(__name__) app = Flask(__name__)
@@ -391,6 +401,28 @@ def index():
@app.route("/login/<provider_name>/", methods=["GET", "POST"]) @app.route("/login/<provider_name>/", methods=["GET", "POST"])
def login(provider_name): def login(provider_name):
# Authomatic 1.3.0 (oauth2.login) only runs "phase 1" — redirect to Spotify —
# when there are no query parameters, or only ``user_state``:
# elif (not self.params or (len(self.params) == 1 and 'user_state' in self.params))
# A mobile client opening ``/login/spotify/?redirect_uri=...`` therefore matches
# no branch; login() returns without calling redirect() → empty body and HTTP 200
# (white page). Stash the app callback in the session and reload without query args.
if (
provider_name == "spotify"
and request.args.get("redirect_uri")
and "code" not in request.args
and "error" not in request.args
):
requested = request.args.get("redirect_uri", "")
if requested != ALLOWED_SPOTIFY_APP_POST_LOGIN_REDIRECT:
return jsonify({
"ok": False,
"error": "redirect_uri not allowed for this client",
}), 400
session["spotify_oauth_app_redirect_uri"] = requested
session.modified = True
return redirect(url_for("login", provider_name=provider_name), code=302)
response = make_response() response = make_response()
# Let Authomatic handle the OAuth2 handshake. # Let Authomatic handle the OAuth2 handshake.
@@ -398,7 +430,7 @@ def login(provider_name):
WerkzeugAdapter(request, response), WerkzeugAdapter(request, response),
provider_name, provider_name,
session=session, session=session,
session_saver=lambda: session.modified session_saver=lambda: setattr(session, "modified", True),
) )
# If result is None, Authomatic is still redirecting/processing. # If result is None, Authomatic is still redirecting/processing.
@@ -469,6 +501,27 @@ def login(provider_name):
# keep the Spotify user id in session # keep the Spotify user id in session
session["spotify_user_id"] = result.user.id session["spotify_user_id"] = result.user.id
app_redirect = session.pop("spotify_oauth_app_redirect_uri", None)
if app_redirect:
if app_redirect != ALLOWED_SPOTIFY_APP_POST_LOGIN_REDIRECT:
return jsonify({
"ok": False,
"error": "Stored app redirect_uri does not match allowlist",
}), 400
sep = "&" if ("?" in app_redirect) else "?"
target = (
f"{app_redirect}{sep}"
+ urlencode(
{
"access_token": access_token,
"refresh_token": refresh_token,
"expires_in": str(expires_in),
"token_type": token_type or "",
},
)
)
return redirect(target, code=302)
return jsonify({ return jsonify({
"ok": True, "ok": True,
"spotify_user_id": result.user.id, "spotify_user_id": result.user.id,