1varunvc / snyder

MIT License
0 stars 0 forks source link

Backend: Implement Spotify Authentication with Passport.js #2

Closed 1varunvc closed 1 day ago

1varunvc commented 2 days ago
  1. Install Passport.js and the Spotify strategy (npm install passport passport-spotify).
  2. Register your app with Spotify to get client ID and secret.
  3. Set up authentication routes (/login, /callback) in Express.
  4. Handle authentication flow and store user tokens securely.
1varunvc commented 1 day ago

Implementing Spotify Authentication with Passport.js in Node.js

This guide demonstrates how to implement Spotify authentication in your Node.js app using Passport.js and the passport-spotify strategy, matching your actual implementation with separate files and added security features.


Prerequisites


1. Install Required Packages

Navigate to your backend directory and install the necessary packages:

npm install passport passport-spotify express-session axios express-rate-limit

2. Obtain Spotify API Credentials

Follow the steps outlined in the original guide to obtain your Client ID and Client Secret, and set your Redirect URI.

Update your .env file:

SPOTIFY_CLIENT_ID=your_spotify_client_id
SPOTIFY_CLIENT_SECRET=your_spotify_client_secret
SPOTIFY_REDIRECT_URI=http://localhost:3000/auth/spotify/callback
SESSION_SECRET=your_session_secret
NODE_ENV=development

3. Set Up Express Session Middleware

In your server.js, configure express-session with enhanced security settings.

// server.js
require('dotenv').config();

const express = require('express');
const session = require('express-session');
const passport = require('passport');
const { router: authRoutes } = require('./auth'); // Adjust the path if necessary
const spotifyRoutes = require('./spotifyRoutes'); // Adjust the path if necessary
const rateLimit = require('express-rate-limit');

const app = express();

// Global rate limiter (e.g., max 1000 requests per 15 minutes per IP)
const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 1000, // limit each IP to 1000 requests per windowMs
  message: {
    error: 'Too many requests, please try again later.',
  },
});

// Apply the global rate limiter to all requests
app.use(globalLimiter);

// Configure Express session
app.use(
  session({
    secret: process.env.SESSION_SECRET, // Ensure SESSION_SECRET is set in .env
    resave: false,
    saveUninitialized: false,
    cookie: {
      secure: process.env.NODE_ENV === 'production', // Set to true in production
      httpOnly: true, // Helps prevent XSS
      maxAge: 24 * 60 * 60 * 1000, // 1 day
    },
  })
);

// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());

// Use authentication routes
app.use(authRoutes);

// Use Spotify data routes
app.use(spotifyRoutes);

// Define other routes
app.get('/', (req, res) => {
  res.send('Welcome to the Snyder App Backend');
});

// Start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

Notes:


4. Initialize Passport.js in server.js

Already included in the server.js above:

// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());

5. Configure Passport Spotify Strategy in auth.js

Create a new file auth.js:

// auth.js
const express = require('express');
const passport = require('passport');
const SpotifyStrategy = require('passport-spotify').Strategy;
const axios = require('axios'); // Use axios for HTTP requests

const router = express.Router();

// Serialize and deserialize user instances to and from the session
passport.serializeUser((user, done) => {
  done(null, user);
});

passport.deserializeUser((obj, done) => {
  done(null, obj);
});

// Configure the Spotify strategy for use by Passport
passport.use(
  new SpotifyStrategy(
    {
      clientID: process.env.SPOTIFY_CLIENT_ID,
      clientSecret: process.env.SPOTIFY_CLIENT_SECRET,
      callbackURL: process.env.SPOTIFY_REDIRECT_URI,
    },
    (accessToken, refreshToken, expires_in, profile, done) => {
      const user = {
        spotifyId: profile.id,
        displayName: profile.displayName,
        photos: profile.photos,
        accessToken: accessToken,
        refreshToken: refreshToken,
        expiresIn: Date.now() + expires_in * 1000, // Convert expires_in to timestamp
      };
      return done(null, user);
    }
  )
);

Notes:


6. Set Up Authentication Routes in auth.js

// Authentication route for Spotify
router.get(
  '/auth/spotify',
  passport.authenticate('spotify', {
    scope: ['user-read-email', 'user-read-private'],
    showDialog: true,
  })
);

// Callback route
router.get(
  '/auth/spotify/callback',
  passport.authenticate('spotify', { failureRedirect: '/login' }),
  (req, res) => {
    // Successful authentication
    res.redirect('/');
  }
);

// Logout route
router.get('/logout', (req, res) => {
  req.logout(() => {
    res.redirect('/');
  });
});

// Middleware to ensure user is authenticated
function ensureAuthenticated(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }
  res.redirect('/login'); // Redirect unauthenticated users to login
}

// Export both the router and utility functions
module.exports = { router, ensureAuthenticated };

Notes:


7. Securely Store and Refresh User Tokens in auth.js

a. Refresh Token Function

// Function to refresh the Spotify access token
async function refreshSpotifyToken(refreshToken) {
  const authOptions = {
    method: 'post',
    url: 'https://accounts.spotify.com/api/token',
    params: {
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
    },
    headers: {
      Authorization:
        'Basic ' +
        Buffer.from(
          process.env.SPOTIFY_CLIENT_ID + ':' + process.env.SPOTIFY_CLIENT_SECRET
        ).toString('base64'),
      'Content-Type': 'application/x-www-form-urlencoded',
    },
  };

  try {
    const response = await axios(authOptions);
    const newAccessToken = response.data.access_token;
    const newExpiresIn = Date.now() + response.data.expires_in * 1000;
    return { newAccessToken, newExpiresIn };
  } catch (error) {
    console.error('Failed to refresh access token:', error.response.data);
    throw error;
  }
}

// Export the refresh function
module.exports = { router, ensureAuthenticated, refreshSpotifyToken };

8. Create Spotify Data Routes in spotifyRoutes.js

Create a new file spotifyRoutes.js:

// spotifyRoutes.js
const express = require('express');
const axios = require('axios');
const rateLimit = require('express-rate-limit');
const { ensureAuthenticated, refreshSpotifyToken } = require('./auth'); // Adjust the path if necessary

const router = express.Router();

// Rate limiter middleware (e.g., max 100 requests per 15 minutes per IP)
const spotifyDataLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: {
    error: 'Too many requests, please try again later.',
  },
});

// Utility function to check token expiration
function isTokenExpired(expiresIn) {
  return Date.now() > expiresIn;
}

// Function to fetch user data from Spotify
async function getSpotifyData(accessToken) {
  const options = {
    method: 'get',
    url: 'https://api.spotify.com/v1/me',
    headers: { Authorization: 'Bearer ' + accessToken },
  };

  const response = await axios(options);
  return response.data;
}

// Route to fetch Spotify data
router.get(
  '/api/spotify-data',
  ensureAuthenticated,
  spotifyDataLimiter, // Apply rate limiting to this route
  async (req, res) => {
    try {
      // Check if token has expired
      if (isTokenExpired(req.user.expiresIn)) {
        // Refresh the access token
        const { newAccessToken, newExpiresIn } = await refreshSpotifyToken(
          req.user.refreshToken
        );

        // Update the user's access token and expiration time
        req.user.accessToken = newAccessToken;
        req.user.expiresIn = newExpiresIn;
        req.session.passport.user = req.user; // Update the session
      }

      // Token is valid, proceed to make the API call
      const spotifyData = await getSpotifyData(req.user.accessToken);
      res.json(spotifyData);
    } catch (error) {
      console.error('Error fetching Spotify data:', error);
      res.status(500).json({ error: 'Failed to fetch Spotify data' });
    }
  }
);

module.exports = router;

Notes:


9. Update server.js to Use New Routes

Already shown in step 3, but here's the relevant part:

// Use authentication routes
app.use(authRoutes);

// Use Spotify data routes
app.use(spotifyRoutes);

10. Implement Token Refresh Logic

Already integrated into spotifyRoutes.js:


11. Security Enhancements


12. Testing the Implementation

  1. Install Dependencies:

    npm install
  2. Start Your Server:

    node server.js
  3. Visit the Login Route:

    Open your browser and navigate to:

    http://localhost:3000/auth/spotify
  4. Authenticate with Spotify:

    • Log in and authorize your app.
  5. Access Protected Route:

    • Navigate to /api/spotify-data to fetch your Spotify user data.

13. Frontend Considerations


14. Additional Tips


15. Conclusion

By modularizing your code and incorporating security best practices, you've successfully implemented Spotify authentication in your Node.js app using Passport.js. This setup includes secure handling of user tokens, session management, rate limiting, and structured routes for authentication and accessing protected resources.