Skip to content

Commit 5b5f0e7

Browse files
authored
Token renewal in a interceptor (#86)
1 parent fcb037b commit 5b5f0e7

File tree

7 files changed

+111
-109
lines changed

7 files changed

+111
-109
lines changed

generate/go_client.tpl

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
package client
33

44
import (
5-
"sync"
6-
75
"connectrpc.com/connect"
86
compress "github.com/klauspost/connect-compress/v2"
97

@@ -22,8 +20,6 @@ type (
2220
config *DialConfig
2321

2422
interceptors []connect.Interceptor
25-
26-
sync.Mutex
2723
}
2824
{{ range $name, $api := . -}}
2925
{{ $name | title }} interface {
@@ -55,16 +51,18 @@ func New(config *DialConfig) (Client, error) {
5551
if config.Token != "" {
5652
authInterceptor := &authInterceptor{config: config}
5753
c.interceptors = append(c.interceptors, authInterceptor)
54+
55+
if config.TokenRenewal != nil {
56+
tokenRenewingInterceptor := &tokenRenewingInterceptor{config: config, client: c}
57+
c.interceptors = append(c.interceptors, tokenRenewingInterceptor)
58+
}
5859
}
5960
if config.Log != nil {
6061
loggingInterceptor := &loggingInterceptor{config: config}
6162
c.interceptors = append(c.interceptors, loggingInterceptor)
6263
}
6364
c.interceptors = append(c.interceptors, config.Interceptors...)
6465

65-
// TODO convert to interceptor
66-
go c.startTokenRenewal()
67-
6866
return c, nil
6967
}
7068

go.mod

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ require (
88
connectrpc.com/connect v1.19.1
99
github.com/bufbuild/protocompile v0.14.1
1010
github.com/go-task/slim-sprig/v3 v3.0.0
11-
github.com/golang-jwt/jwt/v5 v5.3.0
11+
github.com/golang-jwt/jwt/v5 v5.3.1
1212
github.com/google/go-cmp v0.7.0
1313
github.com/klauspost/connect-compress/v2 v2.1.1
1414
github.com/stretchr/testify v1.11.1
@@ -19,17 +19,16 @@ require (
1919
cel.dev/expr v0.25.1 // indirect
2020
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
2121
github.com/davecgh/go-spew v1.1.1 // indirect
22-
github.com/google/cel-go v0.26.1 // indirect
22+
github.com/google/cel-go v0.27.0 // indirect
2323
github.com/klauspost/compress v1.18.3 // indirect
2424
github.com/kr/pretty v0.3.1 // indirect
2525
github.com/minio/minlz v1.0.1 // indirect
2626
github.com/pmezard/go-difflib v1.0.0 // indirect
27-
github.com/stoewer/go-strcase v1.3.1 // indirect
2827
github.com/stretchr/objx v0.5.3 // indirect
2928
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
3029
golang.org/x/text v0.33.0 // indirect
31-
google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d // indirect
32-
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d // indirect
30+
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
31+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
3332
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
3433
gopkg.in/yaml.v3 v3.0.1 // indirect
3534
)

go.sum

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,14 @@ github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7
1313
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
1414
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
1515
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
16-
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1716
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1817
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1918
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
2019
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
21-
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
22-
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
23-
github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
24-
github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
20+
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
21+
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
22+
github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=
23+
github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw=
2524
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
2625
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
2726
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
@@ -44,31 +43,24 @@ github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe
4443
github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0=
4544
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
4645
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
47-
github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
48-
github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
49-
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
50-
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
51-
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
5246
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
5347
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
54-
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
55-
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
56-
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
5748
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
5849
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
50+
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
51+
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
5952
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
6053
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
6154
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
6255
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
63-
google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d h1:tUKoKfdZnSjTf5LW7xpG4c6SZ3Ozisn5eumcoTuMEN4=
64-
google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
65-
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1:xXzuihhT3gL/ntduUZwHECzAn57E8dA6l8SOtYWdD8Q=
66-
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
56+
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
57+
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
58+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
59+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
6760
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
6861
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
6962
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
7063
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
7164
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
72-
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
7365
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
7466
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

go/client/client-interceptors.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ package client
22

33
import (
44
"context"
5+
"fmt"
6+
"log/slog"
7+
"sync"
8+
"sync/atomic"
9+
"time"
510

611
"connectrpc.com/connect"
12+
apiv2models "github.com/metal-stack/api/go/metalstack/api/v2"
713
)
814

915
// authinterceptor adds the required auth headers
@@ -65,3 +71,79 @@ func (i *loggingInterceptor) WrapStreamingClient(next connect.StreamingClientFun
6571
func (i *loggingInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc {
6672
return next
6773
}
74+
75+
type tokenRenewingInterceptor struct {
76+
config *DialConfig
77+
client *client
78+
79+
renewing atomic.Bool
80+
81+
sync.Mutex
82+
}
83+
84+
func (i *tokenRenewingInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc {
85+
return connect.UnaryFunc(func(ctx context.Context, request connect.AnyRequest) (connect.AnyResponse, error) {
86+
err := i.renewTokenIfNeeded()
87+
if err != nil {
88+
return nil, err
89+
}
90+
return next(ctx, request)
91+
})
92+
}
93+
94+
func (i *tokenRenewingInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc {
95+
return next
96+
}
97+
98+
func (i *tokenRenewingInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc {
99+
return next
100+
}
101+
102+
func (i *tokenRenewingInterceptor) renewTokenIfNeeded() error {
103+
if i.config.expiresAt.IsZero() {
104+
return nil
105+
}
106+
if i.renewing.Load() {
107+
return nil
108+
}
109+
if i.config.Log == nil {
110+
i.config.Log = slog.Default()
111+
}
112+
113+
replaceBefore := i.config.expiresAt.Sub(i.config.issuedAt) / tokenRenewChecksDuringLifetime
114+
115+
if time.Until(i.config.expiresAt) > replaceBefore {
116+
return nil
117+
}
118+
119+
i.renewing.Store(true)
120+
defer i.renewing.Store(false)
121+
122+
i.config.Log.Info("call token refresh, current token expires soon", "expires", i.config.expiresAt.String())
123+
124+
i.Lock()
125+
defer i.Unlock()
126+
127+
resp, err := i.client.Apiv2().Token().Refresh(context.Background(), &apiv2models.TokenServiceRefreshRequest{})
128+
if err != nil {
129+
return fmt.Errorf("unable to refresh token %w", err)
130+
}
131+
132+
i.config.Token = resp.Secret
133+
err = i.config.parse()
134+
if err != nil {
135+
return fmt.Errorf("unable to parse token %w", err)
136+
}
137+
138+
if i.config.TokenRenewal.PersistTokenFn == nil {
139+
return nil
140+
}
141+
142+
err = i.config.TokenRenewal.PersistTokenFn(i.config.Token)
143+
if err != nil {
144+
return fmt.Errorf("unable to persist token %w", err)
145+
}
146+
147+
i.config.Log.Info("token refreshed, new token expires in", "expires", i.config.expiresAt.String())
148+
return nil
149+
}

go/client/client.go

Lines changed: 5 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go/client/client_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func Test_Client(t *testing.T) {
4141
server.Close()
4242
}()
4343

44-
tokenString, err := generateToken(1 * time.Second)
44+
tokenString, err := generateToken(2 * time.Second)
4545
require.NoError(t, err)
4646

4747
c, err := client.New(&client.DialConfig{
@@ -50,6 +50,7 @@ func Test_Client(t *testing.T) {
5050
Transport: server.Client().Transport,
5151
TokenRenewal: &client.TokenRenewal{
5252
PersistTokenFn: func(token string) error {
53+
ts.token = token
5354
t.Log("token persisted:", token)
5455
return nil
5556
},
@@ -64,7 +65,7 @@ func Test_Client(t *testing.T) {
6465
require.False(t, ts.wasCalled)
6566
require.Equal(t, tokenString, vs.token)
6667

67-
time.Sleep(300 * time.Millisecond)
68+
time.Sleep(1 * time.Second)
6869
v, err = c.Apiv2().Version().Get(t.Context(), &apiv2.VersionServiceGetRequest{})
6970
require.NoError(t, err)
7071
require.NotNil(t, v)
@@ -79,7 +80,7 @@ func Test_Client(t *testing.T) {
7980
require.Equal(t, "1.0", v.Version.Version)
8081

8182
require.True(t, ts.wasCalled)
82-
require.NotEqual(t, tokenString, vs.token, "token must have changed")
83+
require.NotEqual(t, tokenString, ts.token, "token must have changed")
8384
}
8485

8586
func generateToken(duration time.Duration) (string, error) {
@@ -121,6 +122,7 @@ func (m *mockVersionService) Get(ctx context.Context, req *apiv2.VersionServiceG
121122

122123
type mockTokenService struct {
123124
wasCalled bool
125+
token string
124126
}
125127

126128
// Create implements apiv2connect.TokenServiceHandler.

go/client/conn.go

Lines changed: 0 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package client
22

33
import (
4-
"context"
54
"errors"
65
"fmt"
76
"log/slog"
@@ -10,7 +9,6 @@ import (
109

1110
"connectrpc.com/connect"
1211
"github.com/golang-jwt/jwt/v5"
13-
api "github.com/metal-stack/api/go/metalstack/api/v2"
1412
)
1513

1614
const tokenRenewChecksDuringLifetime = 4
@@ -84,70 +82,3 @@ func (dc *DialConfig) parse() error {
8482
}
8583
return nil
8684
}
87-
88-
func (c *client) startTokenRenewal() {
89-
if c.config.TokenRenewal == nil {
90-
return
91-
}
92-
if c.config.expiresAt.IsZero() {
93-
return
94-
}
95-
if c.config.Log == nil {
96-
c.config.Log = slog.Default()
97-
}
98-
99-
replaceBefore := c.config.expiresAt.Sub(c.config.issuedAt) / tokenRenewChecksDuringLifetime
100-
101-
err := c.renewTokenIfNeeded(replaceBefore)
102-
if err != nil {
103-
c.config.Log.Error("unable to renew token", "error", err)
104-
}
105-
106-
ticker := time.NewTicker(replaceBefore)
107-
defer ticker.Stop()
108-
done := make(chan bool)
109-
for {
110-
select {
111-
case <-done:
112-
return
113-
case <-ticker.C:
114-
err := c.renewTokenIfNeeded(replaceBefore)
115-
if err != nil {
116-
c.config.Log.Error("unable to renew token", "error", err)
117-
}
118-
}
119-
}
120-
}
121-
122-
func (c *client) renewTokenIfNeeded(replaceBefore time.Duration) error {
123-
if time.Until(c.config.expiresAt) > replaceBefore {
124-
return nil
125-
}
126-
c.config.Log.Info("call token refresh, current token expires soon", "expires", c.config.expiresAt.String())
127-
128-
c.Lock()
129-
defer c.Unlock()
130-
131-
resp, err := c.Apiv2().Token().Refresh(context.Background(), &api.TokenServiceRefreshRequest{})
132-
if err != nil {
133-
return fmt.Errorf("unable to refresh token %w", err)
134-
}
135-
136-
c.config.Token = resp.Secret
137-
err = c.config.parse()
138-
if err != nil {
139-
return fmt.Errorf("unable to parse token %w", err)
140-
}
141-
142-
if c.config.TokenRenewal.PersistTokenFn == nil {
143-
return nil
144-
}
145-
146-
err = c.config.TokenRenewal.PersistTokenFn(c.config.Token)
147-
if err != nil {
148-
return fmt.Errorf("unable to persist token %w", err)
149-
}
150-
151-
c.config.Log.Info("token refreshed, new token expires in", "expires", c.config.expiresAt.String())
152-
return nil
153-
}

0 commit comments

Comments
 (0)