Welcome back! In the previous lesson, you learned how to generate secure API keys, store them safely using hashing, and create an endpoint for users to request new keys. Now that you know how to create and manage API keys, it is time to learn how to use them to authenticate requests to your API.
In this lesson, you will integrate API key authentication into your application using FastAPI dependencies and middleware. You will see how to validate API keys, check their format, compare them securely with stored hashes, and handle expired keys. You will also learn how to combine API key authentication with JWT authentication, giving your API the flexibility to support both methods. By the end of this lesson, you will be able to implement robust API key authentication and understand how it fits into a secure API architecture.
Before diving into the code, let's quickly review what authentication dependencies are in FastAPI and why they are important. In FastAPI, dependencies are reusable functions that can be injected into route handlers using Depends(). They can inspect, modify, or reject incoming requests. Dependencies are the perfect place to handle authentication because they allow you to check credentials before any sensitive logic runs.
API key authentication dependencies are responsible for checking if a request includes a valid API key and, if so, allowing the request to proceed. If the key is missing or invalid, the dependency raises an exception that stops the request and returns an error. This approach helps protect your API from unauthorized access and ensures that only users with valid keys can use your service.
When a client sends a request to your API, they include their API key in the X-API-Key header. Before you check if the key is valid, you should first make sure it is in the correct format. In our example, every API key starts with the prefix pb_ and is followed by 64 hexadecimal characters, making the total length 67 characters.
Here is the code that checks the format:
If the key does not match this format, the authentication function immediately raises an exception. This helps prevent unnecessary database lookups and can stop some simple attacks. Once the format is confirmed, the function removes the pb_ prefix to get the raw key, which is what was originally generated and given to the user. This raw key is what you will compare against the stored hashes.
For example, if the incoming header is:
The authentication function will extract 4f3c2e1a1234567890abcdef1234567890abcdef1234567890abcdef12345678 for verification.
Now that you have the raw key, you need to check if it matches any of the active API keys stored in your database. Remember from the previous lesson that you never store the raw key itself — only a hash of the key, using bcrypt.
To efficiently find the correct API key to verify, you use the lookupHash field. This field must be indexed in the database model to enable O(1) lookup performance:
The index=True parameter creates a database index that allows fast lookups without scanning all rows. Here is how the authentication function uses the lookup hash:
This two-step approach uses the lookupHash for fast O(1) database lookup, then verifies the key securely with bcrypt. If no match is found, the function raises an APIKeyAuthException with a 401 status code. This means the key is either invalid or does not belong to any active user. If a match is found, the request can continue. This process ensures that even if someone gains access to your database, they cannot use the hashes to authenticate because bcrypt hashes are one-way and cannot be reversed.
If the key is missing or invalid, the response might look like this:
Even if an API key is valid, it might be expired. Each key has an expiresAt field, and the authentication function checks if the current date is past this expiration. If the key is expired, the request is rejected with an error message and the expiration date.
For example, if a key is expired, the response could be:
If the key is valid and not expired, the function attaches the user and API key information to the request state. This allows downstream route handlers to know which user is making the request and which key was used. For example, after authentication, you can access request.state.user and request.state.api_key_id in your route handlers. This is useful for logging, rate limiting, or applying user-specific logic.
In many real-world applications, you may want to support both API key and JWT authentication. For example, your web frontend might use JWTs, while third-party integrations use API keys. The authentication function can be designed to check for an API key first, and if none is found, fall back to checking for a JWT in the Authorization header.
Here is how the flexible authentication function works:
This approach gives your API flexibility and makes it easier to support different types of clients. It also allows you to gradually migrate from one authentication method to another or to offer both for different use cases.
In this lesson, you learned how to integrate API key authentication into your FastAPI application using authentication dependencies. You saw how to validate the format of API keys, extract and verify them securely using bcrypt, handle expired keys, and attach user information to requests. You also learned how to combine API key and JWT authentication for a flexible and secure API.
You are now ready to practice these concepts in the hands-on exercises that follow. Remember, on CodeSignal, all the necessary libraries are pre-installed, so you can focus on writing and testing your code. Keep up the great work — these skills are essential for building secure APIs that can support a variety of clients and use cases.
