Securing your API (the wrong way)

Explore API security beyond simple request authentication. Discover why methods like passing usernames and passwords directly to an API expose vulnerabilities. Learn about secure alternatives like Bearer Tokens that offer robust protection and flexibility for modern applications.

21 days ago   •   8 min read

By Stephen Rees-Carter
Table of contents

We need an API!” your boss suddenly says to you one Friday afternoon. “It needs to return XYZ data and let customers do ABC. I’ve already promised it to a client and it needs to be ready to go next Wednesday!” she adds before heading out the door, and you’re left with a sinking feeling that you’ve lost your weekend… Over the next few days you smash out an API, getting all the data going in and out, and by Wednesday morning you’ve got something working. You’re wrapping up the project and suddenly remember: you forgot authentication! What do you do now??!!

For some of us, this isn’t an unrealistic scenario. Tight deadlines and unexpected projects can lead to overlooked security features, and this is how many vulnerabilities find their ways into applications. Especially when you’re dealing with technologies you’re unfamiliar with! When you’re pressed for time, you’ll often reach for the familiar, even if it’s less robust and less secure, because you know you can get it working faster.

So let’s put ourselves in the shoes of our overworked developer, hurriedly building an API, and work through the various options they could use to implement authentication to secure their API. Starting with the worst (well, second-worst - the worst would be no security at all!), and working towards some really robust solutions.

While we’re only covering API Authentication specifically in this article, there is a lot more to API Security than just authenticating requests. To learn more about these aspects, and check if your APIs have any weaknesses you need to address, check out Treblle’s API Security Auditor.

Passing the Username & Password Directly

The simplest (and worst) solution to authentication with an API is to pass the username and password directly to the API. This is often done with the theory that they already identify the user for normal requests, so they can be used in the API as well for the same purpose.

I’ve seen a few variants of this approach to passing credentials directly:

Query Parameters

GET /projects?username=<username>&password=<password> HTTP/1.1Host: api.example.comUser-Agent: YourApp/1.0

Passing anything sensitive as query parameters is problematic from a security point of view. URLs are stored in access and error logs, caches, proxies, etc, making them easy to discover and usually require lower access privileges when compared to your database. You’ll also be effectively storing passwords in plain-text, which is always a bad idea…

POST Parameters

POST /projects HTTP/1.1
Host: api.example.com
User-Agent: YourApp/1.0
{
  "username": "<username>",
  "password": "<password>",
}

The next logical step is to pass the username & password via POST parameters. This gets them out of the URL and access logs, but you’re limiting your API to non-GET requests. There is still the risk of payloads being logged or stored, which would expose the plain-text password.

This is still not a good approach and causes more problems than it solves

Headers

POST /projects HTTP/1.1
Host: api.example.com
User-Agent: YourApp/1.0
Username: <username>
Password: <password>

So if we can’t use Query or POST parameters, what about headers? 

Headers avoid a number of the problems we’ve just encountered, but they still require sending plain-text passwords on every single request. This significantly increases the risk of these credentials being stolen - they could be logged or cached somewhere without adequate protections, or a privileged attacker could intercept the request and record the credentials before it reaches the API.

What About Basic Authentication?

You’ve probably heard of Basic Authentication for HTTP requests - it’s that weird authentication system that causes the browser to prompt the user for their username and password before the application loads. It’s often used for private or staging sites as an authentication layer in front of the application - the user cannot get through to any part of the app without the Basic Authentication credentials.

If you’re unfamiliar with how Basic Authentication works, here’s an example. See if you can figure out why this isn’t any better than our previous options:

GET /projects HTTP/1.1
Host: api.example.com
User-Agent: YourApp/1.0
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=

(Here’s a hint: base 64…)

Yep, that’s right… Basic Authentication is literally just a Base64 encoded string that contains the raw username & password. So all you’re doing is obfuscating the plain-text password in an easily identifiable and decodable format.

> base64_encode('username:password');
= "dXNlcm5hbWU6cGFzc3dvcmQ="

Ultimately, we’ve still got the same problem, so it’s time for something different!

A Note About Passwords & API Authentication 

So none of the options up to this point have been suitable because all of them rely on sending the password, but the question is why? Why shouldn’t we use passwords for API Authentication?

Firstly, users often reuse passwords across different apps, so if an attacker can steal working username-password credentials from your API, then they may be able to use them to login to the user’s other accounts in different apps. Given the best practice of storing passwords hashed and not passing them around, we want to use plain-text passwords as little as possible, to reduce the risk of them being stolen.

Secondly, users often use terrible passwords, while APIs don’t support Multi-Factor Authentication (MFA) and are designed for automated interactions. These three combine together to make it incredibly difficult to defend against brute force attacks and credential stuffing attacks.

In addition, handing over account credentials gives anyone who needs access to the API full access to the account too. There would be no way to limit permissions or access to sensitive areas like account management or billing. This is particularly of concern when you need to provide API credentials to multiple third-party applications, so they can access each other.

As I said, it’s a terrible idea to use passwords in your API Authentication, so let’s move on!

Its Time For Bearer Tokens

Basic Authentication does get one thing right: it keeps authentication separate from the request data by putting it in a common header. This allows you to send and receive any data through your API without having to consider where authentication fits into the request itself. You can also add in the header separately to building the API requests and handling responses.

In addition, you’ll notice that Basic Authentication uses a generic header name, Authorization, and a prefix of Basic. This allows the API to handle multiple authentication types easily without needing to mess around with multiple headers, and is a format we can follow with other, more robust authentication systems, such as Bearer Tokens.

Quick side note: Authentication and Authorization are annoyingly similar names for related concepts. Authentication is verifying who the user is, while Authorization is verifying they are allowed to do what they want to do. With this in mind, we’re technically using the Authorization header it to tell the API who we are (i.e. Authentication), and then the API itself checks Authorization before responding to the request. But the convention is to call the header Authorization, so let’s continue.

So, what is a bearer token?

At its simplest, a Bearer token is a random unique string that uniquely identifies the user making the request, which you’re probably thinking sounds a lot like a username and password, rolled into one! You’re not wrong there, but there are a few key differences.

Usernames & passwords are user-generated, which makes them predictable and non-unique, while a 40-character random string you generate using a cryptographicly secure algorithm is practically impossible to guess and brute-force. 

If Bearer tokens are stolen, they’ll only provide access to the API they were generated for. You can also easily generate multiple tokens for each account - allowing users to rotate and revoke tokens without their login credentials being at risk.

For most APIs, Bearer tokens are a great solution.

GET /projects HTTP/1.1
Host: api.example.com
User-Agent: YourApp/1.0
Authorization: Bearer d43hQ8XrE6Cvvl4z28KcikNY0WLet9jfrxN4NTTB

Generating Bearer Tokens

Bearer tokens can either be a completely random string, or something meaningful and decodable. Most apps just need the former, so let's take a look at that first.

To generate a basic Bearer token, use your favourite random string generator to make a  string of alpha-numeric characters of sufficient length. 32+ characters is a good number to start with.

For example, here’s how I’d do that in PHP using a randomness package I built:

> use Valorin\Random\Random;
> Random::token(length: 40);
= "d43hQ8XrE6Cvvl4z28KcikNY0WLet9jfrxN4NTTB"

Next, store that in your DB, assign it to a user, and when API requests come in, find the user with a matching token and let them in.

Note, if you’re worried about the token being stolen from your DB, you could use an unsalted hash or encryption to store a repeatable representation of the token. You’d then repeat the hashing or encryption when an API request comes to find the matching token.

Something else you could consider is to add a prefix, to help users identify the token as belonging to your API. This is something GitHub does with their tokens, they use a three-letter prefix that identifies both GH as the origin, and the type of token. For example, ghp_* for personal access tokens and gho_* for OAuth tokens.

Decodable Tokens

If you need your tokens to be decodable by third-party apps or different components within your ecosystem, you can use JSON Web Tokens (JWT) as your Bearer tokens. 

JWTs provide a mechanism for encoding verifiable data within a token, so rather than passing around a random string of characters that needs your database to be linked back to a user, you’re passing around a cryptographically secure string which can be decoded and read by third-party systems, identify the user and their privileges, while at the same time being resistant to tampering so the details cannot be changed.

What About OAuth?

OAuth is a much bigger topic than we have time for here, but to provide a really quick summary: OAuth provides a method for authenticating users, in order to issue them access tokens. Once the access token has been issued, the user uses that for all further communications.

In other words, OAuth is used to authenticate the user (often with a username & password), and then a Bearer token is issued for the API. We’ve already talked about Bearer tokens, but it raises an interesting point to clarify:

OAuth can issue Bearer tokens as part of the authentication process, so the user can access an API automatically, following the authentication flow. 

An alternative approach is to authenticate the user via a normal web session and then let them generate Bearer tokens within the UI. Both are valid approaches and depend on your app and how it works.

What About Pre-Shared Keys?

And we’re back to potentially dangerous waters… 

Pre-Shared Keys are a common secret that is known on both sides of an API, the client and the server, which perform the role of authentication. If the client presents a valid Key, the server will respond to their requests.

This approach is fine if you control both sides - such as within an internal network or between different services. The problem arises when the Key leaves your control, or enters a domain where a user can access it somehow. 

Consider javascript apps or native apps - both rely on code running on the user’s device, which then exposes everything in the code to the user. If you’re using a Pre-Shared Key to authenticate API requests from native client apps - the user can decompile the app (or just intercept the API requests), extract the Key, and then make their own requests.

In short: you need to be careful going down this route!

What To Do Next?

Hopefully by this point you’ve found an authentication system for your API, and you’re confident only authorised users are able to make requests and access information. What next?

A good place to start is to use Treblle’s API Security toolkit to audit your API. It’ll tell you any areas you need to tighten up and highlight potential weaknesses for you to look into. One common weakness to get you started is to check you’re not leaking sensitive data from your API. It’s really easy to return far too much data, especially if you’re in a hurry!

Summary

If you remember one thing from this article, remember that Bearer Tokens are your friend.

They solve a bunch of the problems around passwords and APIs, and are also incredibly flexible around different token needs. Although it can feel like a lot of work to generate tokens, you don’t need anything fancy - just generate a random string and you’ll be right!

As your app grows and your requirements become more complicated, you may need to reach for JWTs or OAuth, but make sure your starting point is at least a Bearer token and you’ll have a solid basis for your securing your API.

I also hope the next time you look at a Basic Authentication header, you’ll reach for a base 64 decoder…

Spread the word

Keep reading