Authorization in developer workflows: best practices and tools

Published by Daniel Maher on June 03, 2025

This blog post was adapted from talks presented by Dan Maher, Senior DevRel Manager at Cerbos, at KubeCon EU (London, UK; March 2025) and FOSS North (Gothenburg, Sweden; April 2025).

Every cloud-native application must answer a fundamental question: "Is this user allowed to do this thing?" It's unavoidable, critical for security, and touches every feature you build. Yet despite its importance, authorization remains one of the most neglected aspects of our architectures—treated as an afterthought rather than a foundational concern.

This creates what I call the authorization paradox: something every developer implements but few have good tools for. The result? Friction, security gaps, and countless hours spent wrestling with permission logic instead of building features.

But it doesn't have to be this way.

The evolution gap

Consider how cloud-native development has evolved over the past decade. Networking progressed from manual load balancer configurations to service mesh and CNI. Storage advanced from manual volume management to persistent volume claims and CSI. Deployment transformed from bash scripts to GitOps pipelines and sophisticated CI/CD workflows.

Each area evolved from infrastructure concerns into developer primitives, making teams more productive while improving quality. The pattern is clear: when we create the right abstractions and tooling, developers can focus on business problems rather than infrastructure complexity.

Authorization, however, remains stuck in the era of duct tape and bailing twine, metaphorically-speaking.

Why authorization has lagged behind

Several factors explain why authorization hasn't evolved like other cloud-native primitives.

First, authorization is deeply coupled with application logic, making standardization difficult. Unlike networking or storage, which have clear boundaries, permission checks often live inside business logic itself. Relatedly, requirements tend to be intensely domain-specific. For example, financial services companies operate in different regulatory frameworks than, say, healthcare or an AI video editing service.

The scale of large-scale platforms today is also a factor. Modern applications have intricate permission models that make simple solutions inadequate. Think about Google Drive's sharing permissions or Slack's workspace roles—these systems require sophisticated relationship modeling that generic frameworks struggle to support. Consequently, every framework and language has reinvented authorization differently, creating a fragmented landscape without clear best practices.

The result? Authorization sits awkwardly between infrastructure and application—too complex to standardize yet too common to reinvent constantly.

The high cost of complexity

This complexity costs development teams significantly. Developers constantly context-switch between business logic and authorization, breaking the flow states that are crucial for productivity. When you're deep in implementing a feature and suddenly need to figure out permission logic, that mental gear-shifting kills momentum and increases the likelihood of bugs.

Security suffers when authorization is inconsistently implemented across codebases. When authorization is hard to implement correctly, developers find workarounds that compromise security. I've seen teams implement "quick fixes" that bypass proper permission checks because the existing system was too cumbersome to work with correctly.

Velocity takes a hit because authorization changes require extensive regression testing without clear visibility of impacts. I've personally witnessed teams spend six months refactoring permission logic that could have been designed properly from the start—six months of engineering time lost because authorization wasn't treated as a first-class concern from the beginning.

New team members struggle to understand permission models scattered throughout the codebase. When authorization logic is spread across dozens of files with different patterns and assumptions, onboarding becomes a painful process of archeology rather than clear documentation reading.

This isn't just a security problem—it's fundamentally a developer experience problem that leads to security shortcuts when processes become too painful.

Workflow-first transformation

What if authorization became a creative tool rather than just a constraint? What if it enabled flow instead of breaking it?

The key insight is treating authorization as a foundational architectural concern rather than scattered implementation details. While we carefully architect authentication with well-designed flows and identity management, authorization typically degrades into scattered if-else statements throughout our code. We need to elevate authorization decisions to first-class architectural elements, just like we did with database access patterns or API design.

We also need a crucial mindset shift in how we frame authorization decisions. Instead of asking "What can't users do?"—a negative framing that relegates authorization to security restrictions—ask "How do we enable the right access?" This positive approach transforms permissions from security restrictions into product features that shape user experience. Consider Slack's workspace permissions: they're not just security controls but collaboration features that enhance teamwork and productivity.

Design principles for workflow-first authorization

Implementing this transformation requires four core design principles that work together to create authorization systems that enhance (rather than hinder) development workflows.

Domain-driven authorization

The first principle is domain-driven authorization. This means modeling permissions around business domains, not technical constructs. Permissions should speak the language of the product, not the codebase. Instead of technical operations like update:documents, use business concepts like Editor and Reviewer, i.e. real terms that product teams, developers, and users all understand. This creates a ubiquitous language that aligns security with how users actually think about your application. When a product manager talks about document permissions, they think in terms of roles and capabilities, not database operations—your authorization system should reflect this natural mental model.

Declarative policies

The second principle involves moving from imperative checks scattered throughout code to declarative policies that describe intent. Traditional approaches bury authorization logic in conditional statements throughout your application, making it hard to understand the overall permission model. Declarative policies express what should be allowed rather than how to check it, creating human-readable artifacts that can be version-controlled, reviewed independently of application code, and understood by non-developers. As a bonus, when authorization rules are declared clearly in one place, they become canonical documentation too!

Externalized decision-making

The third principle is using external decision points to decouple authorization from application logic entirely. Rather than embedding permission checks throughout your code, your application asks "Can this user do this?" to a dedicated service. This pattern enables consistent enforcement across services—the same policies apply everywhere—and allows authorization logic to scale independently. It also makes authorization testable in isolation, so you can verify your permission model without running the entire application. Developers still need to understand the model, but they can focus on business logic while using a consistent authorization API.

Context-awareness

The fourth principle is making authorization context-aware, moving beyond simple role-based access control by considering attributes like time, location, and resource properties. Context-aware authorization adapts to situations without creating an explosion of roles. A healthcare system might allow record access during business hours but require additional approval after hours—same user, same data, different context. This approach enables fine-grained control while keeping permission models manageable and intuitive.

Development lifecycle integration

These principles integrate throughout your development process, creating a continuous thread of authorization awareness from conception to production. During requirements gathering, define permission models alongside features rather than as an afterthought. Ask early: what access patterns are needed, and how do they integrate with existing models? This upfront thinking prevents architectural debt later.

In API design, document authorization requirements explicitly in API specifications. Make it clear who can call each endpoint and what context is required. This documentation becomes a contract that both client developers and security reviewers can understand and verify. During implementation, provide clear interfaces for authorization checks so developers never invent new ways to check permissions. Consistency here prevents the scattered approaches that make systems hard to maintain and audit.

Create dedicated test suites for permission logic, separate from business logic tests. Authorization bugs are security vulnerabilities, so they deserve the same testing rigor as any other critical system component.

In operations, implement monitoring and observability for authorization decisions. Track patterns like unexpected denials or performance bottlenecks in authorization calls. This visibility helps you understand how your permission system behaves in production and identify areas for improvement.

Taken as a whole, this creates continuous authorization awareness—not an afterthought but woven into every stage of development.

Authorization ecosystem

A rich ecosystem of open source tools has emerged to implement these principles, with many projects within the CNCF driving standardization efforts. Understanding this landscape helps you choose the right tool for your specific needs and architecture.

Open Policy Agent (OPA) is a graduated CNCF project that serves as a general-purpose policy engine extending beyond authorization to any policy enforcement need. It uses the Rego policy language, which offers tremendous flexibility at the cost of a steep learning curve. Rego can express virtually any policy logic you need, but the syntax takes significant investment to master. OPA works well for teams needing broad policy enforcement beyond just authorization and willing to invest in developing Rego expertise.

OpenFGA takes a different approach, focusing specifically on relationship-based authorization based on Google's Zanzibar paper—the same system that powers Google Drive permissions. OpenFGA excels at modeling complex relationships at massive scale, handling scenarios like "users can view documents shared with groups they belong to" with high performance. However, Zanzibar was originally designed to solve Google-scale problems, so OpenFGA makes most sense for applications with genuinely complex relationship-based permissions that need to scale to millions of users; frankly, this is not the use-case for most organizations.

Cerbos takes a developer-workflow approach, using human-readable YAML policies with IDE integrations and built-in testing frameworks. The focus on developer experience makes it particularly strong for teams prioritizing workflow integration over maximum scalability. Yes, I get to work on Cerbos, so I'm obviously biased, but the developer-centric approach addresses many of the workflow friction points we've discussed.

OpenID and AuthZen

The OpenID Foundation's AuthZen working group represents another important development, focusing on standardizing authorization APIs and interfaces rather than providing specific implementations. Building on the success of OAuth 2.0 and OpenID Connect in authentication, AuthZen aims to create common interfaces across different authorization implementations. This standardization effort ensures different systems can work together coherently, which becomes increasingly important as organizations adopt multiple authorization tools for different use cases.

Infrastructure patterns that work

Understanding the tools is just the beginning. Successful implementation requires proven patterns that address different architectural and workflow needs. Here's a few to consider.

Authorization as a Service

For architectural patterns, Authorization as a Service encapsulates all authorization logic in a dedicated service that your applications query with explicit requests: "Can user X perform action Y on resource Z, given N conditions?" This pattern enables consistent enforcement across all services calling the same authorization service and simplifies policy updates since you only need to change the policy service rather than touching application code. The pattern works particularly well for microservice architectures where centralized governance is important and policy changes are frequent.

Sidecar deployments

Sidecar deployment takes a different approach, deploying authorization engines alongside each service instance, typically as container sidecars in Kubernetes. This eliminates network hops for authorization decisions, providing extremely low latency while maintaining policy consistency through centralized distribution. However, it's important to note that each sidecar ultimately pulls from the same policy repository, so you get both performance and (evnetual) consistency. Choose this approach when authorization performance is the thing you're optimizing for, or if you need to eliminate network dependencies for authorization decisions.

Multi-layer models

Multi-layer authorization implements authorization at multiple system layers, with each layer handling what it does best. Use API gateways for coarse-grained checks like authentication validation, service-level enforcement for business logic authorization, and data-layer controls for fine-grained access. You might combine API gateway rules, service policies, and database row-level security in a complete implementation that provides both flexibility and comprehensive protection.

Development patterns that work

As with the infra patterns noted above, there are any number of development patterns to consider as well—so here's a few to get you started.

Policy-driven design

For development patterns, policy-driven design reverses the typical implementation flow by starting with authorization requirements before writing code. Define policies in domain-specific languages first, creating a source of truth for permissions. From these definitions, generate test cases and drive API design decisions. If your policies state that managers can approve expenses, your API must support this capability. This approach separates business logic from authorization from the very beginning, preventing the tangled coupling that makes systems hard to maintain.

Context enrichment

Request context enrichment addresses a common challenge: authorization decisions need rich context that isn't always readily available in requests. Instead of passing simple user IDs, augment requests with comprehensive context objects including user attributes, resource metadata, and environmental factors. Implement this using middleware or interceptors that populate context before reaching your authorization layer. This approach enables sophisticated, attribute-based decisions that go beyond simple role checks.

Test, then test again

Testing patterns deserve special attention since authorization bugs are security vulnerabilities. Policy unit testing treats policies as first-class artifacts with dedicated test suites, using table-driven approaches that cover both allowed and denied cases, including edge conditions like empty values or unusual access scenarios. This catches policy bugs before production, just as code unit tests catch implementation bugs.

Authorization test fixtures simplify permission testing by creating reusable components: standard test users with various permission profiles, test resources with different access patterns, and helpers that set up permission scenarios efficiently. Instead of configuring complex permission states for each test, developers can reference predefined scenarios like "use editor user on team document" and let the fixtures handle the setup details.

Putting it all together

These patterns come together through practical workflow integration that makes authorization a natural part of development rather than an obstacle. Local development becomes smoother with policy simulation tools that let developers test authorization scenarios without running the full application stack. IDE integration provides real-time policy validation and syntax checking, catching authorization errors as early as possible in the development process.

CI/CD integration includes automated checks for policy consistency and breaking changes, ensuring that authorization changes don't break existing functionality. Documentation generation keeps authorization documentation synchronized with actual policies, solving the common problem of outdated permission documentation. Debugging tools provide clear explanations of why authorization decisions were made, turning mysterious access denials into understandable explanations that developers can act on quickly.

The Three Transformations

This comprehensive approach creates three fundamental transformations in how organizations handle authorization.

The first transformation moves teams from friction to flow in their development process. Traditional authorization interrupts development flow, acting as a productivity tax that slows feature development. Workflow-first authorization enhances productivity instead, removing obstacles and providing clear patterns that developers can follow confidently. When authorization becomes seamless, creativity flourishes as teams focus on business problems rather than fighting permission systems. The difference is measurable in both developer satisfaction and output quality.

The second transformation implements security by design rather than security as an afterthought. Instead of adding security controls at the end of development, authorization primitives are built into architecture from day one. This creates a foundation for secure development that fundamentally changes your application's security posture. Defense in depth becomes natural rather than retrofitted, and security and maintainability become aligned goals rather than competing concerns.

The third transformation creates empowered developers who understand authorization and make better security decisions throughout development. Knowledge is power: when developers understand the reasoning behind security choices, they make better decisions at every level. Clear patterns and tools democratize security expertise instead of keeping it siloed with security teams. This creates a positive security culture across development teams where security becomes everyone's responsibility, not just the security team's burden.

How to get started

Ready to transform your approach to authorization? The journey begins with understanding your current state. Map where authorization decisions happen in your codebase—you might be surprised by how scattered they are. Look for patterns in how different parts of your system handle permissions, and identify the biggest pain points developers encounter.

Choose your first use case carefully. Pick either a new feature where you can implement clean authorization patterns from the start, or an existing problematic area where the pain is already visible and the value of improvement will be clear. Starting with a greenfield implementation often provides the clearest demonstration of the benefits.

Select appropriate tools based on your architecture, team size, and specific requirements. Use the guidance above, but remember that you can start simple and evolve. Many teams begin with basic external authorization services and add sophistication as their needs grow.

Implement one pattern at a time rather than trying to revolutionize everything at once. Policy-driven design works well for new features, while Authorization-as-a-Service can help centralize scattered existing logic. Focus on establishing good patterns that other team members can follow and extend.

Measure the impact of your changes. Track developer productivity metrics like time spent on authorization-related tasks, frequency of authorization bugs, and developer satisfaction with permission-related workflows. These measurements help justify continued investment and identify areas for further improvement.

Conclusion

Authorization should enable great software, not hinder it. The tools, patterns, and mindset shifts exist today to transform your approach from security constraint to creative enabler. Every application needs authorization, but not every application needs to suffer through poorly designed authorization systems.

The question isn't whether you need better authorization—every application does. The question is whether you'll treat it as a first-class concern that deserves the same architectural attention as your databases, APIs, and deployment pipelines. When you do, you'll discover that great authorization isn't just about security—it's about unleashing your team's creativity by removing the friction that keeps them from building amazing things.

The transformation from afterthought to workflow enabler requires investment, but the payoff compounds over time. Better developer experience leads to better security outcomes, which creates more confidence to build sophisticated features, which drives business value that justifies further investment in good practices. It's a virtuous cycle that begins with recognizing authorization as the developer workflow challenge it truly is.

If you want to dive deeper into implementing and managing authorization, join one of our engineering demos or check out our in-depth documentation.

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