- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Laravel Policies and Gates: Authorization Done Properly
About Post
Every Laravel codebase I've opened has at least one of these somewhere in a controller:
if ($contract->tenant_id !== auth()->id() && ! auth()->user()->is_admin) {
abort(403);
}
It works. Then the same check gets copied into the show method, the download method, the API controller and the mobile endpoint. Then someone adds a "leasing staff" role, updates four of the five copies, and the fifth one becomes the bug you hear about from a user instead of from your tests.
Authorization doesn't fail because it's hard. It fails because it's scattered. Laravel's policies and gates exist to put it in one place.
Gates vs policies in one minute
Both answer the same question: "is this user allowed to do this?" The difference is what the question is about.
- A gate is a single named closure for an action that isn't about one specific model. "Can this user see the financial reports page?"
- A policy is a class that groups every rule for one model. "Can this user view, update or delete this contract?"
In practice, most rules are about models, so most of your authorization should live in policies. Gates are for the leftovers.
// AppServiceProvider::boot()
Gate::define('view-financial-reports', function (User $user) {
return $user->hasRole('finance');
});
(hasRole() here and below is your own method on the User model, or one from a roles package.)
A policy per model
Generate one with php artisan make:policy ContractPolicy --model=Contract. If it lives in app/Policies and follows the ModelPolicy naming, Laravel discovers it automatically. No registration needed.
use Illuminate\Auth\Access\Response;
class ContractPolicy
{
public function view(User $user, Contract $contract): bool
{
return $user->id === $contract->tenant_id
|| $user->hasRole('leasing');
}
public function update(User $user, Contract $contract): Response
{
if ($contract->isSigned()) {
return Response::deny('Signed contracts cannot be edited.');
}
return $user->hasRole('leasing')
? Response::allow()
: Response::deny();
}
}
Two things worth noticing. Methods can return a plain bool, or a Response when you want a useful message. And business rules ("signed contracts are locked") belong here too, not only role checks. The policy is the one place that answers "can this happen?"
The before() trap
Policies support a before() method that runs ahead of every other check. It's the usual home for "admins can do anything":
public function before(User $user, string $ability): ?bool
{
return $user->isAdmin() ? true : null;
}
Return true to allow, false to deny, and null to fall through to the normal method. That null matters: return false by accident and you've locked out everyone who isn't an admin.
The bigger trap is subtler. With the before() above, an admin can now edit a signed contract, because before() skips the update() method entirely. If a rule should apply to everyone, including admins, either check for it inside before() or don't use before() for that policy.
A practical rule: use before() for "who you are" shortcuts, never for skipping "what state the record is in". Locked, signed, archived and paid usually mean locked for everyone.
Where to call it
There are several ways to trigger a policy. Pick one per layer and be consistent.
In controllers, use Gate::authorize(). It throws a 403 (with your message) when the check fails:
public function update(UpdateContractRequest $request, Contract $contract)
{
Gate::authorize('update', $contract);
$contract->update($request->validated());
return to_route('contracts.show', $contract);
}
A gotcha if you learned Laravel a few versions ago: since Laravel 11, the default base Controller is empty, so $this->authorize() isn't there unless you add the AuthorizesRequests trait yourself. Gate::authorize() works everywhere without it.
On routes, the can method is great for simple cases: Route::put('/contracts/{contract}', ...)->can('update', 'contract');
For actions without a model instance, like create or viewAny, pass the class name: Gate::authorize('create', Contract::class).
In Blade, hide what the user can't use:
@can('update', $contract)
<a href="{{ route('contracts.edit', $contract) }}">Edit</a>
@endcan
Hiding the button is a courtesy, not security. The controller check is the real lock. The Blade check just stops you from showing doors people can't open.
What policies don't do: filter lists
This one surprises people. viewAny answers "can this user see the contracts index at all?" It does not decide which contracts appear in it. If your index runs Contract::paginate(), every user sees every contract, policy or not.
For lists, use a query scope that mirrors the policy's logic, for example Contract::visibleTo($user)->paginate(). Keep the two side by side in your head (and in your tests), because they need to agree.
403 or 404?
A 403 confirms the record exists. Sometimes that's fine. Sometimes it leaks information, like which IDs are valid. When it matters, return Response::denyAsNotFound() from the policy and the user gets a 404 instead.
Test the rules, not just the pages
Authorization bugs are rarely about the happy path. They're about the user who shouldn't get in. So write the "should be blocked" tests first:
public function test_tenant_cannot_edit_another_tenants_contract(): void
{
$contract = Contract::factory()->create();
$otherTenant = User::factory()->tenant()->create();
$this->actingAs($otherTenant)
->put(route('contracts.update', $contract), ['notes' => 'hi'])
->assertForbidden();
}
For the policy itself, quick checks like $this->assertFalse($user->can('update', $signedContract)) run fast and cover every role and state combination without HTTP. Once you have several roles, as with the 50+ staff users of Kate PMS, a data-driven test that walks a table of "role × action × expected result" becomes the clearest documentation of who can do what.
The checklist
- One policy per model; gates only for non-model actions.
- Business state rules (signed, paid, archived) live in the policy too.
before()returnsnullto fall through, and never skips state rules.Gate::authorize()in controllers,@canin views for the UI.- Lists are filtered by a query scope, not by
viewAny. - Tests for the users who should be denied.
What's the strangest authorization rule you've had to implement? I'm always curious which ones end up living outside the policy, and why.

Be first to comment it...