The importance of stateless architecture in authorization systems

Published by James Walker on March 12, 2024
image

Stateless is a software architecture where the application holds no information about its operating state or the conditions in which it exists. This contrasts with stateful applications, which do maintain a persistent state throughout their lifetime.

Going stateless might sound complicated, but it can actually simplify system deployment, maintenance, and integration. This article explores what stateless is, how it's implemented, and why it's so beneficial to real-world systems such as authorization services.

Understanding stateless architecture

The state of a software system refers to the information available when a function is performed, such as the identity of the logged-in user. A stateful service persists this information, while stateless services reestablish the state each time they're used.

The paradigm shifts the responsibility for maintaining state onto the client. For example, in a stateless authorization context, the client should provide the authorization layer with the context describing the user issuing the request and the resource they’re trying to access. The service can then use this information to deliver an authorization outcome without having to fetch any further data itself. As a result, each request is completely independent of any other, and the authorization layer has no external dependencies.

The benefits of stateless architecture

Stateless systems have several advantages over their stateful counterparts such as better scalability, improved fault tolerance, and simplicity in implementation.

Their characteristics make them easy to understand and reason about. Each session takes the form of a self-contained transaction where the client issues a request and receives a response. The interaction is handled in the same way each time it occurs because the service holds no state that could affect its outputs.

The following are a few other benefits of going stateless:

  • Scalability: Stateless services don't require access to persistent storage, nor do they depend on replicas of a deployment that need to successfully synchronize with each other. This improves scalability—you can launch an unlimited number of instances across a fleet of physical hosts, providing great operating flexibility.
  • Fault tolerance: Stateless reduces the number of dependencies in your system. The service is naturally more fault tolerant as there's fewer places where errors can occur. If an issue is encountered, the client can simply repeat its request without having to wait for a state correction to apply on the server.
  • Simpler debugging and troubleshooting: No server-side state makes it easier to find and fix issues with your services. When a problem occurs in production, you can repeat the client's request against one of your service's development instances to observe the sequence of events involved. Errors experienced by stateful services can be difficult to reproduce if they're caused by the exact server state that prevailed at the time, which isn't always possible to replicate.

Stateless Architecture Examples

Stateless architecture has received more attention in recent years due to the rising interest in microservices and serverless computing. 

However, stateless is a well-established strategy that underpins some of the software systems we take for granted:

  • HTTP: The ubiquitous HTTP protocol is arguably the best-known example of a stateless system. A new connection between the server and client is created for each request and then destroyed once the response is received. Stateful sessions are only possible when the client identifies itself in each request.
  • RESTful APIs: REST APIs are stateless services provided over HTTP. Each request that the client makes must provide all the information that the server needs to issue a useful response. The server shouldn't be assumed to understand a request just because the same client has previously connected.
  • Serverless computing: Serverless is an increasingly popular model for quickly building and launching apps without having to maintain your own infrastructure. Serverless systems are implemented as stateless functions that run on demand, dynamically creating their operating environment each time they're called.

The role of stateless in authorization

Stateless architecture is particularly beneficial to authorization layers. Authorization, the act of checking what a user can do within a system—as opposed to authentication, which verifies a user is who they claim to be—is traditionally implemented as a stateful server-side process, requiring complex dependencies to fetch the user’s details and the permissions they hold. This impedes scalability: database performance will reduce with load, and any synchronization problems could cause different instances of the service to return conflicting authorization outcomes.

By contrast, stateless authorization facilitates straightforward decisions with predictable results. The application can fetch the state it requires upfront then present it to the authorization layer as a signed token. The token provides the service with all the data it needs to produce an authorization decision, eliminating external dependencies.

The stateless authorization layer is therefore simpler, more scalable, and easier to implement, especially when multiple clients need to interact with the system. It removes the need to persist and synchronize state across the authorization layer’s instances. Although fetching the state within the application can appear to be an overhead, in reality, most systems will already be holding the required information on the client.

Implementing stateless authorization

Stateless authorization services use the following high-level architecture:

1. The application calls the authorization layer when it needs to check whether the active user can perform a particular action.

2. The data sent to the authorization layer includes everything that the service needs to produce an authorization outcome, such as the user’s identity, any permissions or special capabilities they hold, and the identity of the resource that’s been requested.

3. The authorization layer evaluates the provided data and delivers an authorization result (either allow or deny) back to the application.

4. If an “allow” result was issued, the application permits the user to perform the requested action against the resource. Otherwise, the user is deemed unauthorized and is blocked from continuing.

Crucially, all the information required to produce the authorization result is provided by the application. The authorization layer isn’t responsible for fetching any details about the user or the resource.

Best practices for stateless authorization

As we've seen above, implementing a basic stateless authorization system is relatively straightforward. Once you've issued a signed token to a client, it should be able to present it to any instance of your service to access protected resources.

Nonetheless, the difference between a simple and a production-ready authorization layer depends on the details. Here are some best practices to keep in mind.

Ensure proper token management

The integrity of stateless auth depends on proper token management. Fortunately, standards such as JWT have solved most of the challenges in this space. It's safest to issue and consume them using established community libraries such as jwt-go for Go or jsonwebtoken for Node.js. Rolling your own code for these functions can make you vulnerable to security issues if your signature verification proves to be incorrect.

It's also important to plan how you'll handle token expiration and revocation. Ensure that each token carries an expiration time that your service checks during its authorization procedure. If you need to immediately expire a set of tokens, you can rotate the private key that the server used to generate their signatures.

Note that stateless does not mean no persistence at all—stateless in this context refers to not holding the state that's relevant to a particular client. For more precise expiration controls, you can maintain a central index of issued tokens. When a client presents a token, retrieve the token ID from its payload and check there's still a match in your index. This allows you to revoke tokens by deleting them from the index.

Use HTTPS for Everything

Any authorization data should be communicated over a TLS-protected connection. This helps prevent leaking of sensitive values such as passwords, authorization grants, and access tokens.

Tokens such as JWTs aren't encrypted by default—anyone can retrieve a payload by Base64 decoding the token. HTTPS mitigates the risk by preventing bad actors from eavesdropping on authorization activity, but in especially demanding situations, you might also want to encrypt sensitive payload fields for an additional layer of protection.

Select a centralized authorization server

Use a centralized authorization server to store authorization data such as users, roles, role assignments, and details of issued tokens. This permits you to make changes in one place and then immediately apply them across all your services.

Choosing a decentralized approach can be attractive at first, but it tends to become less manageable over the life of your service. It forces you to maintain several authorization server instances and ensure stable synchronization between them. Problems can cause errors and discrepancies to occur.

Centralized authorization eliminates these problems. Although it creates a single point of failure, you can mitigate against this by running multiple replicas of the authorization server to achieve high availability. The overall experience will be more reliable and easier to configure than decentralized systems.

Build upon an established standard

Frameworks such as OAuth and SAML are purposely designed to simplify and standardize authorization procedures. Using them as the basis of your stateless authorization implementation will reduce the risk of security oversights and improve interoperability with other services, such as identity providers.

When you build your own authorization protocol from scratch, you're responsible for defining how services and clients interact, the ways in which tokens are used, and how authorization is requested, validated, and granted. OAuth and SAML provide a reference for these high-level characteristics, allowing you to focus on building the app-specific authorization policies relevant to your users and resources.

Conclusion

Stateless services don't persist any information between client sessions. They restore their state for each new session, using information provided by the client.

Stateless architecture is particularly ideal for authorization systems. The ease of scalability and absence of server-side synchronization logic allow you to develop decoupled authorization systems that are both reliable and simple to maintain. The resulting systems can be easily deployed to distributed environments, ready for use by multiple independent microservices.

The next time you're implementing authorization in your software projects, consider exploring stateless authorization. Using established stateless-compatible protocols such as OAuth 2.0 allows you to easily integrate with identity platforms and reduce the complexity in your apps.

GUIDE

Book a free Policy Workshop to discuss your requirements and get your first policy written by the Cerbos team