User Authentication Level 3
Enhancing Security with Passport.js , Secure Authentication Leveraging Sessions and Cookies
Introduction
In modern web applications, user authentication is a critical feature to ensure secure access control. Passport.js, a popular Node.js middleware, simplifies user authentication by providing robust and flexible strategies. This blog delves into implementing user authentication using Passport.js alongside Express.js, PostgreSQL, and bcrypt. We'll explore the given code and break it down to understand its functionalities.
What is Passport.js?
Passport.js is an authentication middleware for Node.js that supports a wide range of authentication strategies. Its modular design makes it easy to integrate various methods, such as local username-password authentication, OAuth, and more. In this implementation, we use the passport-local strategy for authenticating users with email and password.
Sessions and cookies are not inherently secure by themselves, but they are essential tools used in secure authentication systems when implemented correctly. Here's how they contribute to secure authentication and the potential risks to manage:
Sessions in Secure Authentication
Purpose: Sessions store user-specific data on the server after the user logs in. This can include a session ID that identifies the authenticated user.
How It Works:
A unique session ID is generated upon successful login.
This session ID is sent to the client as a cookie.
On subsequent requests, the server verifies the session ID to identify the user.
Security Measures:
Use secure session storage mechanisms (e.g., encrypted databases or in-memory stores like Redis).
Regenerate session IDs on sensitive actions to prevent session fixation attacks.
Set session expiration to limit their validity.
Cookies in Secure Authentication
Purpose: Cookies store data on the client side and are used to transmit the session ID to the server.
How It Works:
The session ID or token is stored in a cookie and sent with every request to the server.
The server uses this ID to validate the session and authenticate the user.
Security Measures:
Use
HttpOnly
cookies to prevent access via JavaScript and reduce XSS risks.Use
Secure
cookies to ensure they are only sent over HTTPS.Implement cookie expiration and ensure cookies are invalidated upon logout.
Consider using
SameSite
cookies to mitigate CSRF attacks.
Why Sessions and Cookies Need Additional Security
While sessions and cookies are mechanisms for maintaining state and managing authentication, their security depends on proper implementation. To enhance secure authentication:
Combine sessions and cookies with secure authentication practices like OAuth, JWT, or Passport.js strategies.
Use encrypted connections (HTTPS) to protect session cookies during transmission.
Implement robust server-side checks to validate sessions and manage session lifecycles.
Thus, sessions and cookies are not inherently secure but are vital for secure authentication systems when combined with proper configurations, encryption, and other security measures
Step-by-Step Breakdown of the Code
1. Setting up Dependencies
import express from "express";
import bodyParser from "body-parser";
import pg from "pg";
import bcrypt from "bcrypt";
import session from "express-session";
import passport from "passport";
import { Strategy } from "passport-local";
The application uses Express.js for building the server, body-parser for parsing HTTP request bodies, and pg for interacting with the PostgreSQL database. The bcrypt library hashes user passwords, and Passport.js handles authentication.
2. Configuring Middleware
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static("public"));
This enables the application to parse incoming request bodies and serve static files from the "public" directory.
3. Session and Passport Initialization
app.use(session({
secret: "TOPSECRET",
resave: false,
saveUninitialized: true,
cookie: {
maxAge: 1000 * 60 * 60 * 24
}
}));
app.use(passport.initialize());
app.use(passport.session());
Session Management: The
express-session
middleware manages user sessions. A secret key is used to sign session cookies.Passport Initialization:
passport.initialize()
sets up Passport.js, andpassport.session()
integrates it with the session middleware.
4. Database Connection
const db = new pg.Client({
user: "postgres",
host: "localhost",
database: "secrets",
password: "mspostgres",
port: 5432,
});
db.connect();
The PostgreSQL client is configured and connected to the database.
5. Route Definitions
app.get("/", (req, res) => {
res.render("home.ejs");
});
app.get("/login", (req, res) => {
res.render("login.ejs");
});
app.get("/register", (req, res) => {
res.render("register.ejs");
});
- Home, Login, and Register Routes: These render the respective views for home, login, and registration pages using EJS templates.
6. Protected Route
app.get("/secrets", (req, res) => {
if (req.isAuthenticated()) {
res.render("secrets.ejs");
} else {
res.redirect("/login");
}
});
The /secrets
route is protected using Passport.js. Users must be authenticated to access it; otherwise, they are redirected to the login page.
7. Registration Handler
app.post("/register", async (req, res) => {
const email = req.body.username;
const password = req.body.password;
try {
const checkResult = await db.query("SELECT * FROM users WHERE email = $1", [email]);
if (checkResult.rows.length > 0) {
res.send("Email already exists. Try logging in.");
} else {
bcrypt.hash(password, saltRounds, async (err, hash) => {
if (err) {
console.error("Error hashing password:", err);
} else {
const result = await db.query(
"INSERT INTO users (email, password) VALUES ($1, $2) RETURNING *",
[email, hash]
);
const user = result.rows[0];
req.login(user, (err) => {
res.redirect("/secrets");
});
}
});
}
} catch (err) {
console.log(err);
}
});
Checks if the user already exists in the database.
Hashes the password with bcrypt.
Stores the hashed password in the database and logs in the user upon successful registration.
8. Login Handler
app.post("/login", passport.authenticate("local", {
successRedirect: "/secrets",
failureRedirect: "/login"
}));
This uses Passport's local
strategy to authenticate users. Successful logins redirect to /secrets
, while failures redirect back to /login
.
9. Passport Local Strategy
passport.use(new Strategy(async function verify(username, password, cb) {
try {
const result = await db.query("SELECT * FROM users WHERE email = $1", [username]);
if (result.rows.length > 0) {
const user = result.rows[0];
const storedHashedPassword = user.password;
bcrypt.compare(password, storedHashedPassword, (err, result) => {
if (result) {
return cb(null, user);
} else {
return cb(null, false);
}
});
} else {
return cb("User not found");
}
} catch (err) {
return cb(err);
}
}));
The passport-local
strategy verifies the user's credentials:
Fetches the user by email.
Compares the provided password with the stored hashed password using bcrypt.
10. Serialization and Deserialization
passport.serializeUser((user, cb) => {
cb(null, user);
});
passport.deserializeUser((user, cb) => {
cb(null, user);
});
These methods manage user session data, allowing Passport.js to persist authentication across requests.
11. Starting the Server
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
The server listens for incoming connections on port 3000.
Conclusion
This implementation demonstrates a complete user authentication system using Passport.js. By combining session management, bcrypt hashing, and the passport-local
strategy, the application ensures secure user login and registration. Passport.js's extensibility allows developers to implement advanced authentication features seamlessly.