mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: do not refresh tokens that have already failed refreshing (#15608)
Once a token refresh fails, we remove the `oauth_refresh_token` from the database. This will prevent the token from hitting the IDP for subsequent refresh attempts. Without this change, a bad script can cause a failing token to hit a remote IDP repeatedly with each `git` operation. With this change, after the first hit, subsequent hits will fail locally, and never contact the IDP. The solution in both cases is to authenticate the external auth link. So the resolution is the same as before.
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbmem"
|
||||
"github.com/coder/coder/v2/coderd/database/dbmock"
|
||||
"github.com/coder/coder/v2/coderd/externalauth"
|
||||
"github.com/coder/coder/v2/coderd/promoauth"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
@@ -62,7 +64,7 @@ func TestRefreshToken(t *testing.T) {
|
||||
_, err := config.RefreshToken(ctx, nil, link)
|
||||
require.Error(t, err)
|
||||
require.True(t, externalauth.IsInvalidTokenError(err))
|
||||
require.Contains(t, err.Error(), "refreshing is disabled")
|
||||
require.Contains(t, err.Error(), "refreshing is either disabled or refreshing failed")
|
||||
})
|
||||
|
||||
// NoRefreshNoExpiry tests that an oauth token without an expiry is always valid.
|
||||
@@ -141,6 +143,73 @@ func TestRefreshToken(t *testing.T) {
|
||||
require.True(t, validated, "token should have been attempted to be validated")
|
||||
})
|
||||
|
||||
// RefreshRetries tests that refresh token retry behavior works as expected.
|
||||
// If a refresh token fails because the token itself is invalid, no more
|
||||
// refresh attempts should ever happen. An invalid refresh token does
|
||||
// not magically become valid at some point in the future.
|
||||
t.Run("RefreshRetries", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var refreshErr *oauth2.RetrieveError
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
mDB := dbmock.NewMockStore(ctrl)
|
||||
|
||||
refreshCount := 0
|
||||
fake, config, link := setupOauth2Test(t, testConfig{
|
||||
FakeIDPOpts: []oidctest.FakeIDPOpt{
|
||||
oidctest.WithRefresh(func(_ string) error {
|
||||
refreshCount++
|
||||
return refreshErr
|
||||
}),
|
||||
// The IDP should not be contacted since the token is expired and
|
||||
// refresh attempts will fail.
|
||||
oidctest.WithDynamicUserInfo(func(_ string) (jwt.MapClaims, error) {
|
||||
t.Error("token was validated, but it was expired and this should never have happened.")
|
||||
return nil, xerrors.New("should not be called")
|
||||
}),
|
||||
},
|
||||
ExternalAuthOpt: func(cfg *externalauth.Config) {},
|
||||
})
|
||||
|
||||
ctx := oidc.ClientContext(context.Background(), fake.HTTPClient(nil))
|
||||
// Expire the link
|
||||
link.OAuthExpiry = expired
|
||||
|
||||
// Make the failure a server internal error. Not related to the token
|
||||
refreshErr = &oauth2.RetrieveError{
|
||||
Response: &http.Response{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
},
|
||||
ErrorCode: "internal_error",
|
||||
}
|
||||
_, err := config.RefreshToken(ctx, mDB, link)
|
||||
require.Error(t, err)
|
||||
require.True(t, externalauth.IsInvalidTokenError(err))
|
||||
require.Equal(t, refreshCount, 1)
|
||||
|
||||
// Try again with a bad refresh token error
|
||||
// Expect DB call to remove the refresh token
|
||||
mDB.EXPECT().RemoveRefreshToken(gomock.Any(), gomock.Any()).Return(nil).Times(1)
|
||||
refreshErr = &oauth2.RetrieveError{ // github error
|
||||
Response: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
},
|
||||
ErrorCode: "bad_refresh_token",
|
||||
}
|
||||
_, err = config.RefreshToken(ctx, mDB, link)
|
||||
require.Error(t, err)
|
||||
require.True(t, externalauth.IsInvalidTokenError(err))
|
||||
require.Equal(t, refreshCount, 2)
|
||||
|
||||
// When the refresh token is empty, no api calls should be made
|
||||
link.OAuthRefreshToken = "" // mock'd db, so manually set the token to ''
|
||||
_, err = config.RefreshToken(ctx, mDB, link)
|
||||
require.Error(t, err)
|
||||
require.True(t, externalauth.IsInvalidTokenError(err))
|
||||
require.Equal(t, refreshCount, 2)
|
||||
})
|
||||
|
||||
// ValidateFailure tests if the token is no longer valid with a 401 response.
|
||||
t.Run("ValidateFailure", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
Reference in New Issue
Block a user