From 25a286819fa0c33a227032add22b696b43011037 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Wed, 6 May 2026 09:06:46 -0700 Subject: [PATCH 1/3] fix: ensure we don't reject tokens with unknown fields --- auth/grants.go | 8 +++++++- auth/verifier_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/auth/grants.go b/auth/grants.go index df5e684bd..8fc0bf00a 100644 --- a/auth/grants.go +++ b/auth/grants.go @@ -35,6 +35,12 @@ var tokenMarshaler = protojson.MarshalOptions{ EmitDefaultValues: false, } +// tokenUnmarshaler discards unknown fields so that older servers can accept +// tokens issued by newer clients that include fields the server does not yet know about. +var tokenUnmarshaler = protojson.UnmarshalOptions{ + DiscardUnknown: true, +} + var ErrSensitiveCredentials = errors.New("room configuration should not contain sensitive credentials") func (c *RoomConfiguration) Clone() *RoomConfiguration { @@ -49,7 +55,7 @@ func (c *RoomConfiguration) MarshalJSON() ([]byte, error) { } func (c *RoomConfiguration) UnmarshalJSON(data []byte) error { - return protojson.Unmarshal(data, (*livekit.RoomConfiguration)(c)) + return tokenUnmarshaler.Unmarshal(data, (*livekit.RoomConfiguration)(c)) } // CheckCredentials checks if the room configuration contains sensitive credentials diff --git a/auth/verifier_test.go b/auth/verifier_test.go index 37d8f2ab3..580b4fe92 100644 --- a/auth/verifier_test.go +++ b/auth/verifier_test.go @@ -18,7 +18,9 @@ import ( "testing" "time" + "github.com/go-jose/go-jose/v3" "github.com/go-jose/go-jose/v3/json" + "github.com/go-jose/go-jose/v3/jwt" "github.com/stretchr/testify/require" "github.com/livekit/protocol/auth" @@ -95,6 +97,42 @@ func TestVerifier(t *testing.T) { require.EqualValues(t, attrs, decoded.Attributes) }) + t.Run("unknown roomConfig fields are ignored for forward compatibility", func(t *testing.T) { + // Simulate a token issued by a newer client whose RoomConfiguration includes + // a field this server's proto does not yet know about. The server should + // still accept the token rather than failing with `unknown field`. + sig, err := jose.NewSigner( + jose.SigningKey{Algorithm: jose.HS256, Key: []byte(secret)}, + (&jose.SignerOptions{}).WithType("JWT"), + ) + require.NoError(t, err) + + claims := map[string]interface{}{ + "iss": apiKey, + "sub": "me", + "nbf": jwt.NewNumericDate(time.Now()), + "exp": jwt.NewNumericDate(time.Now().Add(time.Minute)), + "video": map[string]interface{}{ + "roomJoin": true, + "room": "myroom", + }, + "roomConfig": map[string]interface{}{ + "name": "myroom", + "someFutureFieldName": "future-value", + }, + } + token, err := jwt.Signed(sig).Claims(claims).CompactSerialize() + require.NoError(t, err) + + v, err := auth.ParseAPIToken(token) + require.NoError(t, err) + + _, decoded, err := v.Verify(secret) + require.NoError(t, err) + require.NotNil(t, decoded.RoomConfig) + require.Equal(t, "myroom", decoded.RoomConfig.Name) + }) + t.Run("nil permissions are handled", func(t *testing.T) { grant := &auth.VideoGrant{ Room: "myroom", From 574017d49c06b5eda4452349e81a011a73ad9fc2 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Wed, 6 May 2026 09:08:25 -0700 Subject: [PATCH 2/3] changeset --- .changeset/witty-donuts-nail.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/witty-donuts-nail.md diff --git a/.changeset/witty-donuts-nail.md b/.changeset/witty-donuts-nail.md new file mode 100644 index 000000000..20e300749 --- /dev/null +++ b/.changeset/witty-donuts-nail.md @@ -0,0 +1,5 @@ +--- +"github.com/livekit/protocol": patch +--- + +fix: ensure we don't reject tokens on unknown fields From 2394c5554ddb52915bd50843402b38efed61540c Mon Sep 17 00:00:00 2001 From: David Zhao Date: Wed, 6 May 2026 09:11:27 -0700 Subject: [PATCH 3/3] add other top level fields to test --- auth/verifier_test.go | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/auth/verifier_test.go b/auth/verifier_test.go index 580b4fe92..c608f340f 100644 --- a/auth/verifier_test.go +++ b/auth/verifier_test.go @@ -97,10 +97,11 @@ func TestVerifier(t *testing.T) { require.EqualValues(t, attrs, decoded.Attributes) }) - t.Run("unknown roomConfig fields are ignored for forward compatibility", func(t *testing.T) { - // Simulate a token issued by a newer client whose RoomConfiguration includes - // a field this server's proto does not yet know about. The server should - // still accept the token rather than failing with `unknown field`. + t.Run("unknown fields are ignored for forward compatibility", func(t *testing.T) { + // Simulate a token issued by a newer client whose claims include fields + // this server does not yet know about. The server should still accept the + // token rather than failing with `unknown field`. This guards against + // requiring server upgrades before client upgrades can roll out. sig, err := jose.NewSigner( jose.SigningKey{Algorithm: jose.HS256, Key: []byte(secret)}, (&jose.SignerOptions{}).WithType("JWT"), @@ -112,13 +113,18 @@ func TestVerifier(t *testing.T) { "sub": "me", "nbf": jwt.NewNumericDate(time.Now()), "exp": jwt.NewNumericDate(time.Now().Add(time.Minute)), + // unknown top-level claim grants field + "someFutureGrant": map[string]interface{}{"enabled": true}, "video": map[string]interface{}{ "roomJoin": true, "room": "myroom", + // unknown field inside a known grant + "someFutureVideoField": "future-value", }, "roomConfig": map[string]interface{}{ - "name": "myroom", - "someFutureFieldName": "future-value", + "name": "myroom", + // unknown field inside a protojson-decoded message + "someFutureRoomConfigField": "future-value", }, } token, err := jwt.Signed(sig).Claims(claims).CompactSerialize() @@ -129,6 +135,9 @@ func TestVerifier(t *testing.T) { _, decoded, err := v.Verify(secret) require.NoError(t, err) + require.NotNil(t, decoded.Video) + require.Equal(t, "myroom", decoded.Video.Room) + require.True(t, decoded.Video.RoomJoin) require.NotNil(t, decoded.RoomConfig) require.Equal(t, "myroom", decoded.RoomConfig.Name) })