Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Authorize Spotify using React and Nginx in Docker

I am building a dockerized REST API app with the following structure:

../
  web/
  nginx/
       dev.conf   
       Dockerfile-dev  
  client/
        build/
        conf/
        Dockerfile-dev
        node_modules/
        package_json
        public/
        src/
           App.jsx   
           components/ 
                     SpotifyRedirect.jsx                  
 spotify-client/ 
                Dockerfile-dev
                node_modules
                package-lock.json
                package.json
                authorization_code/
                                  app.js 

NOTE: In this project, user needs to go through two authorize/authenticate processes:

  1. one with my app, which generates a native token (this has been dealt with already)
  2. another with Spotify ( which requires a redirect URI and provides its own token for its API access)

    2a) So, at localhost, either after localhost/auth/register or localhost/auth/login submit, I would have Spotify redirect URI (http://localhost:8888), take me to this page:

enter image description here

2b) Then, clicking at the login button I would be asked to connect my app with Spotify, like so:

enter image description here

And with a last OK there I would be granted permission and handed a token which I could save and even refresh at my React client.


The building blocks to this project were extracted from this tutorial:

using-spotifys-awesome-api-with-react

However, I already have configured a React client, which serves other purposes as well, apart from this authorization process.

The following is the relevant code which tries to integrate these two services: a more general client and spotify-client.


Relevant code:

So my first try was to create a specific service for spotify-client, below client service, exposing it to port 8888, like so:

docker-compose-dev.yml

  nginx:
    build:
      context: ./services/nginx
      dockerfile: Dockerfile-dev
    restart: always
    ports:
      - 80:80
    depends_on:
      - web
      - client
      - spotify-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

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

Then, I set each node process on its own Dockerfile, like so:

client/Dockerfile-dev

# base image
FROM node:11.6.0-alpine

# set working directory
WORKDIR /usr/src/app

# add `/usr/src/app/node_modules/.bin` to $PATH
ENV PATH /usr/src/app/node_modules/.bin:$PATH

# install and cache app dependencies
COPY package.json /usr/src/app/package.json
RUN npm install --silent
RUN npm install [email protected] -g --silent

# start app
CMD ["npm", "start"]

and:

spotify-client/Dockerfile-dev // NEW

which has a different process running, as per required in Spotify web docs:

# base image
FROM node:11.6.0-alpine

# set working directory
WORKDIR /usr/src/app/authorization_code

# add `/usr/src/app/node_modules/.bin` to $PATH
ENV PATH /usr/src/app/node_modules/.bin:$PATH

# install and cache app dependencies
COPY package.json /usr/src/app/package.json
RUN npm install --silent
RUN npm install [email protected] -g --silent

# start app <-- NOT npm start
CMD ["node", "app.js"] 

My reverse proxy, I've tried:

nginx/dev.conf

server {

  listen 80;
  listen 8888; // NEW

  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;
  }


  location /auth {    // <-- app authorization, not Spotify's
    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;
  }

At my frontend I create a component for my redirect link:

client/src/components/SpofityRedirect.jsx

import React, { Component } from 'react';

class SpotifyRedirect extends Component{
    render(){
        return (
            <div className='SpotifyRedirect'>
                <a href='http://localhost:8888'> Log in with Spotify </a>
            </div>
        );
    }
}

export default SpotifyRedirect;

and here I show this redirect link at '/'.

client/src/App.jsx

import SpotifyRedirect from './components/SpotifyRedirect';

 (...)
 <Switch
  <Route exact path='/' render={() => (
   <SpotifyRedirect/>
  )} />

 (...)
 </Switch>

More:

spotify-client/authorization_code/app.js

(this is provided by Spofity, and I only inserted http://localhost:3000)

var express = require('express'); // Express web server framework
var request = require('request'); // "Request" library
var cors = require('cors');
var querystring = require('querystring');
var cookieParser = require('cookie-parser');

var client_id = 'is'; // Your client id
var client_secret = 'secret'; // Your secret
var redirect_uri = 'http://localhost:8888'; // Your redirect uri

/**
 * Generates a random string containing numbers and letters
 * @param  {number} length The length of the string
 * @return {string} The generated string
 */
var generateRandomString = function(length) {
  var text = '';
  var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

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

var stateKey = 'spotify_auth_state';

var app = express();

app.use(express.static(__dirname + '/public'))
   .use(cors())
   .use(cookieParser());

app.get('/login', function(req, res) {

  var state = generateRandomString(16);
  res.cookie(stateKey, state);

  // your application requests authorization
  var scope = 'user-read-private user-read-email user-read-playback-state playlist-modify-public playlist-modify-private';
  res.redirect('https://accounts.spotify.com/authorize?' +
    querystring.stringify({
      response_type: 'code',
      client_id: client_id,
      scope: scope,
      redirect_uri: redirect_uri,
      state: state
    }));
});

app.get('/callback', function(req, res) {

  // your application requests refresh and access tokens
  // after checking the state parameter

  var code = req.query.code || null;
  var state = req.query.state || null;
  var storedState = req.cookies ? req.cookies[stateKey] : null;

  if (state === null || state !== storedState) {
    res.redirect('/#' +
      querystring.stringify({
        error: 'state_mismatch'
      }));
  } else {
    res.clearCookie(stateKey);
    var authOptions = {
      url: 'https://accounts.spotify.com/api/token',
      form: {
        code: code,
        redirect_uri: redirect_uri,
        grant_type: 'authorization_code'
      },
      headers: {
        'Authorization': 'Basic ' + (new Buffer(client_id + ':' + client_secret).toString('base64'))
      },
      json: true
    };

    request.post(authOptions, function(error, response, body) {
      if (!error && response.statusCode === 200) {

        var access_token = body.access_token,
            refresh_token = body.refresh_token;

        var options = {
          url: 'https://api.spotify.com/v1/me',
          headers: { 'Authorization': 'Bearer ' + access_token },
          json: true
        };

        // use the access token to access the Spotify Web API
        request.get(options, function(error, response, body) {
          console.log(body);
        });

        // we can also pass the token to the browser to make requests from there
        res.redirect('http://localhost:3000/#' + //NEW
          querystring.stringify({
            access_token: access_token,
            refresh_token: refresh_token
          }));
      } else {
        res.redirect('/#' +
          querystring.stringify({
            error: 'invalid_token'
          }));
      }
    });
  }
});

app.get('/refresh_token', function(req, res) {

  // requesting access token from refresh token
  var refresh_token = req.query.refresh_token;
  var authOptions = {
    url: 'https://accounts.spotify.com/api/token',
    headers: { 'Authorization': 'Basic ' + (new Buffer(client_id + ':' + client_secret).toString('base64')) },
    form: {
      grant_type: 'refresh_token',
      refresh_token: refresh_token
    },
    json: true
  };

  request.post(authOptions, function(error, response, body) {
    if (!error && response.statusCode === 200) {
      var access_token = body.access_token;
      res.send({
        'access_token': access_token
      });
    }
  });
});

console.log('Listening on 8888');
app.listen(8888);

______

Docker services at command line:

CONTAINER ID        IMAGE                      COMMAND                  CREATED             STATUS              PORTS                            NAMES
0e9870a7412c        dev3_nginx                 "nginx -g 'daemon of…"   9 seconds ago       Up 6 seconds        0.0.0.0:80->80/tcp               dev3_nginx_1
e6bc5bbff630        dev3_spotify-client        "node app.js"            26 minutes ago      Up 26 minutes       0.0.0.0:3000->8888/tcp           dev3_spotify-auth-server_1
a6b9e84953a3        dev3_client                "npm start"              25 hours              "/start.sh"              25 hours ago        Up 25 hours         80/tcp, 0.0.0.0:3008->8080/tcp  
16fb623ca2b3        dev3_web                   "/usr/src/app/entryp…"   25 hours 

Finally, before build, I run:

$ export REACT_APP_WEB_SERVICE_URL=http://localhost


So far, with configuration aboce, when I click on Log in with Soptify, I'm getting:

enter image description here


QUESTION:

How can I use the configuration above with my nginx reverse proxy in order to:

  1. Provide location / with a link for my Spotify's redirect uri http://localhost:8888
  2. Authorize the app with Spotify
  3. End up back at location '/' when authorization is all done?
like image 548
8-Bit Borges Avatar asked Mar 14 '19 15:03

8-Bit Borges


1 Answers

Problem: No container is listening on the port 8888. You can publish spotify-client:8888 directly on port 8888 (without nginx). Update docker-compose-dev.yml:

 spotify-client:
    ports:
      - 8888:8888

If you really need nginx, then you will need to play with nginx config + you need to publish nginx on the port 8888 as well. nginx/dev.conf example:


  server {
    listen 80;
    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;
    }
    location /auth {
      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;
    }
  }

  # reverse proxy on the port 8888 for spotify-client
  server {
    listen  8888;
    location / {
      proxy_pass http://<spotify-client service/ip>:<port>/;
      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;
    }
  }

docker-compose-dev.yml and nginx published ports:

  nginx:
    ports:
      - 80:80
      - 8888:8888

Generally, you need to configure properly nginx:8888->spotify-client:port.

IMHO: you don't need spotify-client service at all. Just use implicit flow in your app to get Spotify token. It is more better choice for React/Angular (browser JS code). Keep in mind that refresh tokens doesn't exist in this flow, so you will need to implement silent refresh as well.

like image 99
Jan Garaj Avatar answered Sep 24 '22 16:09

Jan Garaj