Skip to main content

Securing a REST API

6 min read

Older Article

This article was published 8 years ago. Some information may be outdated or no longer applicable.

The source code for the example application discussed in this article - with setup instructions - is available on GitHub.

Application architecure

Three-tiered architectures have been around for ages. LAMP stack, anyone? More recently, JavaScript has muscled its way across all tiers, and that brings real benefits for lean, rapid application development.

The beauty of separating presentation, application processing and data management is that each tier owns its job. The user interface handles views and workflow. The middle tier holds logic and business rules. The database tier stores data. They talk over HTTP.

Running JavaScript across the whole stack means one data model (JSON) and one execution environment. No wrestling with different data structures or mapping them to domain models.

Custom REST APIs

The middle tier exposes service endpoints. Based on those endpoints, data flows from a database to the client (a browser, say) over HTTP. Node.js and the MarkLogic Node.js Client API make this quick to set up.

But what happens when you want to lock down an endpoint so only authenticated users can reach it? MarkLogic has solid built-in security, but sometimes you need finer control at the middle tier and browser tier.

MarkLogic also has a built-in security module which you can get familiar with by reading this Security Guide.

Authentication

Server-based authentication

Before we talk about securing an API, we need to understand the architecture behind it a bit more.

Picture classic, server-based authentication. HTTP is stateless, so something has to remember who the user is. Normally that’s a cookie: user info gets serialised and deserialised after each request.

Server-based authentication brings problems. Scalability gets messy. Session management gets complicated.

Token based authentication

With token-based authentication, you generate a stateless token. Once you’ve got a valid one, you attach it to any HTTP request as a header. The check on each request is simple: does a valid token exist?

JWT

JWT (JSON Web Tokens, pronounced “jot”) is an open standard defining how to securely transmit information between two parties as a JSON object.

JWT tokens have three parts, separated by dots. The sections are Header, Payload and Signature.

Example

Let’s build a bare-bones example: two endpoints in Node.js pulling data from a MarkLogic database.

In order to setup the project please follow the setup instructions in the README file.

//code snippet
const characters = (req, res) => {
  db.documents
    .query(qb.where(qb.directory('/character/')).slice(0, 100))
    .result()
    .then((response) => {
      let characterNames = response.map((character) => {
        return character.content.name;
      });
      res.json(characterNames);
    })
    .catch((error) => {
      console.log(error);
    });
};

const vehicles = (req, res) => {
  db.documents
    .query(qb.where(qb.directory('/vehicle/')).slice(0, 100))
    .result()
    .then((response) => {
      let vehicleNames = response.map((vehicle) => {
        return vehicle.content.name;
      });
      res.json(vehicleNames);
    })
    .catch((error) => {
      console.log(error);
    });
};

router.route('/api/characters').get(characters);
router.route('/api/vehicles').get(vehicles);

Standard setup for handling HTTP GET requests through the middle tier. You can test it with a tool like Postman.

Securing an endpoint

Let’s lock down one endpoint. We want /api/characters to reject any request that doesn’t carry a valid JWT token.

Creating a token

To hit the secured endpoint, the HTTP request needs a token. As mentioned, a token has three parts: Header, Payload, Signature. In a full three-tiered app, the JWT would be minted when a user registers or logs in. The token then gets stashed on the client (session storage or local storage, typically) and rides along with every subsequent authenticated request.

For our example, we’ll generate a token via a Node.js command line script, pretending someone just logged in.

The GitHub repository for this sample application contains a file that generates JWT tokens. All you need to do is execute node create-token.js which will return you a token.

The script inserts a JSON document into MarkLogic, mocking a user registration.

Note: Never ever store passwords in plain text in any database document. Use a hashing algorithm like the bcrypt-nodejs npm package.

Once the user is “registered”, we generate a token from their info. Specifically, we build a payload containing the username.

You can pack quite a few options into the payload (roles, for instance). Here we also set the token to expire after one hour, so it doesn’t linger indefinitely.

We also need a “secret” to sign the token.

Note: The secret for the token should never be stored inline in the code - ideally it should be stored as an environment variable and it should be a complex set of strings and numbers.

db.documents
  .write({
    uri: '/user/tamas.json',
    contentType: 'application/json',
    content: {
      name: 'tamas',
      password: 'password',
    },
  })
  .result()
  .then((response) => {
    return db.documents.read('/user/tamas.json').result();
  })
  .then((response) => {
    let secret = 's3cr3t';
    let expire = 3600;
    let tokenValue = { username: response[0].content.name };
    let token = jwt.sign(tokenValue, secret, { expiresIn: expire });
    console.log(token);
  })
  .catch((error) => {
    console.log(error);
  });

Running the script spits out a token that looks something like: eyJhbGciOiJI[...]CJ9.eyJ1c2VybmF[...]4OTF9.gGiJD[...]Hg. Copy it. You’ll need it in a moment.

Using the token

Time to lock down the endpoint. Only requests carrying a valid token should get through to the database query in our Node.js middle tier.

We do this with an Express.js middleware. A middleware is just a function with access to the request and response objects. It can run code, modify those objects, and end the request-response cycle.

What we need: a middleware that checks for a JWT, validates it, and (if it’s good) hands control to the next function in the chain. If it’s not good, the request stops here.

Building one takes just a handful of lines:

const authenticate = (req, res, next) => {
  let authHeader = req.headers.authorization;
  if (authHeader) {
    let token = authHeader.split(' ')[1];
    jwt.verify(token, 's3cr3t', (error, decoded) => {
      if (error) {
        console.log(error);
        res.sendStatus(401);
      } else {
        req.username = decoded.username;
        next();
      }
    });
  } else {
    res.status(403).send({ message: 'No token provided.' });
  }
};

First we check for the Authorization header. If it’s there, we grab the JWT and run it through jwt.verify(). Valid token? We attach the decoded username to the request object (req.username) and call next(). Invalid? We fire back a 401.

The last step: wire the middleware into the Express router.

router.route('/api/characters').get(authenticate, characters);

Now, hitting /api/characters with an HTTP GET (and no token) returns a “no token provided” message.

Attach the token via the Authorization header and the data comes back.

Remember you need to add the Authorization header with the following structure: Authorization: Bearer [token] (the token is without the [ ] characters)

Conclusion

MarkLogic ships with excellent built-in security for restricting document access. But sometimes you also need to guard REST API endpoints at the middle tier. In a three-tiered architecture with single page applications, that’s tricky. JWT solves the problem cleanly.