Secure Your API in Node.js

The OWASP Top 10 2017 RC has two new entries that should be of great interest to any REST API developer.

A4 – Broken Access Control

A10 – Underprotected APIs

Broken access control is a major problem. OWASP rates it as easily detectable, easily exploitable, and widespread across the Internet.

Underprotected APIs refers to the tendency at times for developers to not treat REST APIs that are called from a single-page application (SPA) with the same rigor as a monolithic application.

If you are a backend or REST API developer, there are three key pieces of information you can take away from these common weaknesses:

  1. What is access control and why is it so important?
  2. Why authentication alone is not enough
  3. A practical example of authentication and authorization working together to keep an API protected.

The Sample Application

To assist developers in properly securing their APIs in a practical way, I have developed a small sample application. The source code is available here under the MIT license.

I had fun building this little sample application. Feel free to play around with it or even submit a pull request if you see any issues. After all, I’m here to learn as well.

The sample application is called “Banker” (original, I know) and is a simple banking app. It allows you to request a balance, deposit and withdraw funds, and transfer funds to another account.

There is no UI in this code because this code (and these vulnerabilities) are focused on the API. The client code doesn’t matter much here and any client technology can consume this code.

The purpose of the sample application is to demonstrate sound access control principles in REST API code. Different clients can consume the API. Developers need to make sure that the API is properly locked down to prevent abuse.

The best way to explain these concepts is to watch the code become secure. Let’s take a look at the “vanilla” REST service that needs to be secured.

First Pass – Basic Functionality

The basic functionality is as described. The application allows the caller to deposit, withdraw, and transfer funds as well as request a balance for an account.

MongoDB is the database for this service, so we’ll start with a Mongoose model for the account.

Auth_ZN_Account_Model

As models go, this model is pretty straightforward. It stores the account number and balance. It also exposes some methods that manipulate the balance of the account.

The server.js file is now pretty basic as well. It has the basic routes and middleware required for any REST application using Node.js.

Auth_ZN_server_before_auth

We’ll take a look at how this changes when you add the proper authentication and authorization controls to your Node.js application.

Let’s follow a REST call through my code. This will help to build familiarity and see how we can add complete mediation for this application with minimal changes to the backend code.

The index.js file in the app/bank/ directory implements the main logic of the routes. Let’s use /banker/account/123456 as an example.

The request hits the GET route and is passed into retrieveAccountInfo function exposed by app/bank/index file.

auth_zn_retrieve

This function uses the Mongoose model to search for the requested account number and returns it in the response as JSON.

Right now there is no authentication with this web service. As long as you know the account number, you can withdraw funds, transfer funds, and deposit funds.

Adding Authentication with JSON Web Tokens

The first step in adding authentication is to build the ability to store a user in the MongoDB database. Let’s create a User model using Mongoose.

Auth_zn_user

This user model is using bcrypt to store the user’s password securely. We will be using local authentication for this example as opposed to OAuth.

The next step in implementing security functionality should always be creating a set of security test cases to validate that your application functions properly and securely.

Let’s say that the requirements say that a user must be authenticated in order to perform deposits, transfers, withdrawals, and balance inquiries. We should have these expressed in integration tests that prove our authentication is working.

First we write a test that states that any withdrawal requires authentication and that an unauthenticated user should receive a 401 Unauthorized error. Of course, this test fails at this point.

Auth_zn_fail_auth_test

We need to check first if the user is authorized. If not, then we reject the request with a 401.

The first step in authorizing users is deciding what technology should be used to provide authentication services to our clients. There are several options but for this we will use JSON web tokens.

JSON web tokens (or JWTs) are useful tools when dealing with stateless REST services such as that one we are building. Other frameworks often depend on sessions stored in cookies, which can be troublesome to test and deal with in this case.

JWTs are much simpler but still a secure option. In the Node.js world, there is a ‘jsonwebtoken’ module that makes the creation and verification of web tokens very simple.

In a nutshell, when a user logs in, a token is created for that user by creating a payload and then cryptographically signing it using a secret known to the application. There is also an expiration attached to the token.

When a protected URL is called, it should always verify that a token is present and that it is valid. We can do this with some happy middleware.

In the /app/utils directory, the createToken and verifyToken files provide the basic functionality we need. Let’s take a look at the verifyToken function.

Auth_zn_verify_token

You may notice that we are only checking the request body and headers for the token and not query parameters. The JWT value is just as sensitive as a session id and should never be exposed in a URL.

We can now pass this function in as middleware on our routes to protect them.

Auth_zn_verify_routes

Now our security test passes. A 401 code is returned from the API because the user is not authenticated.

Auth_zn_passing_token_verify

Now is a good time to step back and consider what we’ve accomplished so far.

We started by creating a basic service that allows for functions related to banking including requesting a balance, transferring, depositing and withdrawing funds.

We then protected these services by requiring the client to log into the application. This returns a token that the client then uses going forward to authenticate to the service.

However, we are not yet done. Authentication alone is not enough.

Adding Authorization

There is still a problem with this code that is the basis for the OWASP vulnerability. Let’s illustrate it with a test.

Auth_zn_test_authorized_account

This test creates a test user in the database and that is authorized to use account 123456. It then logs in as that same user and attempts to withdraw from another account.

Auth_zn_unauthorized_account_test_fail

The test fails because it actually allows this to occur.

As you can see, just being authenticated does not mean that the user is authorized to act on any account in the system. With this setup, if you know another account number and login, you can take money out of that account.

I’m not sure, but something tells me that users should not be able to take money out of any arbitrary account.

This example illustrates the fact that it is important to authorize data as well as functionality. It’s not enough to say whether or not a user can withdraw money from an account. You also need to dictate the accounts on which the user is authorized to act.

Let’s add a list of accounts to the UserModel called authorizedAccounts. This will hold a list of account numbers on which the user is authorized to act.

Next, we’ll create a middleware function that will verify that the account number passed in from the client is in the approved list of accounts for a user. This ensures that a user can’t act upon any account.

Auth_zn_authorize_for_account

This middleware simply checks to make sure that the user has the account given in the request in their list of authorized accounts before passing the request on to the main function that handles it.

The username is taken from the decoded token that is added to the request in the previous middleware.

Now our withdraw test is working as expected.

Auth_zn_withdraw_auth_passing

The logic to authorize transfers is slightly different but follows the same principles. You can check out authorizeForTransferAccount.js in the Github repository.

Authorization for functionality specifically can be done with Role-Based Access Control (RBAC). RBAC is the process of defining roles that a user can have and what permissions, or entitlements, that role provides. You then add users to a role to give them the permissions required.

In our case, a bank teller perhaps uses the same API and has access to deposit, transfer, withdraw, and check a balance on any account. A “Teller” role could be created that allows this functionality.

Specifically in the Node world, there is a nifty module called node_acl that provides RBAC capability. It allows the creation of roles and permissions as well as inclusion of these as middleware in Express.

I’ll leave the application of RBAC in our example as an exercise for the reader. Use this code to try out different RBAC libraries and see what you like best.

Lessons Learned

We’ve covered much in this discussion. The application used was just an example to illustrate the main points you should understand about A4 and A10 in the OWASP Top 10. Here is a quick list of what you should take away from this example.

  • Authentication and authorization are not the same thing. They are both needed for a complete solution.
  • Authorization of data is just as important as authorization of functionality. Many companies make the mistake of not securing data and that leads to breaches.
  • Use automated security test cases to ensure that the basic vulnerabilities that are often found are not present in your application.
  • The principles explored here are applicable to any technology stack.

Most attacks that occur are caused by simple, “boring” errors that are easily prevented. These vulnerabilities don’t always get the headlines, but cause most of the day-to-day trouble.

Like combing your hair or brushing your teeth in the morning, these can be simple and mundane, but people will notice if you don’t do it.

So take the time and care necessary to give your code the best foundation in security. It’ll pay off big time down the road.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s