How to whitelist your Next.js serverless deployments on Now

A quick tip on how to use Now to easily deploy your Next.js projects without allowing the whole world to access it

Posted in Tips & Tricks on

I like Next.js.

It allows me to build rich data-driven web applications in React that are generated server-side. What I don't like is setting up servers and installing process managers to run node applications for my development and staging servers. To solve this, the clever people at Zeit have developed Now, a serverless platform where you can deploy your web applications with ease.


Rougly two weeks ago Zeit has released Next 8. With it comes support for serverless deployments using lambdas. A new feature I'd love to use more, where it not for one small problem. I want to shield my web applications from the public world while I'm still working on it. A very common workflow is to use protected development and staging servers before pushing changes to production servers. Unfortunately, that's something the Next/Now.sh combination is not facilitating at this moment.


The way Now handles security, is by prefixing your project name with a random unique identifier. For instance, if your project is named my-hidden-project, you can access your deployment on a url like http://my-hidden-project-**kdoe2jfh2**.now.sh

"The UID is derived from a large cryptographically random number encoded as 9 alphanumeric characters. However, if you want additional privacy, you can also replace the .now.sh suffix with any custom domain of your choice, making the URL 100% unguessable, so that not even the very existence of the deployment can be verified by third parties."


This sounds like security through obscurity to me. That's simply not good enough. I want to be 100% certain that I can deploy a web application and that it's only viewable by me or my team members, even if it has a predictable URL.


I've googled around but I could not find a solution to this problem. So, here's my hacky solution to this serious problem which I hope gets solved in a native way soon.

Checking for visitor IP address in your Now Lambdas


When running a Next.js project with a serverless target in the Next.js Builder (@now/next), it passes the request and response objects, or [IncomingMessage and ServerResponse](https://nextjs.org/docs/#serverless-deployment) as the docs call them to your lambdas. It also passes these arguments to the getInitialProps method, which is always executed on the server before the server returns any DOM representation at all.
Here's a barebone example of disallowing everyone to visit your page.

const Page = (props) => {
  return <div>Welcome to next.js!</div>;
};
Page.getInitialProps = async ({ req, res }) => {
  res.statusCode = 403;
  res.end(`Not allowed`);
};

export default Page;


Go ahead and try that out. It's not very useful yet, but it proves you can exit your render early and return a 403 response to the browser. From here, it's a matter of expanding on the example to your liking.


Here's an enhanced version that disallows everyone by default, and whitelists a few IP addresses.

const Page = (props) => {
  return <div>Welcome to next.js! Your IP address is: {props.ip}</div>;
};
Page.getInitialProps = async ({ req, res }) => {
  // Disallow access by default
  let shouldDisallowAccess = true;

  // Specify IP's that you want to whitelist. This could be an office/VPN IP address.
  const allowedIps = ['192.168.100.100', '192.168.100.101'];

  // Read the request objects headers to find our who's trying to access this page
  var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;

  // This if statement is only valid if the app is deployed through Now. It gets ignored on other environments.
  if (process.env.NOW_REGION) {
    // Check if the visitors IP address is allowed
    if (!allowedIps.includes(ip)) {
      shouldDisallowAccess = false;
    }
  }

  // Return the 403 status code
  if (shouldDisallowAccess) {
    res.statusCode = 403;
    res.end(`Not allowed for ${ip}`);
    return;
  }
  // This object gets injected into the Pages props, if you want to use them
  return { shouldDisallowAccess, ip };
};

export default Page;

Update


Tim Neutkens expanded on the idea and suggested I could use HTTP Basic Auth as well. That way, whenever a visitor is not in the allowed IPs whitelist, you could fallback to a username/password authentication box. Ideal for sharing your deployments with remote workers, clients and other stakeholders.


Because I'm lazy, I'm not going to write the logic for authorization myself, so I've added in a depenency on the basic-auth package.


In order to keep the logic re-useable and out of the way of my pages, I've abstracted away the authentication logic in a new helper file

import useAuthentication from '../lib/useAuthentication';

const Page = (props) => {
  return <div>Welcome to next.js!</div>;
};
Page.getInitialProps = async ({ req, res }) => {
  const securitySettings = useAuthentication(req, res);
  return securitySettings;
};

export default Page;
const auth = require('basic-auth');

const checkAuth = (req) => {
  var user = auth(req);
  if (user && user.name === 'user' && user.pass === 'password') {
    return false;
  }
  return user.name;
};

const useAuthentication = (req, res) => {
  let shouldDisallowAccess = false;
  let username;

  // Whitelist these IPs
  const allowedIps = ['x.x.x.x', 'x.x.x.x'];
  var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;

  // Only check security when deployed through Now
  if (process.env.NOW_REGION) {
    // Disallow access when you are not whitelisted
    if (!allowedIps.includes(ip)) {
      shouldDisallowAccess = true;
      // Check if the visitor sent a user/pass combination with basic auth
      username = checkAuth(req, res);
      // If we found a valid user, allow access again
      if (username) {
        shouldDisallowAccess = false;
      }
    }
  }

  // Disallow access, and give the visitor a chance to enter their credentials with basic auth
  if (shouldDisallowAccess) {
    res.statusCode = 401;
    res.setHeader('WWW-Authenticate', 'Basic realm="My protected Next app"');
    res.end(`Not allowed for ${ip}`);
    return;
  }
  return { shouldDisallowAccess, username };
};

export default useAuthentication;

And there you go. If the user is whitelisted, they will never see a login screen. If their not whitelisted, they still have a chance to log in through a username and password combination.


Of course, this is a very basic example and I'm sure there's some security issues here. Take care when implementing this in your lambda deployments!

Alternative solutions

  • If you don't want to use lambdas, it should be possible to use the Next.js middleware and wrap it in your own server. I haven't tried it yet, but I imagine it would be easy to add basic authentication for disallowed IP addresses with this solution.
  • If you have control over your own custom domain, it should be possible to point it to Cloudflares DNS servers, and use their firewall to only allow access from specific IP addresses or even IP ranges. That way it should be possible to have a single source of truth of protection - for all projects under that domain - without having to call the getInitialProps method on every page. Of course you lose the benefits of letting Now manage your domain, but do you really need that for your development servers?