From 58d4de419050bf3c29b3d3b61ab28f7ef942b279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A1ndor=20Istv=C3=A1n=20Kr=C3=A1cser?= Date: Wed, 30 Sep 2026 13:50:37 +0200 Subject: [PATCH] fix(token): don't panic on static JWTs without exp GetExpirationTime returns nil when the exp claim is absent, which StaticIdentityTokenProvider then dereferenced. Treat such tokens as never expiring, matching how the other providers handle a zero ExpiresAt. --- pkg/token/static_token_provider.go | 9 +++- pkg/token/static_token_provider_test.go | 69 +++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 pkg/token/static_token_provider_test.go diff --git a/pkg/token/static_token_provider.go b/pkg/token/static_token_provider.go index 0e28646..6c0b4eb 100644 --- a/pkg/token/static_token_provider.go +++ b/pkg/token/static_token_provider.go @@ -6,6 +6,7 @@ package token import ( "context" "sync" + "time" "emperror.dev/errors" "github.com/golang-jwt/jwt/v5" @@ -52,9 +53,15 @@ func (s *StaticIdentityTokenProvider) GetToken(ctx context.Context, opts ...opti return credential.Token{}, errors.WrapIf(err, "failed to get token expiration time") } + // exp is optional; a zero ExpiresAt means the token never expires. + var expiresAt time.Time + if exp != nil { + expiresAt = exp.Time + } + return credential.Token{ Token: t.Raw, - ExpiresAt: exp.Time, + ExpiresAt: expiresAt, }, nil } diff --git a/pkg/token/static_token_provider_test.go b/pkg/token/static_token_provider_test.go new file mode 100644 index 0000000..c8348b6 --- /dev/null +++ b/pkg/token/static_token_provider_test.go @@ -0,0 +1,69 @@ +// Copyright (c) 2026 Riptides Labs, Inc. +// SPDX-License-Identifier: MIT + +package token_test + +import ( + "testing" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.riptides.io/tokenex/pkg/token" +) + +func newJWT(t *testing.T, claims jwt.MapClaims) string { + t.Helper() + + s, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte("test-key")) + require.NoError(t, err) + + return s +} + +func TestStaticIdentityTokenProvider_GetToken(t *testing.T) { + t.Parallel() + + exp := time.Now().Add(time.Hour) + + tests := []struct { + name string + token string + wantExpiresAt time.Time + wantErr bool + }{ + { + name: "with exp", + token: newJWT(t, jwt.MapClaims{"sub": "workload", "exp": exp.Unix()}), + wantExpiresAt: time.Unix(exp.Unix(), 0), + }, + { + name: "without exp never expires", + token: newJWT(t, jwt.MapClaims{"sub": "workload"}), + }, + { + name: "not a JWT", + token: "not-a-jwt", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + tok, err := token.NewStaticIdentityTokenProvider(tt.token).GetToken(t.Context()) + if tt.wantErr { + require.Error(t, err) + + return + } + + require.NoError(t, err) + assert.Equal(t, tt.token, tok.Token) + assert.True(t, tt.wantExpiresAt.Equal(tok.ExpiresAt), "ExpiresAt = %v, want %v", tok.ExpiresAt, tt.wantExpiresAt) + }) + } +}