Secure Your APIs (Part 1) : Leakage and Proxying

Photo by Walkator on Unsplash

Secure Your APIs (Part 1) : Leakage and Proxying

API Leakage

Everybody uses APIs in their applications but did you know that your API request might contain sensitive information which you might not wanna share with others, such as API keys/tokens.

API keys/tokens are a way in which you can authorize yourself and make valid API requests. These are passed in mostly headers but also sometimes query params, depends on the API.

const response = await fetch(url, {
    headers: {
        'Content-Type': 'application/json',
        'API_KEY': 'API_KEY_VALUE'
    }   
}

// OR

const response = await fetch(`${url}?&apikey=${API_KEY}`);

This is how a normal get request with headers/params looks like. Notice how your API key is being exposed ? This is why you almost never call APIs in the client side. Well then how would you fetch/post your data ? You guessed it right !, using backend servers.

Proxy Servers

Proxy means letting somebody else handle your task in your absence. Similarly when you create a server for handling tasks on behalf of a client/another server, It is called a Proxy Server. It is a great way to hide sensitive information from the client side and instead manage everything from the server side.

Let me show you how to make a proxy server and make secure API requests.

Setup (Terminal)

Open your terminal and enter the following lines one by one

mkdir proxy-server
cd proxy-server

This will be our root folder.

npm init -y
npm i express cors dotenv axios
touch index.js
mkdir routes
mkdir controllers
mkdir middleware

Now open vs code here

code .

Execution (VS Code)

proxy-server/index.js

const express = require('express');
const cors = require('cors');
const middleware = require('./middleware/middleware');
const joke = require('./routes/joke');

require('dotenv').config();

const app = express();
app.use(cors());
app.set('trust proxy',1); // tells the client to trust this proxy server


const port = process.env.PORT || 5000;

app.use('/api/v1',joke); // custom endpoint managed by "joke" route

app.listen(port, (req,res)=>{
    console.log(`Server running on port ${port} !`);
});

app.use(middleware.notFound); // custome error handling middlewares
app.use(middleware.errorHandler);

proxy-server/routes/joke.js

const express = require('express');
const makeAJoke = require('../controller/make-a-joke');

const router = express.Router();

router.get('/joke', makeAJoke);

module.exports = router;

proxy-server/controllers/make-a-joke.js

const axios = require('axios');

const BASE_URL = 'https://v2.jokeapi.dev/joke/Dark?'; 
// intentionally not hidden

const generate = async (req, res, next) => {
    try{
        const params = new URLSearchParams({
            [process.env.API_KEY]: process.env.API_KEY_VALUE,
            // you can also pass query params if any
        });

        const {data} = await axios.get(`${BASE_URL}${params}`);
        // you can use the same trick for passing your api key in 
        // headers instead of params as well

        return res.json(data);
    }catch (err){
        return next(err);
    }
}

module.exports = generate;

proxy-server/middleware/middleware.js

function notFound(req, res, next) {
    res.status(404);
    const error = new Error(`🔍 - Not Found - ${req.originalUrl}`);
    next(error);
  }

  /* eslint-disable no-unused-vars */
 function errorHandler(err, req, res, next) {
    /* eslint-enable no-unused-vars */
    const statusCode = res.statusCode !== 200 ? res.statusCode : 500;
    res.status(statusCode);
    res.json({
      message: err.message,
      stack: process.env.NODE_ENV === 'production' ? '🥞' : err.stack
    });
  }

module.exports = {
    notFound,
    errorHandler
};

Create a ".env" inside your root folder and follow he below steps

proxy-server/.env

NODE_ENV=development
PORT=5000
API_KEY=type # Just for example
API_KEY_VALUE=single # Just for example

Testing

Great ! Your proxy serve has been successfully set up ! The server you just created follows an MVC (Model View Controller) pattern. Open your terminal again in the root folder and run:

node index.js

Open localhost:5000/api/v1/joke on your browser.

Question

Why not just use .env in the client side ? Because that way the API_KEY is hidden only on the code side, not on the browser, therefore someone can still see your request parameters or headers from the client side.

In this way you only need to make a simple fetch request to your proxy server and the rest will be handled by the server.

const response = await fetch('http://localhost:5000/api/v1/joke');

Now nobody can read your API keys or params from the client side as they are simply "Not there".

Part 2 (Disclaimer)

  • API Validator

  • Rate Limiting

  • Caching

Summary

To summarize, API proxy servers are used to make private requests and implement confidentiality. Your request will no longer be visible on the client side and you can avoid API Leakage. You now also have the option to optimize your API using "Caching", "Polling" or "Validator" techniques (will be covered in part 2).

Thanks !

This was the 1st part of the 2-part blog post. The next part will cover "creating your own Private API Validator*,* Request Rate Limiter and Response Caching Techniques". Thanks for reading it through. I hope you liked it :)

Leave a like if you did and comment your questions/suggestions if you have any. I am open to any discussions !