Search code examples
reactjsspotify

Spotify Implicit Grant Flow with React - user login


This React Component below connects an app to Spotify using the Implicit Grant Flow, redirecting the app back to my client after token is obtained for user.

import React, { Component } from 'react';
import Credentials from './spotify-auth.js'
import './Spotify.css'


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


  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() {
    //if (this.props.isAuthenticated) {
    const params = this.getHashParams();

    const access_token = params.access_token;
    const state = params.state;
    const storedState = localStorage.getItem(Credentials.stateKey);
    localStorage.setItem('spotifyAuthToken', access_token);
    localStorage.getItem('spotifyAuthToken');

    if (window.localStorage.getItem('authToken')) {
      this.setState({ isAuthenticatedWithSpotify: true });
    };
    if (access_token && (state == null || state !== storedState)) {
      alert('Click "ok" to finish authentication with Spotify');
    } else {
      localStorage.removeItem(Credentials.stateKey);
    }
    // DO STUFF WITH ACCEES TOKEN HERE
    this.props.onConnectWithSpotify(access_token); 
  };

  handleRedirect(event) {
    event.preventDefault()
    this.props.createMessage('You linked your Spotify account!', 'success');

    const params = this.getHashParams();
    const access_token = params.access_token;
    console.log(access_token);

    const state = this.generateRandomString(16);
    localStorage.setItem(Credentials.stateKey, state);

    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);
    window.location = url; 
  };

  render() {
      return (
        <div className="button_container">
            <h1 className="title is-4"><font color="#C86428">Welcome</font></h1>
            <div className="Line" /><br/>
              <button className="sp_button" onClick={(event) => this.handleRedirect(event)}>
                <strong>LINK YOUR SPOTIFY ACCOUNT</strong>
              </button>
        </div>
      )
  }
}
export default SpotifyAuth;

However, before redirect I would like to depict the following page, or a pop up, with defined scopes and 'agree' button:


enter image description here


According to Spotify docs for Implicit Flow, redirecting the user to https://accounts.spotify.com/authorize?client_id=5fe01282e94241328a84e7c5cc169164&redirect_uri=http:%2F%2Fexample.com%2Fcallback&scope=user-read-private%20user-read-email&response_type=token&state=123

...performs a couple of actions:

The user is asked to authorize access within the scopes. The Spotify Accounts >service presents details of the scopes for which access is being sought. If the user is not logged in, they are prompted to do so using their Spotify >username and password. When the user is logged in, they are asked to authorize access to the data sets >defined in the scopes.

None of the actions above happen with my code above when I call https://accounts.spotify.com/authorize?, I just get the token.

What is wrong?


I have found this codepen example, where a user is prompted a log in page:

Spotify Implicit Grant Auth Popup

How do I add this functionality to my component above?


Solution

  • NOTE: Once you have accepted the scopes using one email id you wont be shown scope page again. Scopes are asked just once in the start. If you want to see scope page again then you have to authorize new email id.

    I have created a react app to feel how your code is working. I ran the following code:

    import React, { Component } from 'react';
    
    export const authEndpoint = 'https://accounts.spotify.com/authorize';
    class SpotifyAuth extends Component {
      constructor(props) {
        super(props);
        this.state = {
          isAuthenticatedWithSpotify: false
          // menu: this.props.userId.menu
        };
        this.state.handleRedirect = this.handleRedirect.bind(this);
      }
    
      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() {
        //if (this.props.isAuthenticated) {
        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 (window.localStorage.getItem('authToken')) {
          this.setState({ isAuthenticatedWithSpotify: true });
        }
        if (access_token && (state == null || state !== storedState)) {
          alert('Click "ok" to finish authentication with Spotify');
        } else {
          localStorage.removeItem('stateKey');
        }
        console.log(access_token);
        // DO STUFF WITH ACCEES TOKEN HERE
        // this.props.onConnectWithSpotify(access_token);
      }
    
      handleRedirect(event) {
        event.preventDefault();
        console.log('You linked your Spotify account!', 'success');
    
        const params = this.getHashParams();
        const access_token = params.access_token;
        console.log(access_token);
    
        const state = this.generateRandomString(16);
        localStorage.setItem('stateKey', state);
    
        // let url = 'https://accounts.spotify.com/authorize';
        // url += '?response_type=token';
        // url +=
        //   '&client_id=' + encodeURIComponent('f09fbf600009433dadce5836c57584c3');
        // url += '&scope=' + encodeURIComponent('user-top-read');
        // url += '&redirect_uri=' + encodeURIComponent('http://localhost:3000/abc');
        // url += '&state=' + encodeURIComponent(state);
        // url += '&show_dialog=true';
        let url =
          'https://accounts.spotify.com/authorize' +
          '?response_type=code' +
          '&client_id=f09fbf600009433dadce5836c57584c3' +
          '&scope=' +
          encodeURIComponent('user-read-private%20user-read-email') +
          '&redirect_uri=' +
          encodeURIComponent('http://localhost:3000/loginsuccess');
        window.location = url;
      }
    
      render() {
        return (
          <div className="button_container">
            <h1 className="title is-4">
              <font color="#C86428">Welcome</font>
            </h1>
            <div className="Line" />
            <br />
            <button
              className="sp_button"
              onClick={(event) => this.handleRedirect(event)}
            >
              <strong>LINK YOUR SPOTIFY ACCOUNT</strong>
            </button>
          </div>
        );
      }
    }
    
    export default SpotifyAuth;
    

    I believe that there is no problem with the code when I hardcoded the url instead of taking values from Credential. I was able to see the scope page It will be better if you could verify that scopes saved in Credential are correct.

    Also I have commented out this.props.createMessage method which is abstract for me and might cause some issue.

    enter image description here