skip to Main Content

I’m working on my first react App right now, which uses the Spotify API. But I ran into troubles with the Authentication Process. Mainly the Problem is that my custom Hook useAuth, responsible for getting an accessToken is not connecting to my server.

My client runs on localhost:5173. My Server on localhost:3001.

My useAuth.js:

import { useState, useEffect } from 'react'
import axios from "axios";

const API_URL = 'http://localhost:3001';

export default function useAuth(code) {
  const [accessToken, setAccessToken] = useState(null);
  const [refreshToken, setRefreshToken] = useState(null);
  const [expiresIn, setExpiresIn] = useState(null);

  useEffect(() => {
    if (!code) {
      console.warn('No code provided, skipping authentication.');
      return;
    }

    axios.post(`${API_URL}/login`, { code }, { withCredentials: true })
      .then(res => {
        console.log('Successfully obtained tokens:', res.data);
        setAccessToken(res.data.accessToken);
        setRefreshToken(res.data.refreshToken);
        setExpiresIn(res.data.expiresIn);
        window.history.pushState({}, null, '/');
      })
      .catch(err => {
        console.error('Error during authentication:', err.response || err.message);
        console.log(code);
        alert('Failed to authenticate. Please log in again.');
        window.location = '/';
      });
  }, [code]);

  useEffect(() => {
    if (!refreshToken || !expiresIn) return;
    const interval = setInterval(() => {

      axios
      .post(`${API_URL}/refresh`, {
        refreshToken,
      })
      .then(res => {
        console.log('Token refreshed successfully:', res.data);
        setAccessToken(res.data.accessToken);
        setExpiresIn(res.data.expiresIn);
      })
      .catch(error => {
        console.error('Error during token refresh:', error);
        alert('Session expired. Please log in again.');
        window.location = '/';
      })
    }, (expiresIn - 60) * 1000);

    return () => clearInterval(interval);
  }, [refreshToken, expiresIn])


  return accessToken;
}

And my server.js:

import 'dotenv/config';
console.log('Process object:', process.env);
import express from 'express';
import SpotifyWebApi from 'spotify-web-api-node';
import cors from 'cors';
import bodyParser from 'body-parser';
import session from 'express-session';

console.log('SPOTIFY_CLIENT_ID:', process.env.SPOTIFY_CLIENT_ID);
console.log('SPOTIFY_CLIENT_SECRET:', process.env.SPOTIFY_CLIENT_SECRET);
console.log('REDIRECT_URI:', process.env.REDIRECT_URI);

const app = express();
const port = 3001;

const corsOptions = {
  origin: 'http://localhost:5173',
  credentials: true,  // Allow credentials to be sent/received
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],  // Allowed HTTP methods
  allowedHeaders: ['Content-Type', 'Authorization'],  // Allowed headers
};

app.use(cors(corsOptions));


app.use(bodyParser.json());

app.use(express.static('public')); // If serving static files

app.use((req, res, next) => {
  res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;");
  next();
});

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: true,
  cookie: {
    secure: false, // Use 'true' in production with HTTPS
    httpOnly: true, // Helps to prevent XSS attacks by making the cookie inaccessible via JavaScript
    sameSite: 'None', // Allows cross-site requests with the cookie
  }
}));

const spotifyApi = new SpotifyWebApi({
  clientId: process.env.SPOTIFY_CLIENT_ID,
  clientSecret: process.env.SPOTIFY_CLIENT_SECRET,
  redirectUri: process.env.REDIRECT_URI,
});

app.use(async (req, res, next) => {
  req.session = req.session || {};

  if (!req.session.accessToken || req.session.tokenExpirationTimestampMs <= Date.now()) {
    if (req.session.refreshToken) {
      spotifyApi.setRefreshToken(req.session.refreshToken);
      try {
        const data = await spotifyApi.refreshAccessToken();
        req.session.accessToken = data.body['access_token'];
        req.session.tokenExpirationTimestampMs = Date.now() + (data.body['expires_in'] * 1000);
        spotifyApi.setAccessToken(req.session.accessToken);
        next(); // Continue to the next middleware or route
      } catch (err) {
        console.error('Could not refresh access token', err);
        return res.status(401).json({ error: 'Unauthorized. Please log in again.' });
      }
    } else {
      return res.status(401).json({ error: 'Unauthorized. Please log in again.' });
    }
  } else {
    spotifyApi.setAccessToken(req.session.accessToken);
    next(); // Continue to the next middleware or route
  }
});

app.post('/login', async (req, res) => {
  const code = req.body.code;
  try {
    const data = await spotifyApi.authorizationCodeGrant(code);
    res.json({
      accessToken: data.body.access_token,
      refreshToken: data.body.refresh_token,
      expiresIn: data.body.expires_in,
    });
  } catch (err) {
    console.error('Spotify authorization error:', err);  // Log the detailed error
    res.status(401).json({ error: 'Unauthorized. Please log in again.' });
  }
});

app.post('/refresh', async (req, res) => {
  const refreshToken = req.body.refreshToken;

  try {
    spotifyApi.setRefreshToken(refreshToken);
    const data = await spotifyApi.refreshAccessToken();

    req.session.accessToken = data.body.access_token;
    req.session.tokenExpirationTimestampMs = Date.now() + (data.body.expires_in * 1000);

    res.json({
      accessToken: data.body.access_token,
      expiresIn: data.body.expires_in,
    });
  } catch (err) {
    console.error('Error in /refresh', err);
    res.status(400).json({ error: 'Failed to refresh token. Please log in again.' });
  }
});

app.get('/api/userSubscriptionLevel', async (req, res) => {
  try {
    const user = await spotifyApi.getMe();
    const product = user.body.product || 'free'; // Default to 'free' if no product found
    res.json({ product });
  } catch (error) {
    console.error('Error in /api/userSubscriptionLevel:', error);
    res.status(500).json({ error: 'Internal Server Error' });
  }
});

app.get('/api/getAccessToken', async (req, res) => {
  try {
    const accessToken = spotifyApi.getAccessToken();
    console.log('Access token:', accessToken);
    res.json({ accessToken });
  } catch (error) {
    console.error('Error in /api/getAccessToken', error);
    res.status(500).json({ error: 'Internal Server Error' });
  }
});


app.listen(port, () => {
  console.log(`Server listening on port ${port}`);
});

I tryied to solve the issue with chat-gpt, which obviously did’t work out to well. I might have overcomplicated things at some point.

2

Answers


  1. Chosen as BEST ANSWER

    That was very helpful, thanks. But I decided to do some restructuring, because other issues where popping up. But I still have the same Issue, that I don't get an accessToken because of a bad request 400.

    The Errror in the Terminal is: WebapiAuthenticationError: An authentication error occurred while communicating with Spotify's Web API.

    I triple checked my redirect URI and cliendID, but seems all fine.

    This is my flow right now:

    I use this main.jsx as entry point:

    import { StrictMode } from 'react'
    import { createRoot } from 'react-dom/client'
    import Auth from './components/Auth/Auth'
    import { LoadingProvider } from './context/LoadingContext';
    import './index.css'
    
    createRoot(document.getElementById('root')).render(
      <StrictMode>
        <LoadingProvider>
          <Auth />
        </LoadingProvider>
      </StrictMode>
    );
    

    Then my Auth.jsx chcecks for a code and if non directs to my Login:

    import React from 'react'
    import Login from '../Login/Login'
    import App from '../App/App';
    
    const code = new URLSearchParams(window.location.search).get('code');
    
    export default function Auth() {
      return code ? <App code={code} /> : <Login />
    }
    

    My Login is for now just a simple button which should direct the user to the Spotify Login:

    import React from 'react'
    const clientId = import.meta.env.VITE_SPOTIFY_CLIENT_ID;
    const redirectUri = import.meta.env.VITE_REDIRECT_URI;
    
    
    const AUTH_URL = `https://accounts.spotify.com/authorize?client_id=${clientId}&response_type=code&redirect_uri=${redirectUri}&scope=streaming%20user-read-email%20user-read-private%20user-library-read%20user-library-read%20user-library-modify%20user-read-playback-state%20user-modify-playback-state`;
    
    export default function Login() {
      return (
        <div className='flex flex-1 h-screen justify-center items-center '>
            <button className="text-white bg-neon-pink rounded-2xl px-3 py-0.5 h-min mt-2 hover:bg-neon-purple transition-colors duration-300 text-sm font-bold">
              <a href={AUTH_URL} >Login With Spotify</a>
            </button>
        </div>
      )
    }
    

    And in my app I call the useAuth.js with the aquired code:

    import { useState, useEffect, useRef } from 'react';
    import useAuth from '../../hooks/useAuth';
    import usePlaylist from '../../hooks/usePlaylist';
    import styles from './App.module.scss';
    import SearchBar from '../SearchBar/SearchBar';
    import SearchResults from '../SearchResults/SearchResults';
    import Tracklist from '../Tracklist/Tracklist';
    import Playlist from '../Playlist/Playlist';
    import Spotify from '../../services/Spotify';
    import { useLoading } from '../../context/LoadingContext';
    import LoadingSpinner from '../LoadingSpinner/LoadingSpinner';
    
    
    function App({code}) {
      const accessToken = useAuth(code);
    ...
    

    And my useAuth should handle getting an accessToken and refreshToken by directing to the server:

    import { useState, useEffect } from 'react'
    import axios from "axios";
    
    const API_URL = 'http://localhost:3001';
    
    export default function useAuth(code) {
      const [accessToken, setAccessToken] = useState(null);
      const [refreshToken, setRefreshToken] = useState(null);
      const [expiresIn, setExpiresIn] = useState(null);
    
      useEffect(() => {
        axios.post(`${API_URL}/login`, {
          code,
        }).then(res => {
          setAccessToken(res.data.accessToken);
          setRefreshToken(res.data.refreshToken);
          setExpiresIn(res.data.expiresIn);
          window.history.pushState({}, null, '/');
        }).catch(() => {
          // window.location = '/';
        })
      }, [code]);
    
      useEffect(() => {
        if (!refreshToken ||!expiresIn) return;
        const interval = setInterval(() => {
    
          axios.post(`${API_URL}/refresh`, {
            code,
          }).then(res => {
            setAccessToken(res.data.accessToken);
            setExpiresIn(res.data.expiresIn);
          }).catch(() => {
            window.location = '/';
          })
        }, (expiresIn - 60) * 1000)
    
        return () => clearInterval(interval);
      }, [refreshToken, expiresIn])
    
    
      return accessToken;
    }
    

    And this is how my server.js looks like now:

    import 'dotenv/config';
    import express from 'express';
    import SpotifyWebApi from 'spotify-web-api-node';
    import cors from 'cors';
    import bodyParser from 'body-parser';
    
    const app = express();
    app.use(cors());
    app.use(bodyParser.json());
    
    app.post('/refresh', (req, res) => {
      const refreshToken = req.body.refreshToken;
      console.log('Hi');
      const spotifyApi = new SpotifyWebApi({
        clientId: process.env.SPOTIFY_CLIENT_ID,
        clientSecret: process.env.SPOTIFY_CLIENT_SECRET,
        redirectUri: process.env.SPOTIFY_REDIRECT_URI,
        refreshToken
        })
    
        spotifyApi.refreshAccessToken()
        .then((data) => {
            res.json({
              accessToken: data.body.access_token,
              expiresIn: data.body.expires_in,
            })
          }).catch(err => {
            console.log(err);
            res.sendStatus(400)
          })
    });
    
    app.post('/login', (req, res) => {
      const code = req.body.code;
      const spotifyApi = new SpotifyWebApi({
      clientId: process.env.SPOTIFY_CLIENT_ID,
      clientSecret: process.env.SPOTIFY_CLIENT_SECRET,
      redirectUri: process.env.SPOTIFY_REDIRECT_URI
      })
    
      spotifyApi.authorizationCodeGrant(code).then(data => {
        res.json({
          accessToken: data.body.access_token,
          refreshToken: data.body.refresh_token,
          expires_in: data.body.expires_in
        })
      }).catch((err) => {
        console.log(err);
        res.sendStatus(400)
      });
    });
    
    app.listen(3001)
    

  2. Problem is in using this wrongly

    app.use(async (req, res, next) => {
      req.session = req.session || {};
    
      if (!req.session.accessToken || req.session.tokenExpirationTimestampMs <= Date.now()) {
        if (req.session.refreshToken) {
          spotifyApi.setRefreshToken(req.session.refreshToken);
          try {
            const data = await spotifyApi.refreshAccessToken();
            req.session.accessToken = data.body['access_token'];
            req.session.tokenExpirationTimestampMs = Date.now() + (data.body['expires_in'] * 1000);
            spotifyApi.setAccessToken(req.session.accessToken);
            next(); // Continue to the next middleware or route
          } catch (err) {
            console.error('Could not refresh access token', err);
            return res.status(401).json({ error: 'Unauthorized. Please log in again.' });
          }
        } else {
          return res.status(401).json({ error: 'Unauthorized. Please log in again.' });
        }
      } else {
        spotifyApi.setAccessToken(req.session.accessToken);
        next(); // Continue to the next middleware or route
      }
    });
    

    as the express work from top to down. so whatever req you made to server it runs from top to bottom. The way you setup middleware it runs before any req you made to server, so the authentication parts run before reaching to your actual req.
    And I think you also can’t get to /login route in browser; it gives you unauthorized access (401) as setup. It’s because of that above so the simple solution to this is setup this middleware after defining /login route or any other route which doesn’t require authentication or use individually in each route (Whatever you preferred, search a little about this ,Thank you )

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search