Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/auth-verify-issued-at.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"github.com/livekit/protocol": patch
"@livekit/protocol": patch
---

Validate the `iat` claim when verifying access tokens (`auth.APIKeyTokenVerifier.Verify`). Previously, a correctly-signed token that omitted `nbf` and carried a far-future `iat` would verify successfully immediately, regardless of how far in the future it claimed to have been issued.
7 changes: 7 additions & 0 deletions auth/verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ func (v *APIKeyTokenVerifier) Verify(key interface{}) (*jwt.RegisteredClaims, *C
// or third-party minter that forgets it silently mints a permanent
// credential.
jwt.WithExpirationRequired(),
// Without this, iat is never validated. First-party SDKs always set
// nbf (see AccessToken.ToJWT), which the parser already validates by
// default when present, but a token that omits nbf entirely verifies
// immediately regardless of how far in the future it claims to have
// been issued unless iat is checked too. (A token omitting both iat
// and nbf is unaffected by this check either way.)
jwt.WithIssuedAt(),
)
if err != nil {
return nil, nil, err
Expand Down
25 changes: 25 additions & 0 deletions auth/verifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,31 @@ func TestVerifier(t *testing.T) {
require.Error(t, err)
})

t.Run("token issued in the future without nbf is rejected", func(t *testing.T) {
// hand-rolled JWT with iat 2h in the future and no nbf claim. The Go
// SDK always sets nbf (see AccessToken.ToJWT), so build this directly
// to model a third-party minter that omits it: without nbf, and
// without WithIssuedAt() on the parser, nothing stops a token from
// verifying immediately no matter how far in the future it claims to
// have been issued.
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"iss": apiKey,
"iat": jwt.NewNumericDate(time.Now().Add(2 * time.Hour)),
"exp": jwt.NewNumericDate(time.Now().Add(3 * time.Hour)),
"video": map[string]interface{}{
"roomCreate": true,
},
})
authToken, err := token.SignedString([]byte(secret))
require.NoError(t, err)

v, err := auth.ParseAPIToken(authToken)
require.NoError(t, err)

_, _, err = v.Verify(secret)
require.Error(t, err)
})

t.Run("unexpired token is verified", func(t *testing.T) {
claim := auth.VideoGrant{RoomCreate: true}
at := auth.NewAccessToken(apiKey, secret).
Expand Down
Loading