Passport.js Complete Guide | Authentication Middleware for Node.js

Passport.js Complete Guide | Authentication Middleware for Node.js

이 글의 핵심

Passport.js is authentication middleware for Node.js. It supports 500+ authentication strategies including local, OAuth, and OpenID, making it the standard for authentication.

Introduction

Passport.js is authentication middleware for Node.js. It’s extremely flexible and modular, supporting authentication via username/password, OAuth (Google, Facebook, Twitter), and 500+ other strategies.

Why Passport?

Manual authentication (complex):

// Sessions, cookies, OAuth flows, password hashing...
// Hundreds of lines of code per provider

With Passport:

passport.use(new LocalStrategy(verify));
app.post('/login', passport.authenticate('local'));

1. Installation

npm install passport passport-local express-session

2. Local Strategy (Username/Password)

Setup

const express = require('express');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const session = require('express-session');
const bcrypt = require('bcrypt');

const app = express();

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

// Session configuration
app.use(session({
  secret: 'your-secret-key',
  resave: false,
  saveUninitialized: false,
  cookie: {
    maxAge: 24 * 60 * 60 * 1000, // 24 hours
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
  }
}));

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

// Configure Local Strategy
passport.use(new LocalStrategy(
  async (username, password, done) => {
    try {
      // Find user
      const user = await db.users.findOne({ username });
      
      if (!user) {
        return done(null, false, { message: 'Incorrect username' });
      }
      
      // Verify password
      const isValid = await bcrypt.compare(password, user.password);
      
      if (!isValid) {
        return done(null, false, { message: 'Incorrect password' });
      }
      
      return done(null, user);
    } catch (error) {
      return done(error);
    }
  }
));

// Serialize user for session
passport.serializeUser((user, done) => {
  done(null, user.id);
});

// Deserialize user from session
passport.deserializeUser(async (id, done) => {
  try {
    const user = await db.users.findById(id);
    done(null, user);
  } catch (error) {
    done(error);
  }
});

Login Route

app.post('/login', passport.authenticate('local', {
  successRedirect: '/dashboard',
  failureRedirect: '/login',
  failureFlash: true,
}));

// Or with callback
app.post('/login', (req, res, next) => {
  passport.authenticate('local', (err, user, info) => {
    if (err) {
      return next(err);
    }
    
    if (!user) {
      return res.status(401).json({ error: info.message });
    }
    
    req.logIn(user, (err) => {
      if (err) {
        return next(err);
      }
      
      return res.json({
        message: 'Login successful',
        user: { id: user.id, username: user.username }
      });
    });
  })(req, res, next);
});

Registration

app.post('/register', async (req, res) => {
  try {
    const { username, email, password } = req.body;
    
    // Check if user exists
    const existing = await db.users.findOne({ username });
    if (existing) {
      return res.status(409).json({ error: 'Username already exists' });
    }
    
    // Hash password
    const hashedPassword = await bcrypt.hash(password, 10);
    
    // Create user
    const user = await db.users.create({
      username,
      email,
      password: hashedPassword,
    });
    
    // Auto-login after registration
    req.login(user, (err) => {
      if (err) {
        return res.status(500).json({ error: 'Login failed' });
      }
      
      res.status(201).json({
        message: 'Registration successful',
        user: { id: user.id, username: user.username }
      });
    });
  } catch (error) {
    res.status(500).json({ error: 'Registration failed' });
  }
});

Logout

app.post('/logout', (req, res) => {
  req.logout((err) => {
    if (err) {
      return res.status(500).json({ error: 'Logout failed' });
    }
    
    res.json({ message: 'Logged out successfully' });
  });
});

3. Protected Routes

// Middleware to check authentication
function ensureAuthenticated(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }
  
  res.status(401).json({ error: 'Not authenticated' });
}

// Protected route
app.get('/dashboard', ensureAuthenticated, (req, res) => {
  res.json({
    message: 'Welcome to dashboard',
    user: req.user,
  });
});

// Optional authentication
app.get('/profile/:id', (req, res) => {
  if (req.isAuthenticated()) {
    // Show full profile
    res.json({ profile: 'full', user: req.user });
  } else {
    // Show public profile
    res.json({ profile: 'public' });
  }
});

4. Google OAuth

npm install passport-google-oauth20
const GoogleStrategy = require('passport-google-oauth20').Strategy;

passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: '/auth/google/callback',
  },
  async (accessToken, refreshToken, profile, done) => {
    try {
      // Find or create user
      let user = await db.users.findOne({ googleId: profile.id });
      
      if (!user) {
        user = await db.users.create({
          googleId: profile.id,
          email: profile.emails[0].value,
          name: profile.displayName,
          avatar: profile.photos[0].value,
        });
      }
      
      return done(null, user);
    } catch (error) {
      return done(error);
    }
  }
));

// Routes
app.get('/auth/google',
  passport.authenticate('google', { scope: ['profile', 'email'] })
);

app.get('/auth/google/callback',
  passport.authenticate('google', { failureRedirect: '/login' }),
  (req, res) => {
    res.redirect('/dashboard');
  }
);

5. GitHub OAuth

npm install passport-github2
const GitHubStrategy = require('passport-github2').Strategy;

passport.use(new GitHubStrategy({
    clientID: process.env.GITHUB_CLIENT_ID,
    clientSecret: process.env.GITHUB_CLIENT_SECRET,
    callbackURL: '/auth/github/callback',
  },
  async (accessToken, refreshToken, profile, done) => {
    try {
      let user = await db.users.findOne({ githubId: profile.id });
      
      if (!user) {
        user = await db.users.create({
          githubId: profile.id,
          username: profile.username,
          name: profile.displayName,
          avatar: profile.photos[0].value,
        });
      }
      
      return done(null, user);
    } catch (error) {
      return done(error);
    }
  }
));

// Routes
app.get('/auth/github',
  passport.authenticate('github', { scope: ['user:email'] })
);

app.get('/auth/github/callback',
  passport.authenticate('github', { failureRedirect: '/login' }),
  (req, res) => {
    res.redirect('/dashboard');
  }
);

6. JWT Strategy

npm install passport-jwt jsonwebtoken
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const jwt = require('jsonwebtoken');

// Configure JWT Strategy
passport.use(new JwtStrategy({
    jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    secretOrKey: process.env.JWT_SECRET,
  },
  async (payload, done) => {
    try {
      const user = await db.users.findById(payload.sub);
      
      if (!user) {
        return done(null, false);
      }
      
      return done(null, user);
    } catch (error) {
      return done(error, false);
    }
  }
));

// Login route (generate JWT)
app.post('/api/login', async (req, res) => {
  const { username, password } = req.body;
  
  const user = await db.users.findOne({ username });
  
  if (!user || !await bcrypt.compare(password, user.password)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  // Generate JWT
  const token = jwt.sign(
    { sub: user.id, username: user.username },
    process.env.JWT_SECRET,
    { expiresIn: '7d' }
  );
  
  res.json({ token, user: { id: user.id, username: user.username } });
});

// Protected route
app.get('/api/profile',
  passport.authenticate('jwt', { session: false }),
  (req, res) => {
    res.json({ user: req.user });
  }
);

7. Multiple Strategies

// Local + Google + GitHub
passport.use('local', new LocalStrategy(/* ... */));
passport.use('google', new GoogleStrategy(/* ... */));
passport.use('github', new GitHubStrategy(/* ... */));

// Login with local
app.post('/login', passport.authenticate('local'));

// Login with Google
app.get('/auth/google', passport.authenticate('google'));

// Login with GitHub
app.get('/auth/github', passport.authenticate('github'));

8. Custom Callback

app.post('/login', (req, res, next) => {
  passport.authenticate('local', (err, user, info) => {
    if (err) {
      return res.status(500).json({ error: 'Internal error' });
    }
    
    if (!user) {
      return res.status(401).json({ error: info.message || 'Login failed' });
    }
    
    req.logIn(user, (err) => {
      if (err) {
        return res.status(500).json({ error: 'Session error' });
      }
      
      // Custom response
      return res.json({
        success: true,
        user: {
          id: user.id,
          username: user.username,
          email: user.email,
        },
        token: generateToken(user), // If using JWT
      });
    });
  })(req, res, next);
});

9. Session Store (Production)

With Redis

npm install connect-redis redis
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');

// Create Redis client
const redisClient = createClient({
  host: process.env.REDIS_HOST,
  port: process.env.REDIS_PORT,
});

redisClient.connect();

// Configure session with Redis
app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    maxAge: 24 * 60 * 60 * 1000,
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
  }
}));

10. Remember Me

// Login with remember me
app.post('/login', (req, res, next) => {
  passport.authenticate('local', (err, user, info) => {
    if (err || !user) {
      return res.status(401).json({ error: 'Login failed' });
    }
    
    req.logIn(user, (err) => {
      if (err) {
        return res.status(500).json({ error: 'Session error' });
      }
      
      // Set longer expiry for remember me
      if (req.body.rememberMe) {
        req.session.cookie.maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days
      }
      
      res.json({ success: true, user });
    });
  })(req, res, next);
});

11. Real-World Example

const express = require('express');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const session = require('express-session');
const bcrypt = require('bcrypt');

const app = express();

// Middleware
app.use(express.json());
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
}));

app.use(passport.initialize());
app.use(passport.session());

// Local Strategy
passport.use(new LocalStrategy(
  { usernameField: 'email' },
  async (email, password, done) => {
    try {
      const user = await db.users.findOne({ email });
      
      if (!user) {
        return done(null, false, { message: 'User not found' });
      }
      
      const isValid = await bcrypt.compare(password, user.password);
      
      if (!isValid) {
        return done(null, false, { message: 'Invalid password' });
      }
      
      return done(null, user);
    } catch (error) {
      return done(error);
    }
  }
));

passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser(async (id, done) => {
  try {
    const user = await db.users.findById(id);
    done(null, user);
  } catch (error) {
    done(error);
  }
});

// Routes
app.post('/api/register', async (req, res) => {
  const { email, password, name } = req.body;
  const hashedPassword = await bcrypt.hash(password, 10);
  
  const user = await db.users.create({
    email,
    password: hashedPassword,
    name,
  });
  
  req.login(user, (err) => {
    if (err) return res.status(500).json({ error: err.message });
    res.json({ user: { id: user.id, email: user.email, name: user.name } });
  });
});

app.post('/api/login', passport.authenticate('local'), (req, res) => {
  res.json({ user: req.user });
});

app.post('/api/logout', (req, res) => {
  req.logout(() => {
    res.json({ message: 'Logged out' });
  });
});

app.get('/api/me', (req, res) => {
  if (!req.isAuthenticated()) {
    return res.status(401).json({ error: 'Not authenticated' });
  }
  res.json({ user: req.user });
});

app.listen(3000);

12. Best Practices

1. Secure Session Configuration

app.use(session({
  secret: process.env.SESSION_SECRET, // Use env var
  resave: false,
  saveUninitialized: false,
  cookie: {
    maxAge: 24 * 60 * 60 * 1000,
    httpOnly: true, // Prevent XSS
    secure: true, // HTTPS only in production
    sameSite: 'strict', // CSRF protection
  }
}));

2. Rate Limit Login Attempts

const rateLimit = require('express-rate-limit');

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: 'Too many login attempts',
});

app.post('/login', loginLimiter, passport.authenticate('local'));

3. Use HTTPS in Production

if (process.env.NODE_ENV === 'production') {
  app.use((req, res, next) => {
    if (!req.secure) {
      return res.redirect(`https://${req.headers.host}${req.url}`);
    }
    next();
  });
}

Summary

Passport.js provides flexible authentication:

  • 500+ strategies - local, OAuth, OpenID
  • Session management built-in
  • Easy to extend with custom strategies
  • Works with JWT for APIs
  • Production-ready and battle-tested

Key Takeaways:

  1. Use sessions for web apps, JWT for APIs
  2. Secure session configuration is critical
  3. Support multiple auth strategies easily
  4. Rate limit login attempts
  5. Always use HTTPS in production

Next Steps:

  • Secure with bcrypt
  • Add JWT
  • Build Express API

Resources: