mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: keep original token refresh error in external auth (#19339)
External auth refresh errors lose the original error thrown on the first refresh. This PR saves that error to the database to be raised on subsequent refresh attempts
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
@@ -28,6 +29,13 @@ import (
|
||||
"github.com/coder/retry"
|
||||
)
|
||||
|
||||
const (
|
||||
// failureReasonLimit is the maximum text length of an error to be cached to the
|
||||
// database for a failed refresh token. In rare cases, the error could be a large
|
||||
// HTML payload.
|
||||
failureReasonLimit = 400
|
||||
)
|
||||
|
||||
// Config is used for authentication for Git operations.
|
||||
type Config struct {
|
||||
promoauth.InstrumentedOAuth2Config
|
||||
@@ -121,11 +129,12 @@ func (c *Config) RefreshToken(ctx context.Context, db database.Store, externalAu
|
||||
return externalAuthLink, InvalidTokenError("token expired, refreshing is either disabled or refreshing failed and will not be retried")
|
||||
}
|
||||
|
||||
refreshToken := externalAuthLink.OAuthRefreshToken
|
||||
|
||||
// This is additional defensive programming. Because TokenSource is an interface,
|
||||
// we cannot be sure that the implementation will treat an 'IsZero' time
|
||||
// as "not-expired". The default implementation does, but a custom implementation
|
||||
// might not. Removing the refreshToken will guarantee a refresh will fail.
|
||||
refreshToken := externalAuthLink.OAuthRefreshToken
|
||||
if c.NoRefresh {
|
||||
refreshToken = ""
|
||||
}
|
||||
@@ -136,15 +145,30 @@ func (c *Config) RefreshToken(ctx context.Context, db database.Store, externalAu
|
||||
Expiry: externalAuthLink.OAuthExpiry,
|
||||
}
|
||||
|
||||
// Note: The TokenSource(...) method will make no remote HTTP requests if the
|
||||
// token is expired and no refresh token is set. This is important to prevent
|
||||
// spamming the API, consuming rate limits, when the token is known to fail.
|
||||
token, err := c.TokenSource(ctx, existingToken).Token()
|
||||
if err != nil {
|
||||
// TokenSource can fail for numerous reasons. If it fails because of
|
||||
// a bad refresh token, then the refresh token is invalid, and we should
|
||||
// get rid of it. Keeping it around will cause additional refresh
|
||||
// attempts that will fail and cost us api rate limits.
|
||||
//
|
||||
// The error message is saved for debugging purposes.
|
||||
if isFailedRefresh(existingToken, err) {
|
||||
reason := err.Error()
|
||||
if len(reason) > failureReasonLimit {
|
||||
// Limit the length of the error message to prevent
|
||||
// spamming the database with long error messages.
|
||||
reason = reason[:failureReasonLimit]
|
||||
}
|
||||
dbExecErr := db.UpdateExternalAuthLinkRefreshToken(ctx, database.UpdateExternalAuthLinkRefreshTokenParams{
|
||||
OAuthRefreshToken: "", // It is better to clear the refresh token than to keep retrying.
|
||||
// Adding a reason will prevent further attempts to try and refresh the token.
|
||||
OauthRefreshFailureReason: reason,
|
||||
// Remove the invalid refresh token so it is never used again. The cached
|
||||
// `reason` can be used to know why this field was zeroed out.
|
||||
OAuthRefreshToken: "",
|
||||
OAuthRefreshTokenKeyID: externalAuthLink.OAuthRefreshTokenKeyID.String,
|
||||
UpdatedAt: dbtime.Now(),
|
||||
ProviderID: externalAuthLink.ProviderID,
|
||||
@@ -156,12 +180,28 @@ func (c *Config) RefreshToken(ctx context.Context, db database.Store, externalAu
|
||||
}
|
||||
// The refresh token was cleared
|
||||
externalAuthLink.OAuthRefreshToken = ""
|
||||
externalAuthLink.UpdatedAt = dbtime.Now()
|
||||
}
|
||||
|
||||
// Unfortunately have to match exactly on the error message string.
|
||||
// Improve the error message to account refresh tokens are deleted if
|
||||
// invalid on our end.
|
||||
//
|
||||
// This error messages comes from the oauth2 package on our client side.
|
||||
// So this check is not against a server generated error message.
|
||||
// Error source: https://github.com/golang/oauth2/blob/master/oauth2.go#L277
|
||||
if err.Error() == "oauth2: token expired and refresh token is not set" {
|
||||
if externalAuthLink.OauthRefreshFailureReason != "" {
|
||||
// A cached refresh failure error exists. So the refresh token was set, but was invalid, and zeroed out.
|
||||
// Return this cached error for the original refresh attempt.
|
||||
return externalAuthLink, InvalidTokenError(fmt.Sprintf("token expired and refreshing failed %s with: %s",
|
||||
// Do not return the exact time, because then we have to know what timezone the
|
||||
// user is in. This approximate time is good enough.
|
||||
humanize.Time(externalAuthLink.UpdatedAt),
|
||||
externalAuthLink.OauthRefreshFailureReason,
|
||||
))
|
||||
}
|
||||
|
||||
return externalAuthLink, InvalidTokenError("token expired, refreshing is either disabled or refreshing failed and will not be retried")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user