Skip to content

[AIGOR-418] Support multiple decode keys for signing key rotation - #12

Merged
underwear merged 2 commits into
mainfrom
decode-key-rotation
Aug 13, 2026
Merged

underwear merged 2 commits into
mainfrom
decode-key-rotation

Conversation

@underwear

@underwear underwear commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What

Stage 1 of the JWT signing secret rotation (AIGOR-418): the decode side now accepts multiple keys, so new tokens can be signed with a new secret while previously issued tokens keep verifying against the old one.

  • LARAVEL_JWT_DECODE_KEY accepts a comma-separated list (safe separator: keys are base64:..., no commas in the alphabet). A single key keeps working as before — fully backward compatible.
  • JwtDecoder tries the keys in order; the first key must match the encode key, the rest are fallbacks. JwtIssuer is untouched.
  • New JwtDecodedUsingFallbackKey event (decoded Jwt + matched key index) is dispatched whenever a token verifies with a fallback key — listen to it to watch legacy-key traffic drain before withdrawing the old secret (stage 2).
  • Empty decode key list now throws InvalidConfigException instead of silently failing auth.

Chose ordered try-over-list instead of kid-based resolution: all tokens already in the wild carry no kid, so the legacy fallback path is needed either way, and with 2–3 HS256 keys the extra verify attempt costs microseconds.

Tests & infra

  • Added a PHPUnit + Orchestra Testbench suite (first tests in the package) covering the full path from config parsing to decode: primary/fallback/unknown key, event dispatch, expired and malformed tokens, single-key BC, empty-list validation. New CI workflow runs it on push.
  • Ignored the firebase/php-jwt advisory PKSA-y2cr-5h3j-g3ys (low severity, affects all 6.x, no fixed release exists): composer 2.9+ refuses to resolve the dependency otherwise, which would break fresh installs in all CI workflows since the lock file isn't committed.

Rotation usage

LARAVEL_JWT_ENCODE_KEY="base64:NEW"
LARAVEL_JWT_DECODE_KEY="base64:NEW,base64:OLD"

Verification

  • PHPUnit: 8 tests, 14 assertions, green (PHP 8.4, Laravel 12.66, Testbench 10.11)
  • Pint, phpcs: clean; phpstan: no new errors (2 pre-existing PHP 8.4 deprecation warnings remain)

🤖 Generated with Claude Code

underwear and others added 2 commits August 13, 2026 17:16
LARAVEL_JWT_DECODE_KEY now accepts a comma-separated list of keys.
Tokens are verified against the keys in order, so new tokens are
signed with the encode key (which must be first in the list) while
previously issued tokens stay valid against the fallback keys.

Whenever a token is verified by a fallback key, the package dispatches
JwtDecodedUsingFallbackKey so the application can track how much
traffic still relies on the old key before withdrawing it.

Also adds a PHPUnit/Testbench test suite with a CI workflow, and
ignores the unfixed low-severity firebase/php-jwt advisory that
otherwise blocks fresh composer installs on composer 2.9+.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both workflows have been failing on the Setup PHP step since at least
July (also on the laravel-13-support branch). Bump them to PHP 8.3 to
match the tests workflow and drop --ignore-platform-reqs, which is no
longer needed once the runtime satisfies the package constraints.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@underwear
underwear merged commit 65b9700 into main Aug 13, 2026
3 checks passed
@underwear
underwear deleted the decode-key-rotation branch August 13, 2026 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant