Building a Node.js Server with Basic Authentication Using Express.js

 This tutorial shows how to create a Node.js server with Express.js in JavaScript, implementing Basic Authentication to secure a /user endpoint. The server authenticates users against a userlist.json file, supports CORS for a React Client at localhost:5173, and mimics the functionality of a TypeScript HTTP server. Perfect for learning Express and authentication!

Prerequisites

Step 1: Set Up the Project

Create a project folder and initialize it:

mkdir nodejs-express-auth
cd nodejs-express-auth
npm init -y

Install Express.js:

npm install express

Create server.js:

const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

Optional: Configuring nodemon and browser-sync for Live Reloading

Now that you’ve built your Express “Hello World” server, you might have noticed that changes to server.js require a manual restart and browser refresh. To streamline development, we can configure nodemon for server-side hot reloading and browser-sync for automatic browser refreshes. This chapter guides you through the setup, perfect for enhancing your productivity as a developer working on Node.js applications.

Why Use nodemon and browser-sync?

  • nodemon: Monitors your Node.js files and restarts the server automatically when changes are detected, saving you from manual restarts.
  • browser-sync: Proxies your Express server and reloads the browser when files change, providing a seamless development experience.
  • Together, they’re ideal for rapid iteration, aligning with modern development practices you might use in Angular or AWS deployments.

Install Required Packages

Install nodemon, browser-sync, and concurrently (to run both tools simultaneously):

npm install --save-dev nodemon browser-sync concurrently
  • Add --save-dev to include them in devDependencies in package.json.

Update package.json

Modify your package.json to include a development script:

{
  "scripts": {
    "start": "node server.js",
    "dev": "concurrently \"npx nodemon server.js\" \"browser-sync start --proxy http://localhost:3000 --files server.js\""
  }
}
  • "start": Runs the server normally.
  • "dev": Uses concurrently to run nodemon (via npx to avoid global installation) and browser-sync together.
  • --files server.js: Tells browser-sync to watch only server.js for changes (adjust if monitoring multiple files).

Run the Development Server

Start the setup with:

npm run dev
  • Expect output like:
    • [0] [nodemon] starting server.js“
    • [1] [Browsersync] Proxying: http://localhost:3000
    • [1] Local: http://localhost:3001 (browser-sync UI)
  • Open http://localhost:3001 in your browser to see the proxied server.

Test Live Reloading

  • Edit server.js (e.g., change res.send('Hello World!') to res.send('Hello Updated World!')):
const express = require('express'); const app = express(); const port = 3000; app.get('/', (req, res) => { res.send('Hello Updated World!'); }); app.listen(port, () => { console.log(`Server running at http://localhost:${port}`); });
  • Save the file. nodemon will restart the server, and browser-sync will refresh the browser automatically.

Troubleshooting Common Issues

  • “nodemon: command not found”:
    • Ensure nodemon is installed. If using npx fails, install globally:npm install -g nodemon
    • Then update the script to "dev": "concurrently \"nodemon server.js\" \"browser-sync start --proxy http://localhost:3000 --files server.js\"".
  • Browser Not Reloading:
    • Verify browser-sync is proxying the correct port (e.g., match port in server.js).
    • Check the --files flag; use --files *.js to watch all .js files if needed.
  • Port Conflict:
    • If port 3000 is in use, change it in server.js (e.g., const port = 8080) and update the --proxy flag.
  • Deprecation Warnings:
    • Ignore [DEP0060] from browser-sync for now; update to the latest version (npm install browser-sync@latest) if persistent.

Optional Enhancements

  • Custom nodemon Configuration:
    • Create nodemon.json to watch specific files:{ "watch": ["server.js"], "ext": "js" }
  • Multiple File Watching:
    • Adjust --files to src/*.js if you move server.js to a src folder later.
  • Docker Integration:
    • For AWS deployment, add nodemon to your Dockerfile:RUN npm install -g nodemon CMD ["npm", "run", "dev"]

Step 2: Create the User List

Create userlist.json in the project root:

[
  {
    "username": "admin",
    "password": "password123",
    "role": "admin"
  },
  {
    "username": "user",
    "password": "secret",
    "role": "user"
  }
]

Step 3: Write the Server Code

Update server.js:

const express = require('express');
const fs = require('fs');
const path = require('path');

const app = express();
console.log('Express application initialized');

const userListPath = path.join(__dirname, 'userlist.json');
console.log('User list path set to:', userListPath);

// Middleware for CORS (handles preflight OPTIONS)
app.use((req, res, next) => {
  console.log('CORS middleware triggered for request:', req.url);
  if (req.method === 'OPTIONS') {
    console.log('Handling OPTIONS preflight request for:', req.url);
    res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5173');
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type');
    return res.status(204).end();
  }
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5173');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type');
  next();
});

// Authentication middleware (unchanged)
const authenticate = (req, res, next) => {
  console.log('Authentication middleware started for request:', req.url);
  const authHeader = req.headers.authorization;
  if (!authHeader) {
    console.log('No authorization header found');
    res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"');
    return res.status(401).end();
  }
  console.log('Authorization header received:', authHeader);
  const base64Credentials = authHeader.split(' ')[1];
  const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii');
  console.log('Decoded credentials:', credentials);
  const [username, password] = credentials.split(':');

  let users = [];
  try {
    console.log('Attempting to read userlist.json');
    const data = fs.readFileSync(userListPath, 'utf8');
    users = JSON.parse(data);
    console.log('User list loaded successfully:', users);
  } catch (err) {
    console.error('Error reading userlist.json:', err);
    return res.status(500).send('Server error');
  }

  console.log('Searching for user:', username);
  const user = users.find(u => u.username === username && u.password === password);
  if (!user) {
    console.log('No matching user found for:', username);
    res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"');
    return res.status(401).end();
  }

  console.log('User authenticated:', user);
  req.user = { username: user.username, role: user.role };
  next();
};

// Routes
app.get('/user', authenticate, (req, res) => {
  console.log('Processing /user route for user:', req.user.username);
  res.json({ username: req.user.username, role: req.user.role });
});

app.get('/', (req, res) => {
  console.log('Processing / route');
  res.send('Public page: Hello World');
});

const PORT = 3000;
app.listen(PORT, () => {
  console.log('Server listening on port:', PORT);
  console.log(`Server running at http://localhost:${PORT}/`);
});

What’s Happening?

  • Express: Simplifies routing and middleware compared to raw HTTP.
  • CORS: Allows Angular at localhost:4200 to connect.
  • Authentication: Checks Authorization header (Basic <Base64(username:password)>) against userlist.json.
  • Routes:
    • /user: Protected, returns { username, role }.
    • /: Public, returns “Hello World”.
  • Preflight: Handles OPTIONS requests for CORS.

Step 4: Run the Server

Start the server:

node server.js

See: Server running at http://localhost:3000/.

Step 5: Test the Server

  • Browser:
    • Visit http://localhost:3000/ → “Public page: Hello World”.
    • Visit http://localhost:3000/user → Enter admin:password123{"username":"admin","role":"admin"}. Wrong credentials show “Invalid credentials”.
  • cURL:
curl http://localhost:3000 
# Public: "Public page: Hello World" 
curl -u admin:password123 http://localhost:3000/user 
# Secure: {"username":"admin","role":"admin"} 
curl http://localhost:3000/user 
# No auth: "Authentication required"

Security Notes

  • JSON File: Replace with a database (e.g., PostgreSQL) in production.
  • HTTPS: Use HTTPS to secure Basic Auth credentials.
  • Error Handling: Add robust validation for userlist.json.
  • Rate Limiting: Use express-rate-limit to prevent brute-force attacks.

Leave a Reply