From efe9a8c57be5ba9fc9b0fb71e3fc0a83038eb0ef Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Tue, 6 Oct 2026 10:34:28 +0200 Subject: [PATCH] fix: don't encode validity_in_seconds in user/call token payloads validity_in_seconds is an SDK option used only to compute exp, but it was passed through to jwt.sign and ended up in the token. Strip it before signing, and stop mutating the caller's payload object. Co-Authored-By: Claude --- __tests__/create-token.test.ts | 32 ++++++++++++++++++++++++++++++++ src/StreamClient.ts | 19 +++++++++++++------ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/__tests__/create-token.test.ts b/__tests__/create-token.test.ts index 5a25dc4..2fb65d4 100644 --- a/__tests__/create-token.test.ts +++ b/__tests__/create-token.test.ts @@ -58,6 +58,26 @@ describe('creating tokens', () => { } }); + it('should not encode validity_in_seconds in the payload', () => { + const token = client.generateUserToken({ + user_id: userId, + validity_in_seconds: 120, + }); + const decodedToken = jwt.verify(token, secret) as any; + + expect(decodedToken).not.toHaveProperty('validity_in_seconds'); + expect(Object.keys(decodedToken).sort()).toEqual( + ['exp', 'iat', 'user_id'].sort(), + ); + }); + + it('should not mutate the provided payload', () => { + const payload = { user_id: userId, validity_in_seconds: 120 }; + client.generateUserToken(payload); + + expect(payload).toEqual({ user_id: userId, validity_in_seconds: 120 }); + }); + it(`should make sure iat is correct`, () => { for (let i = 0; i < 5; i++) { const token = client.generateUserToken({ user_id: userId }); @@ -118,6 +138,18 @@ describe('creating tokens', () => { expect(decodedToken.exp).toBeDefined(); }); + it('should not encode validity_in_seconds in the payload', () => { + const token = client.generateCallToken({ + user_id: userId, + call_cids, + validity_in_seconds: 120, + }); + const decodedToken = jwt.verify(token, secret) as any; + + expect(decodedToken).not.toHaveProperty('validity_in_seconds'); + expect(decodedToken.exp - decodedToken.iat).toBe(120); + }); + it('with default expiration', () => { for (let i = 0; i < 5; i++) { const token = client.generateCallToken({ user_id: userId, call_cids }); diff --git a/src/StreamClient.ts b/src/StreamClient.ts index 1cd8efb..f38ac43 100644 --- a/src/StreamClient.ts +++ b/src/StreamClient.ts @@ -133,12 +133,17 @@ export class StreamClient extends CommonApi { iat?: number; } & Record, ) => { + // validity_in_seconds is only used to compute exp, don't encode it + const { validity_in_seconds, ...claims } = payload; const defaultIat = Math.floor((Date.now() - 1000) / 1000); - payload.iat = payload.iat ?? defaultIat; - const validityInSeconds = payload.validity_in_seconds ?? 60 * 60; - payload.exp = payload.exp ?? payload.iat + validityInSeconds; + const iat = claims.iat ?? defaultIat; + const exp = claims.exp ?? iat + (validity_in_seconds ?? 60 * 60); - return JWTUserToken(this.secret, payload as UserTokenPayload); + return JWTUserToken(this.secret, { + ...claims, + iat, + exp, + } as UserTokenPayload); }; /** @@ -154,9 +159,11 @@ export class StreamClient extends CommonApi { } & Record, ) => { const defaultIat = Math.floor((Date.now() - 1000) / 1000); - payload.iat = payload.iat ?? defaultIat; - return JWTUserToken(this.secret, payload as UserTokenPayload); + return JWTUserToken(this.secret, { + ...payload, + iat: payload.iat ?? defaultIat, + } as UserTokenPayload); }; /**