There are countless debates on which authentication method is best for securing your API - API Keys or JSON Web Tokens (JWT). The truth is that if you want to provide the best experience for your users (be they internal, external, or both) your API should support both. Allowing your users to choose the authentication method that is best for their situation gives your API the best chance of being successfully adopted by your customers. In this article, we'll explore the benefits and challenges of using both API Keys and JWTs for authentication, and how to implement them effectively in your API.
API Keys
API Keys are the easiest way to secure your API. They are simple, unique identifiers that are generated for each client and are used to authenticate requests. API Keys are typically sent in the request header or as a query parameter. API Key authentication is by far the easiest way for developers to get started with your API, as it requires minimal setup and is straightforward to use - no complicated OAuth flows to worry about, just create a key and make a request.
JSON Web Tokens (JWT)
JSON Web Tokens (JWT) are self-contained tokens that can be validated without needing to query a database or call an authentication server. They contain information about the client and are signed to ensure their integrity. JWTs also typically have an expiration time - this can offer an added layer of security, but adds to the complexity of calling your API.
Using Both API Keys and JWT
The best APIs offer both API Keys and JWT authentication options. This allows developers to choose the method that best suits their needs. For example, you might use API Keys for simple applications or for internal use, while using JWTs for more complex applications or for external clients that require more security.
Challenges of Using Both Methods
While using both API Keys and JWTs can provide flexibility, it can also introduce challenges in terms of implementing and managing authentication logic. With JWT tokens, the subject's identity is embedded directly in the token, but with API Keys you need to look up the key in a database to determine the client. This can lead to increased complexity in your authentication logic and may require additional infrastructure to manage API Keys effectively.
Identifying the User
When using both API Keys and JWTs, you'll end up with two different ways to identify the user. With JWTs you can easily extract the user's identity from the token, but with API Keys you need to call an external system to look up the key and determine the user.
Doing this right can be tricky. You'll need to account for users having multiple API keys so they can be rotated without downtime, store metadata with the keys to determine permissions/scopes, expirations, and more. Most importantly, you'll need to design this system so your API's logic doesn't need to know which authentication method was used - it should just be able to identify the user and their permissions regardless of how they authenticated.
Scopes and Permissions
When authorizing users with JWT tokens, you typically authorize based on the
user's identity (i.e. you get the sub from the token and check if they have
access to the resource). If you use scopes in the JWT token, you can narrow the
access for that particular token (i.e. a token with read:users scope can only
read users, but not write them).
While you could just build a system where the API Key has all the permissions of the user who created it, this isn't how your customers expect API Keys to work - they expect to be able to create API Keys with specific permissions/scopes. Often this is the entire point of API Keys - to allow users to create keys with specific permissions that can be easily revoked if needed. This means you'll need to build a system for managing all of this metadata for API Keys, and ensure that your entire system can handle both types of authentication and authorization.

Multiple Backends and Microservices
In my experience, the biggest challenge with implementing both API Keys and JWTs in your API is that your backend often spans multiple services. Maybe you have a Kubernetes cluster with multiple backends or you have a serverless architecture with multiple functions. In either case, you need to ensure that all of your services can handle both types of authentication and authorization. This can be tricky, especially if you have different teams working on different services. You'll need to ensure that all of your services are using the same authentication and authorization logic, and that they can handle both API Keys and JWTs seamlessly.
Centralized Authentication
The obvious solution is centralization of authentication logic - let one service handle authentication logic and then pass the user information to the other services. This way your backend services don't need to worry about the details of authentication and can just focus on their core logic. This allows a single source of truth for logging, security patching, audit logs, and more. This is the point where an API Gateway makes sense - a single entry point responsible for authenticating all requests and securely passing user information to the backend services in a consistent way.
Implementing Both API Keys and JWTs with Zuplo
Zuplo's API Management platform is the best way to secure your API with both API Keys and JWTs. This article will walk you through how to quickly implement both authentication methods in your API without needing to make major changes to your backend services.
Create your Zuplo Project
There are multiple ways to create a Zuplo project - you can login to the Zuplo Portal where you'll create and build your project or you can use the Zuplo CLI to build your project locally and then deploy it to the cloud.
This tutorial will start with the assumption that you have a Zuplo project created and ready to go. If you don't, follow the links above to get started.
Add API Key Authentication
To add API Key authentication to your Zuplo project, add the
API Key Authentication Policy
to the route or routes you want to protect. This policy will authenticate
incoming requests based on the presence of a valid API Key in a request header
(typically the Authorization header). You can configure the policy to look for
the API Key in a different header or query parameter if needed.
When you add the policy to your route, you will change the default configuration
such that the allowUnauthenticatedRequests property is set to true - this
allows requests that don't have an API Key to be passed through to the next
policy in the chain, which will be the JWT Authentication policy.

API Keys in Zuplo can contain metadata such as the user who created the key, the scopes/permissions associated with the key, and more. This metadata can be used to determine the user's identity and permissions when they authenticate with an API Key. For example, if you wanted to store the same types of data as a JWT token, you might set the metadata to the following:
{
"userId": "12345",
"scopes": ["read:users", "write:users"]
}
Add JWT Authentication
To add JWT authentication to your Zuplo project, add the
JWT Authentication Policy.
This policy will authenticate incoming requests based on the presence of a valid
JWT in the Authorization header. You can configure the policy to look for the
JWT in a different header or query parameter if needed.
Zuplo also offers policies specific for various providers such as Auth0, Clerk, and more. These policies are built on top of the JWT Authentication Policy and provide additional configuration options specific to those providers. If you're using one of these providers, it's recommended to use the specific policy for that provider.
Just as you did with the API Key policy, you will change the default
configuration of the JWT Authentication policy such that the
allowUnauthenticatedRequests property is set to true.

Enforce Authentication
Now you have configured your API to allow both API Key and JWT authentication,
but you still need to enforce that one of these methods is used. By default all
Zuplo authentication policies prevent unauthenticated requests from being passed
to the next policy in the chain. By setting allowUnauthenticatedRequests to
true you have allowed unauthenticated requests to pass through, but you still
need to enforce that at least one of the authentication methods is used.
To enforce that at least one authentication method is used, you will add a simple Custom Policy that checks if the user information is present in the request context. If the user information is not present, the policy will return a 401 Unauthorized response. This policy should be added after both the API Key and JWT Authentication policies in the policy chain.
More information on using multiple authentication policies can be found in the Zuplo documentation.
Using Zuplo's programmability we can easily create a policy that enforces that at least one authentication method is used. Here's an example of how you might implement this policy:
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
export default async function policy(
request: ZuploRequest,
context: ZuploContext,
) {
if (!request.user) {
return new Response("Unauthorized", { status: 401 });
}
}
Additionally, you can also use this custom policy to map the API Key metadata to
the same format as the JWT Token so that later policies don't need to worry
about which authentication method was used. This can be done by accessing the
metadata on request.user.data.
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
export default async function policy(
request: ZuploRequest,
context: ZuploContext,
) {
if (!request.user) {
return new Response("Unauthorized", { status: 401 });
}
if (request.user.data?.userId) {
// User is authenticated with API Key, map the userId from the
// API Key metadata to the sub so that it matches the JWT token format
request.user.sub = request.user.data.userId;
}
}
Send User Information to Backend Services
Now that you have authentication setup on your API Gateway, you need to forward the request on to your backend and include the user information in the request. This allows your backend service to be agnostic to the authentication method used and just focus on the core logic of the request.
To do this, you can either create a new custom policy or just set the header from the existing custom policy that you created to enforce authentication. This header can then be read by your backend service to determine the user's identity and permissions.
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
export default async function policy(
request: ZuploRequest,
context: ZuploContext,
) {
if (!request.user) {
return new Response("Unauthorized", { status: 401 });
}
if (request.user.data?.userId) {
// User is authenticated with API Key, map the userId from the
// API Key metadata to the sub so that it matches the JWT token format
request.user.sub = request.user.data.userId;
}
// Set the user information in a header to be sent to the backend service
request.headers.set("X-User-Id", request.user.sub);
}
Conclusion
By supporting both API Keys and JWTs for authentication, you can provide a flexible and secure API that meets the needs of a wide range of users. While implementing both methods can introduce some complexity, using an API Gateway like Zuplo can help you manage this complexity and ensure that your API is secure and easy to use. With Zuplo's powerful policies and programmability, you can easily implement both authentication methods and enforce that at least one method is used for all requests.
Next Steps
To start building your API with both API Keys and JWT authentication, sign up for a free account and create your first Zuplo project today.
Before you go to production, you'll want to make sure that you have secured your backend to only accept requests from your API Gateway. There are many ways to do this using Zuplo. For more information see the Zuplo documentation on securing your backend.