Docker (React / Flask / Nginx) - Spotify 授权码
Docker ( React / Flask / Nginx) - Spotify Authorization Code
基于 SO answer, I'm trying to implement Spotify Authorization Code,因为我需要用户永久登录。
与隐式流程不同,在授权代码流程中,应用程序必须提供 client_secret 并获取刷新令牌以进行无限制访问,因此数据交换必须发生 服务器到服务器 .
Nginx Proxy
我的后端服务器在 http://localhost:5000, and my Frontend runs with React
at http://localhost:3000 上运行 Flask
。
两种服务都在 nginx
反向代理后面,配置如下:
location / {
proxy_pass http://client:3000;
proxy_redirect default;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
location /callback {
proxy_pass http://web:5000;
proxy_redirect default;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
根据上面的回答,我正在做以下事情:
- 在我的前端页面上提供一个链接到您的页面的按钮
https://accounts.spotify.com/authorize/{...} URL。
(这不能是 AJAX 请求调用,否则会引发
CORS
个问题)
- 用户将继续向我的应用程序授予
scope
参数中指定的权限,并且
将被定向回您在 REDIRECT_URI 参数中指定的 URL。
- 这是您获取授权码的地方,您可以在
https://accounts.spotify.com/api/token/{...} 端点
React
这里我向用户提供授权按钮:
render() {
var state = generateRandomString(16);
const Credentials = {
stateKey: 'spotify_auth_state',
client_id: 'my_id',
redirect_uri: 'http://localhost:5000/callback',
scope: 'playlist-modify-public playlist-modify-private'
}
let url = 'https://accounts.spotify.com/authorize';
url += '?response_type=token';
url += '&client_id=' + encodeURIComponent(Credentials.client_id);
url += '&scope=' + encodeURIComponent(Credentials.scope);
url += '&redirect_uri=' + encodeURIComponent(Credentials.redirect_uri);
url += '&state=' + encodeURIComponent(state);
return (
<div className="button_container">
<h1 className="title is-3"><font color="#C86428">{"Welcome"}</font></h1>
<div className="Line" /><br/>
<a href={url} > Login to Spotify </a>
</div>
)
}
Flask
这里是我希望将应用程序重定向到的位置,以便将令牌保存到数据库,并且理想情况下之后再重定向回我的前端。
# spotify auth
@spotify_auth_bp.route("/spotify_auth", methods=['GET', 'POST'])
def spotify_auth():
#Auth Step 1: Authorization
# Client Keys
CLIENT_ID = os.environ.get('SPOTIPY_CLIENT_ID')
CLIENT_SECRET = os.environ.get('SPOTIPY_CLIENT_SECRET')
# Spotify URLS
SPOTIFY_AUTH_URL = "https://accounts.spotify.com/authorize"
#SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"
SPOTIFY_API_BASE_URL = "https://api.spotify.com"
API_VERSION = "v1"
SPOTIFY_API_URL = "{}/{}".format(SPOTIFY_API_BASE_URL, API_VERSION)
# Server-side Parameters
CLIENT_SIDE_URL = os.environ.get('REACT_APP_WEB_SERVICE_URL')
REDIRECT_URI = os.environ.get('REACT_APP_WEB_SERVICE_URL')
#PORT = 5000
#REDIRECT_URI = "{}:{}/callback".format(CLIENT_SIDE_URL, PORT)
SCOPE = os.environ.get('SPOTIPY_SCOPE')
STATE = ""
SHOW_DIALOG_bool = True
SHOW_DIALOG_str = str(SHOW_DIALOG_bool).lower()
auth_query_parameters = {
"response_type": "code",
"redirect_uri": 'http://localhost/callback',
"scope": 'user-read-currently-playing user-read-private user-library-read user-read-email user-read-playback-state user-follow-read playlist-read-private playlist-modify-public playlist-modify-private',
# "state": STATE,
# "show_dialog": SHOW_DIALOG_str,
"client_id": CLIENT_ID
}
url_args = "&".join(["{}={}".format(key, quote(val)) for key, val in auth_query_parameters.items()])
auth_url = "{}/?{}".format(SPOTIFY_AUTH_URL, url_args)
return redirect(auth_url)
@spotify_auth_bp.route("/callback", methods=['GET', 'POST'])
def callback():
# Auth Step 4: Requests refresh and access tokens
CLIENT_ID = 'my_id'
CLIENT_SECRET = 'my_secret'
CLIENT_SIDE_URL = 'http://localhost'
PORT = 5000
REDIRECT_URI = "{}:{}/callback".format(CLIENT_SIDE_URL, PORT)
SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"
auth_token = request.args['code']
code_payload = {
"grant_type": "authorization_code",
"code": auth_token,
"redirect_uri": 'http://localhost/callback',
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
}
auth_str = '{}:{}'.format(CLIENT_ID, CLIENT_SECRET)
b64_auth_str = base64.urlsafe_b64encode(auth_str.encode()).decode()
headers = {
"Content-Type" : 'application/x-www-form-urlencoded',
"Authorization" : "Basic {}".format(b64_auth_str)}
post_request = requests.post(SPOTIFY_TOKEN_URL, data=code_payload)
# Auth Step 5: Tokens are Returned to Application
response_data = json.loads(post_request.text)
print ('RESPONSE DATA', response_data)
access_token = response_data["access_token"]
refresh_token = response_data["refresh_token"]
token_type = response_data["token_type"]
expires_in = response_data["expires_in"]
template = render_template("index.html")
response_object = {
'status': 'success',
'message': 'success',
'data': [{'access_token': access_token,
'refresh_token': refresh_token,
'token_type': token_type,
'content': template}]
}
return jsonify(response_object), 200
Redirects whitelisted with Spotify
http://localhost:5000
http://localhost:5000/callback
http://web:5000
http://web:5000/callback
http://localhost/callback
但是,当我单击具有前两个重定向的按钮时,出现错误:
localhost refused to connect.
为什么?
如果我点击 http://localhost/callback
作为 redirect_uri 的按钮,我得到:
KeyError: 'access_token'
我错过了什么?
QUESTION
我想要一个像上面那样的 Flask 端点,我可以在其中获取访问令牌(如果过期则更新)。
一个无需 Javascript 验证代码的解决方案,将是完美的。容器化服务器是否可行?
授权代码流没有按应有的方式实现。
此流程的开始应该是从前端(反应)到后端(烧瓶)的请求。后端负责使用正确的参数向身份提供商 (Spotify) 触发 302 Redirect
。
@spotify_auth_bp.route("/auth", methods=['GET'])
def auth():
CODE = "code"
CLIENT_ID = os.environ.get('SPOTIPY_CLIENT_ID')
SCOPE = "playlist-modify-public playlist-modify-private"
SPOTIFY_AUTH_URL = "https://accounts.spotify.com/authorize"
REDIRECT_URI = "http://localhost/callback"
return redirect("{}?response_type={}&client_id={}&scope={}&redirect_uri={}".format(SPOTIFY_AUTH_URL, CODE, CLIENT_ID, SCOPE, REDIRECT_URI), code=302)
前端应该完全不知道身份提供者,后端不应该将 access_token
转发给前端,而是在用户通过身份验证时生成自己的令牌(最好是 Cookie)身份提供者。
你根本没有在客户端使用client_secret
,客户端不应该知道它。顾名思义,它应该是秘密的,一旦将它包含在 JavaScript 代码中,它就不再是秘密了。通过将 client_secret
保留在后端,您可以完全隐藏最终用户(尤其是恶意用户)。
也就是说,您观察到此错误的原因是应该在响应中包含 access_token
的 POST
请求实际上没有。
原因是?response_type=token
不对,最初请求时应该是?response_type=code
来源:https://developer.spotify.com/documentation/general/guides/authorization-guide/
这是回调端点的示例:
@spotify_auth_bp.route("/callback", methods=['GET', 'POST'])
def callback():
# Auth Step 4: Requests refresh and access tokens
SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"
CLIENT_ID = os.environ.get('SPOTIPY_CLIENT_ID')
CLIENT_SECRET = os.environ.get('SPOTIPY_CLIENT_SECRET')
REDIRECT_URI = os.environ.get('SPOTIPY_REDIRECT_URI')
auth_token = request.args['code']
code_payload = {
"grant_type": "authorization_code",
"code": auth_token,
"redirect_uri": 'http://localhost/callback',
}
post_request = requests.post(SPOTIFY_TOKEN_URL, data=code_payload)
# Auth Step 5: Tokens are Returned to Application
response_data = json.loads(post_request.text)
access_token = response_data["access_token"]
refresh_token = response_data["refresh_token"]
token_type = response_data["token_type"]
expires_in = response_data["expires_in"]
# At this point, there is to generate a custom token for the frontend
# Either a self-contained signed JWT or a random token
# In case the token is not a JWT, it should be stored in the session (in case of a stateful API)
# or in the database (in case of a stateless API)
# In case of a JWT, the authenticity can be tested by the backend with the signature so it doesn't need to be stored at all
# Let's assume the resulting token is stored in a variable named "token"
res = Response('http://localhost/about', status=302)
res.set_cookie('auth_cookie', token)
return res
基于
与隐式流程不同,在授权代码流程中,应用程序必须提供 client_secret 并获取刷新令牌以进行无限制访问,因此数据交换必须发生 服务器到服务器 .
Nginx Proxy
我的后端服务器在 http://localhost:5000, and my Frontend runs with React
at http://localhost:3000 上运行 Flask
。
两种服务都在 nginx
反向代理后面,配置如下:
location / {
proxy_pass http://client:3000;
proxy_redirect default;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
location /callback {
proxy_pass http://web:5000;
proxy_redirect default;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
根据上面的回答,我正在做以下事情:
- 在我的前端页面上提供一个链接到您的页面的按钮
https://accounts.spotify.com/authorize/{...} URL。
(这不能是 AJAX 请求调用,否则会引发
CORS
个问题) - 用户将继续向我的应用程序授予
scope
参数中指定的权限,并且 将被定向回您在 REDIRECT_URI 参数中指定的 URL。 - 这是您获取授权码的地方,您可以在 https://accounts.spotify.com/api/token/{...} 端点
React
这里我向用户提供授权按钮:
render() {
var state = generateRandomString(16);
const Credentials = {
stateKey: 'spotify_auth_state',
client_id: 'my_id',
redirect_uri: 'http://localhost:5000/callback',
scope: 'playlist-modify-public playlist-modify-private'
}
let url = 'https://accounts.spotify.com/authorize';
url += '?response_type=token';
url += '&client_id=' + encodeURIComponent(Credentials.client_id);
url += '&scope=' + encodeURIComponent(Credentials.scope);
url += '&redirect_uri=' + encodeURIComponent(Credentials.redirect_uri);
url += '&state=' + encodeURIComponent(state);
return (
<div className="button_container">
<h1 className="title is-3"><font color="#C86428">{"Welcome"}</font></h1>
<div className="Line" /><br/>
<a href={url} > Login to Spotify </a>
</div>
)
}
Flask
这里是我希望将应用程序重定向到的位置,以便将令牌保存到数据库,并且理想情况下之后再重定向回我的前端。
# spotify auth
@spotify_auth_bp.route("/spotify_auth", methods=['GET', 'POST'])
def spotify_auth():
#Auth Step 1: Authorization
# Client Keys
CLIENT_ID = os.environ.get('SPOTIPY_CLIENT_ID')
CLIENT_SECRET = os.environ.get('SPOTIPY_CLIENT_SECRET')
# Spotify URLS
SPOTIFY_AUTH_URL = "https://accounts.spotify.com/authorize"
#SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"
SPOTIFY_API_BASE_URL = "https://api.spotify.com"
API_VERSION = "v1"
SPOTIFY_API_URL = "{}/{}".format(SPOTIFY_API_BASE_URL, API_VERSION)
# Server-side Parameters
CLIENT_SIDE_URL = os.environ.get('REACT_APP_WEB_SERVICE_URL')
REDIRECT_URI = os.environ.get('REACT_APP_WEB_SERVICE_URL')
#PORT = 5000
#REDIRECT_URI = "{}:{}/callback".format(CLIENT_SIDE_URL, PORT)
SCOPE = os.environ.get('SPOTIPY_SCOPE')
STATE = ""
SHOW_DIALOG_bool = True
SHOW_DIALOG_str = str(SHOW_DIALOG_bool).lower()
auth_query_parameters = {
"response_type": "code",
"redirect_uri": 'http://localhost/callback',
"scope": 'user-read-currently-playing user-read-private user-library-read user-read-email user-read-playback-state user-follow-read playlist-read-private playlist-modify-public playlist-modify-private',
# "state": STATE,
# "show_dialog": SHOW_DIALOG_str,
"client_id": CLIENT_ID
}
url_args = "&".join(["{}={}".format(key, quote(val)) for key, val in auth_query_parameters.items()])
auth_url = "{}/?{}".format(SPOTIFY_AUTH_URL, url_args)
return redirect(auth_url)
@spotify_auth_bp.route("/callback", methods=['GET', 'POST'])
def callback():
# Auth Step 4: Requests refresh and access tokens
CLIENT_ID = 'my_id'
CLIENT_SECRET = 'my_secret'
CLIENT_SIDE_URL = 'http://localhost'
PORT = 5000
REDIRECT_URI = "{}:{}/callback".format(CLIENT_SIDE_URL, PORT)
SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"
auth_token = request.args['code']
code_payload = {
"grant_type": "authorization_code",
"code": auth_token,
"redirect_uri": 'http://localhost/callback',
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
}
auth_str = '{}:{}'.format(CLIENT_ID, CLIENT_SECRET)
b64_auth_str = base64.urlsafe_b64encode(auth_str.encode()).decode()
headers = {
"Content-Type" : 'application/x-www-form-urlencoded',
"Authorization" : "Basic {}".format(b64_auth_str)}
post_request = requests.post(SPOTIFY_TOKEN_URL, data=code_payload)
# Auth Step 5: Tokens are Returned to Application
response_data = json.loads(post_request.text)
print ('RESPONSE DATA', response_data)
access_token = response_data["access_token"]
refresh_token = response_data["refresh_token"]
token_type = response_data["token_type"]
expires_in = response_data["expires_in"]
template = render_template("index.html")
response_object = {
'status': 'success',
'message': 'success',
'data': [{'access_token': access_token,
'refresh_token': refresh_token,
'token_type': token_type,
'content': template}]
}
return jsonify(response_object), 200
Redirects whitelisted with Spotify
http://localhost:5000
http://localhost:5000/callback
http://web:5000
http://web:5000/callback
http://localhost/callback
但是,当我单击具有前两个重定向的按钮时,出现错误:
localhost refused to connect.
为什么?
如果我点击 http://localhost/callback
作为 redirect_uri 的按钮,我得到:
KeyError: 'access_token'
我错过了什么?
QUESTION
我想要一个像上面那样的 Flask 端点,我可以在其中获取访问令牌(如果过期则更新)。
一个无需 Javascript 验证代码的解决方案,将是完美的。容器化服务器是否可行?
授权代码流没有按应有的方式实现。
此流程的开始应该是从前端(反应)到后端(烧瓶)的请求。后端负责使用正确的参数向身份提供商 (Spotify) 触发 302 Redirect
。
@spotify_auth_bp.route("/auth", methods=['GET'])
def auth():
CODE = "code"
CLIENT_ID = os.environ.get('SPOTIPY_CLIENT_ID')
SCOPE = "playlist-modify-public playlist-modify-private"
SPOTIFY_AUTH_URL = "https://accounts.spotify.com/authorize"
REDIRECT_URI = "http://localhost/callback"
return redirect("{}?response_type={}&client_id={}&scope={}&redirect_uri={}".format(SPOTIFY_AUTH_URL, CODE, CLIENT_ID, SCOPE, REDIRECT_URI), code=302)
前端应该完全不知道身份提供者,后端不应该将 access_token
转发给前端,而是在用户通过身份验证时生成自己的令牌(最好是 Cookie)身份提供者。
你根本没有在客户端使用client_secret
,客户端不应该知道它。顾名思义,它应该是秘密的,一旦将它包含在 JavaScript 代码中,它就不再是秘密了。通过将 client_secret
保留在后端,您可以完全隐藏最终用户(尤其是恶意用户)。
也就是说,您观察到此错误的原因是应该在响应中包含 access_token
的 POST
请求实际上没有。
原因是?response_type=token
不对,最初请求时应该是?response_type=code
来源:https://developer.spotify.com/documentation/general/guides/authorization-guide/
这是回调端点的示例:
@spotify_auth_bp.route("/callback", methods=['GET', 'POST'])
def callback():
# Auth Step 4: Requests refresh and access tokens
SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"
CLIENT_ID = os.environ.get('SPOTIPY_CLIENT_ID')
CLIENT_SECRET = os.environ.get('SPOTIPY_CLIENT_SECRET')
REDIRECT_URI = os.environ.get('SPOTIPY_REDIRECT_URI')
auth_token = request.args['code']
code_payload = {
"grant_type": "authorization_code",
"code": auth_token,
"redirect_uri": 'http://localhost/callback',
}
post_request = requests.post(SPOTIFY_TOKEN_URL, data=code_payload)
# Auth Step 5: Tokens are Returned to Application
response_data = json.loads(post_request.text)
access_token = response_data["access_token"]
refresh_token = response_data["refresh_token"]
token_type = response_data["token_type"]
expires_in = response_data["expires_in"]
# At this point, there is to generate a custom token for the frontend
# Either a self-contained signed JWT or a random token
# In case the token is not a JWT, it should be stored in the session (in case of a stateful API)
# or in the database (in case of a stateless API)
# In case of a JWT, the authenticity can be tested by the backend with the signature so it doesn't need to be stored at all
# Let's assume the resulting token is stored in a variable named "token"
res = Response('http://localhost/about', status=302)
res.set_cookie('auth_cookie', token)
return res