RefreshToken automatically refreshes the token if expired and permitted.
(ctx context.Context, db database.Store, externalAuthLink database.ExternalAuthLink)
| 189 | |
| 190 | // RefreshToken automatically refreshes the token if expired and permitted. |
| 191 | func (c *Config) RefreshToken(ctx context.Context, db database.Store, externalAuthLink database.ExternalAuthLink) (database.ExternalAuthLink, error) { |
| 192 | // If the token is expired and refresh is disabled, we prompt |
| 193 | // the user to authenticate again. |
| 194 | if c.NoRefresh && |
| 195 | // If the time is set to 0, then it should never expire. |
| 196 | // This is true for github, which has no expiry. |
| 197 | !externalAuthLink.OAuthExpiry.IsZero() && |
| 198 | externalAuthLink.OAuthExpiry.Before(dbtime.Now()) { |
| 199 | return externalAuthLink, InvalidTokenError("token expired, refreshing is either disabled or refreshing failed and will not be retried") |
| 200 | } |
| 201 | |
| 202 | refreshToken := externalAuthLink.OAuthRefreshToken |
| 203 | |
| 204 | // This is additional defensive programming. Because TokenSource is an interface, |
| 205 | // we cannot be sure that the implementation will treat an 'IsZero' time |
| 206 | // as "not-expired". The default implementation does, but a custom implementation |
| 207 | // might not. Removing the refreshToken will guarantee a refresh will fail. |
| 208 | if c.NoRefresh { |
| 209 | refreshToken = "" |
| 210 | } |
| 211 | |
| 212 | existingToken := &oauth2.Token{ |
| 213 | AccessToken: externalAuthLink.OAuthAccessToken, |
| 214 | RefreshToken: refreshToken, |
| 215 | Expiry: externalAuthLink.OAuthExpiry, |
| 216 | } |
| 217 | |
| 218 | // Note: The TokenSource(...) method will make no remote HTTP requests if the |
| 219 | // token is expired and no refresh token is set. This is important to prevent |
| 220 | // spamming the API, consuming rate limits, when the token is known to fail. |
| 221 | // |
| 222 | // External providers (GitHub in particular) intermittently fail token |
| 223 | // refreshes with transient errors such as 5xx responses, network timeouts, |
| 224 | // and rate-limited 429s. Retry with exponential backoff before surfacing |
| 225 | // the failure so a brief upstream blip does not force users to |
| 226 | // re-authenticate. Errors classified as permanent by isFailedRefresh |
| 227 | // (e.g. revoked or rotated refresh tokens) are not retried since those |
| 228 | // will never succeed and retrying wastes the refresh quota. |
| 229 | token, err := c.refreshTokenWithRetry(ctx, existingToken) |
| 230 | if err != nil { |
| 231 | // TokenSource can fail for numerous reasons. If it fails because of |
| 232 | // a bad refresh token, then the refresh token is invalid, and we should |
| 233 | // get rid of it. Keeping it around will cause additional refresh |
| 234 | // attempts that will fail and cost us api rate limits. |
| 235 | // |
| 236 | // The error message is saved for debugging purposes. |
| 237 | if isFailedRefresh(existingToken, err) { |
| 238 | // Before caching the failure, re-read the external auth link |
| 239 | // from the database. A concurrent request may have already |
| 240 | // refreshed the token successfully, consuming the single-use |
| 241 | // refresh token (e.g., GitHub App tokens). In that case our |
| 242 | // "bad_refresh_token" error is a false positive from losing |
| 243 | // the race, and we should use the winner's updated token |
| 244 | // instead of poisoning the database with a cached failure. |
| 245 | currentLink, readErr := db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{ |
| 246 | ProviderID: externalAuthLink.ProviderID, |
| 247 | UserID: externalAuthLink.UserID, |
| 248 | }) |