Learn how to register users, securely store passwords, log users in, manage sessions, and protect private routes in a Node.js application.
| Authentication | Authorization |
|---|---|
| Verifies who the user is. | Checks what the user is allowed to do. |
| Usually happens during login. | Usually happens when accessing a protected resource. |
| Example: Verify a student's password. | Example: Allow only an admin to delete students. |
Create a folder named node-auth-app.
mkdir node-auth-app
cd node-auth-app
npm init -y
Install the required packages:
npm install express bcryptjs express-session dotenv
npm install --save-dev nodemon
| Package | Purpose |
|---|---|
| express | Creates the web server and routes. |
| bcryptjs | Hashes passwords and verifies them. |
| express-session | Manages login sessions. |
| dotenv | Loads environment variables from a file. |
| nodemon | Restarts the server when files change during development. |
Passwords should never be stored as plain text in a database. A password hash is a one-way transformation used to verify a password without storing the original password.
{
"email": "student@example.com",
"password": "mypassword123"
}
Storing the original password like this exposes it if the database is accessed by an unauthorized person.
Use a password-hashing algorithm such as bcrypt or Argon2. Store the resulting hash, not the original password.
const bcrypt = require("bcryptjs");
async function hashPassword(password) {
const hash = await bcrypt.hash(password, 12);
return hash;
}
The work factor controls the computational cost of hashing. Choose it based on current security guidance and your server's capacity.
To keep this first example easy to run, we will use an in-memory array instead of a database. The users will disappear when the server restarts.
Create a file named users.js.
const users = [];
module.exports = users;
Create a file named server.js. This example uses
server-side sessions and bcrypt password hashing.
require("dotenv").config();
const express = require("express");
const bcrypt = require("bcryptjs");
const session = require("express-session");
const users = require("./users");
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
if (!process.env.SESSION_SECRET) {
throw new Error("SESSION_SECRET must be configured");
}
app.use(session({
name: "auth.sid",
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 1000 * 60 * 60
}
}));
// Register a user
app.post("/register", async (req, res) => {
try {
const { name, email, password } = req.body;
if (
typeof name !== "string" ||
!name.trim() ||
typeof email !== "string" ||
!email.trim() ||
typeof password !== "string" ||
password.length < 8 ||
password.length > 72
) {
return res.status(400).json({
message: "Provide a name, email, and password of 8 to 72 characters"
});
}
const normalizedEmail = email.trim().toLowerCase();
const existingUser = users.find(
user => user.email === normalizedEmail
);
if (existingUser) {
return res.status(409).json({
message: "Email already registered"
});
}
const passwordHash = await bcrypt.hash(password, 12);
const user = {
id: users.length + 1,
name: name.trim(),
email: normalizedEmail,
passwordHash: passwordHash
};
users.push(user);
res.status(201).json({
message: "Registration successful",
user: {
id: user.id,
name: user.name,
email: user.email
}
});
} catch (error) {
console.error("Registration error:", error.message);
res.status(500).json({
message: "Unable to register user"
});
}
});
// Login a user
app.post("/login", async (req, res, next) => {
try {
const { email, password } = req.body;
if (
typeof email !== "string" ||
typeof password !== "string"
) {
return res.status(400).json({
message: "Email and password are required"
});
}
const normalizedEmail = email.trim().toLowerCase();
const user = users.find(
item => item.email === normalizedEmail
);
if (!user) {
return res.status(401).json({
message: "Invalid email or password"
});
}
const passwordMatches = await bcrypt.compare(
password,
user.passwordHash
);
if (!passwordMatches) {
return res.status(401).json({
message: "Invalid email or password"
});
}
// Regenerate the session after successful authentication.
req.session.regenerate((error) => {
if (error) {
return next(error);
}
req.session.userId = user.id;
req.session.save((saveError) => {
if (saveError) {
return next(saveError);
}
res.json({
message: "Login successful",
user: {
id: user.id,
name: user.name,
email: user.email
}
});
});
});
} catch (error) {
next(error);
}
});
// Authentication middleware
function requireLogin(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({
message: "Please log in first"
});
}
next();
}
// Protected profile route
app.get("/profile", requireLogin, (req, res) => {
const user = users.find(
item => item.id === req.session.userId
);
if (!user) {
return res.status(401).json({
message: "User session is no longer valid"
});
}
res.json({
id: user.id,
name: user.name,
email: user.email
});
});
// Logout
app.post("/logout", requireLogin, (req, res, next) => {
req.session.destroy((error) => {
if (error) {
return next(error);
}
res.clearCookie("auth.sid", {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax"
});
res.json({
message: "Logout successful"
});
});
});
// Basic error handler
app.use((error, req, res, next) => {
console.error("Server error:", error.message);
if (res.headersSent) {
return next(error);
}
res.status(500).json({
message: "Internal server error"
});
});
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Create a file named .env in your project directory.
Generate a long, random secret for your session.
SESSION_SECRET=replace_with_a_long_random_secret
PORT=5000
NODE_ENV=development
For local development, you can generate a secret using Node.js:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Replace the placeholder with the generated value. Keep the
.env file private and add it to .gitignore.
.env
node_modules/
Start the server:
node server.js
Server running on port 5000
Send a POST request to:
http://localhost:5000/register
Choose Body → raw → JSON and enter:
{
"name": "Rahul Kumar",
"email": "rahul@example.com",
"password": "ExamplePass123"
}
{
"message": "Registration successful",
"user": {
"id": 1,
"name": "Rahul Kumar",
"email": "rahul@example.com"
}
}
Status code: 201 Created
Send a POST request to:
http://localhost:5000/login
Enter the registered credentials:
{
"email": "rahul@example.com",
"password": "ExamplePass123"
}
{
"message": "Login successful",
"user": {
"id": 1,
"name": "Rahul Kumar",
"email": "rahul@example.com"
}
}
The server also sends a session cookie. Postman can store this cookie and send it with later requests to the same server.
After logging in, send a GET request to:
http://localhost:5000/profile
Make sure your API client sends the session cookie received during login.
{
"id": 1,
"name": "Rahul Kumar",
"email": "rahul@example.com"
}
If you access the route without a valid session, the API returns
401 Unauthorized.
Send a POST request to:
http://localhost:5000/logout
Send the session cookie with the request. The server destroys the session and clears the cookie.
{
"message": "Logout successful"
}
Try opening /profile again. The old session should
no longer grant access.
| Problem | Possible Solution |
|---|---|
| Invalid email or password | Check the submitted credentials and registration data. |
| 401 Unauthorized | Log in and send the session cookie with the protected request. |
| Session secret missing | Configure SESSION_SECRET in the environment. |
| Session disappears after restart | The example uses in-memory users and the default session store. |
| Cookie not sent | Check cookie settings, client behavior, HTTPS, and origin configuration. |
| Email already registered | Use a different email or implement a database-backed account recovery flow. |
Create a simple authentication API using Node.js and Express.
Bonus Task: Replace the in-memory user array with a MySQL or MongoDB database and add unique email constraints at the database level.
Select the correct answer for each question.