fix(coderd/externalauth): save refreshed token before validation (#24332)

GitHub rotates refresh tokens on use, invalidating the old token
immediately. If post-refresh validation fails (e.g. rate-limited
403 from /user), the new token was silently discarded because the
DB save only happened after successful validation. The next refresh
attempt would use the stale refresh token, fail permanently, and
destroy the token.

Move the UpdateExternalAuthLink call to immediately after
TokenSource.Token() succeeds. The post-validation save block is
removed (dead code after the early save). The DB write uses a
detached context (context.WithoutCancel) so a canceled request
cannot prevent persistence of the already-consumed refresh token.
This commit is contained in:
Mathias Fredriksson
2026-04-18 14:28:29 +03:00
committed by GitHub
parent 2ea27e897b
commit 2a1984f0e8
2 changed files with 284 additions and 29 deletions
+41 -27
View File
@@ -261,6 +261,37 @@ func (c *Config) RefreshToken(ctx context.Context, db database.Store, externalAu
return externalAuthLink, xerrors.Errorf("generate token extra: %w", err)
}
// Persist the refreshed token to the DB before validation. GitHub
// rotates refresh tokens on every use, so the old refresh token is
// already invalid on the IDP side. If we validated first and the
// validation endpoint was unavailable (e.g. rate-limited 403), the
// new token would be silently lost and the user would be forced to
// re-authenticate manually.
// Use a detached context for the DB write only. The IDP already
// consumed the old refresh token, so if the caller's request
// context is canceled mid-save, the new token would be lost.
persistCtx, persistCancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
defer persistCancel()
originalAccessToken := externalAuthLink.OAuthAccessToken
if token.AccessToken != originalAccessToken {
updatedAuthLink, err := db.UpdateExternalAuthLink(persistCtx, database.UpdateExternalAuthLinkParams{
ProviderID: c.ID,
UserID: externalAuthLink.UserID,
UpdatedAt: dbtime.Now(),
OAuthAccessToken: token.AccessToken,
OAuthAccessTokenKeyID: sql.NullString{}, // dbcrypt will update as required
OAuthRefreshToken: token.RefreshToken,
OAuthRefreshTokenKeyID: sql.NullString{}, // dbcrypt will update as required
OAuthExpiry: token.Expiry,
OAuthExtra: extra,
})
if err != nil {
return updatedAuthLink, xerrors.Errorf("persist refreshed token: %w", err)
}
externalAuthLink = updatedAuthLink
}
r := retry.New(50*time.Millisecond, 200*time.Millisecond)
// See the comment below why the retry and cancel is required.
retryCtx, retryCtxCancel := context.WithTimeout(ctx, time.Second)
@@ -285,35 +316,18 @@ validate:
return externalAuthLink, InvalidTokenError("token failed to validate")
}
if token.AccessToken != externalAuthLink.OAuthAccessToken {
updatedAuthLink, err := db.UpdateExternalAuthLink(ctx, database.UpdateExternalAuthLinkParams{
ProviderID: c.ID,
UserID: externalAuthLink.UserID,
UpdatedAt: dbtime.Now(),
OAuthAccessToken: token.AccessToken,
OAuthAccessTokenKeyID: sql.NullString{}, // dbcrypt will update as required
OAuthRefreshToken: token.RefreshToken,
OAuthRefreshTokenKeyID: sql.NullString{}, // dbcrypt will update as required
OAuthExpiry: token.Expiry,
OAuthExtra: extra,
// Update the associated user's github.com user ID if the token
// is for github.com and validation returned user info.
if token.AccessToken != originalAccessToken && IsGithubDotComURL(c.AuthCodeURL("")) && user != nil {
err = db.UpdateUserGithubComUserID(ctx, database.UpdateUserGithubComUserIDParams{
ID: externalAuthLink.UserID,
GithubComUserID: sql.NullInt64{
Int64: user.ID,
Valid: true,
},
})
if err != nil {
return updatedAuthLink, xerrors.Errorf("update external auth link: %w", err)
}
externalAuthLink = updatedAuthLink
// Update the associated users github.com username if the token is for github.com.
if IsGithubDotComURL(c.AuthCodeURL("")) && user != nil {
err = db.UpdateUserGithubComUserID(ctx, database.UpdateUserGithubComUserIDParams{
ID: externalAuthLink.UserID,
GithubComUserID: sql.NullInt64{
Int64: user.ID,
Valid: true,
},
})
if err != nil {
return externalAuthLink, xerrors.Errorf("update user github com user id: %w", err)
}
return externalAuthLink, xerrors.Errorf("update user github com user id: %w", err)
}
}