Skip to content

fix(auth): validate iat claim when verifying access tokens - #1807

Open
cipherprofessor wants to merge 1 commit into
livekit:mainfrom
cipherprofessor:fix/auth-verify-issued-at
Open

cipherprofessor wants to merge 1 commit into
livekit:mainfrom
cipherprofessor:fix/auth-verify-issued-at

Conversation

@cipherprofessor

Copy link
Copy Markdown

Fixes #1710

Problem

APIKeyTokenVerifier.Verify() calls jwt.ParseWithClaims without jwt.WithIssuedAt(), so the iat claim is never validated. A token correctly signed with the API secret but carrying a far-future iat, with no nbf claim, verifies successfully immediately — regardless of how far in the future it claims to have been issued.

First-party SDKs always set nbf (see AccessToken.ToJWT), which the parser already validates by default when present, so this only matters for hand-rolled or third-party-minted tokens that omit nbf.

Fix

Adds jwt.WithIssuedAt() alongside the existing jwt.WithExpirationRequired() (added in #1706 for a related but distinct gap — a missing exp claim).

Backward compatibility, verified against the actual vendored golang-jwt/v5 v5.3.1 source (not assumed): WithIssuedAt() calls verifyIssuedAt with required=false, so a token that omits iat entirely is completely unaffected — no breaking change for any minter that doesn't set it. Every existing SDK-minted and hand-rolled test token without an iat claim continues to verify exactly as before.

Known, intentional scope boundary: a token that omits both iat and nbf remains unaffected by this check either way, since iat is still not required — only validated when present. That matches this issue's own scope (a token that sets iat but omits nbf), not something missed. Making iat mandatory would be a materially bigger, more debatable change (RFC 7519 treats iat as informational, unlike exp) that could reasonably break minters that omit it — happy to discuss if that's wanted, but didn't want to bundle it into this fix unasked.

webhook/verifier.go's Receive() calls the same APIKeyTokenVerifier.Verify, so it inherits this fix automatically — confirmed there's no parallel jwt.ParseWithClaims call anywhere else in the repo that would need a matching change.

Testing

Added a regression test modeling a hand-rolled token with iat 2 hours in the future and no nbf claim, following the existing "token without exp is rejected" sibling test's style and conventions. Confirmed it fails against pre-fix code and passes after (TDD red/green).

go build, go vet, gofmt, and the full auth package test suite are all clean. Two pre-existing, unrelated issues found while validating, both confirmed identical on unmodified main via stash-and-rerun, neither touched by this diff:

  • TestURLNotifierFilter/none in the webhook package fails with a require.Eventually timeout (looks flaky/environment-dependent).
  • gofmt flags 3 files elsewhere in the repo (auth/grants.go, tools/tools.go, utils/jwtutil/jwtutil.go).

Included a changeset per the repo's convention.

APIKeyTokenVerifier.Verify() called jwt.ParseWithClaims without
jwt.WithIssuedAt(), so the iat claim was never validated. A token
correctly signed with the API secret but carrying a far-future iat,
with no nbf claim, verified successfully immediately regardless of
how far in the future it claimed to have been issued. First-party
SDKs always set nbf (see AccessToken.ToJWT), which the parser already
validates by default when present, so this only mattered for
hand-rolled or third-party-minted tokens that omit nbf.

Adds jwt.WithIssuedAt() alongside the existing
jwt.WithExpirationRequired() (added in livekit#1706 for a related but
distinct gap). Confirmed against the vendored golang-jwt/v5 v5.3.1
source that this does not require iat to be present -- verifyIssuedAt
is called with required=false, so a token that omits iat entirely is
completely unaffected, and existing SDK-minted and hand-rolled test
tokens without an iat claim continue to verify exactly as before. A
token that omits both iat and nbf remains unaffected by this check
either way, since iat is still not required -- that's an intentional,
narrower boundary matching this issue's own scope, not something
missed.

webhook/verifier.go's Receive() calls the same APIKeyTokenVerifier.Verify,
so it inherits this fix with no separate change needed; confirmed
there's no parallel jwt.ParseWithClaims call anywhere else in the repo.

Added a regression test modeling a hand-rolled token with iat 2 hours
in the future and no nbf claim, following the existing "token without
exp is rejected" sibling test's style. Confirmed it fails against
pre-fix code and passes after.

go build, go vet, gofmt, and the full auth package test suite are all
clean. One pre-existing, unrelated test failure exists in the webhook
package (TestURLNotifierFilter/none, a flaky require.Eventually-style
timeout) -- confirmed identical on unmodified main via stash-and-rerun.
Three pre-existing gofmt violations exist elsewhere in the repo
(auth/grants.go, tools/tools.go, utils/jwtutil/jwtutil.go) -- confirmed
identical on unmodified main, untouched by this diff.

Fixes livekit#1710
@changeset-bot

changeset-bot Bot commented Sep 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ff09d63

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
github.com/livekit/protocol Patch
@livekit/protocol Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

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.

auth: verifier accepts future-issued tokens without nbf

1 participant