Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Security Laravel Package

barryvdh/laravel-security

barryvdh/laravel-security provides security-focused helpers and middleware for Laravel applications, making it easier to apply common protections and safer defaults with minimal configuration. A lightweight option for adding basic hardening to projects without replacing Laravel's built-in security f...

View on GitHub
Deep Wiki
Context7

Getting Started

Know what you're installing. The description floating around ("adds security headers, CSP, X-Frame-Options…") is wrong for this repo. What barryvdh/laravel-security actually wires up is symfony/security-core's Voter + AccessDecisionManager into Laravel's container, so you can express "can this user edit this object?" using Symfony voter semantics instead of (or alongside) Laravel's Gate/Policy. Read src/SecurityServiceProvider.php first — it's small and it's the whole package.

Install and confirm the dependency resolution.

composer require barryvdh/laravel-security
composer show barryvdh/laravel-security symfony/security-core
php -v

With zero dependents you are the first integration test across whatever Laravel/PHP minor you're on. If symfony/security-core resolves to a major that mismatches your PHP or Laravel, stop and decide (pin, fork, or bail) before writing any code. This is a five-minute check that saves a sprint.

Where to look, in order:

  1. composer.json → extra.laravel.providers — is auto-discovery declared? If not, register Barryvdh\Security\SecurityServiceProvider manually in config/app.php.
  2. src/SecurityServiceProvider.php — which bindings are overridable (security.voter, security.access_decision_manager, token storage) and how voters get registered (config array vs. $this->app->tag([...], 'security.voter')). The tag name matters; get it wrong and supports() is never called, which looks like a deny.
  3. Publish config: php artisan vendor:publish --provider="Barryvdh\Security\SecurityServiceProvider". Inspect the strategy key — affirmative, unanimous, or consensus — and the voter list.
  4. Locate the facade or helper (Security::isGranted($attribute, $subject) is the typical shape). Confirm it returns a Symfony bool decision, not an exception-throwing Laravel-style authorize().

First vertical slice — one voter, one attribute, one controller.

namespace App\Security;

use App\Models\Post;
use App\Models\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class PostVoter extends Voter
{
    protected function supports(string $attribute, $subject): bool
    {
        return in_array($attribute, ['VIEW', 'EDIT'], true) && $subject instanceof Post;
    }

    protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();
        if (! $user instanceof User) {
            return false; // fail closed
        }

        /** @var Post $subject */
        return match ($attribute) {
            'VIEW' => $subject->is_published || $subject->user_id === $user->id,
            'EDIT' => $subject->user_id === $user->id,
            default => false,
        };
    }
}

Register it exactly as the provider expects, then guard:

abort_unless(app('security.authorization_checker')->isGranted('EDIT', $post), 403);

That slice is enough to compare against Gate::authorize('edit', $post) and make a real call. Don't build more until it works.

Implementation Patterns

Wrap the package behind an app-owned interface on day one. Calling AccessDecisionManager directly from controllers couples you to a low-traffic dependency. Define App\Contracts\Authorization with isGranted(string $attribute, mixed $subject): bool and implement it over the package. Every consumer (middleware, controllers, policies, Blade) talks to your interface. If the package goes stale, you swap the implementation, not 200 call sites.

One voter per aggregate root, and keep supports() free of I/O. The decision manager iterates every registered voter and calls supports() on each. If supports() hits the DB, you're N-voters per request. Keep it to instanceof and in_array; put all logic in the model ($post->isOwnedBy($user)) so the voter stays declarative and testable.

The Symfony token problem. Symfony's token.getUser() returns UserInterface|null, and Laravel's auth()->user() returns App\Models\User. Provide a token factory so every request has a consistent bridge — typically a custom TokenStorage implementation or a middleware that populates the token from Auth::user() on booted(). Do this once, centrally; do not reimplement it in each voter. If you skip this, $token->getUser() will be a Symfony string token and every voter returns false, and you'll waste hours debugging "why does everything 403?"

Pick the strategy deliberately.

  • affirmative (default): any voter returning true wins. Good for additive grants.
  • unanimous: every voter must return true. Good for compliance/multi-tenant checks where you want a veto.
  • priority: an ordered voter list; first non-abstain wins. Good when a super-admin voter should short-circuit.

Set it in config, document why in a comment, and write a test asserting the chosen semantics. This is the single easiest thing to get wrong silently.

Compose against native Gates/Policies, don't replace them. The realistic pattern in a Laravel codebase:

  • Coarse, model-less checks (can-view-admin-panel, can-impersonate) → Laravel Gate::allows.
  • Fine, object-scoped checks with many intersecting rules → Symfony voter.
  • Policy for the model's default CRUD (PostPolicy), voter for the multi-dimensional business rules ("can this underwriter approve this claim given tier, region, and reviewer conflict-of-interest").

You can bridge by having a Laravel Policy method delegate into the voter:

public function approve(User $user, Claim $claim): bool
{
    return app(Authorization::class)->isGranted('APPROVE', $claim);
}

This gives you Laravel's Gate::authorize('approve', $claim) in controllers while the rule lives in Symfony voter territory.

Middleware pattern for route-level object checks.

// routes/web.php
Route::get('/posts/{post}/edit', [PostController::class, 'edit'])
    ->middleware('can:edit,post');

with a PostPolicy::edit() that delegates to the voter. Now route-level authorization uses idiomatic Laravel and the voter handles the actual rule. Alternatively, a dedicated middleware that resolves the subject from the route model and calls your Authorization interface — useful when the same attribute (VIEW) applies across many models and you don't want N policies.

Blade / frontend. Register once:

// AppServiceProvider::boot()
Blade::if('granted', fn (string $attr, $subj = null) => 
    app(Authorization::class)->isGranted($attr, $subj));
@granted('EDIT', $post)
  <a href="{{ route('posts.edit', $post) }}">Edit</a>
@endgranted

Testing voters is a first-class activity, not an afterthought. Voters are pure functions of (attribute, subject, token) — trivially unit-testable. Pattern:

$voter = new PostVoter();
$token = new PostAuthenticationToken($user, 'main', $user->roles);
$this->assertTrue($voter->vote($token, $post, ['EDIT']) === VoterInterface::ACCESS_GRANTED);

Because you own this test surface (upstream has none you can rely on), it also becomes your upgrade regression suite when symfony/security-core bumps.

Shadow-run before cutover. If migrating from Gates/Policies to voters (or vice versa), log both decisions for a week. Compare and alert on disagreement. Authorization regressions are silent — a shadow comparison is the cheapest insurance.

Gotchas and Tips

The description is misleading; treat the README with suspicion. Every "what this package does" summary you encounter (security headers, CSP) is a different package. The only source of truth is composer.json + src/. Don't onboard a junior engineer against the docs.

Fail-open is the default footgun. A voter that throws, a token that's not set, a misconfigured strategy — none of these surface as loud errors. In affirmative mode, a single misbehaving voter returning true grants access. Enforce fail-closed at your adapter layer: catch exceptions in your Authorization::isGranted() implementation and return false, log, and (in staging) throw so tests catch it.

supports() returning false is not a deny — it's an abstain. Symfony treats non-supporting voters as ACCESS_ABSTAIN. In unanimous mode, abstains are ignored (which is usually what you want), but the absence of any supporting voter means the decision is denied only if you have no default-deny guard. Add a test: "unknown attribute → false".

The tag/config registration name is the silent killer. If you register voters via the config array but the provider reads a container tag (or the string has a .voter suffix mismatch), Symfony sees zero voters, every check abstains, and every request 403s. First debugging move when "nothing works": dd(app()->tagged('security.voter')) (or whatever tag the provider uses) and confirm your class is there. Second move: log inside supports().

Verify the token storage is populated before the first request. In an artisan test that hits a controller, Auth::user() is set but the Symfony token store is empty. Add to your TestCase::setUp() or a RefreshDatabase-adjacent trait:

$this->actingAs($user);
app('security.token_storage')->setToken(
    new PostAuthenticationToken($user, 'main', $user->getRoles())
);

Otherwise every feature test that should pass returns 403 and you'll chase the wrong bug.

Cache and config quirks. php artisan config:cache bakes the strategy and voter list at boot. Adding a voter after deploy requires a config:clear / redeploy — not just a code push. Put a note in the deploy checklist. Similarly, php artisan optimize and Octane will hold a stale service-provider boot; add a test or a php artisan about snapshot to catch this.

Symfony version drift is the maintenance task. symfony/security-core has had breaking changes across majors (v5 → v6 → v7) — VoterInterface return constants, TokenInterface signature, AccessDecisionManager constructor signature. This package has no CI demonstrating it tracks them. Pin symfony/security-core in your composer.json and subscribe to the Symfony upgrade guide. If you can't commit to that, don't adopt.

Log the decision, not just the outcome. When a voter misbehaves in prod, you want to know which attribute, which subject id, which strategy result, which voter granted/denied. Wrap the adapter:

Log::info('authz', [
    'attribute' => $attribute,
    'subject' => $subject ? get_class($subject) . ':' . $subject->getKey() : null,
    'user' => auth()->id(),
    'decision' => $granted,
]);

Ship this at info behind a config flag; it's the difference between a 15-minute fix and a two-day investigation.

AccessDecisionManager does not short-circuit in affirmative by default — it polls all voters. Fine for 3 voters, problematic at 30. If you have many, use priority strategy and place cheap deterministic voters first, or compose into a single "compound" voter per aggregate.

Don't confuse roles with attributes. Symfony's RoleVoter grants on ROLE_* attributes automatically — if you register it (some integrations do by default), a user with ROLE_ADMIN will pass isGranted('ROLE_ADMIN', $anything). That's handy, but it also silently grants where you meant a subject-scoped check. Audit which built-in voters the provider registers before you trust the deny path.

Forking is a realistic plan, not a failure state. At 52 stars, single maintainer, zero dependents: pick the exit now. Minimal exit cost is low, because the package surface is tiny — re-implementing SecurityServiceProvider + adapter against symfony/security-core directly is maybe 100 lines. Keep that plan in the ADR. Also pin to a specific tag/commit, and consider composer require with --prefer-source for reproducibility.

Upgrade testing is your responsibility. When Laravel, PHP, or Symfony bumps, run the same voter tests you wrote for your business rules. Those double as a compatibility suite. If they pass, you've validated the bridge against the new major. If they don't, you've discovered a break before prod.

Escape hatch. If, after the spike, native Gate/Policy handles your cases cleanly, remove the dependency. The composition work you did (interface, voter unit tests, shadow logging) transfers — you can reimplement the voter logic as a Policy class and delete the package in an afternoon.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console