From 4259f467b0d8c6eb12ed9c377c20f6739af9b7bb Mon Sep 17 00:00:00 2001 From: OpenGuidou <73480729+OpenGuidou@users.noreply.github.com> Date: Thu, 2 Apr 2026 02:54:22 +0200 Subject: [PATCH] fix(server): Ensure OIDC config is refreshed at server restart (#26913) Signed-off-by: OpenGuidou --- server/server.go | 7 ++-- server/server_test.go | 94 +++++++++++++++++++++++++++++++++++++++++++ util/oidc/oidc.go | 4 ++ 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/server/server.go b/server/server.go index 255f775f73..33d13112e4 100644 --- a/server/server.go +++ b/server/server.go @@ -334,8 +334,6 @@ func NewServer(ctx context.Context, opts ArgoCDServerOpts, appsetOpts Applicatio appsetLister := appFactory.Argoproj().V1alpha1().ApplicationSets().Lister() userStateStorage := util_session.NewUserStateStorage(opts.RedisClient) - ssoClientApp, err := oidc.NewClientApp(settings, opts.DexServerAddr, opts.DexTLSConfig, opts.BaseHRef, cacheutil.NewRedisCache(opts.RedisClient, settings.UserInfoCacheExpiration(), cacheutil.RedisCompressionNone)) - errorsutil.CheckError(err) sessionMgr := util_session.NewSessionManager(settingsMgr, projLister, opts.DexServerAddr, opts.DexTLSConfig, userStateStorage) enf := rbac.NewEnforcer(opts.KubeClientset, opts.Namespace, common.ArgoCDRBACConfigMapName, nil) enf.EnableEnforce(!opts.DisableAuth) @@ -383,7 +381,6 @@ func NewServer(ctx context.Context, opts ArgoCDServerOpts, appsetOpts Applicatio a := &ArgoCDServer{ ArgoCDServerOpts: opts, ApplicationSetOpts: appsetOpts, - ssoClientApp: ssoClientApp, log: logger, settings: settings, sessionMgr: sessionMgr, @@ -586,6 +583,10 @@ func (server *ArgoCDServer) Run(ctx context.Context, listeners *Listeners) { if server.RedisClient != nil { cacheutil.CollectMetrics(server.RedisClient, metricsServ, server.userStateStorage.GetLockObject()) } + // OIDC config needs to be refreshed at each server restart + ssoClientApp, err := oidc.NewClientApp(server.settings, server.DexServerAddr, server.DexTLSConfig, server.BaseHRef, cacheutil.NewRedisCache(server.RedisClient, server.settings.UserInfoCacheExpiration(), cacheutil.RedisCompressionNone)) + errorsutil.CheckError(err) + server.ssoClientApp = ssoClientApp // Don't init storage until after CollectMetrics. CollectMetrics adds hooks to the Redis client, and Init // reads those hooks. If this is called first, there may be a data race. diff --git a/server/server_test.go b/server/server_test.go index f0b79858ed..9907f00a4c 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -488,6 +488,100 @@ func TestGracefulShutdown(t *testing.T) { assert.True(t, shutdown) } +func TestOIDCRefresh(t *testing.T) { + port, err := test.GetFreePort() + require.NoError(t, err) + mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}} + cm := test.NewFakeConfigMap() + cm.Data["oidc.config"] = ` +name: Test OIDC +issuer: $oidc.myoidc.issuer +clientID: $oidc.myoidc.clientId +clientSecret: $oidc.myoidc.clientSecret +` + secret := test.NewFakeSecret() + issuerURL := "http://oidc.127.0.0.1.nip.io" + updatedIssuerURL := "http://newoidc.127.0.0.1.nip.io" + secret.Data["oidc.myoidc.issuer"] = []byte(issuerURL) + secret.Data["oidc.myoidc.clientId"] = []byte("myClientId") + secret.Data["oidc.myoidc.clientSecret"] = []byte("myClientSecret") + + kubeclientset := fake.NewSimpleClientset(cm, secret) + redis, redisCloser := test.NewInMemoryRedis() + defer redisCloser() + s := NewServer( + t.Context(), + ArgoCDServerOpts{ + ListenPort: port, + Namespace: test.FakeArgoCDNamespace, + KubeClientset: kubeclientset, + AppClientset: apps.NewSimpleClientset(), + RepoClientset: mockRepoClient, + RedisClient: redis, + }, + ApplicationSetOpts{}, + ) + projInformerCancel := test.StartInformer(s.projInformer) + defer projInformerCancel() + appInformerCancel := test.StartInformer(s.appInformer) + defer appInformerCancel() + appsetInformerCancel := test.StartInformer(s.appsetInformer) + defer appsetInformerCancel() + clusterInformerCancel := test.StartInformer(s.clusterInformer) + defer clusterInformerCancel() + + shutdown := false + + lns, err := s.Listen() + require.NoError(t, err) + runCtx := t.Context() + + var wg gosync.WaitGroup + wg.Add(1) + go func(shutdown *bool) { + defer wg.Done() + s.Run(runCtx, lns) + *shutdown = true + }(&shutdown) + + for !s.available.Load() { + time.Sleep(10 * time.Millisecond) + } + assert.True(t, s.available.Load()) + assert.Equal(t, issuerURL, s.ssoClientApp.IssuerURL()) + + // Update oidc config + secret.Data["oidc.myoidc.issuer"] = []byte(updatedIssuerURL) + secret.ResourceVersion = "12345" + _, err = kubeclientset.CoreV1().Secrets(test.FakeArgoCDNamespace).Update(runCtx, secret, metav1.UpdateOptions{}) + require.NoError(t, err) + + // Wait for graceful shutdown + wg.Wait() + for s.available.Load() { + time.Sleep(10 * time.Millisecond) + } + + assert.False(t, s.available.Load()) + + shutdown = false + wg.Add(1) + go func(shutdown *bool) { + defer wg.Done() + s.Run(runCtx, lns) + *shutdown = true + }(&shutdown) + + for !s.available.Load() { + time.Sleep(10 * time.Millisecond) + } + assert.True(t, s.available.Load()) + assert.Equal(t, updatedIssuerURL, s.ssoClientApp.IssuerURL()) + + s.stopCh <- syscall.SIGINT + wg.Wait() +} + func TestAuthenticate(t *testing.T) { type testData struct { test string diff --git a/util/oidc/oidc.go b/util/oidc/oidc.go index a1f0b35f93..d856be7e04 100644 --- a/util/oidc/oidc.go +++ b/util/oidc/oidc.go @@ -1069,3 +1069,7 @@ func FormatAccessTokenCacheKey(sub string) string { func formatOidcTokenCacheKey(sub string, sid string) string { return fmt.Sprintf("%s_%s_%s", OidcTokenCachePrefix, sub, sid) } + +func (a *ClientApp) IssuerURL() string { + return a.issuerURL +}