Skip to content

Commit 09ee10a

Browse files
ptrckmllrPatrick MüllerFyusel
authored
fix(key_flow): Add 5 second leeway to refresh access tokens early (#2194)
* fix(key_flow): Add 5 second leeway to refresh access tokens early * clarify comment * move leeway to KeyFlow struct and pass to tokenExpired func * Update changelogs * Update CHANGELOG.md Co-authored-by: Alexander Dahmen <[email protected]> * Update CHANGELOG.md Co-authored-by: Alexander Dahmen <[email protected]> * Update CHANGELOG.md Co-authored-by: Alexander Dahmen <[email protected]> * Update core/CHANGELOG.md Co-authored-by: Alexander Dahmen <[email protected]> --------- Co-authored-by: Patrick Müller <[email protected]> Co-authored-by: Alexander Dahmen <[email protected]> Co-authored-by: Alexander Dahmen <[email protected]>
1 parent 60b3c2c commit 09ee10a

File tree

4 files changed

+37
-7
lines changed

4 files changed

+37
-7
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## Release (2025-YY-XX)
2+
- `core`: [v0.17.2](core/CHANGELOG.md#v0172-2025-05-22)
3+
- **Bugfix:** Access tokens generated via key flow authentication are refreshed 5 seconds before expiration to prevent timing issues with upstream systems which could lead to unexpected 401 error responses
4+
15
## Release (2025-05-15)
26
- `alb`:
37
- [v0.4.0](services/alb/CHANGELOG.md#v040-2025-05-15)

core/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## v0.17.2 (2025-05-22)
2+
- **Bugfix:** Access tokens generated via key flow authentication are refreshed 5 seconds before expiration to prevent timing issues with upstream systems which could lead to unexpected 401 error responses
3+
14
## v0.17.1 (2025-04-09)
25
- **Improvement:** Improve error message for key flow authentication
36

core/clients/key_flow.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ const (
3232
tokenAPI = "https://service-account.api.stackit.cloud/token" //nolint:gosec // linter false positive
3333
defaultTokenType = "Bearer"
3434
defaultScope = ""
35+
36+
defaultTokenExpirationLeeway = time.Second * 5
3537
)
3638

3739
// KeyFlow handles auth with SA key
@@ -45,6 +47,10 @@ type KeyFlow struct {
4547

4648
tokenMutex sync.RWMutex
4749
token *TokenResponseBody
50+
51+
// If the current access token would expire in less than TokenExpirationLeeway,
52+
// the client will refresh it early to prevent clock skew or other timing issues.
53+
tokenExpirationLeeway time.Duration
4854
}
4955

5056
// KeyFlowConfig is the flow config
@@ -129,6 +135,8 @@ func (c *KeyFlow) Init(cfg *KeyFlowConfig) error {
129135
c.config.TokenUrl = tokenAPI
130136
}
131137

138+
c.tokenExpirationLeeway = defaultTokenExpirationLeeway
139+
132140
if c.rt = cfg.HTTPTransport; c.rt == nil {
133141
c.rt = http.DefaultTransport
134142
}
@@ -204,7 +212,7 @@ func (c *KeyFlow) GetAccessToken() (string, error) {
204212
}
205213
c.tokenMutex.RUnlock()
206214

207-
accessTokenExpired, err := tokenExpired(accessToken)
215+
accessTokenExpired, err := tokenExpired(accessToken, c.tokenExpirationLeeway)
208216
if err != nil {
209217
return "", fmt.Errorf("check access token is expired: %w", err)
210218
}
@@ -252,6 +260,10 @@ func (c *KeyFlow) validate() error {
252260
}
253261
c.privateKeyPEM = pem.EncodeToMemory(privKeyPEM)
254262

263+
if c.tokenExpirationLeeway < 0 {
264+
return fmt.Errorf("token expiration leeway cannot be negative")
265+
}
266+
255267
return nil
256268
}
257269

@@ -268,7 +280,7 @@ func (c *KeyFlow) recreateAccessToken() error {
268280
}
269281
c.tokenMutex.RUnlock()
270282

271-
refreshTokenExpired, err := tokenExpired(refreshToken)
283+
refreshTokenExpired, err := tokenExpired(refreshToken, c.tokenExpirationLeeway)
272284
if err != nil {
273285
return err
274286
}
@@ -389,7 +401,7 @@ func (c *KeyFlow) parseTokenResponse(res *http.Response) error {
389401
return nil
390402
}
391403

392-
func tokenExpired(token string) (bool, error) {
404+
func tokenExpired(token string, tokenExpirationLeeway time.Duration) (bool, error) {
393405
if token == "" {
394406
return true, nil
395407
}
@@ -400,11 +412,15 @@ func tokenExpired(token string) (bool, error) {
400412
if err != nil {
401413
return false, fmt.Errorf("parse token: %w", err)
402414
}
415+
403416
expirationTimestampNumeric, err := tokenParsed.Claims.GetExpirationTime()
404417
if err != nil {
405418
return false, fmt.Errorf("get expiration timestamp: %w", err)
406419
}
407-
expirationTimestamp := expirationTimestampNumeric.Time
408-
now := time.Now()
409-
return now.After(expirationTimestamp), nil
420+
421+
// Pretend to be `tokenExpirationLeeway` into the future to avoid token expiring
422+
// between retrieving the token and upstream systems validating it.
423+
now := time.Now().Add(tokenExpirationLeeway)
424+
425+
return now.After(expirationTimestampNumeric.Time), nil
410426
}

core/clients/key_flow_test.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ func TestSetToken(t *testing.T) {
190190
}
191191

192192
func TestTokenExpired(t *testing.T) {
193+
tokenExpirationLeeway := 5 * time.Second
193194
tests := []struct {
194195
desc string
195196
tokenInvalid bool
@@ -209,6 +210,12 @@ func TestTokenExpired(t *testing.T) {
209210
expectedErr: false,
210211
expectedIsExpired: true,
211212
},
213+
{
214+
desc: "token almost expired",
215+
tokenExpiresAt: time.Now().Add(tokenExpirationLeeway),
216+
expectedErr: false,
217+
expectedIsExpired: true,
218+
},
212219
{
213220
desc: "token invalid",
214221
tokenInvalid: true,
@@ -229,7 +236,7 @@ func TestTokenExpired(t *testing.T) {
229236
}
230237
}
231238

232-
isExpired, err := tokenExpired(token)
239+
isExpired, err := tokenExpired(token, tokenExpirationLeeway)
233240
if err != nil && !tt.expectedErr {
234241
t.Fatalf("failed on valid input: %v", err)
235242
}

0 commit comments

Comments
 (0)