Browse Source

feat: add Doppler OIDC-based authentication (#5475)

Co-authored-by: Gergely Brautigam <skarlso777@gmail.com>
Mike Sellitto 4 months ago
parent
commit
71251633dc

+ 25 - 2
apis/externalsecrets/v1/secretstore_doppler_types.go

@@ -22,9 +22,17 @@ import (
 
 // Set DOPPLER_BASE_URL and DOPPLER_VERIFY_TLS environment variables to override defaults
 
-// DopplerAuth defines the authentication method for the Doppler provider.
+// DopplerAuth configures authentication with the Doppler API.
+// Exactly one of secretRef or oidcConfig must be specified.
+// +kubebuilder:validation:XValidation:rule="(has(self.secretRef) && !has(self.oidcConfig)) || (!has(self.secretRef) && has(self.oidcConfig))",message="Exactly one of 'secretRef' or 'oidcConfig' must be specified"
 type DopplerAuth struct {
-	SecretRef DopplerAuthSecretRef `json:"secretRef"`
+	// SecretRef authenticates using a Doppler service token stored in a Kubernetes Secret.
+	// +optional
+	SecretRef *DopplerAuthSecretRef `json:"secretRef,omitempty"`
+
+	// OIDCConfig authenticates using Kubernetes ServiceAccount tokens via OIDC.
+	// +optional
+	OIDCConfig *DopplerOIDCAuth `json:"oidcConfig,omitempty"`
 }
 
 // DopplerAuthSecretRef contains the secret reference for accessing the Doppler API.
@@ -35,6 +43,21 @@ type DopplerAuthSecretRef struct {
 	DopplerToken esmeta.SecretKeySelector `json:"dopplerToken"`
 }
 
+// DopplerOIDCAuth configures OIDC authentication with Doppler using Kubernetes ServiceAccount tokens.
+type DopplerOIDCAuth struct {
+	// Identity is the Doppler Service Account Identity ID configured for OIDC authentication.
+	Identity string `json:"identity"`
+
+	// ServiceAccountRef specifies the Kubernetes ServiceAccount to use for authentication.
+	ServiceAccountRef esmeta.ServiceAccountSelector `json:"serviceAccountRef"`
+
+	// ExpirationSeconds sets the ServiceAccount token validity duration.
+	// Defaults to 10 minutes.
+	// +kubebuilder:default=600
+	// +optional
+	ExpirationSeconds *int64 `json:"expirationSeconds,omitempty"`
+}
+
 // DopplerProvider configures a store to sync secrets using the Doppler provider.
 // Project and Config are required if not using a Service Token.
 type DopplerProvider struct {

+ 31 - 1
apis/externalsecrets/v1/zz_generated.deepcopy.go

@@ -1356,7 +1356,16 @@ func (in *Device42SecretRef) DeepCopy() *Device42SecretRef {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *DopplerAuth) DeepCopyInto(out *DopplerAuth) {
 	*out = *in
-	in.SecretRef.DeepCopyInto(&out.SecretRef)
+	if in.SecretRef != nil {
+		in, out := &in.SecretRef, &out.SecretRef
+		*out = new(DopplerAuthSecretRef)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.OIDCConfig != nil {
+		in, out := &in.OIDCConfig, &out.OIDCConfig
+		*out = new(DopplerOIDCAuth)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DopplerAuth.
@@ -1386,6 +1395,27 @@ func (in *DopplerAuthSecretRef) DeepCopy() *DopplerAuthSecretRef {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DopplerOIDCAuth) DeepCopyInto(out *DopplerOIDCAuth) {
+	*out = *in
+	in.ServiceAccountRef.DeepCopyInto(&out.ServiceAccountRef)
+	if in.ExpirationSeconds != nil {
+		in, out := &in.ExpirationSeconds, &out.ExpirationSeconds
+		*out = new(int64)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DopplerOIDCAuth.
+func (in *DopplerOIDCAuth) DeepCopy() *DopplerOIDCAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(DopplerOIDCAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *DopplerProvider) DeepCopyInto(out *DopplerProvider) {
 	*out = *in
 	if in.Auth != nil {

+ 56 - 4
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -1828,9 +1828,58 @@ spec:
                         description: Auth configures how the Operator authenticates
                           with the Doppler API
                         properties:
+                          oidcConfig:
+                            description: OIDCConfig authenticates using Kubernetes
+                              ServiceAccount tokens via OIDC.
+                            properties:
+                              expirationSeconds:
+                                default: 600
+                                description: |-
+                                  ExpirationSeconds sets the ServiceAccount token validity duration.
+                                  Defaults to 10 minutes.
+                                format: int64
+                                type: integer
+                              identity:
+                                description: Identity is the Doppler Service Account
+                                  Identity ID configured for OIDC authentication.
+                                type: string
+                              serviceAccountRef:
+                                description: ServiceAccountRef specifies the Kubernetes
+                                  ServiceAccount to use for authentication.
+                                properties:
+                                  audiences:
+                                    description: |-
+                                      Audience specifies the `aud` claim for the service account token
+                                      If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                                      then this audiences will be appended to the list
+                                    items:
+                                      type: string
+                                    type: array
+                                  name:
+                                    description: The name of the ServiceAccount resource
+                                      being referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      Namespace of the resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                required:
+                                - name
+                                type: object
+                            required:
+                            - identity
+                            - serviceAccountRef
+                            type: object
                           secretRef:
-                            description: DopplerAuthSecretRef contains the secret
-                              reference for accessing the Doppler API.
+                            description: SecretRef authenticates using a Doppler service
+                              token stored in a Kubernetes Secret.
                             properties:
                               dopplerToken:
                                 description: |-
@@ -1865,9 +1914,12 @@ spec:
                             required:
                             - dopplerToken
                             type: object
-                        required:
-                        - secretRef
                         type: object
+                        x-kubernetes-validations:
+                        - message: Exactly one of 'secretRef' or 'oidcConfig' must
+                            be specified
+                          rule: (has(self.secretRef) && !has(self.oidcConfig)) ||
+                            (!has(self.secretRef) && has(self.oidcConfig))
                       config:
                         description: Doppler config (required if not using a Service
                           Token)

+ 56 - 4
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -1828,9 +1828,58 @@ spec:
                         description: Auth configures how the Operator authenticates
                           with the Doppler API
                         properties:
+                          oidcConfig:
+                            description: OIDCConfig authenticates using Kubernetes
+                              ServiceAccount tokens via OIDC.
+                            properties:
+                              expirationSeconds:
+                                default: 600
+                                description: |-
+                                  ExpirationSeconds sets the ServiceAccount token validity duration.
+                                  Defaults to 10 minutes.
+                                format: int64
+                                type: integer
+                              identity:
+                                description: Identity is the Doppler Service Account
+                                  Identity ID configured for OIDC authentication.
+                                type: string
+                              serviceAccountRef:
+                                description: ServiceAccountRef specifies the Kubernetes
+                                  ServiceAccount to use for authentication.
+                                properties:
+                                  audiences:
+                                    description: |-
+                                      Audience specifies the `aud` claim for the service account token
+                                      If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                                      then this audiences will be appended to the list
+                                    items:
+                                      type: string
+                                    type: array
+                                  name:
+                                    description: The name of the ServiceAccount resource
+                                      being referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      Namespace of the resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                required:
+                                - name
+                                type: object
+                            required:
+                            - identity
+                            - serviceAccountRef
+                            type: object
                           secretRef:
-                            description: DopplerAuthSecretRef contains the secret
-                              reference for accessing the Doppler API.
+                            description: SecretRef authenticates using a Doppler service
+                              token stored in a Kubernetes Secret.
                             properties:
                               dopplerToken:
                                 description: |-
@@ -1865,9 +1914,12 @@ spec:
                             required:
                             - dopplerToken
                             type: object
-                        required:
-                        - secretRef
                         type: object
+                        x-kubernetes-validations:
+                        - message: Exactly one of 'secretRef' or 'oidcConfig' must
+                            be specified
+                          rule: (has(self.secretRef) && !has(self.oidcConfig)) ||
+                            (!has(self.secretRef) && has(self.oidcConfig))
                       config:
                         description: Doppler config (required if not using a Service
                           Token)

+ 98 - 6
deploy/crds/bundle.yaml

@@ -3795,8 +3795,53 @@ spec:
                         auth:
                           description: Auth configures how the Operator authenticates with the Doppler API
                           properties:
+                            oidcConfig:
+                              description: OIDCConfig authenticates using Kubernetes ServiceAccount tokens via OIDC.
+                              properties:
+                                expirationSeconds:
+                                  default: 600
+                                  description: |-
+                                    ExpirationSeconds sets the ServiceAccount token validity duration.
+                                    Defaults to 10 minutes.
+                                  format: int64
+                                  type: integer
+                                identity:
+                                  description: Identity is the Doppler Service Account Identity ID configured for OIDC authentication.
+                                  type: string
+                                serviceAccountRef:
+                                  description: ServiceAccountRef specifies the Kubernetes ServiceAccount to use for authentication.
+                                  properties:
+                                    audiences:
+                                      description: |-
+                                        Audience specifies the `aud` claim for the service account token
+                                        If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                                        then this audiences will be appended to the list
+                                      items:
+                                        type: string
+                                      type: array
+                                    name:
+                                      description: The name of the ServiceAccount resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        Namespace of the resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  required:
+                                    - name
+                                  type: object
+                              required:
+                                - identity
+                                - serviceAccountRef
+                              type: object
                             secretRef:
-                              description: DopplerAuthSecretRef contains the secret reference for accessing the Doppler API.
+                              description: SecretRef authenticates using a Doppler service token stored in a Kubernetes Secret.
                               properties:
                                 dopplerToken:
                                   description: |-
@@ -3830,9 +3875,10 @@ spec:
                               required:
                                 - dopplerToken
                               type: object
-                          required:
-                            - secretRef
                           type: object
+                          x-kubernetes-validations:
+                            - message: Exactly one of 'secretRef' or 'oidcConfig' must be specified
+                              rule: (has(self.secretRef) && !has(self.oidcConfig)) || (!has(self.secretRef) && has(self.oidcConfig))
                         config:
                           description: Doppler config (required if not using a Service Token)
                           type: string
@@ -15370,8 +15416,53 @@ spec:
                         auth:
                           description: Auth configures how the Operator authenticates with the Doppler API
                           properties:
+                            oidcConfig:
+                              description: OIDCConfig authenticates using Kubernetes ServiceAccount tokens via OIDC.
+                              properties:
+                                expirationSeconds:
+                                  default: 600
+                                  description: |-
+                                    ExpirationSeconds sets the ServiceAccount token validity duration.
+                                    Defaults to 10 minutes.
+                                  format: int64
+                                  type: integer
+                                identity:
+                                  description: Identity is the Doppler Service Account Identity ID configured for OIDC authentication.
+                                  type: string
+                                serviceAccountRef:
+                                  description: ServiceAccountRef specifies the Kubernetes ServiceAccount to use for authentication.
+                                  properties:
+                                    audiences:
+                                      description: |-
+                                        Audience specifies the `aud` claim for the service account token
+                                        If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                                        then this audiences will be appended to the list
+                                      items:
+                                        type: string
+                                      type: array
+                                    name:
+                                      description: The name of the ServiceAccount resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        Namespace of the resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  required:
+                                    - name
+                                  type: object
+                              required:
+                                - identity
+                                - serviceAccountRef
+                              type: object
                             secretRef:
-                              description: DopplerAuthSecretRef contains the secret reference for accessing the Doppler API.
+                              description: SecretRef authenticates using a Doppler service token stored in a Kubernetes Secret.
                               properties:
                                 dopplerToken:
                                   description: |-
@@ -15405,9 +15496,10 @@ spec:
                               required:
                                 - dopplerToken
                               type: object
-                          required:
-                            - secretRef
                           type: object
+                          x-kubernetes-validations:
+                            - message: Exactly one of 'secretRef' or 'oidcConfig' must be specified
+                              rule: (has(self.secretRef) && !has(self.oidcConfig)) || (!has(self.secretRef) && has(self.oidcConfig))
                         config:
                           description: Doppler config (required if not using a Service Token)
                           type: string

+ 74 - 1
docs/api/spec.md

@@ -3505,7 +3505,8 @@ External Secrets meta/v1.SecretKeySelector
 <a href="#external-secrets.io/v1.DopplerProvider">DopplerProvider</a>)
 </p>
 <p>
-<p>DopplerAuth defines the authentication method for the Doppler provider.</p>
+<p>DopplerAuth configures authentication with the Doppler API.
+Exactly one of secretRef or oidcConfig must be specified.</p>
 </p>
 <table>
 <thead>
@@ -3525,6 +3526,22 @@ DopplerAuthSecretRef
 </em>
 </td>
 <td>
+<em>(Optional)</em>
+<p>SecretRef authenticates using a Doppler service token stored in a Kubernetes Secret.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>oidcConfig</code></br>
+<em>
+<a href="#external-secrets.io/v1.DopplerOIDCAuth">
+DopplerOIDCAuth
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>OIDCConfig authenticates using Kubernetes ServiceAccount tokens via OIDC.</p>
 </td>
 </tr>
 </tbody>
@@ -3563,6 +3580,62 @@ The Key attribute defaults to dopplerToken if not specified.</p>
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1.DopplerOIDCAuth">DopplerOIDCAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.DopplerAuth">DopplerAuth</a>)
+</p>
+<p>
+<p>DopplerOIDCAuth configures OIDC authentication with Doppler using Kubernetes ServiceAccount tokens.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>identity</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Identity is the Doppler Service Account Identity ID configured for OIDC authentication.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>serviceAccountRef</code></br>
+<em>
+<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#ServiceAccountSelector">
+External Secrets meta/v1.ServiceAccountSelector
+</a>
+</em>
+</td>
+<td>
+<p>ServiceAccountRef specifies the Kubernetes ServiceAccount to use for authentication.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>expirationSeconds</code></br>
+<em>
+int64
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>ExpirationSeconds sets the ServiceAccount token validity duration.
+Defaults to 10 minutes.</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1.DopplerProvider">DopplerProvider
 </h3>
 <p>

+ 30 - 1
docs/provider/doppler.md

@@ -6,6 +6,12 @@ Sync secrets from the [Doppler SecretOps Platform](https://www.doppler.com/) to
 
 ## Authentication
 
+Doppler supports two authentication methods:
+
+> **NOTE:** When using a `ClusterSecretStore`, be sure to set `namespace` in `secretRef.dopplerToken` (for token auth) or `serviceAccountRef` (for OIDC auth).
+
+### Service Token Authentication
+
 Doppler [Service Tokens](https://docs.doppler.com/docs/service-tokens) are recommended as they restrict access to a single config.
 
 ![Doppler Service Token](../pictures/doppler-service-tokens.png)
@@ -30,7 +36,30 @@ Then to create a generic `SecretStore`:
 {% include 'doppler-generic-secret-store.yaml' %}
 ```
 
-> **NOTE:** In case of a `ClusterSecretStore`, be sure to set `namespace` in `secretRef.dopplerToken`.
+### OIDC Authentication
+
+For OIDC authentication, you'll need to configure a Doppler [Service Account Identity](https://docs.doppler.com/docs/service-account-identities) and create a Kubernetes ServiceAccount.
+
+First, create a Kubernetes ServiceAccount:
+
+```yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: doppler-oidc-sa
+  namespace: external-secrets
+```
+
+Next, create a Doppler Service Account Identity with:
+- **Issuer**: Your cluster's OIDC discovery URL
+- **Audience**: The resource-specific audience for the SecretStore (`secretStore:<namespace>:<storeName>` or `clusterSecretStore:<storeName>`), e.g. `secretStore:external-secrets:doppler-oidc-sa` or `clusterSecretStore:doppler-auth-api`
+- **Subject**: The Kubernetes ServiceAccount (`system:serviceaccount:<serviceAccountNamespace>:<serviceAccountName>`), e.g. `system:serviceaccount:external-secrets:doppler-oidc-sa`
+
+Then configure the SecretStore:
+
+```yaml
+{% include 'doppler-oidc-secret-store.yaml' %}
+```
 
 
 ## Use Cases

+ 17 - 0
docs/snippets/doppler-oidc-secret-store.yaml

@@ -0,0 +1,17 @@
+apiVersion: external-secrets.io/v1
+kind: SecretStore
+metadata:
+  name: doppler-auth-api
+spec:
+  provider:
+    doppler:
+      auth:
+        oidcConfig:
+          identity: "00000000-0000-0000-0000-000000000000"
+          serviceAccountRef:
+            name: doppler-oidc-sa
+            namespace: external-secrets
+            # expirationSeconds defaults to 600 if not supplied
+            # expirationSeconds: 600
+      project: my-project
+      config: my-config

+ 253 - 0
providers/v1/doppler/auth_oidc.go

@@ -0,0 +1,253 @@
+/*
+Copyright © 2025 ESO Maintainer Team
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+		https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package doppler
+
+import (
+	"bytes"
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"sync"
+	"time"
+
+	authv1 "k8s.io/api/authentication/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+const (
+	defaultTokenTTL = 600
+	minTokenBuffer  = 60
+	dopplerOIDCPath = "/v3/auth/oidc"
+)
+
+// OIDCTokenManager manages OIDC token exchange with Doppler.
+type OIDCTokenManager struct {
+	corev1    typedcorev1.CoreV1Interface
+	store     *esv1.DopplerProvider
+	namespace string
+	storeKind string
+	storeName string
+	baseURL   string
+	verifyTLS bool
+
+	mu          sync.RWMutex
+	cachedToken string
+	tokenExpiry time.Time
+}
+
+// NewOIDCTokenManager creates a new OIDCTokenManager for handling Doppler OIDC authentication.
+func NewOIDCTokenManager(
+	corev1 typedcorev1.CoreV1Interface,
+	store *esv1.DopplerProvider,
+	namespace string,
+	storeKind string,
+	storeName string,
+) *OIDCTokenManager {
+	baseURL := "https://api.doppler.com"
+	if customURL := os.Getenv(customBaseURLEnvVar); customURL != "" {
+		baseURL = customURL
+	}
+
+	verifyTLS := os.Getenv(verifyTLSOverrideEnvVar) != "false"
+
+	return &OIDCTokenManager{
+		corev1:    corev1,
+		store:     store,
+		namespace: namespace,
+		storeKind: storeKind,
+		storeName: storeName,
+		baseURL:   baseURL,
+		verifyTLS: verifyTLS,
+	}
+}
+
+// Token returns a valid Doppler API token, refreshing it if necessary.
+func (m *OIDCTokenManager) Token(ctx context.Context) (string, error) {
+	m.mu.RLock()
+	if m.isTokenValid() {
+		token := m.cachedToken
+		m.mu.RUnlock()
+		return token, nil
+	}
+	m.mu.RUnlock()
+
+	return m.refreshToken(ctx)
+}
+
+func (m *OIDCTokenManager) isTokenValid() bool {
+	if m.cachedToken == "" {
+		return false
+	}
+	return time.Until(m.tokenExpiry) > minTokenBuffer*time.Second
+}
+
+func (m *OIDCTokenManager) refreshToken(ctx context.Context) (string, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if m.isTokenValid() {
+		return m.cachedToken, nil
+	}
+
+	saToken, err := m.createServiceAccountToken(ctx)
+	if err != nil {
+		return "", fmt.Errorf("failed to create service account token: %w", err)
+	}
+
+	dopplerToken, expiry, err := m.exchangeTokenWithDoppler(ctx, saToken)
+	if err != nil {
+		return "", fmt.Errorf("failed to exchange token with Doppler: %w", err)
+	}
+
+	m.cachedToken = dopplerToken
+	m.tokenExpiry = expiry
+
+	return dopplerToken, nil
+}
+
+func (m *OIDCTokenManager) createServiceAccountToken(ctx context.Context) (string, error) {
+	oidcAuth := m.store.Auth.OIDCConfig
+
+	audiences := []string{m.baseURL}
+
+	// Add custom audiences from serviceAccountRef
+	if len(oidcAuth.ServiceAccountRef.Audiences) > 0 {
+		audiences = append(audiences, oidcAuth.ServiceAccountRef.Audiences...)
+	}
+
+	// Add resource-specific audience for cryptographic binding
+	if m.storeKind == esv1.ClusterSecretStoreKind {
+		audiences = append(audiences, fmt.Sprintf("clusterSecretStore:%s", m.storeName))
+	} else {
+		audiences = append(audiences, fmt.Sprintf("secretStore:%s:%s", m.namespace, m.storeName))
+	}
+
+	expirationSeconds := oidcAuth.ExpirationSeconds
+	if expirationSeconds == nil {
+		tmp := int64(defaultTokenTTL)
+		expirationSeconds = &tmp
+	}
+
+	tokenRequest := &authv1.TokenRequest{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: m.namespace,
+		},
+		Spec: authv1.TokenRequestSpec{
+			Audiences:         audiences,
+			ExpirationSeconds: expirationSeconds,
+		},
+	}
+
+	// For ClusterSecretStores, we use the ServiceAccountRef.Namespace if specified
+	if m.storeKind == esv1.ClusterSecretStoreKind && oidcAuth.ServiceAccountRef.Namespace != nil {
+		tokenRequest.Namespace = *oidcAuth.ServiceAccountRef.Namespace
+	}
+
+	tokenResponse, err := m.corev1.ServiceAccounts(tokenRequest.Namespace).
+		CreateToken(ctx, oidcAuth.ServiceAccountRef.Name, tokenRequest, metav1.CreateOptions{})
+	if err != nil {
+		return "", fmt.Errorf("failed to create token for service account %s: %w",
+			oidcAuth.ServiceAccountRef.Name, err)
+	}
+
+	return tokenResponse.Status.Token, nil
+}
+
+func (m *OIDCTokenManager) exchangeTokenWithDoppler(ctx context.Context, saToken string) (string, time.Time, error) {
+	oidcAuth := m.store.Auth.OIDCConfig
+	url := m.baseURL + dopplerOIDCPath
+
+	requestBody := map[string]string{
+		"identity": oidcAuth.Identity,
+		"token":    saToken,
+	}
+
+	jsonBody, err := json.Marshal(requestBody)
+	if err != nil {
+		return "", time.Time{}, fmt.Errorf("failed to marshal request body: %w", err)
+	}
+
+	req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonBody))
+	if err != nil {
+		return "", time.Time{}, fmt.Errorf("failed to create request: %w", err)
+	}
+
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("Accept", "application/json")
+
+	tlsConfig := &tls.Config{
+		MinVersion: tls.VersionTLS12,
+	}
+	if !m.verifyTLS {
+		tlsConfig.InsecureSkipVerify = true
+	}
+
+	transport := &http.Transport{
+		TLSClientConfig: tlsConfig,
+	}
+
+	client := &http.Client{
+		Timeout:   10 * time.Second,
+		Transport: transport,
+	}
+
+	resp, err := client.Do(req)
+	if err != nil {
+		return "", time.Time{}, fmt.Errorf("failed to make request to Doppler: %w", err)
+	}
+	defer func() {
+		_ = resp.Body.Close()
+	}()
+
+	body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
+	if err != nil {
+		return "", time.Time{}, fmt.Errorf("failed to read response body: %w", err)
+	}
+
+	if resp.StatusCode != http.StatusOK {
+		return "", time.Time{}, fmt.Errorf("Doppler OIDC auth failed with status %d: %s",
+			resp.StatusCode, string(body))
+	}
+
+	var response struct {
+		Success   bool   `json:"success"`
+		Token     string `json:"token"`
+		ExpiresAt string `json:"expires_at"`
+	}
+
+	if err := json.Unmarshal(body, &response); err != nil {
+		return "", time.Time{}, fmt.Errorf("failed to parse response: %w", err)
+	}
+
+	if !response.Success {
+		return "", time.Time{}, fmt.Errorf("Doppler OIDC auth failed: %s", string(body))
+	}
+
+	expiresAt, err := time.Parse(time.RFC3339, response.ExpiresAt)
+	if err != nil {
+		return "", time.Time{}, fmt.Errorf("failed to parse expiration time: %w", err)
+	}
+
+	return response.Token, expiresAt, nil
+}

+ 126 - 0
providers/v1/doppler/auth_oidc_test.go

@@ -0,0 +1,126 @@
+/*
+Copyright © 2025 ESO Maintainer Team
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+		https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package doppler
+
+import (
+	"context"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/runtime/util/fake"
+)
+
+func TestOIDCTokenManager_Token(t *testing.T) {
+	// Mock Doppler OIDC endpoint
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/v3/auth/oidc" {
+			w.Header().Set("Content-Type", "application/json")
+			w.WriteHeader(http.StatusOK)
+			// Return a token that expires in 1 hour
+			expiresAt := time.Now().Add(time.Hour).Format(time.RFC3339)
+			if _, err := w.Write([]byte(`{"success": true, "token": "doppler_token_123", "expires_at": "` + expiresAt + `"}`)); err != nil {
+				t.Errorf("failed to write response: %v", err)
+			}
+			return
+		}
+		w.WriteHeader(http.StatusNotFound)
+	}))
+	defer server.Close()
+
+	store := &esv1.DopplerProvider{
+		Auth: &esv1.DopplerAuth{
+			OIDCConfig: &esv1.DopplerOIDCAuth{
+				Identity:          "test-identity",
+				ServiceAccountRef: esmeta.ServiceAccountSelector{Name: "test-sa"},
+				ExpirationSeconds: func() *int64 { v := int64(600); return &v }(),
+			},
+		},
+	}
+
+	manager := &OIDCTokenManager{
+		corev1:    fake.NewCreateTokenMock().WithToken("k8s_jwt_token"),
+		store:     store,
+		namespace: "test-namespace",
+		storeKind: "SecretStore",
+		storeName: "test-store",
+		baseURL:   server.URL,
+		verifyTLS: false,
+	}
+
+	ctx := context.Background()
+
+	// First call should fetch a new token
+	token1, err := manager.Token(ctx)
+	require.NoError(t, err)
+	assert.Equal(t, "doppler_token_123", token1)
+
+	// Second call should return cached token
+	token2, err := manager.Token(ctx)
+	require.NoError(t, err)
+	assert.Equal(t, token1, token2)
+}
+
+func TestOIDCTokenManager_CreateServiceAccountToken(t *testing.T) {
+	store := &esv1.DopplerProvider{
+		Auth: &esv1.DopplerAuth{
+			OIDCConfig: &esv1.DopplerOIDCAuth{
+				Identity:          "test-identity",
+				ServiceAccountRef: esmeta.ServiceAccountSelector{Name: "test-sa", Namespace: func() *string { s := "custom-ns"; return &s }()},
+				ExpirationSeconds: func() *int64 { v := int64(600); return &v }(),
+			},
+		},
+	}
+
+	manager := &OIDCTokenManager{
+		corev1:    fake.NewCreateTokenMock().WithToken("k8s_jwt_token"),
+		store:     store,
+		namespace: "default-namespace",
+		storeKind: "SecretStore",
+		storeName: "test-store",
+		baseURL:   "https://api.doppler.com",
+		verifyTLS: true,
+	}
+
+	token, err := manager.createServiceAccountToken(context.Background())
+	require.NoError(t, err)
+	assert.Equal(t, "k8s_jwt_token", token)
+}
+
+func TestOIDCTokenManager_TokenExpiry(t *testing.T) {
+	manager := &OIDCTokenManager{
+		cachedToken: "test_token",
+		tokenExpiry: time.Now().Add(30 * time.Second), // Token expires in 30 seconds
+	}
+
+	// Token should be considered invalid (less than 60 second buffer)
+	assert.False(t, manager.isTokenValid())
+
+	// Token with more time should be valid
+	manager.tokenExpiry = time.Now().Add(2 * time.Minute)
+	assert.True(t, manager.isTokenValid())
+
+	// Empty token should be invalid
+	manager.cachedToken = ""
+	assert.False(t, manager.isTokenValid())
+}

+ 57 - 21
providers/v1/doppler/client.go

@@ -28,6 +28,7 @@ import (
 
 	"github.com/external-secrets/external-secrets/runtime/find"
 	corev1 "k8s.io/api/core/v1"
+	typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
 	kclient "sigs.k8s.io/controller-runtime/pkg/client"
 
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
@@ -58,10 +59,12 @@ type Client struct {
 	nameTransformer string
 	format          string
 
-	kube      kclient.Client
-	store     *esv1.DopplerProvider
-	namespace string
-	storeKind string
+	kube        kclient.Client
+	corev1      typedcorev1.CoreV1Interface
+	store       *esv1.DopplerProvider
+	namespace   string
+	storeKind   string
+	oidcManager *OIDCTokenManager
 }
 
 // SecretsClientInterface defines the required Doppler Client methods.
@@ -74,16 +77,39 @@ type SecretsClientInterface interface {
 }
 
 func (c *Client) setAuth(ctx context.Context) error {
-	token, err := resolvers.SecretKeyRef(
-		ctx,
-		c.kube,
-		c.storeKind,
-		c.namespace,
-		&c.store.Auth.SecretRef.DopplerToken)
-	if err != nil {
-		return err
+	if c.store.Auth.SecretRef != nil {
+		token, err := resolvers.SecretKeyRef(
+			ctx,
+			c.kube,
+			c.storeKind,
+			c.namespace,
+			&c.store.Auth.SecretRef.DopplerToken)
+		if err != nil {
+			return err
+		}
+		c.dopplerToken = token
+	} else if c.store.Auth.OIDCConfig != nil {
+		token, err := c.oidcManager.Token(ctx)
+		if err != nil {
+			return fmt.Errorf("failed to get OIDC token: %w", err)
+		}
+		c.dopplerToken = token
+	} else {
+		return errors.New("no authentication method configured: either secretRef or oidcConfig must be specified")
+	}
+	return nil
+}
+
+func (c *Client) refreshAuthIfNeeded(ctx context.Context) error {
+	if c.store != nil && c.store.Auth != nil && c.store.Auth.OIDCConfig != nil && c.oidcManager != nil {
+		token, err := c.oidcManager.Token(ctx)
+		if err != nil {
+			return fmt.Errorf("failed to refresh OIDC token: %w", err)
+		}
+		if doppler, ok := c.doppler.(*dclient.DopplerClient); ok {
+			doppler.DopplerToken = token
+		}
 	}
-	c.dopplerToken = token
 	return nil
 }
 
@@ -104,7 +130,10 @@ func (c *Client) Validate() (esv1.ValidationResult, error) {
 }
 
 // DeleteSecret removes a secret from Doppler.
-func (c *Client) DeleteSecret(_ context.Context, ref esv1.PushSecretRemoteRef) error {
+func (c *Client) DeleteSecret(ctx context.Context, ref esv1.PushSecretRemoteRef) error {
+	if err := c.refreshAuthIfNeeded(ctx); err != nil {
+		return err
+	}
 	request := dclient.UpdateSecretsRequest{
 		ChangeRequests: []dclient.Change{
 			{
@@ -131,12 +160,13 @@ func (c *Client) SecretExists(_ context.Context, _ esv1.PushSecretRemoteRef) (bo
 }
 
 // PushSecret creates or updates a secret in Doppler.
-func (c *Client) PushSecret(_ context.Context, secret *corev1.Secret, data esv1.PushSecretData) error {
-	value := secret.Data[data.GetSecretKey()]
-
+func (c *Client) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1.PushSecretData) error {
+	if err := c.refreshAuthIfNeeded(ctx); err != nil {
+		return err
+	}
 	request := dclient.UpdateSecretsRequest{
 		Secrets: dclient.Secrets{
-			data.GetRemoteKey(): string(value),
+			data.GetRemoteKey(): string(secret.Data[data.GetSecretKey()]),
 		},
 		Project: c.project,
 		Config:  c.config,
@@ -151,7 +181,10 @@ func (c *Client) PushSecret(_ context.Context, secret *corev1.Secret, data esv1.
 }
 
 // GetSecret retrieves a secret from Doppler.
-func (c *Client) GetSecret(_ context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
+func (c *Client) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	if err := c.refreshAuthIfNeeded(ctx); err != nil {
+		return nil, err
+	}
 	request := dclient.SecretRequest{
 		Name:    ref.Key,
 		Project: c.project,
@@ -194,7 +227,7 @@ func (c *Client) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRe
 
 // GetAllSecrets retrieves all secrets from Doppler that match the given criteria.
 func (c *Client) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
-	secrets, err := c.getSecrets(ctx)
+	secrets, err := c.secrets(ctx)
 	selected := map[string][]byte{}
 
 	if err != nil {
@@ -229,7 +262,10 @@ func (c *Client) Close(_ context.Context) error {
 	return nil
 }
 
-func (c *Client) getSecrets(_ context.Context) (map[string][]byte, error) {
+func (c *Client) secrets(ctx context.Context) (map[string][]byte, error) {
+	if err := c.refreshAuthIfNeeded(ctx); err != nil {
+		return nil, err
+	}
 	request := dclient.SecretsRequest{
 		Project:         c.project,
 		Config:          c.config,

+ 53 - 4
providers/v1/doppler/doppler_test.go

@@ -398,10 +398,26 @@ func makeSecretStore(fn ...storeModifier) *esv1.SecretStore {
 
 func withAuth(name, key string, namespace *string) storeModifier {
 	return func(store *esv1.SecretStore) *esv1.SecretStore {
-		store.Spec.Provider.Doppler.Auth.SecretRef.DopplerToken = v1.SecretKeySelector{
-			Name:      name,
-			Key:       key,
-			Namespace: namespace,
+		store.Spec.Provider.Doppler.Auth.SecretRef = &esv1.DopplerAuthSecretRef{
+			DopplerToken: v1.SecretKeySelector{
+				Name:      name,
+				Key:       key,
+				Namespace: namespace,
+			},
+		}
+		return store
+	}
+}
+
+func withOIDCAuth(identityID, saName string, saNamespace *string) storeModifier {
+	return func(store *esv1.SecretStore) *esv1.SecretStore {
+		store.Spec.Provider.Doppler.Auth.SecretRef = nil
+		store.Spec.Provider.Doppler.Auth.OIDCConfig = &esv1.DopplerOIDCAuth{
+			Identity: identityID,
+			ServiceAccountRef: v1.ServiceAccountSelector{
+				Name:      saName,
+				Namespace: saNamespace,
+			},
 		}
 		return store
 	}
@@ -437,6 +453,39 @@ func TestValidateStore(t *testing.T) {
 			store: makeSecretStore(withAuth(secretName, "", nil)),
 			err:   nil,
 		},
+		{
+			label: "invalid store missing both auth methods",
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						Doppler: &esv1.DopplerProvider{
+							Auth: &esv1.DopplerAuth{},
+						},
+					},
+				},
+			},
+			err: errors.New("invalid store: either auth.secretRef or auth.oidcConfig must be specified"),
+		},
+		{
+			label: "invalid OIDC missing identityId",
+			store: makeSecretStore(withOIDCAuth("", "sa-name", nil)),
+			err:   errors.New("invalid store: oidcConfig.identity cannot be empty"),
+		},
+		{
+			label: "invalid OIDC missing serviceAccountRef.name",
+			store: makeSecretStore(withOIDCAuth("identity-123", "", nil)),
+			err:   errors.New("invalid store: oidcConfig.serviceAccountRef.name cannot be empty"),
+		},
+		{
+			label: "valid OIDC auth",
+			store: makeSecretStore(withOIDCAuth("identity-123", "sa-name", nil)),
+			err:   nil,
+		},
+		{
+			label: "invalid OIDC namespace not allowed",
+			store: makeSecretStore(withOIDCAuth("identity-123", "sa-name", &namespace)),
+			err:   errors.New("invalid store: namespace should either be empty or match the namespace of the SecretStore for a namespaced SecretStore"),
+		},
 	}
 	p := Provider{}
 	for _, tc := range testCases {

+ 11 - 4
providers/v1/doppler/go.mod

@@ -6,7 +6,10 @@ require (
 	github.com/external-secrets/external-secrets/apis v0.0.0
 	github.com/external-secrets/external-secrets/runtime v0.0.0
 	github.com/google/go-cmp v0.7.0
+	github.com/spf13/pflag v1.0.10
+	github.com/stretchr/testify v1.11.1
 	k8s.io/api v0.34.1
+	k8s.io/apimachinery v0.34.1
 	k8s.io/client-go v0.34.1
 	sigs.k8s.io/controller-runtime v0.22.3
 )
@@ -16,9 +19,10 @@ require (
 	github.com/Masterminds/goutils v1.1.1 // indirect
 	github.com/Masterminds/semver/v3 v3.4.0 // indirect
 	github.com/Masterminds/sprig/v3 v3.3.0 // indirect
+	github.com/aws/aws-sdk-go-v2 v1.39.6 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
-	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
 	github.com/emicklei/go-restful/v3 v3.13.0 // indirect
 	github.com/evanphx/json-patch/v5 v5.9.11 // indirect
@@ -40,10 +44,12 @@ require (
 	github.com/go-openapi/swag/typeutils v0.25.1 // indirect
 	github.com/go-openapi/swag/yamlutils v0.25.1 // indirect
 	github.com/goccy/go-json v0.10.5 // indirect
+	github.com/gofrs/flock v0.13.0 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/google/btree v1.1.3 // indirect
 	github.com/google/gnostic-models v0.7.0 // indirect
 	github.com/google/uuid v1.6.0 // indirect
+	github.com/hashicorp/golang-lru v1.0.2 // indirect
 	github.com/huandu/xstrings v1.5.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/lestrrat-go/blackmagic v1.0.4 // indirect
@@ -57,15 +63,16 @@ require (
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/oracle/oci-go-sdk/v65 v65.103.0 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/client_golang v1.23.2 // indirect
 	github.com/prometheus/client_model v0.6.2 // indirect
 	github.com/prometheus/common v0.67.2 // indirect
 	github.com/prometheus/procfs v0.19.2 // indirect
 	github.com/segmentio/asm v1.2.1 // indirect
 	github.com/shopspring/decimal v1.4.0 // indirect
+	github.com/sony/gobreaker v1.0.0 // indirect
 	github.com/spf13/cast v1.10.0 // indirect
-	github.com/spf13/pflag v1.0.10 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	go.yaml.in/yaml/v2 v2.4.3 // indirect
 	go.yaml.in/yaml/v3 v3.0.4 // indirect
@@ -81,8 +88,8 @@ require (
 	google.golang.org/protobuf v1.36.10 // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 	k8s.io/apiextensions-apiserver v0.34.1 // indirect
-	k8s.io/apimachinery v0.34.1 // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
 	k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect

+ 18 - 14
providers/v1/doppler/go.sum

@@ -6,17 +6,18 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1
 github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
 github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
 github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
-github.com/aws/aws-sdk-go-v2 v1.39.3 h1:h7xSsanJ4EQJXG5iuW4UqgP7qBopLpj84mpkNx3wPjM=
-github.com/aws/aws-sdk-go-v2 v1.39.3/go.mod h1:yWSxrnioGUZ4WVv9TgMrNUeLV3PFESn/v+6T/Su8gnM=
-github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M=
-github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
+github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk=
+github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
+github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
+github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
 github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
@@ -69,8 +70,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
 github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
 github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
 github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
-github.com/gofrs/flock v0.10.0 h1:SHMXenfaB03KbroETaCMtbBg3Yn29v4w1r+tgy4ff4k=
-github.com/gofrs/flock v0.10.0/go.mod h1:FirDy1Ing0mI2+kB6wk+vyyAH+e6xiE+EYA0jnzV9jc=
+github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
+github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
@@ -86,6 +87,8 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgY
 github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
+github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
 github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -128,12 +131,13 @@ github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg
 github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
 github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
 github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
-github.com/oracle/oci-go-sdk/v65 v65.102.1 h1:zLNLz5dVzZxOf5DK/f3WGZUjwrQ9m27fd4abOFwQRCQ=
-github.com/oracle/oci-go-sdk/v65 v65.102.1/go.mod h1:oB8jFGVc/7/zJ+DbleE8MzGHjhs2ioCz5stRTdZdIcY=
+github.com/oracle/oci-go-sdk/v65 v65.103.0 h1:HfyZx+JefCPK3At0Xt45q+wr914jDXuoyzOFX3XCbno=
+github.com/oracle/oci-go-sdk/v65 v65.103.0/go.mod h1:oB8jFGVc/7/zJ+DbleE8MzGHjhs2ioCz5stRTdZdIcY=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
 github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
@@ -142,14 +146,14 @@ github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyA
 github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
 github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
 github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
-github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
-github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
 github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
 github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
 github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
-github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
-github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
+github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
+github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
 github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
 github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
 github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=

+ 121 - 41
providers/v1/doppler/provider.go

@@ -22,14 +22,18 @@ import (
 	"fmt"
 	"os"
 	"strconv"
-	"time"
 
+	"github.com/spf13/pflag"
+	"k8s.io/client-go/kubernetes"
 	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/config"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
-	dclient "github.com/external-secrets/external-secrets/providers/v1/doppler/client"
+	"github.com/external-secrets/external-secrets/runtime/cache"
 	"github.com/external-secrets/external-secrets/runtime/esutils"
+	"github.com/external-secrets/external-secrets/runtime/feature"
+	dclient "github.com/external-secrets/external-secrets/providers/v1/doppler/client"
 )
 
 const (
@@ -45,6 +49,34 @@ type Provider struct{}
 var _ esv1.SecretsClient = &Client{}
 var _ esv1.Provider = &Provider{}
 
+var (
+	oidcClientCache  *cache.Cache[esv1.SecretsClient]
+	defaultCacheSize = 2 << 17
+)
+
+func initCache(cacheSize int) {
+	if oidcClientCache == nil && cacheSize > 0 {
+		oidcClientCache = cache.Must(cacheSize, func(_ esv1.SecretsClient) {
+			// No cleanup is needed when evicting OIDC clients from cache
+		})
+	}
+}
+
+// InitializeFlags registers Doppler-specific flags with the feature system.
+func InitializeFlags() *feature.Feature {
+	var dopplerOIDCCacheSize int
+	fs := pflag.NewFlagSet("doppler", pflag.ExitOnError)
+	fs.IntVar(&dopplerOIDCCacheSize, "doppler-oidc-cache-size", defaultCacheSize,
+		"Maximum size of Doppler OIDC provider cache. Set to 0 to disable caching.")
+
+	return &feature.Feature{
+		Flags: fs,
+		Initialize: func() {
+			initCache(dopplerOIDCCacheSize)
+		},
+	}
+}
+
 // Capabilities returns the provider's supported capabilities.
 func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
 	return esv1.SecretStoreReadOnly
@@ -60,9 +92,18 @@ func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube
 
 	dopplerStoreSpec := storeSpec.Provider.Doppler
 
-	// Default Key to dopplerToken if not specified
-	if dopplerStoreSpec.Auth.SecretRef.DopplerToken.Key == "" {
-		storeSpec.Provider.Doppler.Auth.SecretRef.DopplerToken.Key = "dopplerToken"
+	useCache := dopplerStoreSpec.Auth.OIDCConfig != nil && oidcClientCache != nil
+
+	key := cache.Key{
+		Name:      store.GetObjectMeta().Name,
+		Namespace: namespace,
+		Kind:      store.GetTypeMeta().Kind,
+	}
+
+	if useCache {
+		if cachedClient, ok := oidcClientCache.Get(store.GetObjectMeta().ResourceVersion, key); ok {
+			return cachedClient, nil
+		}
 	}
 
 	client := &Client{
@@ -72,18 +113,67 @@ func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube
 		storeKind: store.GetObjectKind().GroupVersionKind().Kind,
 	}
 
-	if err := client.setAuth(ctx); err != nil {
+	if err := p.setupClientAuth(ctx, client, dopplerStoreSpec, store, namespace); err != nil {
 		return nil, err
 	}
 
+	if err := p.configureDopplerClient(client); err != nil {
+		return nil, err
+	}
+
+	if useCache {
+		oidcClientCache.Add(store.GetObjectMeta().ResourceVersion, key, client)
+	}
+
+	return client, nil
+}
+
+func (p *Provider) setupClientAuth(ctx context.Context, client *Client, dopplerStoreSpec *esv1.DopplerProvider, store esv1.GenericStore, namespace string) error {
+	if dopplerStoreSpec.Auth.SecretRef != nil {
+		if dopplerStoreSpec.Auth.SecretRef.DopplerToken.Key == "" {
+			dopplerStoreSpec.Auth.SecretRef.DopplerToken.Key = "dopplerToken"
+		}
+	} else if dopplerStoreSpec.Auth.OIDCConfig != nil {
+		if err := p.setupOIDCAuth(client, dopplerStoreSpec, store, namespace); err != nil {
+			return err
+		}
+	}
+
+	return client.setAuth(ctx)
+}
+
+func (p *Provider) setupOIDCAuth(client *Client, dopplerStoreSpec *esv1.DopplerProvider, store esv1.GenericStore, namespace string) error {
+	cfg, err := config.GetConfig()
+	if err != nil {
+		return fmt.Errorf("failed to get kubernetes config: %w", err)
+	}
+
+	clientset, err := kubernetes.NewForConfig(cfg)
+	if err != nil {
+		return fmt.Errorf("failed to create kubernetes clientset: %w", err)
+	}
+
+	client.corev1 = clientset.CoreV1()
+	client.oidcManager = NewOIDCTokenManager(
+		client.corev1,
+		dopplerStoreSpec,
+		namespace,
+		store.GetObjectKind().GroupVersionKind().Kind,
+		store.GetObjectMeta().Name,
+	)
+
+	return nil
+}
+
+func (p *Provider) configureDopplerClient(client *Client) error {
 	doppler, err := dclient.NewDopplerClient(client.dopplerToken)
 	if err != nil {
-		return nil, fmt.Errorf(errNewClient, err)
+		return fmt.Errorf(errNewClient, err)
 	}
 
 	if customBaseURL, found := os.LookupEnv(customBaseURLEnvVar); found {
 		if err := doppler.SetBaseURL(customBaseURL); err != nil {
-			return nil, fmt.Errorf(errNewClient, err)
+			return fmt.Errorf(errNewClient, err)
 		}
 	}
 
@@ -94,58 +184,48 @@ func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube
 		}
 	}
 
-	// Wrap the Doppler client with retry logic if retrySettings are configured
-	wrappedClient, err := p.setRetrySettings(doppler, storeSpec)
-	if err != nil {
-		return nil, err
-	}
-
-	client.doppler = wrappedClient
+	client.doppler = doppler
 	client.project = client.store.Project
 	client.config = client.store.Config
 	client.nameTransformer = client.store.NameTransformer
 	client.format = client.store.Format
 
-	return client, nil
+	return nil
 }
 
 // ValidateStore validates the Doppler provider configuration.
 func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
 	storeSpec := store.GetSpec()
 	dopplerStoreSpec := storeSpec.Provider.Doppler
-	dopplerTokenSecretRef := dopplerStoreSpec.Auth.SecretRef.DopplerToken
-	if err := esutils.ValidateSecretSelector(store, dopplerTokenSecretRef); err != nil {
-		return nil, fmt.Errorf(errInvalidStore, err)
-	}
-
-	if dopplerTokenSecretRef.Name == "" {
-		return nil, fmt.Errorf(errInvalidStore, "dopplerToken.name cannot be empty")
-	}
 
-	return nil, nil
-}
+	if dopplerStoreSpec.Auth.SecretRef != nil {
+		dopplerTokenSecretRef := dopplerStoreSpec.Auth.SecretRef.DopplerToken
+		if err := esutils.ValidateSecretSelector(store, dopplerTokenSecretRef); err != nil {
+			return nil, fmt.Errorf(errInvalidStore, err)
+		}
 
-func (p *Provider) setRetrySettings(doppler *dclient.DopplerClient, storeSpec *esv1.SecretStoreSpec) (SecretsClientInterface, error) {
-	if storeSpec.RetrySettings == nil {
-		return doppler, nil
-	}
+		if dopplerTokenSecretRef.Name == "" {
+			return nil, fmt.Errorf(errInvalidStore, "dopplerToken.name cannot be empty")
+		}
+	} else if dopplerStoreSpec.Auth.OIDCConfig != nil {
+		oidcAuth := dopplerStoreSpec.Auth.OIDCConfig
 
-	maxRetries := 3 // default value
-	retryInterval := time.Duration(0)
+		if oidcAuth.Identity == "" {
+			return nil, fmt.Errorf(errInvalidStore, "oidcConfig.identity cannot be empty")
+		}
 
-	if storeSpec.RetrySettings.MaxRetries != nil {
-		maxRetries = int(*storeSpec.RetrySettings.MaxRetries)
-	}
+		if oidcAuth.ServiceAccountRef.Name == "" {
+			return nil, fmt.Errorf(errInvalidStore, "oidcConfig.serviceAccountRef.name cannot be empty")
+		}
 
-	if storeSpec.RetrySettings.RetryInterval != nil {
-		var err error
-		retryInterval, err = time.ParseDuration(*storeSpec.RetrySettings.RetryInterval)
-		if err != nil {
-			return nil, fmt.Errorf(errNewClient, fmt.Errorf("invalid retry interval: %w", err))
+		if err := esutils.ValidateServiceAccountSelector(store, oidcAuth.ServiceAccountRef); err != nil {
+			return nil, fmt.Errorf(errInvalidStore, err)
 		}
+	} else {
+		return nil, fmt.Errorf(errInvalidStore, "either auth.secretRef or auth.oidcConfig must be specified")
 	}
 
-	return newRetryableClient(doppler, maxRetries, retryInterval), nil
+	return nil, nil
 }
 
 // NewProvider creates a new Provider instance.

+ 7 - 0
tests/__snapshot__/clustersecretstore-v1.yaml

@@ -281,6 +281,13 @@ spec:
       host: string
     doppler:
       auth:
+        oidcConfig:
+          expirationSeconds: 600
+          identity: string
+          serviceAccountRef:
+            audiences: [] # minItems 0 of type string
+            name: string
+            namespace: string
         secretRef:
           dopplerToken:
             key: string

+ 7 - 0
tests/__snapshot__/secretstore-v1.yaml

@@ -281,6 +281,13 @@ spec:
       host: string
     doppler:
       auth:
+        oidcConfig:
+          expirationSeconds: 600
+          identity: string
+          serviceAccountRef:
+            audiences: [] # minItems 0 of type string
+            name: string
+            namespace: string
         secretRef:
           dopplerToken:
             key: string