@@ -14,6 +14,7 @@ import (
14
14
"strings"
15
15
"time"
16
16
17
+ "github.com/dustin/go-humanize"
17
18
"golang.org/x/oauth2"
18
19
"golang.org/x/xerrors"
19
20
@@ -28,6 +29,13 @@ import (
28
29
"github.com/coder/retry"
29
30
)
30
31
32
+ const (
33
+ // failureReasonLimit is the maximum text length of an error to be cached to the
34
+ // database for a failed refresh token. In rare cases, the error could be a large
35
+ // HTML payload.
36
+ failureReasonLimit = 400
37
+ )
38
+
31
39
// Config is used for authentication for Git operations.
32
40
type Config struct {
33
41
promoauth.InstrumentedOAuth2Config
@@ -121,11 +129,12 @@ func (c *Config) RefreshToken(ctx context.Context, db database.Store, externalAu
121
129
return externalAuthLink , InvalidTokenError ("token expired, refreshing is either disabled or refreshing failed and will not be retried" )
122
130
}
123
131
132
+ refreshToken := externalAuthLink .OAuthRefreshToken
133
+
124
134
// This is additional defensive programming. Because TokenSource is an interface,
125
135
// we cannot be sure that the implementation will treat an 'IsZero' time
126
136
// as "not-expired". The default implementation does, but a custom implementation
127
137
// might not. Removing the refreshToken will guarantee a refresh will fail.
128
- refreshToken := externalAuthLink .OAuthRefreshToken
129
138
if c .NoRefresh {
130
139
refreshToken = ""
131
140
}
@@ -136,15 +145,30 @@ func (c *Config) RefreshToken(ctx context.Context, db database.Store, externalAu
136
145
Expiry : externalAuthLink .OAuthExpiry ,
137
146
}
138
147
148
+ // Note: The TokenSource(...) method will make no remote HTTP requests if the
149
+ // token is expired and no refresh token is set. This is important to prevent
150
+ // spamming the API, consuming rate limits, when the token is known to fail.
139
151
token , err := c .TokenSource (ctx , existingToken ).Token ()
140
152
if err != nil {
141
153
// TokenSource can fail for numerous reasons. If it fails because of
142
154
// a bad refresh token, then the refresh token is invalid, and we should
143
155
// get rid of it. Keeping it around will cause additional refresh
144
156
// attempts that will fail and cost us api rate limits.
157
+ //
158
+ // The error message is saved for debugging purposes.
145
159
if isFailedRefresh (existingToken , err ) {
160
+ reason := err .Error ()
161
+ if len (reason ) > failureReasonLimit {
162
+ // Limit the length of the error message to prevent
163
+ // spamming the database with long error messages.
164
+ reason = reason [:failureReasonLimit ]
165
+ }
146
166
dbExecErr := db .UpdateExternalAuthLinkRefreshToken (ctx , database.UpdateExternalAuthLinkRefreshTokenParams {
147
- OAuthRefreshToken : "" , // It is better to clear the refresh token than to keep retrying.
167
+ // Adding a reason will prevent further attempts to try and refresh the token.
168
+ OauthRefreshFailureReason : reason ,
169
+ // Remove the invalid refresh token so it is never used again. The cached
170
+ // `reason` can be used to know why this field was zeroed out.
171
+ OAuthRefreshToken : "" ,
148
172
OAuthRefreshTokenKeyID : externalAuthLink .OAuthRefreshTokenKeyID .String ,
149
173
UpdatedAt : dbtime .Now (),
150
174
ProviderID : externalAuthLink .ProviderID ,
@@ -156,12 +180,28 @@ func (c *Config) RefreshToken(ctx context.Context, db database.Store, externalAu
156
180
}
157
181
// The refresh token was cleared
158
182
externalAuthLink .OAuthRefreshToken = ""
183
+ externalAuthLink .UpdatedAt = dbtime .Now ()
159
184
}
160
185
161
186
// Unfortunately have to match exactly on the error message string.
162
187
// Improve the error message to account refresh tokens are deleted if
163
188
// invalid on our end.
189
+ //
190
+ // This error messages comes from the oauth2 package on our client side.
191
+ // So this check is not against a server generated error message.
192
+ // Error source: https://github.com/golang/oauth2/blob/master/oauth2.go#L277
164
193
if err .Error () == "oauth2: token expired and refresh token is not set" {
194
+ if externalAuthLink .OauthRefreshFailureReason != "" {
195
+ // A cached refresh failure error exists. So the refresh token was set, but was invalid, and zeroed out.
196
+ // Return this cached error for the original refresh attempt.
197
+ return externalAuthLink , InvalidTokenError (fmt .Sprintf ("token expired and refreshing failed %s with: %s" ,
198
+ // Do not return the exact time, because then we have to know what timezone the
199
+ // user is in. This approximate time is good enough.
200
+ humanize .Time (externalAuthLink .UpdatedAt ),
201
+ externalAuthLink .OauthRefreshFailureReason ,
202
+ ))
203
+ }
204
+
165
205
return externalAuthLink , InvalidTokenError ("token expired, refreshing is either disabled or refreshing failed and will not be retried" )
166
206
}
167
207
0 commit comments