Recently I investigated a security bug that initially looked like a middleware problem.
The verification rule was completely valid for the public or cabinet area: some actions there really should be available only after email verification.
But the admin UI reused the same endpoints. As a result, an administrative flow accidentally passed through middleware designed for a different authorization boundary.
The middleware was not wrong. The boundary was.
A Correct Security Rule Can Be Wrong in Another Context
Public / cabinet flow authenticated | vemail verified | vallowed action Admin flow authenticated admin | vBoth rules can be correct at the same time.
A cabinet flow may depend on email verification, while admin access depends on an administrative role or permission.
The problem appears when one endpoint is reused by both flows but still carries middleware from only one of them.
A Route Group Often Becomes a Security Boundary
Route::middleware([ 'auth', 'verified',])->group(function (): void { Route::get( '/profile/resources', ResourceIndexAction::class, );});It is easy to see a route group only as a way to avoid repeating auth, a prefix, or a controller namespace.
From a security perspective, it also defines the preconditions for every endpoint inside it.
If an endpoint lives in that group, it effectively accepts the group access rules.
A Shared Endpoint Does Not Always Mean a Shared Authorization Policy
Reusing an endpoint looks attractive: fewer routes, fewer controllers, one implementation.
But when public and admin flows have different access rules, a shared endpoint starts accumulating conditional exceptions.
At some point the code can no longer answer a simple question: "who is allowed to call this action?"
A Role-Specific Middleware Bypass Is a Symptom
if ($user->isAdmin()) { return $next($request);} if (! $user->hasVerifiedEmail()) { abort(403);}This kind of fix is sometimes necessary, but for me it is also a signal to stop and inspect the architectural boundary.
If middleware starts knowing every role and exception in the system, it may already be serving several different security contexts at once.
I Separate Authentication, Verification, and Authorization
This case is closely related to the article about the difference between authentication and authorization.
Authentication answers who the user is. Verification confirms an additional account state. Authorization decides whether the user may perform a specific action.
These checks may appear next to each other in a pipeline, but they are not interchangeable.
Admin Access Should Not Accidentally Inherit Cabinet Prerequisites
Wrong boundary shared endpoint | +-- auth +-- verified +-- maybe admin permission Better boundaries cabinet endpoint | +-- auth +-- verified +-- cabinet policyThis does not mean the business logic has to be duplicated.
Routes or action classes can be separate while both flows call the same application service underneath.
I separate the transport and security boundary from reuse of the business operation.
One Service Can Be Shared Even When Endpoints Are Separate
final class CabinetResourceAction{ public function __invoke( Request $request, ResourceService $service, ) { $this->authorize( 'viewFromCabinet', ); return $service->handle( $request->user(), ); }} Route Reuse and Code Reuse Are Not the Same Thing
This was the key distinction for me in this case.
A desire to avoid code duplication does not mean every context needs one URL and one middleware stack.
The query, service, or DTO can be reused while entry points remain separate with their own security rules.
I Define the Boundary Before Choosing Middleware
Before adding middleware, I now ask several questions:
- Who is the actor for this endpoint?
- Is this a public, cabinet, or admin flow?
- Which prerequisites really belong to this flow?
- Is verification actually required for this action?
- Which permission or policy protects the business action?
- Is the endpoint reused from another security context?
Tests Should Represent Different Security Contexts
Cabinet user authenticated + unverified -> blocked authenticated + verified -> allowed Admin user admin permission present -> allowed admin permission missing -> blockedIf only the cabinet endpoint is tested, an admin regression may remain invisible.
Conversely, a test that only checks "admin gets 200" without the permission boundary may hide overly broad access.
Tests should capture not only the result but also why access is allowed or denied.
Sometimes the Security Fix Is Moving the Endpoint, Not Changing Middleware
This is the most important part of the case.
If verification middleware correctly protects the cabinet group, weakening it just to make the admin UI work may be the wrong fix.
It may be safer to move the admin entry point into its own route group and keep both boundaries explicit.
My Checklist When Middleware Blocks a Legitimate Flow
- I check which user flow the middleware was originally designed for.
- I inspect who else is using the endpoint.
- I separate account prerequisites from action authorization.
- I inspect the route group, prefix, and middleware stack.
- I treat role-specific bypasses as a possible sign of the wrong boundary.
- I check whether entry points can be separated while keeping a shared service.
- I add separate security tests for each context.
Middleware Enforces a Rule Inside a Boundary, but Does Not Prove the Boundary Is Correct
After this case, I am much more careful about treating security middleware as the final answer.
Middleware can be completely correct for its context and still break another flow if that flow accidentally enters the same route group.
So when an access rule looks wrong, I inspect not only the condition. I inspect whether the endpoint sits on the correct authorization boundary.
Sometimes the security fix is not to weaken middleware. It is to move the endpoint back to the boundary where it actually belongs.



