8-Bit Borges
8-Bit Borges

Reputation: 10033

Docker ( React / Flask / Nginx) - Spotify Authorization Code

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:

  1. Supplying a button on my Frontend page that links to your https://accounts.spotify.com/authorize/{...} URL. (this must not be an AJAX request call, or it will raise CORS issues)
  2. The user will proceed to give my application the permissions specified in the scope parameter, and will be directed back to the URL you've specified in the REDIRECT_URI parameter.
  3. This is where you get the authorization code, which you can use in the https://accounts.spotify.com/api/token/{...} endpoint

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

Answers (1)

Guerric P
Guerric P

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

Related Questions