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.
In this repository119 other files it gives its agents.
- strategy-bitrix-d7-incrementalSkill
- strategy-explicit-over-magicSkill
- strategy-framework-conventionsSkill
- strategy-legacy-safe-editsSkill
- strategy-minimal-diffSkill
- strategy-no-untested-refactorSkill
- strategy-preserve-public-apiSkill
- strategy-readonly-immutabilitySkill
- strategy-security-firstSkill
- strategy-test-drivenSkill
- openapiSkill
- paginationSkill
- rest-designSkill
- versioningSkill
- cqrsSkill
- dependency-inversionSkill
- hexagonalSkill
- repository-patternSkill
- service-layerSkill
- bitrixSkill
- findings-reportSkill
- laravelSkill
- performanceSkill
- phpSkill
- project-analysisSkill
- refactoringSkill
- roi-prioritizationSkill
- securitySkill
- stack-detectionSkill
- cache-layersSkill
- component-engineSkill
- d7-eventsSkill
- d7-loaderSkill
- d7-orm-queriesSkill
- iblock-elementsSkill
- kernel-main-moduleSkill
- localizationSkill
- controllersSkill
- ormSkill
- validationSkill
- controllersSkill
- query-builderSkill
- routingSkill
- autoload-psr4Skill
- dependency-managementSkill
- scriptsSkill
- connection-poolingSkill
- mysql-indexingSkill
- redisSkill
- transactionsSkill
- ci-github-actionsSkill
- deployment-strategiesSkill
- docker-phpSkill
- env-configSkill
- associationsSkill
- entities-mappingSkill
- orm-basicsSkill
- entity-apiSkill
- module-developmentSkill
- routing-controllersSkill
- The first 60 shown.
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
- Distinctive: line 78
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 …
- Distinctive: line 79
Do not reach for JWT when a stateful, session-based form login already fits a server-rendered Symfony web app.
- Distinctive: line 116
Verify: The refresh-token table exists and its TTL exceeds the access-token TTL
- Distinctive: line 120
Verify: Posting a valid refresh token to the refresh route returns a new access token
- Distinctive: line 123
Verify: An expired access token refreshes successfully until its refresh row is revoked
- Distinctive: line 142
Verify: The key files exist and the private key is listed in .gitignore
- Distinctive: line 146
Verify: A POST of valid credentials to the login route returns a token
- Very specific: line 147
Add an access_control rule requiring IS_AUTHENTICATED_FULLY on ^/api, then call a protected endpoint with Authorization: Bearer <token> to …
- Distinctive: line 149
Verify: Requests without a token get 401 and requests with a valid token get 200
≈ “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
<!-- 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: trueandjwt: ~, 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 JWTFull 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- Require
gesdinet/jwt-refresh-token-bundle, run its migration to create the refresh-token table, and set a refreshttllonger than the access-token TTL.- Verify: The refresh-token table exists and its TTL exceeds the access-token TTL
- Register the
/api/token/refreshroute 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
- Drop
token_ttlto 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- Require
lexik/jwt-authentication-bundle, then runbin/console lexik:jwt:generate-keypairto create the private/public keys underconfig/jwt/.- Verify: The key files exist and the private key is listed in .gitignore
- In
security.yaml, add aloginfirewall withjson_loginpointing at a/api/login_checkroute, then anapifirewall withpattern: ^/api,stateless: true, andjwt: ~.- Verify: A POST of valid credentials to the login route returns a token
- Add an
access_controlrule requiringIS_AUTHENTICATED_FULLYon^/api, then call a protected endpoint withAuthorization: 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
JWTCreatedEventlistener 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_keyat key files and load thepass_phrasefrom%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-pattern | Why it fails | Do instead |
| Issuing long-lived access tokens to avoid refresh logic | A 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 algorithms | Trusting 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 payload | The 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
- 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/expwindow. Fix: Synchronize servers with NTP and configure a smallclock_skewtolerance on the bundle rather than widening the token TTL to hide the drift. - 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
loginfirewall (withjson_login) above theapifirewall, or scope the api firewall withpattern: ^/apiand keep/api/login_checkoutside it. - 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
providerwhose identity field matches the token'suser_id_claim(e.g. email), and ensure the user still exists.
Production checklist
- Verify the api firewall sets
stateless: true, enablesjwt, and thataccess_controlrequires 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_ttlis 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_throttlingfirewall 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
nonealgorithm, 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 -->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.

