Stateless and externalized authorization for scalable applications

Published by Emre Baran on April 28, 2025
Stateless and externalized authorization for scalable applications

In this post, we’ll break down insights from a recent Software Engineering Radio episode with Cerbos co-founders Emre Baran and Alex Olivier, where they spoke with Priyanka Raghavan, host of the podcast, on building stateless, externalized (decoupled) authorization frameworks.

We’ll explore authentication vs authorization, compare access control models, discuss why granular access is crucial (with real-world failures when it’s missing), examine the benefits and challenges of externalized authorization, and delve into how to implement and evolve a policy-based system. We’ll also touch on when to build vs buy authorization, and even how these concepts apply to AI-driven systems – including why authorization decisions must remain deterministic.

You don’t need to have listened to the podcast to follow along – but if you’re curious, you can check out the recording below. Let’s dive in.

Authentication vs authorization

Before jumping into authorization frameworks, it’s critical to distinguish authentication from authorization. Authentication is about verifying who you are – confirming your identity (for example, via a password, passport, or biometric) and establishing what attributes or roles are tied to you. Authorization, on the other hand, is about determining what you’re allowed to do now that your identity is known​. In other words: just because you’ve logged in (authN) doesn’t mean you can access everything – authorization is the guard that decides which actions you can perform or which resources you can access.

For further details, feel free to review our blog on this exact topic.

Why stress this difference? Because it’s common to implement authentication (login, identity management) and assume authorization is “handled” by simple role checks. But broken authorization is consistently the number one web app security risk (OWASP’s Top 10). Robust authorization needs its own focus – which leads us to thinking about models and frameworks purpose-built for the task.

From RBAC to PBAC – Authorization models 101

So how do we decide who can do what? Over the years, several authorization models have emerged:

  • RBAC (Role-Based Access Control) is the classic model where permissions are tied to roles. Users are assigned roles like “admin,” “manager,” “viewer”, and each role has a set of allowed actions. If a user has the required role, the action is allowed. RBAC is simple and widely used – most apps start here – but it can get coarse-grained (everyone with the role gets the same access) and inflexible as requirements grow.
  • With ABAC (Attribute-Based Access Control), instead of, or in addition to, roles - decisions consider attributes about the user, resource, or environment. Attributes are key-value properties, like department, account status, resource owner, request IP, time of day, etc.. ABAC policies evaluate these attributes to grant or deny access. ABAC is more granular and expressive than pure RBAC – you’re not limited to pre-defined roles – but it introduces complexity in policy management as the number of attributes and rules grows.
  • PBAC (Policy-Based Access Control) is often considered a superset of ABAC. PBAC takes things a step further by treating access logic as standalone, version-controlled policies. Instead of hardcoding access rules into app code, PBAC stores them in external files or services that evaluate permission checks at runtime. This decoupling brings clarity and flexibility: policies can include roles, attributes, relationships, or context – and they can be versioned, tested, reviewed, and updated without redeploying your app. PBAC is especially powerful in large systems where permissions must be consistent across services and easily auditable. It enables a "policy-as-code" workflow: policies live in Git, are tested in CI, and applied via CD – aligning access control with modern DevOps practices.
  • ReBAC (Relationship-Based Access Control), also known as graph-based or relationship checks, bases access on relationships between entities. A canonical example is Google’s Zanzibar model for Google Drive, which treats “who can access what” as a graph of relationships. ReBAC is powerful for scenarios like content sharing, social networks, or multi-tenant SaaS apps where permissions depend on who is connected to what (e.g. “only a project’s collaborators can edit this report”). Frameworks like Zanzibar and its open-source implementations, such as Auth0 FGA, popularized ReBAC. It’s highly flexible, but usually requires dedicated infrastructure to store and query those relationships at scale.

There’s no one-size-fits-all model – often real-world systems blend them. It’s common to start with RBAC, sprinkle in a few attribute checks, making it “RBAC with ABAC elements”, or use relationships for specific features. PBAC often emerges when teams want centralized policy control, better auditability, and faster iteration on permission logic. Alex notes that the right approach “really goes back to your requirements” . The key is to design a model that is granular enough to enforce least privilege, but not so complex that it's unmanageable. For more insights, check out our blog on mapping business requirements to authorization policy.

Why granular access control matters, and what can go wrong

Having fine-grained authorization isn’t just an academic concern – it can make or break the security of your product. Emre emphasizes that many companies learn this the hard way: coarse or ad-hoc access controls often lead to embarrassing and dangerous failures.

For example, imagine a neobank that lets you open a business account. If the app doesn’t support roles or permissions on that account, every user you share the login with could have full access to all features. One employee could initiate unlimited fund transfers or view all financial data simply because the system lacks more granular controls. This “all or nothing” access might be acceptable in a tiny startup, but as usage grows it becomes a massive risk – least privilege goes out the window.

In a more dire case, consider a large ride-sharing company in its early days. They built internal tools for customer support and ops teams – but without proper authorization partitioning. The result? Employees had “God mode” access to everything, including sensitive customer travel records. In one notorious incident, employees were able to pull up private data about celebrity riders’ trips, with no real justification. Obviously, that should never have been allowed – only specific support staff under specific circumstances (say, handling a complaint) should see a user’s trip details. The absence of granular role-based rules or contextual checks was a privacy disaster waiting to happen.

These examples underline a key point: authorization is not optional. Broken access control is consistently the top web security issue for a reason. If you don’t design proper permissions, users will do things they shouldn’t, or internal actors will abuse overly broad access. It can lead to data breaches, compliance violations, and loss of user trust. As a developer or architect, you need to bake in the right authorization model early – otherwise you’ll be scrambling to retrofit it later, likely after an incident forces your hand.

Externalizing authorization – Why do it?

Many teams start with simple in-code checks (an if-then-else logic). That works for a while. But as your application grows – especially if you adopt microservices or multiple modules – those scattered checks become technical debt. Every new permission requirement means hunting down and updating logic in many places, potentially in different languages or repos. It’s error-prone and hard to keep consistent.

The remedy is to centralize and externalize authorization logic into its own component or service – essentially taking all those hardcoded rules out of your application code and moving them into a central policy decision point. Alex explains that if you continue embedding checks in each service, you’ll eventually have “spaghetti code” as requirements evolve​. Instead, by externalizing (decoupling), you aim for a single source of truth for permissions.

What does an externalized authorization architecture look like? In practice, you introduce an authorization service (it could be a standalone server, a library, or sidecar – more on deployment in a bit) that knows how to evaluate policies. Your application code, instead of doing local if role == "admin" logic, will call this service and ask “Is user X allowed to do action Y on resource Z?” The service will evaluate the request against your centralized policies and respond “allow” or “deny.” This design is often drawn from the standard XACML model: a Policy Decision Point (PDP) answering allow/deny, which your app uses before executing sensitive actions.

pdp visualization.png

Source

By doing this, you gain several big advantages:

  • All authorization rules live in one place (the service’s policy store) instead of being scattered. You can update a policy once and have it take effect everywhere, without redeploying dozens of services. For example, if a compliance requirement says Managers can no longer delete records, you update the “delete record” policy in the authorization service – immediately every part of the app will enforce the new rule. This consistency is a huge win for maintainability.
  • Decoupling allows policies to be defined in formats more readable than code – often configuration files or declarative policy languages. This means security teams, auditors, or product managers can understand and even contribute to the rules without digging through application code. Storing policies in version-controlled files (YAML, JSON, Rego, etc.) also enables peer review and discussion on merge requests, increasing confidence that the rules actually match the intent. In short, authorization becomes a policy-as-code problem rather than hidden business logic.
  • When your policies are decoupled, you can test them in isolation. For instance, Cerbos provides a CLI tool to run unit tests against your policies – you supply example users/resources and expected outcomes, and ensure the policy logic permits/denies as intended. This integrates with CI pipelines (Cerbos also offers a GitHub Action) so that any policy change is validated before deployment. Moreover, the centralized approach means you can audit the policies (they’re files under version control) and also audit the decisions at runtime, since calls go through the service. Alex points out that in an externalized model, you get a consistent audit log of every decision in one place – as opposed to combing through many services’ logs. For regulated industries, this is gold: you can prove who accessed what, when, and why, with a single unified trail. Externalized authorization effectively makes robust auditing “free” as a side benefit.
  • It might sound counterintuitive, but moving authorization out-of-process can actually improve scalability when done right. A purpose-built authorization service can be optimized for evaluating thousands of rules quickly, and scaled horizontally independent of your app. Also, because it’s stateless (more on statelessness soon), you can run many instances without sticky sessions or complex clustering. Instead of duplicating complex permission logic in N different services, you have one highly efficient engine to maintain. In many cases, teams have found that a well-designed PDP can respond in nano- or milliseconds, even under heavy load. Cerbos, for example, compiles policies into a highly efficient form – either in-memory for its server or into WASM for an embedded mode – allowing sub-millisecond checks.

In summary, externalizing authorization means treating access control as a first-class component of your architecture rather than an afterthought. It’s about centralizing the “who can do what” logic so that it’s easier to change, reason about, test, and scale.

To learn more about the pros and cons of externalizing authorization, feel free to check out these two blogs: Benefits, trade-offs.

Implementing authorization - The case for a stateless, policy-based approach

Externalizing is step one – but how you implement that authorization service matters too. The modern approach discussed by Emre and Alex is to keep the authorization layer stateless and policy-driven.

As we touched on earlier, PBAC means the rules are expressed as declarative policies (think of them like firewall rules or configuration) rather than hardcoded in code. Cerbos, for instance, uses YAML files to define policies for each resource type (e.g., an “Invoice” policy file might declare who can view, edit, or pay an invoice). These policies can include role checks, attribute conditions, and even relational conditions – basically capturing RBAC/ABAC logic in a structured way. The key benefit is that policies are versionable and testable artifacts. They live in Git, you can do code reviews on them, write tests for them, and evolve them alongside your application. They also provide a layer of abstraction: your app code asks “can X do Y to Z?” and doesn’t need to know the fine details – it’s all in the policy.

Now let’s talk about “stateless”. A stateless authorization service does not maintain its own database of users, roles, or sessions. It doesn’t store who has access to what internally. Instead, all necessary context, such as user attributes, resource attributes, and relationships, must be supplied with each check request, or fetched from external sources. Why stateless? Because it massively simplifies scaling and deployment. If the PDP doesn’t need to replicate any state, you can run a PDP instance anywhere – even one per application instance – without worrying about data consistency between them. Cerbos is designed to be stateless in this manner: it loads the policy files into memory and evaluates requests purely based on the input context, much like a function. This means you can deploy lots of PDP instances (as sidecars, or as a library in-process) to eliminate network latency, since none of them need a shared session or cache. Every instance is identical, just holding the policy logic.

Stateless, however, doesn’t mean “no data”. It means the authorization service itself isn’t the source of truth for user or object data – you might still need to pass in data or references. In practice, adopting stateless authorization pushes you to provide rich context with each check. A common pattern is token enrichment: include the user’s roles, groups, or other claims in their authentication token so that the PDP doesn’t have to ask another service for them. If your IdP (Identity Provider) doesn’t put all needed info in the token, you might extend it or have a middleware fetch extra attributes. The benefit is that a well-enriched token allows the PDP to make a decision immediately and autonomously​. The trade-off is deciding how much to pack into tokens - too much can bloat them and risk stale data. It’s a design consideration, but one that comes with embracing statelessness.

In short, stateless PBAC systems like Cerbos treat authorization as pure functions: given input (who, what, action, context), output a decision. No hidden state. This purity yields consistency and trustworthiness – and as a bonus, it’s easier to test, since you don’t need to simulate some database of users; you just feed in context to your test harness. As Emre noted, building Cerbos specifically for the application layer, and not as a general-purpose policy engine, also allowed them to keep it lightweight and fast, with minimal CPU/memory overhead. In fact, he shared that running Cerbos as a sidecar adds virtually zero noticeable load to an app, but provides huge flexibility in return.

Should you roll your own authorization service?

A question that often arises: “This sounds great, but do we really need a third-party solution? Can’t we just implement our own authorization service internally?” It’s a fair question, especially for organizations with unique requirements. Emre and Alex have firsthand experience here – before founding Cerbos, they collectively built custom authz systems “10 times” across various companies. They realized they were solving the same problem over and over, and it wasn’t adding business value, since authz is critical, but it’s undifferentiated infrastructure for most apps.

For a deep-dive on the build vs. buy question, feel free to read through this blog. Otherwise, let’s continue with a very brief summary.

visuals for blogs - Authorization - build vs buy.jpg

The bottom line is, externalized authorization is becoming a known best practice – and there’s a growing ecosystem of libraries and services such as Cerbos PDP to help you implement it. Unless you have a very compelling reason to create your own from scratch, you’re probably better off evaluating existing options. Even big firms are open-sourcing their authz systems (e.g. OPA, Amazon’s Cedar language, etc.) because the community recognizes the need to not duplicate this sensitive plumbing. Adopting and adapting an existing solution lets your team spend time on features that matter to your users.

New frontiers: Authorization in AI-driven applications

An intriguing part of the discussion was how emerging AI/LLM-based features introduce new challenges for authorization. We’re now in an era where apps might have a chatbot interface or an AI agent that interacts with users and possibly takes actions on their behalf. How does our carefully crafted authorization model apply here? Priyanka (the SE Radio host) posed the scenario of companies building chatbots or using large language models (LLMs) connected to their data. Traditionally, we secured the backend (APIs, databases) and maybe the frontend. But with an AI in the mix, there’s effectively a third layer: the AI system that might bypass normal app logic.

Consider a company analytics chatbot. If a CEO asks “Show me the total payroll for the company,” the bot should fetch and answer with company-wide data, since the CEO is allowed to see everything. If a regional manager asks the same question, the bot should only reveal the payroll for that manager’s region. If our authorization is properly in place, the underlying API call for “get payroll” would enforce that. But what if the AI can access data directly or generate answers from a vector database? If the AI isn’t constrained, it could inadvertently leak information across boundaries. Emre describes this as the AI potentially bypassing your backend and frontend security if not controlled​. We’ve seen real incidents: a certain car dealership’s chatbot was tricked (via prompt injection) into essentially giving away a car for $1 due to no safeguards (the “Chevy chatbot” story Emre alluded to). Airlines also saw people exploit chatbots to get unauthorized discounts or refunds​. These are like new-age security vulnerabilities.

The solution? Apply authorization checks to AI outputs and actions too. Emre suggests that every AI agent or chatbot that can act on user data should have a PDP in front of it, governing its access. In practice, this might mean two things:

  1. Filtering the AI’s inputs/knowledge. When the AI goes to retrieve data to answer a question, only provide it data that the user is allowed to see. This could be done by vector database queries that incorporate user permissions (e.g. embedding search that filters documents by the user’s access rights using Cerbos’s data filtering API). Cerbos has a query planning feature that can return which records a user is permitted for​. For example, if the chatbot is about to use company documents to answer, it first asks Cerbos “out of these 100 docs, which can this user access?” and only feeds those to the LLM. This ensures the model can’t even see forbidden info to potentially leak it.
  2. Filtering the AI’s outputs/actions. Even if the model tries to output something it shouldn’t, you intercept and validate it. For instance, if the LLM-generated answer includes some data, you could parse that answer to see if it references any entities the user isn’t allowed to know about. This is a harder problem, but one approach is to structure the AI’s tasks such that it calls real APIs for any data, and those APIs are guarded by the usual authorization. For example, instead of letting your AI directly read the database, have it call your application functions (with proper authz) to get information. This is aligned with tools like OpenAI’s function calling – the AI describes an action, and your system executes it only if allowed. Emre suggested converting the LLM’s intent into actual API calls and then checking authorization on those calls​. That way, if the AI “asks” to retrieve all payroll data but the user isn’t a CEO, the authorization layer will deny that request, and the AI won’t get those details.

The broader point is that AI does not remove the need for authorization – in fact, it makes it more important to enforce it. Large language models are great at generating outputs from data, but they have no inherent notion of a user’s permissions or privacy. It’s up to us to impose those constraints. By integrating your authz service with your AI system’s retrieval and execution steps, you mitigate the risk of LLM “hallucinations” exposing sensitive info or performing unauthorized actions. You essentially sandbox the AI to operate within the user’s allowed boundaries.

Alex and Emre also discussed where AI/ML can help in the authorization space, as opposed to being a risk. Two promising areas:

  • Policy generation. Using AI to assist developers or product folks in writing policies. For example, you could prompt an LLM with “Managers should be able to view and edit reports from their department, and view (but not edit) reports from other departments” and have it suggest a Cerbos policy snippet. In fact, Alex mentioned you can ask ChatGPT or Claude for a Cerbos policy and they often do a decent job​! This is a great way to speed up initial policy writing or get examples, especially for those less familiar with the syntax. Of course, you’d still review and test the AI-generated policy, but it can handle boilerplate or complex logic suggestions.
  • Analyzing audit logs (anomaly detection). With all those decision logs we’re collecting, AI can help spot unusual patterns. Imagine training a model on your authorization logs to detect anomalies – e.g., a user suddenly accessing a resource type they never did before, or a spike in denials for a particular action indicating a possible misconfiguration or attack. Alex highlighted that mining the log stream for signals is a ripe area for AI​. It could alert you to potential abuse or simply help optimize policies (if something is constantly denied, maybe a policy is too strict or an access request needs review).

However, one area they were adamant not to involve AI: the core decision engine itself. Authorization decisions should be deterministic and based on explicit rules, not the probabilistic whims of an ML model. You wouldn’t want a neural network deciding whether to grant access, possibly giving different answers each time or being sensitive to odd inputs. Alex quipped about not wanting to worry about the “temperature” setting of a model when it comes to security. The risk of false positives/negatives and the lack of explainability make AI unsuitable to replace a rules engine. Instead, keep the enforcement layer clear-cut – every input either meets the policy or not, and the outcome is guaranteed to be the same given the same inputs. Determinism is crucial for trust here. We can leverage AI around this core, to write policies or analyze outcomes, but the enforcement should remain solid code – whether that’s a handcrafted engine like Cerbos or any other predictable system.

This perspective rings true: use AI to enhance the developer and admin experience of managing authorization, but not to actually determine permissions on the fly. The latter could lead to unpredictable security which no one wants.

Conclusion

Modern application security demands a robust approach to authorization – one that is granular, scalable, and maintainable. As we’ve seen, the industry is moving away from sprinkling role checks in code and toward dedicated, stateless authorization services governed by clear policies. Adopting this model brings tangible benefits: you can evolve access rules quickly, avoid security slip-ups that come with ad-hoc implementations, and gain a holistic view of who is doing what in your system.

Modern authorization is about separating policy from code and treating it as a core part of your architecture. The approaches discussed aren’t just theoretical – they’re being used in production by companies large and small to secure everything from B2B SaaS platforms to fintech apps. As the complexity of systems and the stakes of breaches continue to rise, having a solid authorization foundation is as important as having a solid authentication system.

Keep authorization in mind from day one. Embrace granular controls, leverage decoupled frameworks, and you’ll be rewarded with a more secure application that can adapt to change with less fuss. And if you haven’t already, check out some of the open-source projects in this space, such as Cerbos PDP – a little investment now in setting up a PBAC system can save you countless hours and incidents down the road.

Finally, if you’re curious to dig deeper and implement what we’ve discussed in practice, check out the Cerbos documentation and join the Cerbos Slack community. Let’s build applications that are not just feature-rich, but also secure by design – no “God mode” surprises for our users.

FAQ

Why is stateless authorization important?

How is externalized authorization different from embedded authorization?

Can AI systems bypass traditional authorization checks?

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