Reputation: 10033
Based on this SO answer, I'm trying to implement Spotify Authorization Code, since I need the user to be permanently logged in.
Unlike Implicit Flow, in Authorization Code flow the app must provide client_secret and get a refresh token for unlimited access, and thus data exchange must happen server-to-server.
Nginx Proxy
My backend server runs with Flask
at http://localhost:5000, and my Frontend runs with React
at http://localhost:3000.
Both services are behind a nginx
reverse proxy, configured like so:
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;
}
According to the answer above, I'm doing the following:
CORS
issues)scope
parameter, and
will be directed back to the URL you've specified in the REDIRECT_URI parameter.React
Here I supply the authorization button to the user:
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
Here is where I want the app be redirected to, in order to save tokens to database, and ideally having another redirection back to my frontend afterwards.
# 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
When I click on the button with the first two redirects, however, I'm getting the error:
localhost refused to connect.
Why?
If I click the button having http://localhost/callback
as redirect_uri I get:
KeyError: 'access_token'
What am I missing?
QUESTION
I would like to have a Flask endpoint like the one above where I could fetch the access token (renewed if it is expired).
A solution that would dispense with Javascript code for auth and would be perfect. Is it possible with a containerized server?
Upvotes: 2
Views: 696
Reputation: 31815
The authorization code flow is not implemented like it should.
The beginning of this flow should be a request from the frontend (react) to the backend (flask). The backend is responsible to trigger the 302 Redirect
to the identity provider (Spotify) with the correct parameters.
@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)
The frontend should be totally unaware of the identity provider, and the backend should not forward the access_token
to the frontend, but rather generate its own tokens (ideally as a Cookie) when the user is authenticated against the identity provider.
You dont use client_secret
at all on client side, and it should not be known by the client. As the name suggests, it's supposed to be secret, and it's no longer secret as soon as you include it in JavaScript code. By keeping the client_secret
inside the backend, you totally hide it from the end-users (especially from the malicious ones).
That being said, the reason why you are observing this error is that the POST
request which is supposed to contain the access_token
in the response actually doesn't.
The reason is that ?response_type=token
is wrong, it should be ?response_type=code
in the initial request.
Source: https://developer.spotify.com/documentation/general/guides/authorization-guide/
Here is an example of a callback endpoint:
@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
Upvotes: 2