Skip to main content

Express Local Strategy

Introduction

Authentication is a crucial part of web applications, allowing users to securely access personalized content and functionality. In Express.js applications, Passport.js is a popular middleware that simplifies the implementation of various authentication strategies. The Local Strategy is one of the most commonly used authentication methods, which involves authenticating users using a username and password stored in your application's database.

In this tutorial, you'll learn how to implement the Local Strategy in an Express application. We'll cover setting up Passport.js with the passport-local strategy, storing user credentials securely, and creating login and registration routes.

Prerequisites

Before we begin, you should have:

  • Basic knowledge of Express.js
  • Understanding of middleware in Express
  • Familiarity with MongoDB or another database (for storing user data)
  • Node.js installed on your machine

Step 1: Setting Up Your Project

Let's start by creating a new Express application and installing the necessary dependencies:

bash
mkdir express-local-auth
cd express-local-auth
npm init -y
npm install express mongoose passport passport-local passport-local-mongoose express-session connect-flash bcryptjs

Let's understand what each package does:

  • express: Web framework for Node.js
  • mongoose: MongoDB object modeling tool
  • passport: Authentication middleware for Node.js
  • passport-local: Passport strategy for authenticating with a username and password
  • passport-local-mongoose: Mongoose plugin that simplifies building username and password login with Passport
  • express-session: Session middleware for Express
  • connect-flash: For displaying flash messages
  • bcryptjs: For hashing passwords

Step 2: Creating the Express Application

Create a file named app.js and set up a basic Express application:

javascript
const express = require('express');
const mongoose = require('mongoose');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const session = require('express-session');
const flash = require('connect-flash');

const app = express();

// Set up middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Set up session
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false
}));

// Set up flash messages
app.use(flash());

// Initialize Passport and restore authentication state from session
app.use(passport.initialize());
app.use(passport.session());

// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/local-auth-demo', {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('MongoDB connected'))
.catch(err => console.error('MongoDB connection error:', err));

// Routes will be added here

app.listen(3000, () => {
console.log('Server running on port 3000');
});

Step 3: Creating the User Model

Create a models directory and add a file named User.js:

javascript
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const passportLocalMongoose = require('passport-local-mongoose');

const UserSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true
},
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now
}
});

// Add passport-local-mongoose plugin
UserSchema.plugin(passportLocalMongoose);

// Hash password before saving
UserSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();

try {
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error) {
next(error);
}
});

// Method to validate password
UserSchema.methods.isValidPassword = async function(password) {
try {
return await bcrypt.compare(password, this.password);
} catch (error) {
throw error;
}
};

const User = mongoose.model('User', UserSchema);

module.exports = User;

Step 4: Configuring Passport.js with Local Strategy

Now, let's create a separate configuration file for Passport. Create a file named passport-config.js:

javascript
const LocalStrategy = require('passport-local').Strategy;
const User = require('./models/User');

module.exports = function(passport) {
// Configure local strategy for use by Passport
passport.use(
new LocalStrategy({ usernameField: 'email' }, async (email, password, done) => {
try {
// Find user by email
const user = await User.findOne({ email });

if (!user) {
return done(null, false, { message: 'No user with that email' });
}

// Check if password is correct
const isMatch = await user.isValidPassword(password);

if (!isMatch) {
return done(null, false, { message: 'Password incorrect' });
}

// If both match, return the user
return done(null, user);
} catch (error) {
return done(error);
}
})
);

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

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

Update your app.js to include this configuration:

javascript
// Add this line near the top of app.js
const configurePassport = require('./passport-config');

// Add this after initializing passport
configurePassport(passport);

Step 5: Creating Authentication Routes

Create a new directory called routes and add a file named auth.js:

javascript
const express = require('express');
const passport = require('passport');
const User = require('../models/User');

const router = express.Router();

// Register form
router.get('/register', (req, res) => {
res.send(`
<h1>Register</h1>
<form action="/auth/register" method="POST">
<div>
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="email">Email</label>
<input type="email" id="email" name="email" 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="/auth/login">Login</a></p>
`);
});

// Login form
router.get('/login', (req, res) => {
res.send(`
<h1>Login</h1>
${req.flash('error') ? `<p style="color: red">${req.flash('error')}</p>` : ''}
<form action="/auth/login" method="POST">
<div>
<label for="email">Email</label>
<input type="email" id="email" name="email" 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="/auth/register">Register</a></p>
`);
});

// Register handle
router.post('/register', async (req, res) => {
const { username, email, password } = req.body;

try {
// Check if user exists
let user = await User.findOne({ email });

if (user) {
req.flash('error', 'Email already registered');
return res.redirect('/auth/register');
}

// Create new user
user = new User({ username, email, password });
await user.save();

req.flash('success_msg', 'You are now registered and can log in');
res.redirect('/auth/login');
} catch (error) {
console.error(error);
res.redirect('/auth/register');
}
});

// Login handle
router.post('/login', (req, res, next) => {
passport.authenticate('local', {
successRedirect: '/dashboard',
failureRedirect: '/auth/login',
failureFlash: true
})(req, res, next);
});

// Logout handle
router.get('/logout', (req, res) => {
req.logout(function(err) {
if (err) { return next(err); }
req.flash('success_msg', 'You are logged out');
res.redirect('/auth/login');
});
});

module.exports = router;

Step 6: Creating Protected Routes

Let's create a middleware to protect routes that require authentication and implement a dashboard route:

javascript
// In a new file middleware/auth.js
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
req.flash('error', 'Please log in to view this resource');
res.redirect('/auth/login');
}

module.exports = { ensureAuthenticated };

Now, let's create a dashboard route:

javascript
// In a new file routes/dashboard.js
const express = require('express');
const { ensureAuthenticated } = require('../middleware/auth');

const router = express.Router();

router.get('/', ensureAuthenticated, (req, res) => {
res.send(`
<h1>Dashboard</h1>
<h2>Welcome, ${req.user.username}!</h2>
<p>This is your protected dashboard.</p>
<a href="/auth/logout">Logout</a>
`);
});

module.exports = router;

Step 7: Integrating Routes in the Main App

Finally, let's add these routes to our main app.js:

javascript
// Add these lines after setting up passport
app.use('/auth', require('./routes/auth'));
app.use('/dashboard', require('./routes/dashboard'));

// Add a root route
app.get('/', (req, res) => {
res.send(`
<h1>Home Page</h1>
<p>Welcome to our authentication demo!</p>
<div>
<a href="/auth/login">Login</a> |
<a href="/auth/register">Register</a>
</div>
`);
});

Complete Example

When you run the application using node app.js, you'll have a fully functional authentication system with:

  1. A registration page for creating new users
  2. A login page using the local strategy
  3. A protected dashboard that only authenticated users can access
  4. A logout function

Here's what the flow might look like:

  1. User visits the registration page and creates a new account
  2. User is redirected to the login page
  3. User logs in with their credentials
  4. If authentication is successful, they're redirected to the dashboard
  5. User can log out, which destroys their session

Understanding How Local Strategy Works

The Local Strategy follows these steps:

  1. User provides credentials: The user submits their email/username and password through a form.

  2. Passport.js validation: The local strategy receives these credentials and looks up the user in the database.

  3. Password verification: If the user exists, it compares the provided password with the stored hashed password.

  4. Session creation: Upon successful authentication, Passport.js creates a session for the user, storing their ID.

  5. Subsequent requests: For future requests, Passport deserializes the user from the session ID, giving you access to the user object via req.user.

Real-World Improvements

For a production application, consider implementing these enhancements:

1. Better Password Requirements

javascript
// In your registration route
const passwordRegex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*]).{8,}$/;
if (!passwordRegex.test(password)) {
req.flash('error', 'Password must be at least 8 characters and include uppercase, lowercase, numbers, and special characters');
return res.redirect('/auth/register');
}

2. Email Verification

After registration, send a verification email to the user's email address before allowing login:

javascript
// Example email verification flow (pseudocode)
const verificationToken = generateRandomToken();
user.verificationToken = verificationToken;
user.isVerified = false;
await user.save();

sendVerificationEmail(user.email, `http://yourapp.com/verify/${verificationToken}`);

// Then in your login strategy:
if (!user.isVerified) {
return done(null, false, { message: 'Please verify your email before logging in' });
}

3. Password Reset Functionality

javascript
router.post('/forgot-password', async (req, res) => {
const { email } = req.body;
const resetToken = generateRandomToken();

// Store the reset token and its expiration
const user = await User.findOne({ email });
if (!user) {
req.flash('error', 'No account with that email address exists');
return res.redirect('/auth/forgot-password');
}

user.resetPasswordToken = resetToken;
user.resetPasswordExpires = Date.now() + 3600000; // 1 hour
await user.save();

// Send email with reset link
sendResetEmail(user.email, `http://yourapp.com/reset/${resetToken}`);

req.flash('info', 'A password reset link has been sent to your email');
res.redirect('/auth/login');
});

Summary

In this tutorial, you learned how to implement the Passport.js Local Strategy for authentication in an Express application. We covered:

  1. Setting up the project with necessary dependencies
  2. Creating a user model with secure password handling
  3. Configuring Passport.js with the Local Strategy
  4. Creating registration, login, and logout routes
  5. Adding protected routes that require authentication
  6. Understanding the authentication flow
  7. Exploring real-world improvements for production applications

The Local Strategy is an excellent starting point for authentication in web applications. Once you're comfortable with it, you can explore other authentication strategies like OAuth for social login or JWT for API authentication.

Additional Resources

Exercises

  1. Add client-side validation to the registration and login forms.
  2. Implement password reset functionality that sends a reset link to the user's email.
  3. Add "remember me" functionality to keep the user logged in across browser sessions.
  4. Implement rate limiting to prevent brute force attacks.
  5. Create a user profile page where users can update their information and change their password.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)