For backend server applications, getting authentication right for your APIs is a critical component for ensuring the security of your service. It’s also one of the most common API security issues we notice.
Almost all popular web frameworks these days have a concept for authentication built-in that you can use to protect your routes and require a user to be logged in to access it.
However, there is usually more than one way to do this in a given framework. A good start is just making sure that you have standardized your method of authentication and don’t use different techniques for each API or route that you build.
One of the most common security issues that we see when reviewing code is related to doing authentication checks in each route/API.
Should I use a decorator/middleware?
Imagine we are using a hypothetical Python/Django app, but this could apply to almost any language and web framework.
Let’s assume that we have an API that returns the contents of a private repository with a route URL like this:
We would want to use authentication for this API since we shouldn’t be allowing anonymous users to access this private information.
Here’s an example of some ways using Django Python web framework that we can ensure the user is authenticated when accessing the API:
# You can protect a route by checking the request user object def get_repo_contents(request): if not request.user.is_authenticated: return Http401 # else, get repo contents ...
# Or you can protect using a python decorator, @login_required @login_required def get_repo_contents(request): # else, get repo contents ...
If you aren’t familiar with Python decorators, they’re similar to a middleware in other web frameworks (although Django does have a separate middleware concept too). This Python decorator essentially wraps your function and first will verify that the user is authenticated, and if they are not it will not call your route implementation that it’s wrapping.
In this sort of case, I recommend using the second example with the decorator (or middleware in other frameworks) as it involves less code, reducing the chances of accidentally making a mistake.
Check for authentication as early as possible
Using a decorator also protects you from accidentally allowing an action or response before checking the authentication for the request. In general, you should strive to check authentication as early as possible in your request lifecycle.
A common authentication mistake we see looks like this:
def get_repo_contents(request, repo_name): repo = Repo.objects.get(name=repo_name) if not repo: return Http404 if not request.user.is_authenticated: return Http401 # else, get repo contents ...
By delaying the authentication check until after the database query for the Repo, you open yourself up to more surface area for security issues.
First, now a user who is not logged in can call this API guessing the name of a Repo and if they guess correctly and it exists, they will get a 404 Not Found response. Otherwise, they will get a 401 Unauthorized response.
Offering them a different response can leak extra information about your data that could give an attacker a piece of information they need. Once they know this repo name exists, they could then attempt to exploit additional APIs related to this now known private repo name.
Second, this example is making a query to your database before it’s able to reject the request. This may further expose your infrastructure to DoS attacks as usually the authentication check is done using an in-memory caching database like Redis and can reject much quicker than a query to a large persistent relational database.
For example, adding this extra database query may make your worker thread on the server stay blocking/active for five times longer than it would have without it.
In addition, this request would make a query to your database which means that an unauthorized user can affect the query load on your database. So if an attacker were to make a lot of calls to this unauthenticated API from one or thousands of machines, they could potentially take down your site or database due to starvation of resources.
Setting your team up for success with whitelisting
Once you know to look for these authentication issues above, they can be quickly caught in code review by your peers or when using our expert code reviewers. However, there’s another easy authentication step you can take to reduce the chances of someone accidentally exposing your server. By default, all of your routes should require authentication.
This follows the software security principle of whitelisting access vs blacklisting access.
Blacklisting refers to having a default allow-all policy, and only requiring authentication for specified routes. Whitelisting takes the opposite approach in which everything by default requires authentication and you must specifically disable the authentication in cases where you need it.
I wish most web frameworks provided whitelisting as the default option and required you to disable it if you chose. However, it should be doable with your web framework even if it involves some custom middleware.
The basic concept for this would be creating a middleware that runs on all API requests, or on your root API group that enables authentication.
In general, you shouldn’t need many unauthenticated APIs for the average user-based app unless you are creating a publicly browsable service. However, you will usually need at least a “login” API, but often you can use one of that is built into your web framework.
When you do come across an API that you absolutely need to be unauthenticated, then you can provide a special case for it to skip your “always authenticated” middleware.
It may take a little bit of work to convert your authentication to use whitelisting, but once you have it in place, it will be much harder for you or your coworkers to accidentally introduce these type of security issues in the future.