1. Use key management
Static configuration—associating a specific public key with a client—is the most straightforward configuration, but it’s largely restricted to closed systems where clients are known upfront. Public keys can also be stored in a centralized key store where the API can retrieve them. Adding a new client just requires that you register a new public key with the centralized key store. This approach allows for more flexibility than static configurations since clients do not have to be explicitly known to the APIs.
Many systems use a public key infrastructure (PKI) to handle keys. In a PKI-based system, a trusted central authority confirms the trustworthiness of a client key, typically by issuing an X.509 certificate. The API trusts this authority, so it also trusts certificates issued by this authority. The most well-known deployment of this system is the use of TLS/HTTPS on the internet.
2. Use transport layer security
Most of today’s internet traffic relies on transport layer security (TLS) to provide security guarantees. TLS ensures the confidentiality and integrity of the data being transferred. Additionally, a typical TLS connection also provides authenticity assurances about the remote server that the client is connecting to. For example, when you visit https://www.synopsys.com, TLS helps your browser verify that the server responding is indeed a legitimate server hosting the Synopsys website.
An important component of TLS is that it supports two-way authenticity, meaning that the client is also required to prove its identity to the server. This mode of operation is known as mutual TLS (mTLS). TLS fundamentally relies on public/private key pairs and X.509 certificates, so it is well-suited to implement a key-based client authentication mechanism.
Since TLS is a network layer protocol, applications typically do not implement any of the details in code. Instead, they rely on existing libraries to set up a TLS connection. This pattern is one of the strengths of using mTLS for client authentication: There is little to nothing to implement in the application itself.
3. Use JSON web tokens
JSON web tokens (JWTs) are often used to implement key-based client authentication. The client generates a JWT and signs it with a private key. The API then validates the JWT with the client’s public key and uses the embedded claims to make an authorization decision. Due to the nature of JWTs, there are quite a few degrees of freedom when implementing a client authentication scheme.
For example, RFC 7523 describes how to use a JWT for authenticating a client to the authorization server. This scenario is slightly different from using JWTs to authenticate to an API, but it nonetheless provides a good example of how JWTs handle authorization.
A signed JWT can only be used to call a specific endpoint within the validity window between the issued-at and expiration timestamps. The JWT is typically included in the request using a custom header.
When dealing with multiple clients, it is also recommended to include a key identifier in the JWT’s header. This helps the API retrieve the correct key for this particular client from a key provider. After verifying the JWT signature, the API should ensure that the claim corresponds to the identity used to retrieve the key.
Comparing JWTs and mTLS
So, should you use mTLS or a custom JWT-based client authentication scheme?
Overall, mTLS is well-supported and offers robust security properties. However, because mTLS operates at the network layer, it can be too rigid to guarantee the necessary security properties. For example, server-side middleboxes such as reverse proxies, API gateways, and traffic inspection devices require access to the data of network requests, so they have to terminate the TLS connection. This means the TLS connection is established between the client and the middlebox. After inspection, the middlebox sets up a new connection with the API to forward the request information. However, the presence of the middlebox erodes the security benefits of using mTLS.
A JWT-based authentication mechanism does not suffer from this inconvenience. When using JWTs, the middlebox still handles TLS traffic but forwards the entire request to the API, including the JWT provided by the client. When the JWT contains metadata about the request, it guarantees the request’s integrity, preventing the middlebox from tampering with the request information.
For these reasons, mTLS is recommended for native clients that access server-side APIs that reside in a tightly controlled environment. When the limitations of mTLS impact the end-to-end security of your architecture, the use of JWTs provides a viable alternative.
When to use OAuth 2.0
JWTs and mTLS both have their respective drawbacks. Keeping track of authentication state on the back end makes it harder to scale applications up. Making the back end stateless is beneficial to scalability but reduces control over authentication state, harming revocability.
Additionally, both approaches are designed for first-party scenarios, in which the client and back end trust each other. Implementing such an architecture for third-party clients without sacrificing security can be a challenge, and that’s where OAuth 2.0 comes in.
The goal of OAuth 2.0 is to enable clients to access resources, such as APIs, on behalf of a user. OAuth 2.0 is an extensive framework that enables authorization in complex architectures, both for first-party and third-party scenarios.
In an OAuth 2.0 architecture, the client interacts with the authorization server to obtain an access token. This token represents the client’s authority to access APIs on behalf of the user. The client includes this token on any request to the API, allowing the API to make authorization decisions.
OAuth 2.0 supports two types of access tokens: reference tokens and self-contained tokens. When using reference tokens, the API asks the authorization server to translate the identifier into the associated state, which can be considered a stateful approach. When using self-contained tokens, APIs can handle tokens independently, which corresponds to a stateless approach. OAuth 2.0 also provides control mechanisms to reduce the lifetime of self-contained access tokens, reducing the impact if they are stolen.
Making user-based authorization decisions in APIs requires having user authentication information. Users typically authenticate once, after which their authentication state is propagated on every request. The specific mechanism to track authentication state is highly dependent on your application’s architecture.