1varunvc / snyder

MIT License
0 stars 0 forks source link

Backend: Configure Secure Storage of Tokens #8

Open 1varunvc opened 2 days ago

1varunvc commented 2 days ago
  1. Use express-session or JWTs for session management.
  2. Store tokens in server-side sessions or as HttpOnly cookies.
  3. Ensure tokens are not exposed to client-side scripts.
1varunvc commented 1 day ago

Securing user authentication tokens is crucial to protect your users and application from potential security threats like token theft or session hijacking. It is a guide through configuring secure storage of user authentication tokens in your Node.js app using express-session, and explain how to prevent exposure to client-side scripts using HttpOnly cookies.


Table of Contents

  1. Understanding Token Storage Options
  2. Configuring Secure Sessions with express-session
  3. Storing Tokens in Server-Side Sessions
  4. Preventing Token Exposure to Client-Side Scripts
  5. Implementing Secure Session Management
  6. Alternative: Using JWTs Securely
  7. Best Practices and Recommendations
  8. Summary

1. Understanding Token Storage Options

There are two common approaches to storing authentication tokens:

For enhanced security, especially when dealing with access tokens and refresh tokens, it's recommended to use server-side sessions to prevent tokens from being exposed to client-side scripts.


2. Configuring Secure Sessions with express-session

Since you're already using express-session in your application, we'll focus on securing it properly.

a. Install Necessary Packages

Ensure you have express-session installed:

npm install express-session

b. Configure express-session with Secure Options

In your server.js (or main application file), configure express-session with security best practices:

const session = require('express-session');
const MongoStore = require('connect-mongo'); // For session storage in MongoDB (optional but recommended)

// ...

app.use(
  session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    store: MongoStore.create({ mongoUrl: process.env.MONGODB_URI }), // Use MongoDB to store sessions
    cookie: {
      secure: process.env.NODE_ENV === 'production', // Set to true in production (requires HTTPS)
      httpOnly: true, // Prevents client-side JavaScript from accessing the cookie
      sameSite: 'lax', // Helps protect against CSRF attacks
      maxAge: 24 * 60 * 60 * 1000, // Session expiration in milliseconds (e.g., 1 day)
    },
  })
);

Explanation:


3. Storing Tokens in Server-Side Sessions

Modify your authentication strategy to store tokens securely in the session.

a. Update Passport Strategy Callback

In your auth.js file, after successful authentication, store the tokens in the user session without exposing them to the client.

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,
        // Do not include tokens here; store them separately
      };
      // Store tokens in session
      return done(null, { user, accessToken, refreshToken, expiresIn: Date.now() + expires_in * 1000 });
    }
  )
);

b. Serialize and Deserialize User

Modify the serializeUser and deserializeUser functions to handle the tokens separately.

passport.serializeUser((sessionData, done) => {
  // Store necessary user data and tokens in the session
  done(null, sessionData);
});

passport.deserializeUser((sessionData, done) => {
  // Retrieve the user data and tokens from the session
  done(null, sessionData);
});

Explanation:


4. Preventing Token Exposure to Client-Side Scripts

Ensure that tokens are not sent to the client in responses or accessible through client-side code.

a. Remove Tokens from Responses

In your routes, avoid sending tokens in JSON responses.

// In your protected routes (e.g., /dashboard)
router.get('/dashboard', ensureAuthenticated, (req, res) => {
  // Send only necessary user information
  res.json({
    message: 'Welcome to your dashboard',
    user: req.user.user, // Only include user data, exclude tokens
  });
});

b. Access Tokens Server-Side Only

When making API calls to Spotify, use the tokens stored in the session on the server-side.

// In spotifyRoutes.js
router.get('/api/spotify-data', ensureAuthenticated, async (req, res) => {
  try {
    // Use tokens from session (req.user.accessToken)
    // ...
  } catch (error) {
    // Handle errors
  }
});

5. Implementing Secure Session Management

a. Use a Session Store

For production environments, it's recommended to use a session store to persist sessions. You can use MongoDB with connect-mongo.

Install connect-mongo:

npm install connect-mongo

Configure Session Store:

const MongoStore = require('connect-mongo');

app.use(
  session({
    // ... other options
    store: MongoStore.create({ mongoUrl: process.env.MONGODB_URI }),
  })
);

b. Secure Cookie Settings

Ensure your cookie settings are configured to enhance security.

cookie: {
  secure: process.env.NODE_ENV === 'production',
  httpOnly: true,
  sameSite: 'lax',
  maxAge: 24 * 60 * 60 * 1000,
}

c. Use Helmet Middleware

Add Helmet to set secure HTTP headers.

npm install helmet
const helmet = require('helmet');
app.use(helmet());

6. Alternative: Using JWTs Securely

If you prefer to use JWTs, you need to handle them securely to prevent exposure.

a. Install Dependencies

npm install jsonwebtoken cookie-parser

b. Configure JWT Issuance

Issue JWTs on successful authentication and send them as HttpOnly cookies.

const jwt = require('jsonwebtoken');
const cookieParser = require('cookie-parser');

app.use(cookieParser());

// In your authentication callback
router.get(
  '/auth/spotify/callback',
  passport.authenticate('spotify', { failureRedirect: '/login' }),
  (req, res) => {
    // Generate JWT
    const tokenPayload = {
      spotifyId: req.user.spotifyId,
      // Include other necessary user info
    };
    const token = jwt.sign(tokenPayload, process.env.JWT_SECRET, { expiresIn: '1h' });

    // Set token as HttpOnly cookie
    res.cookie('jwt', token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
    });

    res.redirect('/dashboard');
  }
);

c. Verify JWTs in Protected Routes

Create middleware to verify JWTs.

function authenticateJWT(req, res, next) {
  const token = req.cookies.jwt;

  if (token) {
    jwt.verify(token, process.env.JWT_SECRET, (err, userData) => {
      if (err) {
        return res.sendStatus(403);
      }
      req.user = userData;
      next();
    });
  } else {
    res.sendStatus(401);
  }
}

Use the middleware in your routes:

router.get('/dashboard', authenticateJWT, (req, res) => {
  res.json({
    message: 'Welcome to your dashboard',
    user: req.user,
  });
});

Note: When using JWTs, avoid storing sensitive tokens (like access tokens or refresh tokens) in the JWT payload.


7. Best Practices and Recommendations


8. Summary

By configuring your Node.js application to store authentication tokens securely in server-side sessions using express-session, and ensuring that cookies are set with httpOnly and secure flags, you significantly reduce the risk of token exposure to client-side scripts or malicious actors.

Using server-side sessions keeps sensitive tokens away from the client's browser, and configuring cookies properly ensures that session identifiers are protected. If you choose to use JWTs, storing them in HttpOnly cookies and avoiding including sensitive information in the token payload is crucial.