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:
@@ -3319,6 +3319,13 @@ func (q *querier) RegisterWorkspaceProxy(ctx context.Context, arg database.Regis
|
||||
return updateWithReturn(q.log, q.auth, fetch, q.db.RegisterWorkspaceProxy)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) RemoveRefreshToken(ctx context.Context, arg database.RemoveRefreshTokenParams) error {
|
||||
fetch := func(ctx context.Context, arg database.RemoveRefreshTokenParams) (database.ExternalAuthLink, error) {
|
||||
return q.db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{UserID: arg.UserID, ProviderID: arg.ProviderID})
|
||||
}
|
||||
return fetchAndExec(q.log, q.auth, policy.ActionUpdatePersonal, fetch, q.db.RemoveRefreshToken)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) RemoveUserFromAllGroups(ctx context.Context, userID uuid.UUID) error {
|
||||
// This is a system function to clear user groups in group sync.
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
|
||||
|
||||
@@ -1269,6 +1269,14 @@ func (s *MethodTestSuite) TestUser() {
|
||||
UserID: u.ID,
|
||||
}).Asserts(u, policy.ActionUpdatePersonal)
|
||||
}))
|
||||
s.Run("RemoveRefreshToken", s.Subtest(func(db database.Store, check *expects) {
|
||||
link := dbgen.ExternalAuthLink(s.T(), db, database.ExternalAuthLink{})
|
||||
check.Args(database.RemoveRefreshTokenParams{
|
||||
ProviderID: link.ProviderID,
|
||||
UserID: link.UserID,
|
||||
UpdatedAt: link.UpdatedAt,
|
||||
}).Asserts(rbac.ResourceUserObject(link.UserID), policy.ActionUpdatePersonal)
|
||||
}))
|
||||
s.Run("UpdateExternalAuthLink", s.Subtest(func(db database.Store, check *expects) {
|
||||
link := dbgen.ExternalAuthLink(s.T(), db, database.ExternalAuthLink{})
|
||||
check.Args(database.UpdateExternalAuthLinkParams{
|
||||
|
||||
@@ -8512,6 +8512,29 @@ func (q *FakeQuerier) RegisterWorkspaceProxy(_ context.Context, arg database.Reg
|
||||
return database.WorkspaceProxy{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) RemoveRefreshToken(_ context.Context, arg database.RemoveRefreshTokenParams) error {
|
||||
if err := validateDatabaseType(arg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
for index, gitAuthLink := range q.externalAuthLinks {
|
||||
if gitAuthLink.ProviderID != arg.ProviderID {
|
||||
continue
|
||||
}
|
||||
if gitAuthLink.UserID != arg.UserID {
|
||||
continue
|
||||
}
|
||||
gitAuthLink.UpdatedAt = arg.UpdatedAt
|
||||
gitAuthLink.OAuthRefreshToken = ""
|
||||
q.externalAuthLinks[index] = gitAuthLink
|
||||
|
||||
return nil
|
||||
}
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) RemoveUserFromAllGroups(_ context.Context, userID uuid.UUID) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
@@ -2086,6 +2086,13 @@ func (m queryMetricsStore) RegisterWorkspaceProxy(ctx context.Context, arg datab
|
||||
return proxy, err
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) RemoveRefreshToken(ctx context.Context, arg database.RemoveRefreshTokenParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.RemoveRefreshToken(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("RemoveRefreshToken").Observe(time.Since(start).Seconds())
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) RemoveUserFromAllGroups(ctx context.Context, userID uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.RemoveUserFromAllGroups(ctx, userID)
|
||||
|
||||
@@ -4448,6 +4448,20 @@ func (mr *MockStoreMockRecorder) RegisterWorkspaceProxy(arg0, arg1 any) *gomock.
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterWorkspaceProxy", reflect.TypeOf((*MockStore)(nil).RegisterWorkspaceProxy), arg0, arg1)
|
||||
}
|
||||
|
||||
// RemoveRefreshToken mocks base method.
|
||||
func (m *MockStore) RemoveRefreshToken(arg0 context.Context, arg1 database.RemoveRefreshTokenParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RemoveRefreshToken", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RemoveRefreshToken indicates an expected call of RemoveRefreshToken.
|
||||
func (mr *MockStoreMockRecorder) RemoveRefreshToken(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveRefreshToken", reflect.TypeOf((*MockStore)(nil).RemoveRefreshToken), arg0, arg1)
|
||||
}
|
||||
|
||||
// RemoveUserFromAllGroups mocks base method.
|
||||
func (m *MockStore) RemoveUserFromAllGroups(arg0 context.Context, arg1 uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -423,6 +423,10 @@ type sqlcQuerier interface {
|
||||
OrganizationMembers(ctx context.Context, arg OrganizationMembersParams) ([]OrganizationMembersRow, error)
|
||||
ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error
|
||||
RegisterWorkspaceProxy(ctx context.Context, arg RegisterWorkspaceProxyParams) (WorkspaceProxy, error)
|
||||
// Removing the refresh token disables the refresh behavior for a given
|
||||
// auth token. If a refresh token is marked invalid, it is better to remove it
|
||||
// then continually attempt to refresh the token.
|
||||
RemoveRefreshToken(ctx context.Context, arg RemoveRefreshTokenParams) error
|
||||
RemoveUserFromAllGroups(ctx context.Context, userID uuid.UUID) error
|
||||
RemoveUserFromGroups(ctx context.Context, arg RemoveUserFromGroupsParams) ([]uuid.UUID, error)
|
||||
RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error
|
||||
|
||||
@@ -1194,6 +1194,29 @@ func (q *sqlQuerier) InsertExternalAuthLink(ctx context.Context, arg InsertExter
|
||||
return i, err
|
||||
}
|
||||
|
||||
const removeRefreshToken = `-- name: RemoveRefreshToken :exec
|
||||
UPDATE
|
||||
external_auth_links
|
||||
SET
|
||||
oauth_refresh_token = '',
|
||||
updated_at = $1
|
||||
WHERE provider_id = $2 AND user_id = $3
|
||||
`
|
||||
|
||||
type RemoveRefreshTokenParams struct {
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
ProviderID string `db:"provider_id" json:"provider_id"`
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
}
|
||||
|
||||
// Removing the refresh token disables the refresh behavior for a given
|
||||
// auth token. If a refresh token is marked invalid, it is better to remove it
|
||||
// then continually attempt to refresh the token.
|
||||
func (q *sqlQuerier) RemoveRefreshToken(ctx context.Context, arg RemoveRefreshTokenParams) error {
|
||||
_, err := q.db.ExecContext(ctx, removeRefreshToken, arg.UpdatedAt, arg.ProviderID, arg.UserID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateExternalAuthLink = `-- name: UpdateExternalAuthLink :one
|
||||
UPDATE external_auth_links SET
|
||||
updated_at = $3,
|
||||
|
||||
@@ -42,3 +42,14 @@ UPDATE external_auth_links SET
|
||||
oauth_expiry = $8,
|
||||
oauth_extra = $9
|
||||
WHERE provider_id = $1 AND user_id = $2 RETURNING *;
|
||||
|
||||
-- name: RemoveRefreshToken :exec
|
||||
-- Removing the refresh token disables the refresh behavior for a given
|
||||
-- auth token. If a refresh token is marked invalid, it is better to remove it
|
||||
-- then continually attempt to refresh the token.
|
||||
UPDATE
|
||||
external_auth_links
|
||||
SET
|
||||
oauth_refresh_token = '',
|
||||
updated_at = @updated_at
|
||||
WHERE provider_id = @provider_id AND user_id = @user_id;
|
||||
|
||||
Reference in New Issue
Block a user