web-token/jwt-bundle
Symfony bundle integrating the Web-Token JWT Framework, with configuration and services for JWT signing, verification, encryption, key management, JWK/JWKS handling, and algorithm selection via Symfony’s dependency injection.
web-token/jwt-bundle is the Symfony integration for the web-token/jwt-framework (Spomky-Labs). It is an integration layer, not a token library — it wires the framework's algorithm managers, key sets, and builders/verifiers into the container so you don't hand-assemble them per request.
Install and enable:
composer require web-token/jwt-bundle
Flex usually registers WebTokenBundle in config/bundles.php. If not, add it manually.
Look at the reference config before writing anything:
bin/console config:dump-reference web_token
The root key is web_token. Config shape has changed across major versions (key set naming, algorithm enabling) — always trust config:dump-reference over blog posts.
Minimal config — declare one key set you can verify/sign with. Prefer a file over inline key material:
# config/packages/web_token.yaml
web_token:
jwk_sets:
app_tokens:
keys:
- type: jwk
value: '%env(resolve:JWT_SIGNING_KEY)%'
# Only the algorithms you actually use:
algorithms:
- 'ES256'
Put the JWK JSON from JWKFactory::createFromKeyFile()/your keygen output in a secret, not in the repo.
First real use case — sign an access token and verify an inbound one. The framework services (JWSBuilder, JWSVerifier, JWKSet, AlgorithmManager, JWEBuilder/JWEDecrypter) are exposed as services and are normally autowirable. Start with JWSBuilder + JWSVerifier and a single ES256 key; that covers ~80% of real usage (OIDC-style tokens, API auth).
Sanity check before building anything: write a throwaway console command that builds a compact JWS, verifies it, and dumps $jws->getPayload(). If that works, the bundle wiring is correct.
Signing (compact JWS). Build, serialize, hand out. Keep this in a small service, not in the controller:
$jws = $this->builder
->create() // from AlgorithmManager
->withPayload(json_encode($claims, JSON_THROW_ON_ERROR))
->addSignature($this->jwk, ['alg' => 'ES256', 'typ' => 'JWT', 'kid' => $this->jwk->get('kid')])
->build();
$token = $this->serializer->serialize($jws, 0); // 0 = compact
Verification. Verify first, read payload second — getPayload() on an unverified JWS is not a trustworthy value:
$jws = $this->serializer->deserialize($token, JWS::class, 0);
if (!$this->verifier->verifyWithKeySet($jws, $this->keySet, 0)) {
throw new AuthenticationException();
}
$claims = json_decode($jws->getPayload(), true, 512, JSON_THROW_ON_ERROR);
Key sets per purpose, never one global set. Declare separate sets (app_tokens, refresh_tokens, oidc_google) and inject the specific one into the specific service. Autowiring by name is version-dependent — if named injection is awkward, define thin factory services that pull the set from the container/config and return a JWKSet. This keeps kid ambiguity local.
Key rotation. Put multiple keys in one set, each with a distinct kid, sign with the newest, verify against all. The framework's key selection uses the kid header, so rotation is a config change plus a deploy, not a code change. Publish the public half as a JWKS endpoint by serializing $keySet->toPublic() — you'll need it for any external consumer.
Encrypted / nested tokens (JWE). Only reach for JWE if you have a requirement that a bearer token must be opaque (e.g. it carries PII). The pattern is sign → serialize → encrypt the compact JWS as the JWE payload; decrypt → deserialize → verify. Two key sets, two algorithms (ECDH-ES or RSA-OAEP + A256GCM are sane defaults), and it doubles your operational surface. Most apps should not do this.
Symfony Security integration. Write a custom Authenticator (or a TokenExtractor/AccessTokenHandler in newer security versions) that injects the verifier + key set and returns a SelfValidatingPassport. The bundle deliberately does not plug into the security firewall; it gives you primitives and you own the guard.
Enable algorithms narrowly. Listing only the algorithms you use is both a config-hygiene win and a security control — it makes none and weak curves unavailable by default rather than relying on remembering to disable them.
web-token/jwt-framework. Don't file bugs here, and don't treat the low star count as a signal about the underlying library — the framework is the maintained artifact.exp, nbf, aud, iss, jti checking is done for you by the verifier. If you don't add a claim-validation layer (the framework's CheckerManager with ExpirationTimeChecker/AudienceChecker, or your own), you have signature verification and nothing else. This is the single most common production mistake with this bundle.getPayload() before verify. On JWS, reading the payload of an unverified token either returns attacker-controlled data or throws depending on version. Treat verify-then-read as a hard rule.InvalidSignatureException is a subclass of InvalidArgumentException, and JoseException sits above most of them. Catch JoseException at the boundary and map to 401 — don't let a malformed header 500.TimeChecker/validation data or you'll get intermittent exp failures between hosts.kid mismatches fail confusingly. If the header has no kid, selection may fall back or fail depending on set size. Always set kid when signing, always expect it when verifying, and log the decoded protected header on failure — it's the fastest debugging lever you have.%env(resolve:...)% or a file path outside the web root, and set restrictive permissions. Never commit a JWK with a private d/k member.$jws->getSignature(0)->getProtected() gives you the decoded header (alg, kid, typ). Log that plus the exception class — the framework's messages are accurate but terse, and the header usually explains the failure immediately.config:dump-reference/the bundle's CompilerPass for your version rather than copying a snippet from an older release.How can I help you explore Laravel packages today?