Tuck in your APIs safe and sound with these guiding principles for API security

how to build secure APIs

Headlines about cyberattacks are splashed all over the news, and there’s more attacks that we don’t ever hear about. From a security standpoint, modern software’s reliance on APIs is a double-edged sword. APIs allow us integrate with cloud services and to scale our business more easily. However, APIs also increase our attack surface, by opening up access points through which an attacker can penetrate our environment.

When you build your API, you’re exposing your business to potential vulnerabilities. Hackers gonna hack. Black hat hackers try to ferret out confidential user data or compromise critical business services. Even without any malice, users can unintentionally overwhelm your servers without any notice.

Building secure APIs is the responsibility of every developer, not just the InfoSec team.

Protect your APIs with these guiding principles

7 Guiding Principles for API security

Every year, the Open Web Application Security Project (OWASP) publishes their Top 10 Application Security Risks. Almost all of the risks identified in this list apply to API security.

Validate API requests just as rigorously as you’d vet input to your standard web app. Even though you may think requests are only going to come from your mobile app or a trusted third-party, in most cases, an attacker can send a request directly to your API.
So treat all request data received by your API as untrusted, including headers, and don’t rely only on cookies for authentication, as it may make your API vulnerable to cross-site request forgery (CSRF).
- Clint Gibler, NCC Group

When you’re building your own APIs, keep the following principles for API security in mind.

#1 Validate user input

Validating inputs before piping them through to another system is important for protecting against common attacks like XML external entities (XXE), insecure deserialization, and injections.

The server should validate parameters and payloads against a specific set of expectations. For example, ensuring that input is treated as the appropriate data type, or verifying that the incoming Content-Type header and content are the same.

Besides protecting against external security attacks, this optimization can save unnecessary load on your system. Take, for example, a Node package lookup system. We know that an NPM package can only be 212 characters, and all ASCII. You can quickly return if the lookup is out of those criteria.

#2 Protect your data

Encryption is your friend

Your data is vulnerable while in transit and at rest. Encryption is your friend. Encrypt sensitive data before sending it, before storing it, and use encrypted connections (TLS) to protect your data in transit.

Never include your keys or other sensitive information in your URLs or query parameters. They’re not secure for many reasons. If you plan to send secrets for authentication, try including them in a header. Other sensitive information can be sent as an encrypted payload.

#3 Enforce limits

If a client is submitting too many requests, malicious or otherwise, it can take a toll on your system. Consider enforcing a rate limit to discourage abuse, a distributed denial-of-service (DDOS) attack, or handle an unexpected surge of traffic.

In general, DDoS attacks are perpetrated by large botnets that make many requests simultaneously to the target, such as a web app. Since the server can’t handle all of those requests at the same time, legitimate users cannot use it.
- Clint Gibler, NCC Group

Besides limiting the number of requests a client can send over a certain period of time, there’s other ways to control the load on your system:

  • Amount of data transmitted
  • Number of requests submitted from the same IP
  • Results returned per query
  • Number of concurrent sessions
  • Duration of a session
  • Longevity of an access token

#4 Follow an authentication protocol

Salt and hash your secrets

Follow an authentication protocol that allows the server to verify the user is who they say they are.

Never store passwords in plaintext. Keys, tokens, and other sensitive credentials should be salted and hashed for an additional layer of security in case of a data breach.

#5 Establish an authorization policy

Establish an authorization policy that allows users and apps to gain access to specified methods and resources. For example, determine who is allowed access to create and POST data to the database, and who is allowed read-only access to GET data.

Not everyone should have access to everything. Provide access to resources only as it becomes necessary.

Role-based access controls make it easy to provide and revoke access. Follow the Principle of Least Privilege (PoLP) and allow users and applications to access only the information and resources that are necessary for its legitimate purpose.
— Hamidreza Hamedtoolloei, Intuit

#6 Handle and log errors

HTTP status codes issued by the server are intended to give further details on the nature of the response. It’s helpful to have accurate and informative error codes and messages during debugging. However, you should establish a policy for handling and logging errors consistently so that you don’t unintentionally reveal implementation details or provide clues into how your server or database operates.

Studies show the average time to detect a breach is over 200 days. Logging error messages can alert the team to unusual activity and provide clues about a breach that has occurred.

#7 Test and monitor your APIs

Make sure your APIs are behaving as expected

Finally, a benefit to being paranoid! Security though obscurity, or relying on the secrecy of your system’s design or implementation as the main method of providing security, is never a sound strategy.

Periodic auditing can help identify vulnerabilities and alert you when something is going awry. At scale, you can employ some white hat testing or chaos engineering methods to poke at your API and fortify your system’s resilience.

The difference between Authentication and Authorization

When people say “auth”, do they mean authentication or authorization? There’s not a ton of consensus about it because of how the context varies. Understanding the difference between the two will help you communicate clearly in the appropriate contexts.

As an analogy, say somebody asks where I’m from. If I’m talking to someone in the US, I’ll say that I’m from Chicago. If I’m traveling abroad in Istanbul, I’ll say that I’m from the US.

In the same way, the context matters when talking about “auth”. If the conversation is about building APIs, I probably mean “auth” to encompass both authentication and authorization. If the conversation is about the security of APIs, I should talk about authentication and authorization separately. Saying “auth” would be ambiguous without more context clues.

Authentication is verifying that you are who you say you are. A user would prove their identity by providing information that is known only to the user and the authentication system, like a password or fingerprint.

Authorization is verifying if you have permission to access a resource. An administrator will grant rights to access protected resources. Authorization typically requires authentication as a precursor to providing access to protected resources.

Balancing Usability with Security

Creating a great developer experience is key to driving adoption of your API. There’s a bunch of methods for authenticating your API, and some APIs have multiple ways to authenticate. Security is a requirement for your API, but developers often get stuck at the point where they have to authenticate their request.

User experience and security don’t always go hand in hand. It’s important for developers to learn how to satisfy both requirements.

Balancing usability with security is tricky

A recipe for using JSON Web Tokens (JWT) to authenticate and authorize requests in Postman

As you get started developing ironclad APIs, let’s take a look at how we can use Postman to authorize our requests. In this example, we’ll use JSON Web Tokens to secure and access our API.

What is JWT?

JSON Web Token (JWT) is an open standard for securely transmitting information between parties as a JSON object. It’s pronounced jot, or as our Dutch friends would say, yaywaytay.

JWT is commonly used for authorization. JWTs can be signed using a secret or a public/private key pair. Once a user is logged in, each subsequent request will require the JWT, allowing the user to access routes, services, and resources that are permitted with that token.

Set up an API with JWT authentication

Let’s use this example Node.js API from Auth0 that supports username and password authentication with JWTs and has endpoints that return Chuck Norris phrases. If you already have an API that you’re working on, you can skip this step.

For this example, make sure you have Node.js and the npm package manager installed on your machine. Get started by cloning the repository, install the dependencies with npm install, and then start your server locally with node.server.js.

Run your server locally

Click the Run in Postman button at the bottom of the README file to import the sample Postman collection into the Postman app. If you’re working off your own API, substitute your endpoints for the example included in this Postman collection.

The first request in the collection is a POST request to create user. If you already have a user, use the second request in the collection to create a new session. In both cases, you will see the access token included in the JSON response object.

POST request to create a session

Save the JWT as a variable

You could copy the access token from the response to use in your next request, but it’s tedious to do it for every request you want to authorize.

Instead, let’s save the JWT as a variable so that we can reuse the token over and over again in future requests. Create a new environment. Under the Tests tab, save the access token as an environment variable with pm.environment.set(), and re-run the request.

Save the access token as an environment variable under the Tests tab

Under the Quick Look icon, we can see that our JWT is saved as an environment variable. Now we can use our token in subsequent requests.

JWT saved as an environment variable

Add JWT to headers in Postman

There are 2 ways to send your JWT to authorize your requests in Postman: adding a header or using an authorization helper.

Option 1: add an authorization header

The first option is to add a header. Under the Headers tab, add a key called Authorization with the value Bearer <your-jwt-token>. Use the double curly brace syntax to swap in your token’s variable value.

If your authorization accepts a custom syntax, you can manually tweak the prefix here (e.g. Token <your-access-token> instead of Bearer <your-access-token).

Option 1: add a header

Option 2: use an authorization helper

The second option is to use an authorization helper. Under the Authorization tab, select the Bearer Token authorization type. Use the double curly brace syntax to swap in your token’s variable value.

Option 2: use an Authorization helper

Click the orange Preview Request button to see a temporary header has been added under the Headers tab. This temporary header is not saved with your request or collection.

Option 2: generates temporary headers

What’s the difference between these 2 approaches? The approach you use should depend on how you’re planning to use it.

Option 1: add an authorization header

  • User can tweak the prefix (e.g. Token <your-access-token> instead of Bearer <your-access-token>).
  • Authorization header is displayed explicitly in the API documentation.
  • With both of these options, you can share the request and collection with your teammates. Header is saved with the request and collection under the header property.

Option 2: use an authorization helper

  • Can set authorization at the collection-, folder-, or request-level. Easy to set up the same authorization method for every request inside the collection or folder.
  • With both of these options, you can share the request and collection with your teammates. Authorization is saved under the auth property.

Scripts to check token expiration

JWT tokens don’t live forever. After a specified period of time, they expire and you will need to retrieve a fresh one.

Once again, there are 2 approaches for checking the expiration of your JWT. The approach you use choose will depend on your specific circumstances.

Option 1: Separate request at the beginning of the collection

This option is ideal if you’re working with a small collection that runs quickly, or you have a long-lived token that is not likely to expire by the end of the collection run. In this case, create an initial request at the beginning of the collection to retrieve and store the token. You can use the same token value throughout the remainder of your collection run.

Option 2: Pre-request script to run before each request

Check for token expiration before sending your request

This option is good if you’re working with a large collection that might take a while to run, or you have a short-lived token that could expire soon. In this case, add some logic in a pre-request script to check if the current token is expired. If the token is expired, get a fresh one (e.g. using pm.sendRequest()) and then reset your new token’s time to live. With this approach, remember that you can use a collection- or folder-level script to run this check prior to every request in the collection or folder.

Sessions to keep stuff private

Say that you saved your JWT as a Postman environment variable, and you shared the environment with your teammates because you’re collaborating on a project. Can you keep stuff private, so that your teammates don’t have access to it?

Yes, you can!

Postman gives you the best of both worlds — collaboration with security, so that API keys and passwords don’t have to pass through your network.

Sessions are an additional layer within the Postman app that stores variable values locally. By default, sessions do not sync with Postman servers. Changes captured in the individual session remain local to your Postman instance, unless you explicitly sync to the cloud.

Go to your Settings, and toggle off “Automatically persist variable values”.

Under your Settings, toggle off “Automatically persist variable values”

Now when you send a request and set a variable, the CURRENT VALUE is populated. You can think of this as a value that’s stored in a local session.

Current values are stored in the local session

If you want to share this value with your teammates or sync it to the Postman servers, this requires another step to to explicitly sync to the cloud. To sync all of your Current Values to the Initial Values, click Persist All. To sync only a single Current Value to the Initial Value, copy and paste the value from the 3rd column to the second column.

Persist your local changes and sync to the cloud

Session variables allow you to reuse data and keep it secure while working in a collaborative environment. They allow you more granular control over syncing to the server or sharing information with your teammates. Learn more about sessions or watch a video about working with sessions.

A final thought on building secure APIs

Hackers gonna hack, and sometimes the cyber threat landscape evolves faster than we can keep up.

Protocols and methods that were previously deemed best practices by the experts become obsolete as new vulnerabilities are discovered and new technologies emerge. Developers with a security mindset are the frontline of defense in information security.

Having a security mindset is part of every developer’s job

API security is not the sole burden of the user, and building secure APIs is not the sole responsibility of the InfoSec team. Having a security mindset and healthy paranoia puts a spotlight on potential vulnerabilities and risks during development. Building secure APIs will minimize the exposure of current and future services.