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 datetime import datetime, timedelta, timezone
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 authomatic import Authomatic
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_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(
"SPOTIFY_REDIRECT_URI",
#"https://api.lockstep.at/spotify/callback"
"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")
app = Flask(__name__)
@@ -391,6 +401,28 @@ def index():
@app.route("/login/<provider_name>/", methods=["GET", "POST"])
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()
# Let Authomatic handle the OAuth2 handshake.
@@ -398,7 +430,7 @@ def login(provider_name):
WerkzeugAdapter(request, response),
provider_name,
session=session,
session_saver=lambda: session.modified
session_saver=lambda: setattr(session, "modified", True),
)
# If result is None, Authomatic is still redirecting/processing.
@@ -469,6 +501,27 @@ def login(provider_name):
# keep the Spotify user id in session
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({
"ok": True,
"spotify_user_id": result.user.id,