symfony/security-core
Symfony Security Core provides the core building blocks for authentication and authorization: tokens, voters, role hierarchies, access decision management, and user providers. Use it to implement flexible permission checks and separate security logic from user storage.
Installation
composer require symfony/security-core
Laravel already bundles Symfony’s Security component, but this package provides the core classes for custom implementations.
First Use Case: Role-Based Access Control (RBAC)
AccessDecisionManager for granular permissions.AuthenticatesUsers trait to integrate Symfony’s voters.Key Classes to Explore
AccessDecisionManager: Orchestrates authorization logic.RoleHierarchy: Manages role inheritance (e.g., ROLE_ADMIN → ROLE_USER).Voter (e.g., RoleVoter, AuthenticatedVoter): Customize decision-making logic.Where to Look First
app/Http/Middleware/Authenticate.php (to see how tokens are resolved).vendor/symfony/security-core/Authorization/Voter/ (for voter implementations).Token Resolution:
Use AuthenticationTrustResolver to check if a token is fully authenticated (e.g., after login).
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
$trustResolver = new AuthenticationTrustResolver();
if (!$trustResolver->isAuthenticated($token)) {
// Redirect to login or throw exception.
}
Custom Voters:
Create a voter for domain-specific logic (e.g., CanEditPostVoter).
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class CanEditPostVoter extends Voter {
protected function supports(string $attribute, mixed $subject): bool {
return $attribute === 'EDIT_POST' && $subject instanceof Post;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool {
return $subject->author->id === $token->getUser()->id;
}
}
AccessDecisionManager Workflow: Combine voters in a decision manager to evaluate multiple rules.
$decisionManager = new AccessDecisionManager([
new AuthenticatedVoter($trustResolver),
new CanEditPostVoter(),
new RoleVoter(),
]);
if (!$decisionManager->decide($token, ['EDIT_POST'], $post)) {
abort(403);
}
RoleHierarchy instance.
$roleHierarchy = new RoleHierarchy([
'ROLE_SUPER_ADMIN' => ['ROLE_ADMIN', 'ROLE_AUDITOR'],
'ROLE_ADMIN' => ['ROLE_USER'],
]);
RoleHierarchyVoter to automatically grant inherited roles.AccessDecisionManager checks:
namespace App\Http\Middleware;
use Closure;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
class AuthorizeMiddleware {
public function __construct(private AccessDecisionManager $decisionManager) {}
public function handle($request, Closure $next, string $permission) {
if (!$this->decisionManager->decide(
$request->user()->getToken(),
[$permission],
$request->route()->parameter('model')
)) {
abort(403);
}
return $next($request);
}
}
app/Http/Kernel.php:
protected $routeMiddleware = [
'authorize' => \App\Http\Middleware\AuthorizeMiddleware::class,
];
Usage in routes:
Route::get('/admin/posts/{post}', function (Post $post) {
// ...
})->middleware(['auth', 'authorize:EDIT_POST']);
AccessDecisionManager and voters in PHPUnit:
$token = $this->createMock(TokenInterface::class);
$token->method('getUser')->willReturn($user);
$decisionManager = $this->createMock(AccessDecisionManager::class);
$decisionManager->method('decide')->willReturn(true);
$this->app->instance(AccessDecisionManager::class, $decisionManager);
Token Mismatch:
TokenInterface passed to decide() matches the one resolved by Laravel’s auth system.UsernamePasswordToken is rarely used directly in Laravel; prefer Laravel\Sanctum\PersonalAccessToken or Illuminate\Auth\SessionGuard tokens.Circular Role Dependencies:
RoleHierarchy throws CircularReferenceException if roles reference each other (e.g., A → B → A).$roleHierarchy->getReachableRoleNames('ROLE_ADMIN'); // Test reachability.
Voter Order Matters:
CanEditPostVoter) before RoleVoter.Laravel’s Gate vs. Symfony’s Voters:
Gate::forUser() with AccessDecisionManager in the same flow—it can lead to inconsistent results.Log Decisions:
Extend AccessDecisionManager to log voter results:
class DebugAccessDecisionManager extends AccessDecisionManager {
public function decide(TokenInterface $token, array $attributes, $subject = null): bool {
$result = parent::decide($token, $attributes, $subject);
\Log::debug('Authorization decision:', [
'attributes' => $attributes,
'subject' => method_exists($subject, 'getId') ? $subject->getId() : $subject,
'result' => $result,
]);
return $result;
}
}
Inspect Token: Dump the token to verify roles/credentials:
dd($request->user()->getToken()->getRoles());
Custom Attributes:
Replace string attributes (e.g., 'EDIT_POST') with domain objects:
class PostPermission {}
$decisionManager->decide($token, [new PostPermission()], $post);
Dynamic Voters: Load voters from service providers or config:
$voters = collect(config('security.voters'))
->map(fn ($class) => new $class())
->toArray();
Attribute Mappers: Convert Laravel’s Gates to Symfony attributes:
$attributeMapper = new AttributeMapper([
'edit-post' => 'EDIT_POST',
'delete-own' => 'DELETE_OWN_RESOURCE',
]);
$symfonyAttribute = $attributeMapper->map('edit-post');
Role Prefixes:
Symfony expects roles to start with ROLE_ (e.g., ROLE_ADMIN). Laravel’s Gate::allows() uses raw strings. Normalize roles when bridging:
$symfonyRole = 'ROLE_' . strtoupper(str_replace('-', '_', $laravelGate));
Token Storage:
Symfony’s TokenStorage interface differs from Laravel’s AuthManager. Use a bridge:
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class LaravelTokenStorage implements TokenStorageInterface {
public function __construct(private \Illuminate\Contracts\Auth\Authenticatable $user) {}
public function getToken(): ?TokenInterface {
return new UsernamePasswordToken($this->user, 'laravel', $this->user->getRoles());
}
}
RoleHierarchy is thread-safe; instantiate it once and reuse:
$this->app->singleton(RoleHierarchy::class, fn () => new RoleHierarchy($roles));
AuthenticatedVoter and RoleVoter if roles are only checked for authenticated users).How can I help you explore Laravel packages today?