Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Authorize with Spotify using Nginx

I have a docker app running three services:

  • client --> react frontend
  • web --- > flask backend
  • nginx ->- a reverse proxy for both

This is the (simplified) project structure:

docker-compose-dev.yml
services/
        client/
              src/
                 app.jsx
                 components/ 
                           spotify-auth.js
                           Spotify.jsx
         nginx/
              dev.conf
         web/

Here is where I defined exposed ports at built time:

docker-compose-dev.yml

  web:
    build:
      context: ./services/web
      dockerfile: Dockerfile-dev
    volumes:
      - './services/web:/usr/src/app'
    ports:
      - 5001:5000 <----------------
    environment:
      - FLASK_ENV=development
      - APP_SETTINGS=project.config.DevelopmentConfig
    depends_on:  
      - web-db

  nginx:
    build:
      context: ./services/nginx
      dockerfile: Dockerfile-dev
    restart: always
    ports:
      - 80:80     <----------------
      - 8888:8888 <----------------
    depends_on:
      - web
      - client

  client:
    build:
      context: ./services/client
      dockerfile: Dockerfile-dev
    volumes:
      - './services/client:/usr/src/app'
      - '/usr/src/app/node_modules'
    ports:
      - 3007:3000   <----------------
    environment:
      - NODE_ENV=development
      - REACT_APP_WEB_SERVICE_URL=${REACT_APP_WEB_SERVICE_URL}
    depends_on:
      - web

REDIRECT

Client service, on its turn, needs to authenticate with Spotify, which requires a Redirect URI, whitelisted at https://developer.spotify.com. For mine, I have several options:

enter image description here

This is my nginx file, where I try to organize the right ports:

dev.conf

server {

  listen 80;
  listen 8888;

  location / {        // frontend at localhost:3000
    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;
  }

  location /users {   // backend at localhost:5000
    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;
  }

  location /auth {    # this authentication is for the app, not spotify
    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;
  }
}

Finally, there's my jsand jsx files used to:

  1. Authenticate with Spotify -----> Implicit Grant
  2. Redirect my app back to localhost, or '/'

spotify-auth.js

export const stateKey = 'spotify_auth_state';
export const client_id = 'my_client_id'; // Your client id
export const redirect_uri = 'http://localhost:3000'; // my redirect uri
//export const redirect_uri = 'http://localhost:8888'; // my second try for uri
export const scope ='user-read-private user-read-email user-read-playback-state playlist-modify-public playlist-modify-private';

Spotify.jsx

class SpotifyAuth extends Component {  
  constructor (props) {
    super(props);
    this.state = {
      isAuthenticatedWithSpotify: false
    };
    this.state.handleRedirect = this.handleRedirect.bind(this);
    this.loginSpotifyUser = this.loginSpotifyUser.bind(this);
  };

  function generateRandomString(length) {
    let text = '';
    const possible =
      'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

    for (let i = 0; i < length; i++) {
      text += possible.charAt(Math.floor(Math.random() * possible.length));
    }

    return text;
  }

  getHashParams() {
    const hashParams = {};
    const r = /([^&;=]+)=?([^&;]*)/g;
    const q = window.location.hash.substring(1);
    let e = r.exec(q);
    while (e) {
      hashParams[e[1]] = decodeURIComponent(e[2]);
      e = r.exec(q);
    }
    return hashParams;
  }

  componentDidMount() {
    const params = this.getHashParams();

    const access_token = params.access_token;
    const state = params.state;
    const storedState = localStorage.getItem(stateKey);
    //localStorage.setItem('spotifyAuthToken', access_token);
    //localStorage.getItem('spotifyAuthToken');
    if (access_token && (state == null || state !== storedState)) {
      alert('There was an error during the authentication');
    } else {
      localStorage.removeItem(stateKey);
    }   
    // DO STUFF WITH ACCESS TOKEN HERE -- send ajax to backend routes    
  };

  handleRedirect() {
    const state = generateRandomString(16);
    localStorage.setItem(stateKey, state);

    let url = 'https://accounts.spotify.com/authorize';
    url += '?response_type=token';
    url += '&client_id=' + encodeURIComponent(client_id);
    url += '&scope=' + encodeURIComponent(scope);
    url += '&redirect_uri=' + encodeURIComponent(redirect_uri);
    url += '&state=' + encodeURIComponent(state);

    window.location = url;
    // post data to backend
    const url = `${process.env.REACT_APP_WEB_SERVICE_URL}/auth/spotify}`;
    axios.post(url, data)
    .then((res) => {
      this.loginSpotifyUser(res.data.auth_token);
    })
    .catch((err) => { console.log(err); });    
  };

  loginSpotifyUser(token) {
    window.localStorage.setItem('spotifyAuthToken', token);
    this.setState({ isAuthenticatedWithSpotify: true });
    this.props.createMessage('Welcome to Spotify', 'success');
  };

  render() {
    return (
      <div className="button_container">
        <button className="sp_button" onClick={this.handleRedirect}>
          <strong>CONNECT YOUR SPOTIFY ACCOUNT</strong>
        </button>
      </div>
      )
    }
}

export default SpotifyAuth;

and render, like so:

App.jsx

render() {
  return (
  <div>  
    <Switch>
       <Route exact path='/' render={() => (
          <SpotifyAuth/>
        )} 
       />
    </Switch>
   </div>

ERROR:

After all of this is set and run, I get:

INVALID_CLIENT: Invalid redirect URI 

LOGS:

Before building, I export this env variable:

$ export REACT_APP_WEB_SERVICE_URL=http://localhost

After services are built, I get the logs:

client_1   | You can now view client in the browser.
client_1   | 
client_1   |   Local:            http://localhost:3000/

web_1      |  * Environment: development
web_1      |  * Debug mode: on
web_1      |  * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

nginx_1    | 172.21.0.1 - - [27/Mar/2019:03:58:56 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36" "-"
nginx_1    | 172.21.0.1 - - [27/Mar/2019:03:58:56 +0000] "GET /static/js/0.chunk.js HTTP/1.1" 304 0 "http://localhost/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36" "-"
nginx_1    | 172.21.0.1 - - [27/Mar/2019:03:58:56 +0000] "GET /static/js/bundle.js HTTP/1.1" 304 0 "http://localhost/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36" "-"
nginx_1    | 172.21.0.1 - - [27/Mar/2019:03:58:56 +0000] "GET /static/js/main.chunk.js HTTP/1.1" 304 0 "http://localhost/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36" "-"

NOTE:

If I try and use localhost:8888 at Spotify.jsx, the app manages to authenticate with Spotify, but then all locations start with localhost:8888/auth/login and so on, which is not desired.


QUESTION:

Why won't localhost:3000, my client, work as redirect uri? What am I missing?

Is this the most reliable way of authenticating with Spotify on a docker project like this one?

like image 328
8-Bit Borges Avatar asked Aug 23 '26 14:08

8-Bit Borges


1 Answers

Your issue:

Your docker-compose.yml has the client container configured with port mapping 3007:3000. Notice that Docker-compose port mapping is host:container (Compose file reference), this means that port 3007 of your host is mapping to port 3000 of the container.

This way, you're trying to connect to a port of your container that is not available in your host, although nginx is happy with it because it is on the same network as your client, so it can reach it and redirect the request.

If that is the case:

  • http://localhost:3000 doesn't work, because it is closed in the host side.
  • http://localhost:3007 opens your client, because it is redirected to your client container, but you won't be able to use the Spotify auth there unless you whitelisted this URL and changed your redirect_uri.
  • http://localhost:8888 opens your client, because you have nginx setup as reverse proxy, and it can access the client port 3000 because it's on the same network.

Your solution:

Your solution is changing the docker-compose so client is mapped at ports 3000:3000. Then Spotify auth should be fine as the ports are open and the URLs are properly configured.

Extra:

Regarding your request for opinion on the design, nginx feels a bit underused by your design. If you set up a reverse proxy, is so that the services you're redirecting to are hidden in a secure network that cannot be accessed from outside. This way you can for example configure SSL on nginx and forget about HTTPs on the rest of the services. However, if such services can be accessed from other ports, it is of no use that you make this kind of configuration.

On a production setup, you would want to close the client and web ports from your docker-compose (literally, remove the port mapping. Nginx will have no problems accessing your containers as it's on the same network, unlike your host) and leave only nginx exposed to the real world.

You might also want to setup a rewrite rule where the client and server hang on http://localhost/client, http://localhost/server addresses respectively, but nginx rewrites the request and proxies it to the appropiate container so the containers actually see a request coming to http://localhost:3000/. You can see an example on how to configure all this at Stack Exchange - Nginx reverse proxy + URL rewrite.

like image 99
Marc Sances Avatar answered Aug 25 '26 04:08

Marc Sances