These days, the most standard way to secure APIs is via access tokens, which use the JSON Web Token (JWT) format. Although there are many online tutorials about receiving and validating JWTs, these do not usually convey how to manage security on a larger scale. In this article, I will explain how to scale the use of JWTs to real-world backend platforms, where there are many APIs and clients.
OAuth Security Framework
The modern standards for protecting data in APIs, and securing the clients that call APIs, are the OAuth family of specifications, which many experts have vetted. These provide many security design patterns that can be adopted to meet customer use cases.
The heart of OAuth is the authorization server, which sits alongside your APIs. It deals with implementing specifications, storing secrets, implementing user and client authentication, issuing tokens, auditing and other security work.
Essentially, you can think of the authorization server as a specialist API that you plug in rather than build yourself. It is best deployed alongside your other APIs so they can interact with it efficiently when required.
Both APIs and the authorization server often connect to sensitive data sources. It is therefore a hosting best practice to place a reverse proxy or gateway in front of these components. Reverse proxies can then run plugins to perform utility jobs such as token translation in a high-performance manner. This ensures quality security while also simplifying the code in your APIs. These can then focus on their primary security responsibility, which is to authorize requests for data. The following links provide further details on these patterns:
Within each individual API, there are three levels to implementing authorization. First, digitally verify received access tokens according to JWT best practices, after which you can trust the data in the token’s JSON payload.
JWT digital verification involves the use of a security library, which will download token-signing public keys from the authorization server, store them in memory and then use them to verify the JWT’s cryptographic signature. The key lookup occurs rarely and does not adversely affect API performance.
The next authorization step is to make entry-level checks using OAuth scopes to prevent obviously invalid requests. Finally, use claims-based authorization to deny or filter access to resources. This can scale to arbitrarily complex business rules.
A key point to understand about JWTs is that they are digitally verifiable and not vulnerable to man-in-the-middle attacks. When dealing with security-related identifiers, include them in the JWT, rather than in headers or URL path segments that could be altered in flight:
There are many ways to route requests between APIs, and one interesting option I have seen is for apps to sometimes use entry-point APIs dedicated to serving that particular client. This might be used to aggregate calls to core microservices, to keep UI concerns out of microservices or to reduce the number of API endpoints exposed to the internet.
In older architectures, authorization often only occurred at the perimeter, such as in the entry-point API above. These days, with cloud-based hosting and potential threats inside the network, it is instead recommended to use JWTs to implement a zero-trust architecture.
Zero trust involves forwarding the JWT to each related API or microservice. Each API must then digitally verify the end-user identity and perform its own authorization based on scopes and claims relevant to that API’s function.
After the JWT has been validated, claims are the main ingredient for authorization. These values often originate from your business data, as explained further in Claims Best Practices. Therefore, it is important that the authorization server allows you to customize JWTs when needed.
Although forwarding JWTs is the most common setup, in some cases, APIs themselves must act as an OAuth client and obtain another access token, whose scopes and claims are better suited to the target API:
The client credentials flow is often the first OAuth flow developers learn about for machine-to-machine communication. Although this seems very simple at first sight, several related OAuth flows should also be considered as part of your security toolbox. The articles on token sharing, JWT assertions and impersonation approaches provide further details. The end result is that each target API can authorize requests based on its own requirements.
These days, when APIs receive commands that alter data, they often perform immediate work and trigger an event that other APIs can subscribe to. This uses an asynchronous messaging system, resulting in simpler and more decoupled API code.
The event may then be processed sometime in the future but may need access to the original identity at the time the event was published. By default, though, it is easy to lose track of identity, introducing a security risk that the consumer receives unverifiable data in a plain message:
This is another case where a different token can be used to deliver a verifiable identity to consumers. The new token should have a long enough lifetime to handle the event but a reduced scope. For more on this topic, see Batch Processing with OAuth 2.0.
Most real-world software platforms will also interact with external APIs from business partners or third-party providers. A good authorization server and an understanding of OAuth standards will also improve your capabilities for this type of “federation.”
One interesting use case is shown below, where a partner user is authorized to sign in to the company’s app. The partner authorization server could then act as an identity provider to authenticate the user according to the partner’s security policy and with familiar credentials. An embedded token from the business partner could then be used by the company’s APIs to call partner APIs.
There are many other possibilities, and once you have integrated OAuth and OpenID Connect into your business, the design patterns used and the features of the authorization server will enable you to scale the security architecture further.
The API flows we have summarized need to be designed from a reliability viewpoint since a temporary failure could occur in one of the components, perhaps due to a communications failure, application bug, JWT expiry or some other reason:
Companies building microservices, where different APIs use different data stores, will already be using techniques to ensure that retries do not duplicate any data. One option when creating data is for the user app to send a generated unique identifier and for this to remain the same if the request needs to be retried from the client. APIs can then use this identifier to check if a resource exists already.
JWT libraries will allow APIs to use a clock skew when validating access tokens, and it is possible to configure this slightly higher for downstream APIs. It is not recommended to rely on expiry times, however, since there can be multiple reasons for a JWT to fail validation. In some setups, this might be caused by an infrastructure event such as a load balancing failover.
Instead, it is recommended to implement standard expiry handling. In this case, retrying from the client is often the most resilient option. This can be managed by following these simple coding rules, which are easy to scale to many components:
- APIs return a 401 HTTP status code when a request fails JWT validation due to a missing, invalid or expired access token.
- API clients handle 401 responses by trying to refresh the access token and then retrying the request once with the new token.
JWT access tokens communicate a digital identity. APIs first verify a JWT using a security library. The token’s scopes and claims are then used to perform the real authorization work. This processing is fast and will scale across many APIs to enable a zero-trust API platform.
Use an authorization server with good extensibility features and a reverse proxy with good support for plugins. Also, ensure that APIs can receive JWTs with custom scopes and claims. You will then have solid security foundations on which to build.
OAuth is best used as a toolbox, where you adapt specifications to your use cases. To help with this, we have provided Curity Resources, which explain security in terms of unique customer use cases. With the right designs, it only requires simple code to secure an API platform.
Feature image via Pixabay