In part 1 we went over the main components to create a JavaScript weather app with location data. From finding the device location, getting weather data from the internet machine (DarkSky API), and building a basic HTML interface. However, one limitation mentioned was how to keep the DarkSky “secret key” a secret.

In part 2 we will build a simple reverse proxy to hide our key on a server. A reverse proxy is when we redirect resources on behalf of the client from one or more servers. These resources are then returned to the client, appearing as if they originated from the proxy server itself. To create the proxy, we will use Node.js (server-side JavaScript). There are many other approaches, but Node.js allows us to use JavaScript and is one of the most powerful runtimes in use today. And who doesn’t love a little JavaScript!

The team at DarkSky also highly recommends you to protect your key. They feel strongly enough that they disabled cross-origin resource sharing (CORS) as a security precaution.

Your API call includes your secret API key as part of the request. If you were to make API calls from client-facing code, anyone could extract and use your API key, which would result in a bill that you’d have to pay. We disable CORS to help keep your API secret key a secret. To prevent API key abuse, you should set up a proxy server to make calls to our API behind the scenes. Then you can provide forecasts to your clients without exposing your API key. ~ DarkSky FAQ

Yeah, let’s do that… with JavaScript (Node.js)!

Node.js in a nutshell

Node is simply a runtime (environment) that allows us to run JavaScript outside a browser. With Node, we can build web servers, server APIs, web apps, desktop apps, and much more. When running JavaScript server side, you also don’t need to worry about Web Browser compatibility since the JavaScript engine is always the same - and built on Chrome’s JavaScript engine.

Node.js® is a JavaScript runtime built on Chrome’s V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. ~ Node.js

Node.js also uses packages to provide additional functionality, much like modules in Python. Many are included by default, but there are thousands of other great external packages that can help when needed.

For more details on Node.js, there are lots of amazing resources out there. Check out the Node.js website, along with a large library of YouTube videos, and Medium articles should give you an in-depth look into this very popular runtime.

The Proxy

For the proxy, we are going to use 3 modules. We will add a few more optional ones later for some additional security and validation. Dot-Env is also listed here, but could also be considered optional. Dot-Env will keep the key outside of the main code (if you want to post your code to gitHub, but not your key), there are many other ways we could perform that step as well. Here are the main packages:

  • Express: Web application framework that provides a robust set of features for web and mobile applications.
  • Request: Request is designed to make HTTP calls. It supports HTTPS and follows redirects by default.
  • Dot-Env: A zero-dependency module that loads environment variables from a file. This stores the configuration in the environment separate from the code.

The Project Setup

We will use a very simple project structure for this work. A server-side JavaScript file (server.js), and a Dot-Env file (.env) to store our secret key.

The .env file will contain a single variable with the key. The variable can be named whatever you would like - however, no spaces, and all CAPS is recommended. Let’s set this up as “DARKSKYSECRET”. When we are ready to access this variable inside the server application, we just prefix the variable name “process.env.[variable]”. Based on our variable name above: `process.env.DARKSKYSECRET`.

//.env file
DARKSKY_SECRET="abc123"

The Code

To build the reverse proxy server we start by creating the server.js file and add some of the modules we will be using.

//server.js - load the modules
const express = require('express');  
const request = require('request');
require('dotenv').config();
const app = express();

With the modules loaded it’s time to define a route (API endpoint). This endpoint will take the request from the client web application along with URL parameters. Once we have the request on the server, we will add our key and send our own request to the DarkSky API from the server. The response from DarkSky will be handed back to the client.

Reverse Proxy

In the client app, we will change our original code from Part 1 that talks to DarkSky directly - subbing in our server route /darksky/:lat,:long. Both the “:lat” and “:long” keywords are placeholders on the server that will be populated when the client makes a request. A sample request from the client would look like https://myDomain.com/darkSky/-80.123,45.123. The server will interrogate the request and if it matches route name, the request will be forwarded. To access our lat/long variables in the request, we prefix with req.params..

//server.js - setup the route
app.get('/darkSky/:lat,:long', (req,res) => {
    //DarkSky URL we will send the request to
    var dsURL = 'https://api.darksky.net/forecast/';
    //Define a variable pointing to our secret key
    var dsSecret = process.env.DARKSKY_SECRET;
    //Additional DarkSky  URL parameters (optional)
    var dsSettings = '?exclude=minutely,hourly,daily,alerts,flags&units=auto';
    //Build the full DarkSky URL
    var url = dsURL + dsSecret + '/' + req.params.lat + ',' + req.params.long + dsSettings;
    //Send the request and direct the results back to the user
    req.pipe(request(url)).pipe(res);
});
//app starts a server and listens on port 3000 for connections
app.listen(3000, () => console.log('server ready'));

If we were to stop here, this entire exercise wouldn’t be worth the effort. We’ve created a reverse proxy that hides the key, but it doesn’t stop anyone else from using our proxy/routes with external apps. So anyone could leverage the proxy - while DarkSky sends us the bill. We aren’t going to add a full authentication tier, but it’s something to consider for production sites.

Basic protection

With the key hidden, let’s focus on adding some server protection. More could/should be done, and we should consider the below example a bare minimum. To keep this painless, let’s add a few additional modules to do the heavy lifting for us.

  • Helmet: Helps secure your Express apps by setting/blocking various HTTP headers
  • host-Validation: Extends Express (middleware) that protects Node.js servers from DNS Rebinding attacks by validating Host and Referer headers from incoming requests
//server.js - add protection
const helmet = require('helmet');
const hostValidation = require('host-validation');
//configure hostValidation to only accept requests from a specific host () and referer
//  if the following hosts/referers don't match the client, the request will be rejected
app.use(hostValidation({ hosts: ['myDomain.com'], referers: ['https://myDomain.com/weather.html'] }));
//Use helmet's default header rules for added security
app.use(helmet());

Rate Limiting

Rate limiting is used to control the rate of traffic received by the server and is used to prevent DoS attacks. We aren’t going to throttle (slow) each request to our server, just make sure each client doesn’t abuse the server and make thousands of requests per minute (remember, we want to keep this API from spending our beer money). We can set both a timeframe and the maximum number of requests to allow within that timeframe - for each IP requesting a weather update.

//server.js - add rate limiting
const rateLimit = require("express-rate-limit");
//app.enable("trust proxy"); //if already behind a reverse proxy
const limiter = rateLimit({
  windowMs: 5 * 60 * 1000, // 5 minutes
  max: 20 // limit each IP to 20 requests per windowMs
});
app.use(limiter); //apply to all requests

Putting it all together

Here is all the code put together. In total, we will need server.js and the .env file in the root of the project along with all of the modules in the default node_modules folder.

//server.js
const express = require('express');  
const request = require('request');
require('dotenv').config();
const helmet = require('helmet');
const hostValidation = require('host-validation');
const rateLimit = require("express-rate-limit");
const app = express();
app.use(hostValidation({ hosts: ['spatialtimes.com'], referers: ['https://www.spatialtimes.com/weather.html'] }));
app.use(helmet());
const limiter = rateLimit({
  windowMs: 5 * 60 * 1000,
  max: 20
});
app.use(limiter);

//Setup route for DarkSky
app.get('/darkSky/:lat,:long', (req,res) => {
  var dsURL = 'https://api.darksky.net/forecast/';
  var dsSecret = process.env.DARKSKY_SECRET;
  var dsSettings = '?exclude=minutely,hourly,daily,alerts,flags&units=auto';
  var url = dsURL + dsSecret + '/' + req.params.lat + ',' + req.params.long + dsSettings;
  req.pipe(request(url)).pipe(res);
});

const port = 8001;
app.listen(port, () => console.log('server ready'));

Now that our server is ready with the reverse proxy we would just need to change the URL request in the HTML file to call our new endpoint instead of going to the DarkSky API directly.