🟢 Node.js Q105 / 118

How do you implement a simple authentication system using Node.js and Passport.js?

AI-Powered Answer ✓ Answered

Passport.js is a modular authentication middleware for Node.js. It offers a comprehensive set of strategies for authenticating requests, including local (username/password), OAuth, and OpenID. This guide demonstrates how to set up a basic authentication system using Passport's local strategy within an Express.js application, including user registration, login, and protected routes.

1. Project Setup and Dependencies

First, initialize a Node.js project and install the necessary packages: express for the web server, express-session for session management, passport and passport-local for authentication, bcryptjs for password hashing, and ejs as a template engine for views.

bash
mkdir passport-auth-demo
cd passport-auth-demo
npm init -y
npm install express express-session passport passport-local ejs bcryptjs --save

2. Basic Express Application Structure

Create an app.js (or server.js) file. This file will contain your Express application setup, including middleware for parsing requests, session management, and Passport initialization. Replace supersecretkey with a strong, environment-variable-driven secret in a production environment.

javascript
// app.js
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcryptjs'); // For password hashing
const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.set('view engine', 'ejs'); // Set EJS as the view engine
app.use(express.urlencoded({ extended: false })); // For parsing application/x-www-form-urlencoded
app.use(express.json()); // For parsing application/json
app.use(session({
    secret: 'supersecretkey', // Replace with a strong secret in production
    resave: false,
    saveUninitialized: false,
    cookie: { secure: false } // Set to true if using HTTPS
}));

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

// ... Passport Strategy and Routes will be added here ...

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

3. User Model (In-Memory Example)

For this simplified example, we'll use an in-memory array to store user data. In a real application, you would replace this with a database (e.g., MongoDB with Mongoose, PostgreSQL with Sequelize). Add these variables and helper functions at the top of your app.js file, before Passport configuration.

javascript
// app.js (add above Passport strategy)
const users = []; // In-memory user store for demo purposes

// Helper functions to find a user by ID or username
const findUserById = (id, callback) => {
    const user = users.find(u => u.id === id);
    callback(null, user);
};

const findUserByUsername = (username, callback) => {
    const user = users.find(u => u.username === username);
    callback(null, user);
};

4. Passport Local Strategy Configuration

Configure the passport-local strategy. This strategy will be used to verify the username and password provided by the user during login. The serializeUser and deserializeUser functions are crucial for maintaining user sessions.

javascript
// app.js (add after Passport middleware initialization)

passport.use(new LocalStrategy(
    async (username, password, done) => {
        findUserByUsername(username, async (err, user) => {
            if (err) { return done(err); }
            if (!user) {
                return done(null, false, { message: 'Incorrect username.' });
            }
            try {
                const isMatch = await bcrypt.compare(password, user.password);
                if (!isMatch) {
                    return done(null, false, { message: 'Incorrect password.' });
                }
                return done(null, user);
            } catch (error) {
                return done(error);
            }
        });
    }
));

// Serialize user to store in session (stores user ID)
passport.serializeUser((user, done) => {
    done(null, user.id);
});

// Deserialize user from session (retrieves user object from ID)
passport.deserializeUser((id, done) => {
    findUserById(id, (err, user) => {
        done(err, user);
    });
});

5. Authentication Routes and Middleware

Implement routes for user registration, login, a protected dashboard, and logout. A simple isAuthenticated middleware function is used to protect routes that require a logged-in user. Add these routes to your app.js file.

javascript
// app.js (add after Passport strategy)

// Middleware to check if user is authenticated
function isAuthenticated(req, res, next) {
    if (req.isAuthenticated()) {
        return next();
    }
    res.redirect('/login');
}

// Home page
app.get('/', (req, res) => {
    res.render('index', { user: req.user }); // Pass user object to the view
});

// Register GET route
app.get('/register', (req, res) => {
    res.render('register');
});

// Register POST route
app.post('/register', async (req, res) => {
    const { username, password } = req.body;

    findUserByUsername(username, async (err, existingUser) => {
        if (existingUser) {
            return res.render('register', { message: 'Username already taken.' });
        }

        try {
            const hashedPassword = await bcrypt.hash(password, 10); // Hash password
            const newUser = {
                id: Date.now().toString(), // Simple unique ID for demo
                username,
                password: hashedPassword
            };
            users.push(newUser);
            res.redirect('/login');
        } catch (error) {
            console.error(error);
            res.redirect('/register');
        }
    });
});

// Login GET route
app.get('/login', (req, res) => {
    res.render('login');
});

// Login POST route
app.post('/login',
    passport.authenticate('local', {
        successRedirect: '/dashboard',
        failureRedirect: '/login',
        failureFlash: false // Set to true if using connect-flash for flash messages
    })
);

// Protected Dashboard route
app.get('/dashboard', isAuthenticated, (req, res) => {
    res.render('dashboard', { user: req.user }); // Pass user object to the view
});

// Logout route
app.get('/logout', (req, res, next) => {
    req.logout((err) => { // req.logout requires a callback in newer Passport versions
        if (err) { return next(err); }
        res.redirect('/');
    });
});

6. Example EJS Views

Create a views directory in your project root and add the following EJS files. These provide the basic UI for home, registration, login, and the protected dashboard.

views/index.ejs

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Home</title>
</head>
<body>
    <h1>Welcome!</h1>
    <% if (user) { %>
        <p>Hello, <%= user.username %>!</p>
        <p><a href="/dashboard">Go to Dashboard</a></p>
        <p><a href="/logout">Logout</a></p>
    <% } else { %>
        <p><a href="/login">Login</a> or <a href="/register">Register</a></p>
    <% } %>
</body>
</html>

views/register.ejs

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Register</title>
</head>
<body>
    <h1>Register</h1>
    <% if (typeof message !== 'undefined') { %>
        <p style="color: red;"><%= message %></p>
    <% } %>
    <form action="/register" method="POST">
        <div>
            <label for="username">Username:</label>
            <input type="text" id="username" name="username" required>
        </div>
        <div>
            <label for="password">Password:</label>
            <input type="password" id="password" name="password" required>
        </div>
        <button type="submit">Register</button>
    </form>
    <p>Already have an account? <a href="/login">Login here</a></p>
</body>
</html>

views/login.ejs

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login</title>
</head>
<body>
    <h1>Login</h1>
    <form action="/login" method="POST">
        <div>
            <label for="username">Username:</label>
            <input type="text" id="username" name="username" required>
        </div>
        <div>
            <label for="password">Password:</label>
            <input type="password" id="password" name="password" required>
        </div>
        <button type="submit">Login</button>
    </form>
    <p>Don't have an account? <a href="/register">Register here</a></p>
</body>
</html>

views/dashboard.ejs

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dashboard</title>
</head>
<body>
    <h1>Welcome to the Dashboard, <%= user.username %>!</h1>
    <p>This is a protected page.</p>
    <p><a href="/logout">Logout</a></p>
    <p><a href="/">Back to Home</a></p>
</body>
</html>

7. Running the Application

After creating app.js and the views directory with the EJS files, you can run your Node.js application. Open your browser to http://localhost:3000 to test the registration and login flow.

bash
node app.js