feat: allow redirect_uri param to spotify login, for Lockstep Demo app flow
This commit is contained in:
59
api.py
59
api.py
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user