agentleFS
fatonhfatonh/symfony-skills/skills/security-authentication/SKILL.md

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.

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
  1. Very specific:

    Use the hasher service; configure auto so Symfony picks the best algorithm (currently bcrypt/argon2) and can rehash on upgrade.

    line 58
  2. Very specific:

    Check authorization as close to the action as possible (#[IsGranted] attribute or denyAccessUnlessGranted).

    line 184
  3. Very specific:

    Never trust $request for identity — read the authenticated user from the token / #[CurrentUser].

    line 186

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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 = crackable

Firewalls & 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: true for API firewalls — no session cookie.
  • Check authorization as close to the action as possible (#[IsGranted] attribute or denyAccessUnlessGranted).
  • Return 401 for unauthenticated, 403 for authenticated-but-forbidden.
  • Never trust $request for 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 the password_hasher (auto).
  • Agent stores or logs plaintext passwords — only the hash is persisted; eraseCredentials() clears any plaintext.
  • Agent puts owner/tenant checks in access_control or inline if — 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 403 for unauthenticated requests (or 401 for forbidden) — 401 = who are you, 403 = not allowed.
  • Agent forgets ROLE_USER default in getRoles() — every authenticated user should have it.
  • Agent writes a custom login form auth from scratch — use form_login / the built-in authenticators, or json_login for APIs.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.