agentleFS
AnyOrgNameAnyOrgName/php-skills/skills/symfony/security-jwt/SKILL.md

security-jwt

Skill4 starsChanged 4 months agoMITView on GitHub

Add stateless JWT authentication to a Symfony API with LexikJWTAuthenticationBundle — firewall wiring in security.yaml, token TTL and refresh, and a custom authenticator when the defaults are not enough. Use when the user must configure a Symfony firewall for JWT — the login entry point, the api firewall, and the token authenticator in security.yaml; the user needs to authenticate a Symfony API statelessly with JWT tokens issued and verified by LexikJWTAuthenticationBundle; the user wants to set the JWT token TTL, add refresh tokens, or build a custom Symfony authenticator for token handling. Keywords: security, auth, authentication, jwt, symfony, firewall, stateless, api.

The file

We split this skill into the rules it gives an agent

9 rules. About 0 are common, 8 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈8

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Do not use this skill for JWT outside Symfony's security component, such as a Laravel guard, a raw firebase/php-jwt integration, or another …

    line 78
  2. Distinctive:

    Do not reach for JWT when a stateful, session-based form login already fits a server-rendered Symfony web app.

    line 79
  3. Distinctive:

    Verify: The refresh-token table exists and its TTL exceeds the access-token TTL

    line 116
  4. Distinctive:

    Verify: Posting a valid refresh token to the refresh route returns a new access token

    line 120
  5. Distinctive:

    Verify: An expired access token refreshes successfully until its refresh row is revoked

    line 123
  6. Distinctive:

    Verify: The key files exist and the private key is listed in .gitignore

    line 142
  7. Distinctive:

    Verify: A POST of valid credentials to the login route returns a token

    line 146
  8. Very specific:

    Add an access_control rule requiring IS_AUTHENTICATED_FULLY on ^/api, then call a protected endpoint with Authorization: Bearer <token> to …

    line 147
  9. Distinctive:

    Verify: Requests without a token get 401 and requests with a valid token get 200

    line 149

≈ “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
<!-- generated: do not edit — source: knowledge/atoms/symfony/security-jwt/ --><!-- schema: atomVersion=3 skillVersion=2 graphVersion=1 --><!-- compile: @php-skills/compiler v1.0.0 (deterministic build) -->

Symfony JWT Authentication

When to use

  • The user must configure a Symfony firewall for JWT — the login entry point, the api firewall, and the token authenticator in security.yaml.
  • The user needs to authenticate a Symfony API statelessly with JWT tokens issued and verified by LexikJWTAuthenticationBundle.
  • The user wants to set the JWT token TTL, add refresh tokens, or build a custom Symfony authenticator for token handling.

When NOT to use

  • Do not use this skill for JWT outside Symfony's security component, such as a Laravel guard, a raw firebase/php-jwt integration, or another framework.
  • Do not reach for JWT when a stateful, session-based form login already fits a server-rendered Symfony web app.

Core guidance

  • Mark the JWT firewall stateless — Configure the API firewall with stateless: true and jwt: ~, so no session is created and every request is authenticated solely from its bearer token.

Decision Engine

Walk this section before choosing an implementation path.

How should clients authenticate against this API?
├── Long sessions that must stay revocable: → jwt plus refresh — short access-token TTL and a revocable refresh token
├── Stateless API for SPAs, mobile, or service-to-service: → stateless jwt — a stateless firewall with LexikJWTAuthenticationBundle
└── Server-rendered app, one trusted browser client: → session auth — use a stateful form_login firewall, not JWT

Full engine: references/decision-engine.md — Decision Tree, Priority Matrix, Decision Table.

Workflows

Workflow: Add Refresh Tokens

Progress:
- [ ] Require `gesdinet/jwt-refresh-token-bundle`, run its migration to create the
refresh-token table, and set a refresh `ttl` longer than the access-token TTL
- [ ] Register the `/api/token/refresh` route and allow it on the login (not the api)
firewall so a client can exchange a refresh token for a fresh access token without a
valid access token
- [ ] Drop `token_ttl` to a few minutes now that refresh exists, then verify that
deleting a refresh-token row blocks further refreshes for that session
  1. Require gesdinet/jwt-refresh-token-bundle, run its migration to create the refresh-token table, and set a refresh ttl longer than the access-token TTL.
    • Verify: The refresh-token table exists and its TTL exceeds the access-token TTL
  2. Register the /api/token/refresh route and allow it on the login (not the api) firewall so a client can exchange a refresh token for a fresh access token without a valid access token.
    • Verify: Posting a valid refresh token to the refresh route returns a new access token
  3. Drop token_ttl to a few minutes now that refresh exists, then verify that deleting a refresh-token row blocks further refreshes for that session.
    • Verify: An expired access token refreshes successfully until its refresh row is revoked

Workflow: Setup Lexik Jwt

Progress:
- [ ] Require `lexik/jwt-authentication-bundle`, then run
`bin/console lexik:jwt:generate-keypair` to create the private/public keys under
`config/jwt/`
- [ ] In `security.yaml`, add a `login` firewall with `json_login` pointing at a
`/api/login_check` route, then an `api` firewall with `pattern: ^/api`,
`stateless: true`, and `jwt: ~`
- [ ] Add an `access_control` rule requiring `IS_AUTHENTICATED_FULLY` on `^/api`, then
call a protected endpoint with `Authorization: Bearer <token>` to confirm access
  1. Require lexik/jwt-authentication-bundle, then run bin/console lexik:jwt:generate-keypair to create the private/public keys under config/jwt/.
    • Verify: The key files exist and the private key is listed in .gitignore
  2. In security.yaml, add a login firewall with json_login pointing at a /api/login_check route, then an api firewall with pattern: ^/api, stateless: true, and jwt: ~.
    • Verify: A POST of valid credentials to the login route returns a token
  3. Add an access_control rule requiring IS_AUTHENTICATED_FULLY on ^/api, then call a protected endpoint with Authorization: Bearer <token> to confirm access.
    • Verify: Requests without a token get 401 and requests with a valid token get 200

Best practices

  • Rely on the bundle's built-in JWT authenticator for standard bearer tokens; extend AbstractAuthenticator (or an event subscriber) only when the token arrives in a non-standard place or needs extra claim checks
  • Enrich the payload with a JWTCreatedEvent listener to add claims such as roles or a tenant id, but never put secrets or PII in the token, since the payload is base64-encoded, not encrypted
  • Point lexik_jwt_authentication.secret_key / public_key at key files and load the pass_phrase from %env(JWT_PASSPHRASE)%, keeping the private key out of the repo and the passphrase out of committed config
  • Set a short token_ttl (minutes, not days) for access tokens and issue a separate, longer-lived, revocable refresh token via a refresh-token bundle rather than extending the access-token lifetime

Anti-patterns

Anti-patternWhy it failsDo instead
Issuing long-lived access tokens to avoid refresh logicA stateless JWT cannot be revoked before it expires, so a long TTL means a stolen<br>token stays valid for days with no way to cut it off.Keep the access-token TTL short and add a revocable refresh token so long sessions<br>do not depend on a long-lived, unrevocable access token.
Accepting unsigned or client-chosen algorithmsTrusting the token's own algorithm header enables the classic alg: none and<br>RS256-to-HS256 confusion attacks, letting an attacker forge valid tokens.Pin the verification algorithm in bundle config (e.g. RS256) and reject any token<br>whose signature does not verify under that fixed algorithm.
Storing secrets or PII in the JWT payloadThe payload is only base64-encoded, not encrypted, so anyone who intercepts or<br>decodes the token can read every claim in it.Keep the payload to a stable identity and non-sensitive claims (roles, tenant);<br>look up anything sensitive server-side by the identity claim.

See references/anti-patterns.md.

Debugging

  1. Token rejected due to server clock skew — JWT validity is time-based; even a few seconds of drift between machines pushes a token outside its nbf/exp window. Fix: Synchronize servers with NTP and configure a small clock_skew tolerance on the bundle rather than widening the token TTL to hide the drift.
  2. Login path caught by the JWT firewall — Symfony matches the first firewall whose pattern fits the request, so a broad api pattern swallows the login request before the login firewall runs. Fix: Put a dedicated login firewall (with json_login) above the api firewall, or scope the api firewall with pattern: ^/api and keep /api/login_check outside it.
  3. Missing or wrong user provider on the JWT firewall — After verifying the signature the bundle re-loads the user from the provider to refresh roles; a missing or mismatched provider breaks that lookup. Fix: Point the firewall at a provider whose identity field matches the token's user_id_claim (e.g. email), and ensure the user still exists.

Production checklist

  • Verify the api firewall sets stateless: true, enables jwt, and that access_control requires an authenticated role on protected paths.
  • Ensure the token endpoint and all authenticated routes require HTTPS so bearer tokens are never sent in cleartext.
  • Confirm the JWT private key is gitignored and the passphrase is injected via %env(JWT_PASSPHRASE)%, never committed to config or images.
  • Confirm the login / token-issuing endpoint is throttled (login rate limiter or an external limiter) so credentials cannot be brute-forced.
  • Check that token_ttl is short (minutes) and that any long-lived access is served by a revocable refresh token, not a long access-token lifetime.

Version notes

  • New authenticator system on Symfony 6.4 LTS (PHP >=8.1) — On Symfony 6.4 (LTS) the new authenticator-based security system is the default, and recent LexikJWTAuthenticationBundle releases plug into it via jwt: ~ on the firewall — the legacy guard authenticators are gone.
  • Login throttling integration from Symfony 7.x (PHP >=8.1) — On Symfony 7.x the framework's login_throttling firewall option integrates with the RateLimiter component, so a JWT login route can be brute-force protected without a custom authenticator.

Security

  • Pin the algorithm and trust only verified signatures — Pin the verification algorithm (e.g. RS256) in bundle config, reject the none algorithm, and never trust claims from a token whose signature has not verified against your public key.

Testing

  • Activation and decision behavior are verified in eval/scenarios/symfony/security-jwt.yaml.

Additional resources

  • references/anti-patterns.md
  • references/core.md
  • references/decision-engine.md
  • references/examples.md
  • references/pitfalls.md
  • references/recipes.md
  • references/version-differences.md

Knowledge graph

Prerequisites: symfony/dependency-injection
Related: security/jwt-php

<!-- graph-fragment: graph/fragments/symfony/security-jwt.yaml -->

Skill contract

Produces: auth.jwt-configured
Requires: http.api-routes-defined
Risk: medium · Confidence: 0.9

<!-- contract-fragment: contracts/fragments/symfony/security-jwt.yaml -->

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.