-
Notifications
You must be signed in to change notification settings - Fork 29
Expand file tree
/
Copy pathkey_flow_continuous_refresh.go
More file actions
142 lines (125 loc) · 4.54 KB
/
key_flow_continuous_refresh.go
File metadata and controls
142 lines (125 loc) · 4.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package clients
import (
"errors"
"fmt"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
)
var (
defaultTimeStartBeforeTokenExpiration = 30 * time.Minute
defaultTimeBetweenContextCheck = time.Second
defaultTimeBetweenTries = 5 * time.Minute
)
// Continuously refreshes the token of a key flow, retrying if the token API returns 5xx errrors. Writes to stderr when it terminates.
//
// To terminate this routine, close the context in keyFlow.config.BackgroundTokenRefreshContext.
func continuousRefreshToken(keyflow *KeyFlow) {
refresher := &continuousTokenRefresher{
keyFlow: keyflow,
timeStartBeforeTokenExpiration: defaultTimeStartBeforeTokenExpiration,
timeBetweenContextCheck: defaultTimeBetweenContextCheck,
timeBetweenTries: defaultTimeBetweenTries,
}
err := refresher.continuousRefreshToken()
fmt.Fprintf(os.Stderr, "Token refreshing terminated: %v", err)
}
type continuousTokenRefresher struct {
keyFlow *KeyFlow
// Token refresh tries start at [Access token expiration timestamp] - [This duration]
timeStartBeforeTokenExpiration time.Duration
timeBetweenContextCheck time.Duration
timeBetweenTries time.Duration
}
// Continuously refreshes the token of a key flow, retrying if the token API returns 5xx errrors. Always returns with a non-nil error.
//
// To terminate this routine, close the context in refresher.keyFlow.config.BackgroundTokenRefreshContext.
func (refresher *continuousTokenRefresher) continuousRefreshToken() error {
// Compute timestamp where we'll refresh token
// Access token may be empty at this point, we have to check it
var startRefreshTimestamp time.Time
var accessToken string
refresher.keyFlow.tokenMutex.RLock()
if refresher.keyFlow.token != nil {
accessToken = refresher.keyFlow.token.AccessToken
}
refresher.keyFlow.tokenMutex.RUnlock()
if accessToken == "" {
startRefreshTimestamp = time.Now()
} else {
expirationTimestamp, err := refresher.getAccessTokenExpirationTimestamp()
if err != nil {
return fmt.Errorf("get access token expiration timestamp: %w", err)
}
startRefreshTimestamp = expirationTimestamp.Add(-refresher.timeStartBeforeTokenExpiration)
}
for {
err := refresher.waitUntilTimestamp(startRefreshTimestamp)
if err != nil {
return err
}
err = refresher.keyFlow.config.BackgroundTokenRefreshContext.Err()
if err != nil {
return fmt.Errorf("check context: %w", err)
}
ok, err := refresher.refreshToken()
if err != nil {
return fmt.Errorf("refresh tokens: %w", err)
}
if !ok {
startRefreshTimestamp = startRefreshTimestamp.Add(refresher.timeBetweenTries)
continue
}
expirationTimestamp, err := refresher.getAccessTokenExpirationTimestamp()
if err != nil {
return fmt.Errorf("get access token expiration timestamp: %w", err)
}
startRefreshTimestamp = expirationTimestamp.Add(-refresher.timeStartBeforeTokenExpiration)
}
}
func (refresher *continuousTokenRefresher) getAccessTokenExpirationTimestamp() (*time.Time, error) {
refresher.keyFlow.tokenMutex.RLock()
token := refresher.keyFlow.token.AccessToken
refresher.keyFlow.tokenMutex.RUnlock()
// We can safely use ParseUnverified because we are not doing authentication of any kind
// We're just checking the expiration time
tokenParsed, _, err := jwt.NewParser().ParseUnverified(token, &jwt.RegisteredClaims{})
if err != nil {
return nil, fmt.Errorf("parse token: %w", err)
}
expirationTimestampNumeric, err := tokenParsed.Claims.GetExpirationTime()
if err != nil {
return nil, fmt.Errorf("get expiration timestamp: %w", err)
}
return &expirationTimestampNumeric.Time, nil
}
func (refresher *continuousTokenRefresher) waitUntilTimestamp(timestamp time.Time) error {
for time.Now().Before(timestamp) {
err := refresher.keyFlow.config.BackgroundTokenRefreshContext.Err()
if err != nil {
return fmt.Errorf("check context: %w", err)
}
time.Sleep(refresher.timeBetweenContextCheck)
}
return nil
}
// Returns:
// - (true, nil) if successful.
// - (false, nil) if not successful but should be retried.
// - (_, err) if not successful and shouldn't be retried.
func (refresher *continuousTokenRefresher) refreshToken() (bool, error) {
err := refresher.keyFlow.createAccessToken()
if err == nil {
return true, nil
}
// Should be retried if this is an API error with status code 5xx
oapiErr := &oapierror.GenericOpenAPIError{}
if !errors.As(err, &oapiErr) {
return false, err
}
if oapiErr.StatusCode < 500 {
return false, err
}
return false, nil
}