How to connect Spotify PKCE Authorization Boilerplate to Login-Button in React

23 views Asked by At

I am curently working on an App implemented with React. The idea, or the task is to get data from Spotify, filter that data and add those songs to a new playlist which than can be saved to the spotify-profile. I already setup the main functionalities with mocked data. But now I am trying to connect the login button on my app to the authorization flow and I am really stuck.

Spotify-for Developers provides a boilerplate, that is stored on github .

Here the boilerplate is linked as script to the index.html and the app is build with javascript. So I need a different approach. I saved the boilerplate in a js file and tried different ways to connect it with the App.js file in my application. Including the boilerplate directly in the App.js file throws errors because of the 'await' statements in the function.

My last Idea was to import the the provided Clickhandlers and use them via props with the button. But of course it does not work as intended. I get redirected to the Spotify-login-page. But when I click on the login there, i get redirected to my app with no information. Which makes sense, because the rest of the boilerplate-code cant be executed.

What would be the best approach to make this work? Do I have to extract some of the code and bring it to App.js?

This is my App.js:

import {React, useState, useEffect} from "react";
import Navbar from './components/Navbar/Navbar';
import Footer from './components/Footer/Footer';
import Main from './components/Main/Main'
import './assets/global.css';
import {loginWithSpotifyClick, logoutClick, refreshTokenClick} from './data/AuthContext2.js';

const App = () => {
    
    return (
        <div className="appLayout">
            <Navbar login={loginWithSpotifyClick} />
            <Main />
            <Footer />
        </div>
    );
}

export default App;

This is my Navbar-Component that includes the login-button:

import React from 'react';
import './Navbar.css';

const Navbar = ({login}) => {
  
 return (
    <div className="container2">
      <h1 className="ja"><span>ja</span><span className="mmm">mmm</span><span className="ing">ing</span></h1>
      <button className='login' onClick={login}>Login</button>
    </div>
 );
}

export default Navbar;

This is the boilerplate-file (I already deleted the HTML related code from the original file). I named it AuthContext2.js:

/**
 * This is an example of a basic node.js script that performs
 * the Authorization Code with PKCE oAuth2 flow to authenticate 
 * against the Spotify Accounts.
 *
 * For more information, read
 * https://developer.spotify.com/documentation/web-api/tutorials/code-pkce-flow
 */

const clientId = "my client id is hidden!"; // your clientId
const redirectUrl = 'http://localhost:3000/callback';        // your redirect URL - must be localhost URL and/or HTTPS

const authorizationEndpoint = "https://accounts.spotify.com/authorize";
const tokenEndpoint = "https://accounts.spotify.com/api/token";
const scope = 'user-read-private user-read-email';

// Data structure that manages the current active token, caching it in localStorage
const currentToken = {
  get access_token() { return localStorage.getItem('access_token') || null; },
  get refresh_token() { return localStorage.getItem('refresh_token') || null; },
  get expires_in() { return localStorage.getItem('refresh_in') || null },
  get expires() { return localStorage.getItem('expires') || null },

  save: function (response) {
    const { access_token, refresh_token, expires_in } = response;
    localStorage.setItem('access_token', access_token);
    localStorage.setItem('refresh_token', refresh_token);
    localStorage.setItem('expires_in', expires_in);

    const now = new Date();
    const expiry = new Date(now.getTime() + (expires_in * 1000));
    localStorage.setItem('expires', expiry);
  }
};

// On page load, try to fetch auth code from current browser search URL
const args = new URLSearchParams(window.location.search);
const code = args.get('code');

// If we find a code, we're in a callback, do a token exchange
if (code) {
  const token = await getToken(code);
  currentToken.save(token);

  // Remove code from URL so we can refresh correctly.
  const url = new URL(window.location.href);
  url.searchParams.delete("code");

  const updatedUrl = url.search ? url.href : url.href.replace('?', '');
  window.history.replaceState({}, document.title, updatedUrl);
}

// If we have a token, we're logged in, so fetch user data and render logged in template
if (currentToken.access_token) {
  const userData = await getUserData();
}

// Otherwise we're not logged in, so render the login template


async function redirectToSpotifyAuthorize() {
  const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  const randomValues = crypto.getRandomValues(new Uint8Array(64));
  const randomString = randomValues.reduce((acc, x) => acc + possible[x % possible.length], "");

  const code_verifier = randomString;
  const data = new TextEncoder().encode(code_verifier);
  const hashed = await crypto.subtle.digest('SHA-256', data);

  const code_challenge_base64 = btoa(String.fromCharCode(...new Uint8Array(hashed)))
    .replace(/=/g, '')
    .replace(/\+/g, '-')
    .replace(/\//g, '_');

  window.localStorage.setItem('code_verifier', code_verifier);

  const authUrl = new URL(authorizationEndpoint)
  const params = {
    response_type: 'code',
    client_id: clientId,
    scope: scope,
    code_challenge_method: 'S256',
    code_challenge: code_challenge_base64,
    redirect_uri: redirectUrl,
  };

  authUrl.search = new URLSearchParams(params).toString();
  window.location.href = authUrl.toString(); // Redirect the user to the authorization server for login
}

// Soptify API Calls
async function getToken(code) {
  const code_verifier = localStorage.getItem('code_verifier');

  const response = await fetch(tokenEndpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      client_id: clientId,
      grant_type: 'authorization_code',
      code: code,
      redirect_uri: redirectUrl,
      code_verifier: code_verifier,
    }),
  });

  return await response.json();
}

async function refreshToken() {
  const response = await fetch(tokenEndpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: new URLSearchParams({
      client_id: clientId,
      grant_type: 'refresh_token',
      refresh_token: currentToken.refresh_token
    }),
  });

  return await response.json();
}

async function getUserData() {
  const response = await fetch("https://api.spotify.com/v1/me", {
    method: 'GET',
    headers: { 'Authorization': 'Bearer ' + currentToken.access_token },
  });

  return await response.json();
}

// Click handlers
export async function loginWithSpotifyClick() {
  await redirectToSpotifyAuthorize();
}

export async function logoutClick() {
  localStorage.clear();
  window.location.href = redirectUrl;
}

export async function refreshTokenClick() {
  const token = await refreshToken();
  currentToken.save(token);

}
0

There are 0 answers