How do I refresh my Spotify access token when it expires?

I am still working on the Jammming project… making slow progress!

I can now call on the Spotify API to find tracks, and add them to the playlist (but not yet export them).

The step I am now stuck on is “Implement Spotify Search Request > Invalid access tokens: Make sure that you use valid access tokens before making your requests.”

So as I have read on the Spotify API documentation, access tokens expire after 60 minutes, and I can verify that this is the case. Up to 60 minutes everything works fine, after that I get 401 errors.

The code I am using to get the tokens in the first place resides in my SearchBar.jsx module:

import React from "react";
import SearchButton from "./SearchButton";
import "./SearchBar.css";
import axios from "axios";

export default function SearchBar({
  searchKey,
  setSearchKey,
  searchResults,
  setSearchResults,
  token,
}) {
  async function searchArtists(event) {
    event.preventDefault();
    const { data } = await axios.get("https://api.spotify.com/v1/search", {
      headers: {
        Authorization: `Bearer ${token}`,
      },
      params: {
        q: searchKey,
        type: "track",
      },
    });

    setSearchResults(data.tracks.items)
  }

  return (
    <>
      <form className="Form" onSubmit={searchArtists}>
        <input
          type="text"
          id="search"
          className="Input"
          onChange={(event) => setSearchKey(event.target.value)}
        />
        <SearchButton />
      </form>
    </>
  );
}

What I am stuck on is how to get my app to pre-check if the token is valid before trying to make the call, and if not then requesting a new one.

I have thought about using try… catch…, and I have seen talk of axios being able to manage this with interceptors, though I cannot figure out how to use these with my get request.

I understand that I need to do some research on how to do this, but my research so far has sent me round in circles, so a pointer as to the direction I should research would be really helpful.

Of course I can just log out and log in again (I have implemented this) and then I get a new token, but what I would like is for my code to check if the token is valid and automatically request a new one if it isn’t.

You might need some sort of statement management on the time stamp.
You can either do this via react’s state management tools or try a different solution like caching. If you use react’s state management you might have to consider the case where the user opens up a different tab (you don’t want a dozen tokens active for the same user).

If you use local storage, don’t put the raw token as it’s a liability, but you can put the timestamp of when it was acquired (to do the math on whether it’s time to refresh).

Additionally you can just set an async setTimeout.

Or maybe the JWT already has expiry tiime (like .exp) that you just have to guard for in an if statement.

1 Like

You are on the right track by considering the use of a try/catch block to handle authentication errors. You can use an Axios interceptor to automatically check the validity of the token before each request. If the token has expired, the interceptor can request a new token and retry the original request. Example:

axios.interceptors.response.use(null, async error => {
  if (error.response.status === 401) {

    const newToken = await getNewToken();
    
    setToken(newToken);
    
    const newRequest = error.config;
    newRequest.headers['Authorization'] = `Bearer ${newToken}`;
    return axios(newRequest);
  }
  
  return Promise.reject(error);
});

Para utilizar o Axios, você precisa instalá-lo no seu projeto via npm:

npm install axios

ou via yarn:

yarn add axios

In this code, getNewToken would be a function you write to get a new token from Spotify. The interceptor is triggered automatically for any response error, checks if the error is a 401 error (unauthorized), and if so, gets a new token, updates the authorization header in the original request configuration, and tries the request again.

1 Like

Thanks and obrigado @toastedpitabread and @felpsg!

I realize it was a symptom of my tiredness when I wrote my first post that I didn’t actuallly share the logic of how I get my token in the first place. I was following a YouTube tutorial, and came up with this:

import React, { useEffect } from "react";

export default function Login({ token, setToken }) {
  // This is taken from my Spotify API login
  const CLIENT_ID = "662920ae7dd8457fbce6dfd8693fc31b";
  // This is what you need for a development build
  const REDIRECT_URI = "http://localhost:3000";
  const AUTH_ENDPOINT = "https://accounts.spotify.com/authorize";
  const RESPONSE_TYPE = "token";

  useEffect(() => {
    // I guess this is the stuff that comes back in the URI bar after you are redirected with the redirect URI
    const hash = window.location.hash;
    // I suppose this stores the token in local storage
    let token = window.localStorage.getItem("token");

    // If the token is empty, but there is a hash
    if (!token && hash) {
      token = hash
        .substring(1)
        .split("&")
        .find((elem) => elem.startsWith("access_token"))
        .split("=")[1];

      // Deletes the hash, clears the URL
      window.location.hash = "";

      // Stores the token in local storage
      window.localStorage.setItem("token", token);
    }
    // Stores the token as state
    setToken(token);
    // This last dependency I added based on error messages
  }, [setToken]);

  function logout() {
    // Sets the token back to blank
    setToken("");
    // Removes the token from local storage
    window.localStorage.removeItem("token");
  }

  return (
    <div>
    {/*If the token is empty, then display the "Login to Spotify" button.
      TODO: It would be good to do this if the token were also expired*/}
      {!token ? (
    <a
      href={`${AUTH_ENDPOINT}?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=${RESPONSE_TYPE}`}
        >
          Login to Spotify
        </a>
      ) : (
        <button onClick={logout}>Logout</button>
      )}
    </div>
  );
}

As I get the token in the first place via the login process, by extracting it from the hash returned after being redirected, I need to think about how I am going to get it without having to log in again.