Guide to build a secure REST API in Nodejs

profile-picture
Vinayak ShindeSoftware Engineerauthor linkedin
Published On
Updated On
Table of Content
up_arrow

Security with Node.js

  • Secure Communication: Node.js supports TLS/SSL for encrypted communication and provides a robust crypto module for encryption, hashing, and secure operations, ensuring data integrity and confidentiality.

  • Resilience to Attacks: Its non-blocking, event-driven architecture efficiently handles high request volumes, mitigating risks like Denial of Service (DoS) attacks and enhancing application reliability.

  • Input and Policy Enforcement: Node.js enables strict input validation, sanitization, and content security policies through headers, preventing injection attacks, cross-site scripting (XSS), and other vulnerabilities.

  • Hardened Deployment: Applications can be secured by running as non-root users, using reverse proxies like Nginx, employing containerization for isolation, and staying updated with frequent patches to address emerging vulnerabilities.

nodejs_image

What is REST API

  • A REST API (Representational State Transfer Application Programming Interface) is a way for applications to communicate over the web using standard HTTP methods like GET, POST, PUT, and DELETE.
  • It adheres to REST principles, focusing on stateless communication and structured resource representations.
SVG icon for RESTful API

What security provides

In the context of building a secure REST API in Node.js, data integrity, confidentiality, and availability are critical components of a secure system.

  • Data Integrity: Ensuring the data is accurate, consistent, and free from unauthorized modifications during transmission or storage
  • Confidentiality: Protecting sensitive data from unauthorized access during storage and transmission.
  • Availability: Ensuring the API and its services are available when needed, even under adverse conditions.
security

Create a simple express app

Make directories and install express

mkdir my-express-app
cd my-express-app
npm init -
npm install express


Create main application

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

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

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


Run the application

	node app.js


Methods to Secure

Step 1: Add HTTPS

  1. Install an SSL Certificate
    • Use a provider like Let's Encrypt for free SSL.
    • In development, you can generate a self-signed certificate with openssl.
  2. Configure HTTPS in app.js:
    const https = require('https');
    const fs = require('fs');
    const express = require('express');
    const app = express();

    const privateKey = fs.readFileSync('path/to/key.pem', 'utf8');
    const certificate = fs.readFileSync('path/to/cert.pem', 'utf8');

    const credentials = { key: privateKey, cert: certificate };

    https.createServer(credentials, app).listen(3000, () => {
    console.log('Secure server running at https://localhost:3000');
    });


Step 2: Use Authentication

Authentication ensures that users are who they claim to be.


Option 1: Use JWT (JSON Web Tokens)

  1. Install dependencies:
    npm install jsonwebtoken bcrypt
  2. Create and validate JWTs
  • Generate JWT after user login:
    const jwt = require('jsonwebtoken');
    const secret = 'your-secret-key';

    app.post('/login', (req, res) => {
    const user = { id: 1, username: 'testUser' }; // Validate with DB
    const token = jwt.sign(user, secret, { expiresIn: '1h' });
    res.json({ token });
    });

  • Protect routes using middleware:
    function authenticateToken(req, res, next) {
    const token = req.headers['authorization']?.split(' ')[1];
    if (!token) return res.sendStatus(401);

    jwt.verify(token, secret, (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
    });
    }

    app.get('/protected', authenticateToken, (req, res) => {
    res.send('This is a protected route');
    });

Option 2: Use Session-based Authentication

  1. Install dependencies:
    npm install express-session connect-mongo
  2. Set up session storage:
    const session = require('express-session');
    const MongoStore = require('connect-mongo');

    app.use(session({
    secret: 'your-secret-key',
    resave: false,
    saveUninitialized: true,
    store: MongoStore.create({ mongoUrl: 'mongodb://localhost:27017/sessions' }),
    cookie: { secure: true, maxAge: 60000 }
    }));
  3. Use the session:
    app.post('/login', (req, res) => {
    // Validate credentials
    req.session.userId = user.id;
    res.send('Logged in');
    });

    function isAuthenticated(req, res, next) {
    if (req.session.userId) return next();
    res.sendStatus(401);
    }

    app.get('/protected', isAuthenticated, (req, res) => {
    res.send('Welcome to a secure route!');
    });


Step 3: Implement Data Validation

Use libraries like express-validator to validate input:

Install:

npm install express-validator

  1. Validate in routes:
    const { body, validationResult } = require('express-validator');

    app.post('/register', [
    body('email').isEmail(),
    body('password').isLength({ min: 6 }),
    ], (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });

    res.send('User registered successfully');
    });


Step 4: Protect Against Common Vulnerabilities

  1. Install helmet for basic security headers:
    npm install helmet
    Use it in app.js:
    const helmet = require('helmet');
    app.use(helmet());
  2. Sanitize Input to prevent NoSQL Injection:
    const mongoSanitize = require('express-mongo-sanitize');
    app.use(mongoSanitize());
  3. Prevent Cross-Site Scripting (XSS):
    npm install xss-clean
    const xss = require('xss-clean');
    app.use(xss());
  4. Enable Cross-Origin Resource Sharing (CORS):
    npm install cors
    const cors = require('cors');
    app.use(cors({
    origin: 'https://your-frontend-domain.com',
    methods: ['GET', 'POST']
    }));
  5. Rate-limit API requests:
    npm install express-rate-limit
    const rateLimit = require('express-rate-limit');

    const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // Limit each IP to 100 requests per windowMs
    });

    app.use(limiter);


Step 5: Hash Passwords

Never store plaintext passwords! Use bcrypt:

  1. Hash during registration:
    const bcrypt = require('bcrypt');

    app.post('/register', async (req, res) => {
    const hashedPassword = await bcrypt.hash(req.body.password, 10);
    // Save hashedPassword to the database
    res.send('User registered');
    });
  2. Validate during login:
    app.post('/login', async (req, res) => {
    const user = /* Fetch user from database */;
    const match = await bcrypt.compare(req.body.password, user.password);
    if (match) {
    res.send('Login successful');
    } else {
    res.status(401).send('Invalid credentials');
    }
    });


Step 6: Secure Sensitive Data

  1. Use environment variables for secrets:
    npm install dotenv
    Create a .env file:
    SECRET_KEY=mysecretkey
    Use it in your code:
    require('dotenv').config();
    const secret = process.env.SECRET_KEY;
  2. Avoid exposing stack traces:
    app.use((err, req, res, next) => {
    console.error(err);
    res.status(500).send('Internal server error');
    });


Step 7: Monitor and Audit

  1. Install logging middleware:
    npm install morgan
    Use it in app.js:
    const morgan = require('morgan');
    app.use(morgan('combined'));
  2. Monitor for vulnerabilities
  • Use npm audit regularly to check for dependency vulnerabilities.
  • Keep dependencies updated.

Tools

1. Winston: Implements secure and consistent logging.

  • Logs critical events for auditing and troubleshooting.
  • Can mask sensitive data like tokens or passwords in logs.

2. Snyk: Automates security checks for vulnerabilities in dependencies.

  • Integration with CI/CD pipelines for continuous monitoring.
  • Provides patches and fixes for known vulnerabilities.

3. Csurf: Protects against Cross-Site Request Forgery (CSRF) attacks by ensuring requests are legitimate.

  • Generates unique tokens for each session to validate requests.
  • Verifies tokens on POST, PUT, and DELETE requests.

Conclusion

Building a secure REST API in Node.js is essential to protect your application and user data from malicious attacks. By following best practices such as securing communication with HTTPS, implementing strong authentication mechanisms, validating input, and preventing common vulnerabilities like SQL injection and XSS, you can significantly reduce the risk of exploitation.

Additionally, adopting proper error handling strategies, managing sensitive data securely, and regularly updating your dependencies ensures your API remains robust against evolving security threats. Remember, security is not a one-time task but an ongoing process. Stay proactive, perform regular security audits, and keep up with the latest security standards to ensure your REST API remains secure and reliable.

By prioritizing security in every phase of your development process, you'll be well-equipped to build secure, high-performance APIs that users can trust.

Schedule a call now
Start your offshore web & mobile app team with a free consultation from our solutions engineer.

We respect your privacy, and be assured that your data will not be shared