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 *.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.

Hide secrets such as API keys in your server (optional)

Dealing with secrets in web applications such as API keys is often challenging as web browsers are not very good at keeping secrets. Exposing an API key directly in client-side code (e.g., a React or Angular app) risks it being accessed via browser developer tools or intercepted by malicious scripts. For this reason, the only place to keep your information secure is on the server. This section provides a comprehensive guide to enhancing your Node.js Express authentication server by adding a endpoint for the xAI API wrapper, ensuring the API key remains secure. By wrapping the xAI API in a server-side Node.js application, you:

  • Protect the API Key: Store the key securely in server-side environment variables, inaccessible to clients.
  • Control Access: Implement authentication (e.g., Basic Auth, as in your server) to restrict API usage.
  • Enhance Flexibility: Allow for additional logic, such as rate limiting or request validation, before forwarding to the xAI API.
  • Build Trust: Clearly communicate the security benefits to clients, fostering confidence in your solution.

Implementation Steps

  1. Store the xAI API Key Securely:
    • Use the dotenv package to manage environment variables.
    • Install: bashCollapseWrapRunCopynpm install dotenv
    • Create a .env file in your project root: envCollapseWrapCopyXAI_API_KEY=your-xai-api-key
    • Ensure .env is in .gitignore to prevent exposure.
  2. Update server.js with a Endpoint:
...
app.post('/send-message', authenticate, async (req, res) => {
  try {
    let body = '';
    req.on('data', chunk => { body += chunk; });
    req.on('end', async () => {
      const { message, max_tokens = 100, temperature = 0.7 } = JSON.parse(body);
      console.log(`Received /send-message: message="${message}", max_tokens=${max_tokens}, temperature=${temperature}`);
      if (!message) {
        return res.status(400).json({ error: 'Message is required' });
      }

      const grokApiUrl = 'https://api.x.ai/v1/chat/completions';
      const grokApiKey = process.env.GROK_API_KEY;

      try {
        const grokResponse = await axios.post(
          grokApiUrl,
          {
            model: "grok-2-1212",
            messages: [{ role: "user", content: message }],
            temperature,
            max_tokens,
            stream: false,
          },
          {
            headers: {
              'Authorization': `Bearer ${grokApiKey}`,
              'Content-Type': 'application/json'
            }
          }
        );
        res.json({ response: grokResponse.data });
      } catch (apiErr) {
        console.error('Error calling Grok API:', apiErr);
        console.error('Error calling Grok API:', apiErr.response?.data || apiErr.message);
        res.status(502).json({ error: 'Failed to get response from Grok API' });
      }
    });
  } catch (err) {
    console.error('Error in /send-message:', err);
    res.status(500).json({ error: 'Server error' });
  }
});
...

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