Skip to content

Commit 73b136e

Browse files
authored
fix: add exp backoff to validate fresh git auth tokens (#8956)
A customer using GitHub in Australia reported that validating immediately after refreshing the token would intermittently fail with a 401. Waiting a few milliseconds with the exact same token on the exact same request would resolve the issue. It seems likely that the write is not propagating to the read replica in time.
1 parent 694729b commit 73b136e

File tree

2 files changed

+50
-1
lines changed

2 files changed

+50
-1
lines changed

coderd/gitauth/config.go

+17-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/http"
99
"net/url"
1010
"regexp"
11+
"time"
1112

1213
"golang.org/x/oauth2"
1314
"golang.org/x/xerrors"
@@ -17,6 +18,7 @@ import (
1718
"github.com/coder/coder/coderd/database"
1819
"github.com/coder/coder/coderd/httpapi"
1920
"github.com/coder/coder/codersdk"
21+
"github.com/coder/retry"
2022
)
2123

2224
type OAuth2Config interface {
@@ -75,12 +77,26 @@ func (c *Config) RefreshToken(ctx context.Context, db database.Store, gitAuthLin
7577
// we aren't trying to surface an error, we're just trying to obtain a valid token.
7678
return gitAuthLink, false, nil
7779
}
78-
80+
r := retry.New(50*time.Millisecond, 200*time.Millisecond)
81+
// See the comment below why the retry and cancel is required.
82+
retryCtx, retryCtxCancel := context.WithTimeout(ctx, time.Second)
83+
defer retryCtxCancel()
84+
validate:
7985
valid, _, err := c.ValidateToken(ctx, token.AccessToken)
8086
if err != nil {
8187
return gitAuthLink, false, xerrors.Errorf("validate git auth token: %w", err)
8288
}
8389
if !valid {
90+
// A customer using GitHub in Australia reported that validating immediately
91+
// after refreshing the token would intermittently fail with a 401. Waiting
92+
// a few milliseconds with the exact same token on the exact same request
93+
// would resolve the issue. It seems likely that the write is not propagating
94+
// to the read replica in time.
95+
//
96+
// We do an exponential backoff here to give the write time to propagate.
97+
if c.Type == codersdk.GitProviderGitHub && r.Wait(retryCtx) {
98+
goto validate
99+
}
84100
// The token is no longer valid!
85101
return gitAuthLink, false, nil
86102
}

coderd/gitauth/config_test.go

+33
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,39 @@ func TestRefreshToken(t *testing.T) {
7373
require.NoError(t, err)
7474
require.False(t, refreshed)
7575
})
76+
t.Run("ValidateRetryGitHub", func(t *testing.T) {
77+
t.Parallel()
78+
hit := false
79+
// We need to ensure that the exponential backoff kicks in properly.
80+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
81+
if !hit {
82+
hit = true
83+
w.WriteHeader(http.StatusUnauthorized)
84+
w.Write([]byte("Not permitted"))
85+
return
86+
}
87+
w.WriteHeader(http.StatusOK)
88+
}))
89+
config := &gitauth.Config{
90+
ID: "test",
91+
OAuth2Config: &testutil.OAuth2Config{
92+
Token: &oauth2.Token{
93+
AccessToken: "updated",
94+
},
95+
},
96+
ValidateURL: srv.URL,
97+
Type: codersdk.GitProviderGitHub,
98+
}
99+
db := dbfake.New()
100+
link := dbgen.GitAuthLink(t, db, database.GitAuthLink{
101+
ProviderID: config.ID,
102+
OAuthAccessToken: "initial",
103+
})
104+
_, refreshed, err := config.RefreshToken(context.Background(), db, link)
105+
require.NoError(t, err)
106+
require.True(t, refreshed)
107+
require.True(t, hit)
108+
})
76109
t.Run("ValidateNoUpdate", func(t *testing.T) {
77110
t.Parallel()
78111
validated := make(chan struct{})

0 commit comments

Comments
 (0)