Another day, another nasty data breach that could have been easily prevented. On the train ride home from uni I read this article:

No, Panera Bread Doesn’t Take Security Seriously

TLDR: Panera is a large bakery-café chain based in the US. They process orders through their website and keep records of their millions of customers. 8 months ago, a security researcher discovered that their APIs were completely open and unsecured. This allowed anyone to easily crawl their entire database and export sensitive user information, including CC info and addresses. The researcher notified them multiple times but Panera didn't take the reports seriously. Yesterday, news about these security issues spread like wildfire, it wasn't great for Panera's reputation. Their PR strategy was half assed to say the least. The lesson? Secure your APIs... and if someone tells you that you made a mistake, listen and take action, ASAP.

How do I secure my APIs?

I'll split this post into the following chapters:

  • Client side authentication with Oauth (Auth0) in React
    • Oauth Summary
    • Login Callback
    • Client side authentication function
  • Send secure requests from React to an API server
  • Validate the requests on the server
  • Test all api endpoints

Client side authentication with Oauth (Auth0) in React

Oauth Summary

react-node-auth-flow-2

This (simplified) diagram shows the authentication flow. When a user wants to log into my web application, he gets redirected to Auth0's login screen. After successful authentication, the user gets redirected to my web application with an access token. My react web application saves the access token in local browser storage, and uses it to query the authentication server for additional information about the user, such as public profile picture, email address etc.

Login Callback

When a user has successfully logged into my Oauth provider, he gets redirected to a defined callback URL. I handle this url in my client/routes.js.

import { Redirect, Route, Router, Switch } from 'react-router-dom';
import Auth from './utils/Auth';
...
const auth = new Auth();

const handleAuthentication = ({location}) => {
  if (/access_token|id_token|error/.test(location.hash)) {
    auth.handleAuthentication();
  }
}
...
export const makeMainRoutes = () => {
    return(
        <Router history={history}><div><Switch>
        ...
          <Route path="/callback" render={(props) => {
            handleAuthentication(props);
            return <Callback {...props} />
          }}/>
    ...
    );
};

Client side authentication function

This is what a simplified version of my handleAuthentication() function looks like
client/src/utils/Auth.js

  handleAuthentication() {
    this.auth0.parseHash((err, authResult) => {
      if (authResult && authResult.accessToken && authResult.idToken) {
        this.setSession(authResult);
        history.replace('/');
      } else if (err) {
        history.replace('/');
        console.log(err);
        alert(`Error: ${err.error}. Check the console for further details.`);
      }
    });
  }

setSession() just stores user tokens and scopes in local storage

  setSession(authResult) {
    // Set the time that the access token will expire at
    let expiresAt = JSON.stringify(
      authResult.expiresIn * 1000 + new Date().getTime()
    );
    // If there is a value on the `scope` param from the authResult,
    // use it to set scopes in the session for the user. Otherwise
    // use the scopes as requested. If no scopes were requested,
    // set it to nothing
    const scopes = authResult.scope || this.requestedScopes || '';

    localStorage.setItem('access_token', authResult.accessToken);
    localStorage.setItem('id_token', authResult.idToken);
    localStorage.setItem('expires_at', expiresAt);
    localStorage.setItem('scopes', JSON.stringify(scopes));
    // navigate to the home route
    history.replace('/home');
  }

When a user wants to access a route in my client app that requires priviledges, I first check his access token before rendering the component. If his access token looks valid and contains the correct scopes, I render the appropriate component, otherwise, I display an error message.

client/routes.js

...
        <Route path="/item/:id" render={(props) => (
            !auth.isAuthenticated() || !auth.userHasScopes(['read:items']) ? (
              <Redirect to="/" error="unauthorized"/>
            ) : (
              <Item auth={auth} {...props} />
            )
        )}/>
    ...
    </Switch></div> );
}

I propagate the authentication class to the rendered component, and do additional security checks there. My client side authentication class contains functions to fetch an access token from local storage, to evaluate wether a user is authenticated, and to read his access scopes from his access token.

client/src/utils/Auth.js

getAccessToken() {
  const accessToken = localStorage.getItem(ACCESS_TOKEN_KEY);
  if (!accessToken) {
    throw new Error('No access token found');
  }
  return accessToken;
}
...
isAuthenticated() {
    // Check whether the current time is past the
    // access token's expiry time
    let expiresAt = JSON.parse(localStorage.getItem('expires_at'));
    return new Date().getTime() < expiresAt;
}
...
userHasScopes(scopes) {
    const grantedScopes = (JSON.parse(localStorage.getItem('scopes')) || '').split(' ');
    return scopes.every(scope => grantedScopes.includes(scope));
    //return grantedScopes;
}

Send secure requests from React to an API server

The component needs to call an api function to request data from my server. If all goes well, the component state is updated with the response and re-renders with data from the server.

client/src/components/Item.js

import { getItemById } from '../../utils/api';
...
class Item extends Component {
  getItem() {
    var url = this.props.location.pathname.split("/");
    var id = url.pop();
    getItemById(id).then((item) => {
      this.setState({ item });
    });
  }
  componentWillMount = () => {
      this.getItem();
  }
  ... //a bunch of redacted item related functions
  render() {
      return(
          {this.state.item && <ItemComponent data={item}/>}
      );
  }

My api helper class contains my secure api calls. Each api function sends an axios request to my API server, with an Authorization header, containing the Bearer access token obtained while authenticating.

client/src/utils/api.js:

import {getAccessToken} from './Auth';
const BASE_URL = '/api';
...
function getItemById(id) {
  const url = `${BASE_URL}/item/single/${id}`;
  return axios.get(url, { headers: { Authorization: `Bearer ${getAccessToken()}` }}).then(response => response.data);
}

Validate the requests on the server

On the server side, I define my routes, but inject middleware into every router request. The authCheck middleware validates that the access token is coming from a properly authenticated user, while the checkReadScopes middleware checks the access scopes from the token. If a user were to tinker with their token and attempt to grant scopes to himself, this jwtAuthz() function would catch that, because his signature would no longer be valid. This can for example prevent a normal user from accessing routes that should only be accessible to administrators.

server/item.js

var express = require('express');
var mongodb = require('mongodb');
const {ObjectId} = require('mongodb');
const jwt = require('express-jwt');
const jwtAuthz = require('express-jwt-authz');
const jwks = require('jwks-rsa');
var router = express.Router();

var authCheck = jwt({
    secret: jwks.expressJwtSecret({
        cache: true,
        rateLimit: true,
        jwksRequestsPerMinute: 5,
        jwksUri: process.env.AUTH_URI // your Oauth provider's .well-known/jwks.json uri
    }),
    audience: process.env.AUTH_AUDIENCE, // your Oauth provider's audience
    issuer: process.env.AUTH_ISSUER, // Your Oauth provider
    algorithms: ['RS256']
});
...
const checkReadScopes = jwtAuthz([ 'read:items' ]);
...
router.get('/single/:id', authCheck, checkReadScopes, function(req, res, next) {
  query = {"_id": ObjectId(req.params.id)};
  req.mongoDb.collection("items").findOne(query, function(err,results){
    if (err) {
        handleError(res, err.message, "Failed to fetch");
    } else {
        res.status(201).json(results);
    }
  });
});

Test all api endpoints

I highly reccommend Postman for all API debugging purposes. We can also just use the good old command line interface and CURL.

You should try curling all of your api endpoints. Example:

curl https://mywebapp.io/api/items/single/_id

Output:

{
    "name": "UnauthorizedError",
    "message": "No authorization token was found",
    "code":" credentials_required",
    "status": 401,
    "inner": {
        "message":"No authorization token was found"
    }
}

This is good. The API just returns an error if an access token is missing.

Additionally, if there is an active access token, but it has been tinkered with to fake a priviledge scope, you'll get an error about the token signature being invalid. Great!