Next.js is same-site-origin by default but I can still access it

Viewed 1705

Im wondering how to secure my api routes. The documentation says, that api routes are same-site-origin by default.

API Routes do not specify CORS headers, meaning they are same-origin only by default. You can customize such behavior by wrapping the request handler with the cors middleware. Next.js Documentation

But if I use a requesting tool like Postman for example, I can always call it and get the results:

// Next.js API route support: https://nextjs.org/docs/api-routes/introduction

export default (req, res) => {
  res.status(200).json({ name: 'John Doe' })
}

How is that possible? I want to restrict access to my application only.

2 Answers

The first part of your question is answered by: CORS with Postman.

For the second part, I came up with a very small code utilizing the concept of short-lived access tokens (that too simplified).

// utils/protector.js

let oldToken, newToken;
const checkToken = ({ headers: { 'x-access-token': token } }) =>
  token && (token === oldToken || token === newToken);

const refreshToken = () => {
  oldToken = newToken;
  newToken = Math.random().toString(36).substr(2);
  setTimeout(refreshToken, 3 * 60 * 60 * 1000); // each token is valid for 3 hrs
};
refreshToken();

export { checkToken, newToken as accessToken };
// api/user.js

import { checkToken } from '../../utils/protector';

const handler = (req, res) => {
  // just add this line to the top of your handler
  if (!checkToken(req)) return res.status(403).send();

  // your API logic here...
  res.status(200).json({ name: 'John Doe' });
};

export default handler;

Now in the page you wanna use a protected API:

// index.js

import { useState, useEffect } from 'react';

import { accessToken } from '../utils/protector';

const Home = ({ accessToken }) => {
  const [user, setUser] = useState('Guest');

  useEffect(() => {
    // this will be called from the client
    fetch('/api/user', { headers: { 'x-access-token': accessToken } })
      .then((response) => response.json())
      .then((data) => setUser(data.name));

    return () => {};
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return <div>Hello {user}!</div>;
};

const getServerSideProps = async () => {
  return { props: { accessToken } };
};

export default Home;
export { getServerSideProps };

Any page using a protected route should be server side rendered, else the protector code will be bundled with the client side and will get executed in the browser, resulting in different tokens at the server side (APIs) and the client.


The concept of oldToken and newToken may appear quite unclear. Suppose:

  • A user visits your site at 11:59.

  • Your token gets refreshed at 00:00.

  • They triggered the API call at 12:01.

  • Now if there hadn't been an oldToken, the newToken would have changed before the call, and the response would have been a 403 Forbidden Error.


I have assumed a user takes exactly less than 3 hrs before they take an action on your page that invokes the protected API.

The timeout is set to 3 hrs, even if after a token is valid for twice its time because if the timeout is half (i.e. 1.5 hr) and the (above) user triggers the API call at 01:31. Then oldToken is the newToken, and newToken is newer, both won't match what the user has.

Hence to guarantee that the received token will be valid atleast for 3 hrs, the total time to live is double. It may happen a token is valid for 6 hr (like for the user who visited the site at 12:00).

I found https://github.com/vercel/next.js/blob/canary/examples/cms-contentful to be extremely useful in helping me with this same issue of getting away from using API routes as the main data retrieval for pages, etc. In this example they have their data retrieval code in a /lib/api.js file (not in an API route). Whether retrieving data using a page (https://github.com/vercel/next.js/blob/canary/examples/cms-contentful/pages/posts/%5Bslug%5D.js) or an API (https://github.com/vercel/next.js/blob/canary/examples/cms-contentful/pages/api/preview.js) the data retrieval and db connections are kept out of your API routes. I suspect you can skip using API routes all together if you only want your app pages to access the data.

Here is another very simple sandbox example (but I suggest reviewing the GitHub examples above first):

https://codesandbox.io/s/data-retrieval-outside-api-7zlmn?file=/pages/index.js

Related