From 04ed682872f95516ae3d42f2fbbdc7c682cb5add Mon Sep 17 00:00:00 2001 From: SashaMIT Date: Fri, 7 Aug 2026 13:51:37 +0700 Subject: [PATCH] fix(auth): require exp claim when verifying access tokens Verify() parsed with WithValidMethods/WithIssuer/WithLeeway but not WithExpirationRequired, so a token carrying no exp claim at all passed validation and never expired. First-party SDKs always set exp, but any hand-rolled or third-party minter that forgets it silently mints a permanent credential. --- auth/verifier.go | 5 +++++ auth/verifier_test.go | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/auth/verifier.go b/auth/verifier.go index 5bc7c3fb9..60a476d35 100644 --- a/auth/verifier.go +++ b/auth/verifier.go @@ -77,6 +77,11 @@ func (v *APIKeyTokenVerifier) Verify(key interface{}) (*jwt.RegisteredClaims, *C jwt.WithValidMethods(allowedSigningMethods), jwt.WithIssuer(v.apiKey), jwt.WithLeeway(tokenLeeway), + // Without this, a token carrying no exp claim passes validation and + // never expires. First-party SDKs always set exp, but any hand-rolled + // or third-party minter that forgets it silently mints a permanent + // credential. + jwt.WithExpirationRequired(), ) if err != nil { return nil, nil, err diff --git a/auth/verifier_test.go b/auth/verifier_test.go index cefc00613..dd5d23c71 100644 --- a/auth/verifier_test.go +++ b/auth/verifier_test.go @@ -49,6 +49,26 @@ func TestVerifier(t *testing.T) { require.Error(t, err) }) + t.Run("token without exp is rejected", func(t *testing.T) { + // hand-rolled JWT with no exp claim (the Go SDK always sets one, so + // build it directly to model a third-party minter forgetting exp) + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "iss": apiKey, + "nbf": jwt.NewNumericDate(time.Now().Add(-time.Minute)), + "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).