security-authentication
Skill35 starsChanged 5 months agoMITView on GitHub
Use when configuring Symfony Security — firewalls, authenticators, the User entity, password hashing, access control, and voters. Use when the task mentions login, authentication, authorization, roles, voter, firewall, or password.
In this repository16 other files it gives its agents.
- api-platform-resourcesSkill
- bundle-organizationSkill
- console-commandsSkill
- doctrine-migrationsSkill
- doctrine-ormSkill
- doctrine-query-optimizationSkill
- domain-driven-designSkill
- dto-and-validationSkill
- hexagonal-architectureSkill
- jwt-authenticationSkill
- layered-architectureSkill
- messenger-asyncSkill
- problem-details-rfc9457Skill
- rest-controller-conventionsSkill
- testing-pyramidSkill
- transactional-patternsSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 0 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 58
Use the hasher service; configure auto so Symfony picks the best algorithm (currently bcrypt/argon2) and can rehash on upgrade.
- Very specific: line 184
Check authorization as close to the action as possible (#[IsGranted] attribute or denyAccessUnlessGranted).
- Very specific: line 186
Never trust $request for identity — read the authenticated user from the token / #[CurrentUser].
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Symfony Security: Authentication & Authorization
The User entity
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: 'users')]
#[ORM\UniqueConstraint(name: 'uniq_users_email', fields: ['email'])]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id, ORM\Column(type: UuidType::NAME, unique: true)]
private Uuid $id;
#[ORM\Column(length: 180, unique: true)]
private string $email;
/** @var string[] */
#[ORM\Column]
private array $roles = [];
#[ORM\Column] // the *hash*, never the plaintext
private string $password;
public function __construct(string $email)
{
$this->id = Uuid::v7();
$this->email = $email;
}
public function getUserIdentifier(): string { return $this->email; }
/** @return string[] */
public function getRoles(): array
{
$roles = $this->roles;
$roles[] = 'ROLE_USER'; // everyone is at least ROLE_USER
return array_unique($roles);
}
public function getPassword(): string { return $this->password; }
public function setHashedPassword(string $hash): void { $this->password = $hash; }
public function eraseCredentials(): void { /* no plaintext held */ }
}Password hashing — never roll your own
Use the hasher service; configure auto so Symfony picks the best algorithm (currently bcrypt/argon2) and can rehash on upgrade.
# config/packages/security.yaml
security:
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'// Hashing at registration
$user->setHashedPassword($this->passwordHasher->hashPassword($user, $plainPassword));// ❌ NEVER
$user->setPassword(md5($plain)); // broken
$user->setPassword(hash('sha256', $plain)); // unsalted, fast = crackableFirewalls & access control
security:
providers:
app_user_provider:
entity: { class: App\Entity\User, property: email }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api:
pattern: ^/api
stateless: true # APIs are stateless (token auth)
provider: app_user_provider
jwt: ~ # see jwt-authentication
main:
lazy: true
provider: app_user_provider
access_control:
- { path: ^/api/login, roles: PUBLIC_ACCESS }
- { path: ^/api/admin, roles: ROLE_ADMIN }
- { path: ^/api, roles: ROLE_USER }access_control is coarse-grained (path + role). For per-object decisions, use voters.
Custom authenticator (when the built-ins don't fit)
final class ApiKeyAuthenticator extends AbstractAuthenticator
{
public function __construct(private readonly UserRepository $users) {}
public function supports(Request $request): ?bool
{
return $request->headers->has('X-API-KEY');
}
public function authenticate(Request $request): Passport
{
$apiKey = $request->headers->get('X-API-KEY');
if (!$apiKey) {
throw new CustomUserMessageAuthenticationException('No API key provided');
}
return new SelfValidatingPassport(
new UserBadge($apiKey, fn (string $key) => $this->users->findByApiKey($key)),
);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $e): Response
{
return new JsonResponse(['title' => 'Authentication failed'], Response::HTTP_UNAUTHORIZED);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null; // continue the request
}
}Voters for object-level authorization
Authorization that depends on the specific object (owner, tenant, state) belongs in a voter, not in access_control or scattered if checks.
/** @extends Voter<string, Order> */
final class OrderVoter extends Voter
{
public const VIEW = 'ORDER_VIEW';
public const CANCEL = 'ORDER_CANCEL';
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, [self::VIEW, self::CANCEL], true) && $subject instanceof Order;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
return match ($attribute) {
self::VIEW => $subject->getCustomerEmail() === $user->getUserIdentifier(),
self::CANCEL => $subject->getCustomerEmail() === $user->getUserIdentifier()
&& $subject->getStatus() === OrderStatus::Pending,
default => false,
};
}
}// In the controller / handler
$this->denyAccessUnlessGranted(OrderVoter::CANCEL, $order);Conventions
stateless: truefor API firewalls — no session cookie.- Check authorization as close to the action as possible (
#[IsGranted]attribute ordenyAccessUnlessGranted). - Return 401 for unauthenticated, 403 for authenticated-but-forbidden.
- Never trust
$requestfor identity — read the authenticated user from the token /#[CurrentUser]. - Enforce CSRF protection on session-based browser forms; stateless token APIs don't need it.
Gotchas
- Agent hashes passwords with
md5/sha1/sha256— use thepassword_hasher(auto). - Agent stores or logs plaintext passwords — only the hash is persisted;
eraseCredentials()clears any plaintext. - Agent puts owner/tenant checks in
access_controlor inlineif— use a voter for object-level rules. - Agent makes the API firewall stateful (session) — set
stateless: true. - Agent reads the user id from a request param/body — use the authenticated token /
#[CurrentUser]. - Agent returns
403for unauthenticated requests (or401for forbidden) — 401 = who are you, 403 = not allowed. - Agent forgets
ROLE_USERdefault ingetRoles()— every authenticated user should have it. - Agent writes a custom login form auth from scratch — use
form_login/ the built-in authenticators, orjson_loginfor APIs.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

