Repository: external-secrets/external-secrets (go)

Issue:
Pulumi: Extracted secrets contain unwanted characters
**Describe the bug**
When using the extract feature with the pulumi provider, the extracted secrets who are strings contains double quotes

**To Reproduce**
Steps to reproduce the behavior:

#### Manifests
```yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: test-extract
  namespace: test

spec:
  dataFrom:
    - extract:
        conversionStrategy: Default
        key: test
  refreshInterval: 30m
  secretStoreRef:
    kind: ClusterSecretStore
    name: pulumi-store
  target:
    creationPolicy: Owner
    deletionPolicy: Retain
```

On pulumi, values are : 
```json
{
  "test": {
    "bar": {
      "foo": "bar"
    },
    "foo": "bar",
    "foobar": 42
  }
}
```

#### Versions
- K8s version : 1.29.2
- ESO version : 0.9.14

**Expected behavior**
I want to have the string whitout quotes : `bar` instead of `"bar"`

**Screenshots**
If applicable, add screenshots to help explain your problem.

```
$ kubectl get secret test-extract -o yaml | yq  .data
bar: eyJmb28iOiJiYXIifQ==
foo: ImJhciI=
foobar: NDI=

$ kubectl get secret test-extract -o yaml | yq  .data.foo | base64 -d
"bar"
```

**Additional context**
I'll made a PR with tests to fix this.


Pull request description:
Pulumi: Remove unwanted chars when extract secret
Add TestGetSecretMap
Fixes #3332

## Problem Statement

When extracting secret with the pulumi provider, the extracted elements who are strings contains unwanted double quotes at the begining and at the end of the extracted secret. 

## Related Issue

Fixes #3332 

## Proposed Changes

This is due to `json.RawMessage` : With `interface{}`, the `GetByteValue` method correctly handle it and return the value without quotes.

## Checklist

- [x] I have read the [contribution guidelines](https://external-secrets.io/latest/contributing/process/#submitting-a-pull-request)
- [x] All commits are signed with `git commit --signoff`
- [x] My changes have reasonable test coverage
- [x] All tests pass with `make test`
- [x] I ensured my PR is ready for review with `make reviewable`


Patch:
diff --git a/apis/externalsecrets/v1beta1/secretstore_azurekv_types.go b/apis/externalsecrets/v1beta1/secretstore_azurekv_types.go
index 1369673c3..333b7757a 100644
--- a/apis/externalsecrets/v1beta1/secretstore_azurekv_types.go
+++ b/apis/externalsecrets/v1beta1/secretstore_azurekv_types.go
@@ -61,7 +61,7 @@ type AzureKVProvider struct {
 	// Vault Url from which the secrets to be fetched from.
 	VaultURL *string `json:"vaultUrl"`
 
-	// TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
+	// TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type. Optional for WorkloadIdentity.
 	// +optional
 	TenantID *string `json:"tenantId,omitempty"`
 
@@ -72,7 +72,7 @@ type AzureKVProvider struct {
 	// +kubebuilder:default=PublicCloud
 	EnvironmentType AzureEnvironmentType `json:"environmentType,omitempty"`
 
-	// Auth configures how the operator authenticates with Azure. Required for ServicePrincipal auth type.
+	// Auth configures how the operator authenticates with Azure. Required for ServicePrincipal auth type. Optional for WorkloadIdentity.
 	// +optional
 	AuthSecretRef *AzureKVAuth `json:"authSecretRef,omitempty"`
 
@@ -88,10 +88,14 @@ type AzureKVProvider struct {
 
 // Configuration used to authenticate with Azure.
 type AzureKVAuth struct {
-	// The Azure clientId of the service principle used for authentication.
+	// The Azure clientId of the service principle or managed identity used for authentication.
 	// +optional
 	ClientID *smmeta.SecretKeySelector `json:"clientId,omitempty"`
 
+	// The Azure tenantId of the managed identity used for authentication.
+	// +optional
+	TenantID *smmeta.SecretKeySelector `json:"tenantId,omitempty"`
+
 	// The Azure ClientSecret of the service principle used for authentication.
 	// +optional
 	ClientSecret *smmeta.SecretKeySelector `json:"clientSecret,omitempty"`
diff --git a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
index 7598507d6..97259d510 100644
--- a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
+++ b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
@@ -319,6 +319,11 @@ func (in *AzureKVAuth) DeepCopyInto(out *AzureKVAuth) {
 		*out = new(metav1.SecretKeySelector)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.TenantID != nil {
+		in, out := &in.TenantID, &out.TenantID
+		*out = new(metav1.SecretKeySelector)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.ClientSecret != nil {
 		in, out := &in.ClientSecret, &out.ClientSecret
 		*out = new(metav1.SecretKeySelector)
diff --git a/config/crds/bases/external-secrets.io_clustersecretstores.yaml b/config/crds/bases/external-secrets.io_clustersecretstores.yaml
index 6c9470514..9ec502620 100644
--- a/config/crds/bases/external-secrets.io_clustersecretstores.yaml
+++ b/config/crds/bases/external-secrets.io_clustersecretstores.yaml
@@ -2139,11 +2139,12 @@ spec:
                     properties:
                       authSecretRef:
                         description: Auth configures how the operator authenticates
-                          with Azure. Required for ServicePrincipal auth type.
+                          with Azure. Required for ServicePrincipal auth type. Optional
+                          for WorkloadIdentity.
                         properties:
                           clientId:
                             description: The Azure clientId of the service principle
-                              used for authentication.
+                              or managed identity used for authentication.
                             properties:
                               key:
                                 description: |-
@@ -2179,6 +2180,25 @@ spec:
                                   to the namespace of the referent.
                                 type: string
                             type: object
+                          tenantId:
+                            description: The Azure tenantId of the managed identity
+                              used for authentication.
+                            properties:
+                              key:
+                                description: |-
+                                  The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be
+                                  defaulted, in others it may be required.
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                type: string
+                              namespace:
+                                description: |-
+                                  Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults
+                                  to the namespace of the referent.
+                                type: string
+                            type: object
                         type: object
                       authType:
                         default: ServicePrincipal
@@ -2236,7 +2256,8 @@ spec:
                         type: object
                       tenantId:
                         description: TenantID configures the Azure Tenant to send
-                          requests to. Required for ServicePrincipal auth type.
+                          requests to. Required for ServicePrincipal auth type. Optional
+                          for WorkloadIdentity.
                         type: string
                       vaultUrl:
                         description: Vault Url from which the secrets to be fetched
diff --git a/config/crds/bases/external-secrets.io_secretstores.yaml b/config/crds/bases/external-secrets.io_secretstores.yaml
index 037f8bcd7..6cc50bc01 100644
--- a/config/crds/bases/external-secrets.io_secretstores.yaml
+++ b/config/crds/bases/external-secrets.io_secretstores.yaml
@@ -2139,11 +2139,12 @@ spec:
                     properties:
                       authSecretRef:
                         description: Auth configures how the operator authenticates
-                          with Azure. Required for ServicePrincipal auth type.
+                          with Azure. Required for ServicePrincipal auth type. Optional
+                          for WorkloadIdentity.
                         properties:
                           clientId:
                             description: The Azure clientId of the service principle
-                              used for authentication.
+                              or managed identity used for authentication.
                             properties:
                               key:
                                 description: |-
@@ -2179,6 +2180,25 @@ spec:
                                   to the namespace of the referent.
                                 type: string
                             type: object
+                          tenantId:
+                            description: The Azure tenantId of the managed identity
+                              used for authentication.
+                            properties:
+                              key:
+                                description: |-
+                                  The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be
+                                  defaulted, in others it may be required.
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                type: string
+                              namespace:
+                                description: |-
+                                  Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults
+                                  to the namespace of the referent.
+                                type: string
+                            type: object
                         type: object
                       authType:
                         default: ServicePrincipal
@@ -2236,7 +2256,8 @@ spec:
                         type: object
                       tenantId:
                         description: TenantID configures the Azure Tenant to send
-                          requests to. Required for ServicePrincipal auth type.
+                          requests to. Required for ServicePrincipal auth type. Optional
+                          for WorkloadIdentity.
                         type: string
                       vaultUrl:
                         description: Vault Url from which the secrets to be fetched
diff --git a/deploy/crds/bundle.yaml b/deploy/crds/bundle.yaml
index 674d82c97..aa9e4df43 100644
--- a/deploy/crds/bundle.yaml
+++ b/deploy/crds/bundle.yaml
@@ -2657,10 +2657,10 @@ spec:
                       description: AzureKV configures this store to sync secrets using Azure Key Vault provider
                       properties:
                         authSecretRef:
-                          description: Auth configures how the operator authenticates with Azure. Required for ServicePrincipal auth type.
+                          description: Auth configures how the operator authenticates with Azure. Required for ServicePrincipal auth type. Optional for WorkloadIdentity.
                           properties:
                             clientId:
-                              description: The Azure clientId of the service principle used for authentication.
+                              description: The Azure clientId of the service principle or managed identity used for authentication.
                               properties:
                                 key:
                                   description: |-
@@ -2693,6 +2693,23 @@ spec:
                                     to the namespace of the referent.
                                   type: string
                               type: object
+                            tenantId:
+                              description: The Azure tenantId of the managed identity used for authentication.
+                              properties:
+                                key:
+                                  description: |-
+                                    The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be
+                                    defaulted, in others it may be required.
+                                  type: string
+                                name:
+                                  description: The name of the Secret resource being referred to.
+                                  type: string
+                                namespace:
+                                  description: |-
+                                    Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults
+                                    to the namespace of the referent.
+                                  type: string
+                              type: object
                           type: object
                         authType:
                           default: ServicePrincipal
@@ -2747,7 +2764,7 @@ spec:
                             - name
                           type: object
                         tenantId:
-                          description: TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
+                          description: TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type. Optional for WorkloadIdentity.
                           type: string
                         vaultUrl:
                           description: Vault Url from which the secrets to be fetched from.
@@ -7981,10 +7998,10 @@ spec:
                       description: AzureKV configures this store to sync secrets using Azure Key Vault provider
                       properties:
                         authSecretRef:
-                          description: Auth configures how the operator authenticates with Azure. Required for ServicePrincipal auth type.
+                          description: Auth configures how the operator authenticates with Azure. Required for ServicePrincipal auth type. Optional for WorkloadIdentity.
                           properties:
                             clientId:
-                              description: The Azure clientId of the service principle used for authentication.
+                              description: The Azure clientId of the service principle or managed identity used for authentication.
                               properties:
                                 key:
                                   description: |-
@@ -8017,6 +8034,23 @@ spec:
                                     to the namespace of the referent.
                                   type: string
                               type: object
+                            tenantId:
+                              description: The Azure tenantId of the managed identity used for authentication.
+                              properties:
+                                key:
+                                  description: |-
+                                    The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be
+                                    defaulted, in others it may be required.
+                                  type: string
+                                name:
+                                  description: The name of the Secret resource being referred to.
+                                  type: string
+                                namespace:
+                                  description: |-
+                                    Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults
+                                    to the namespace of the referent.
+                                  type: string
+                              type: object
                           type: object
                         authType:
                           default: ServicePrincipal
@@ -8071,7 +8105,7 @@ spec:
                             - name
                           type: object
                         tenantId:
-                          description: TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
+                          description: TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type. Optional for WorkloadIdentity.
                           type: string
                         vaultUrl:
                           description: Vault Url from which the secrets to be fetched from.
diff --git a/docs/api/spec.md b/docs/api/spec.md
index 0d9d6a917..e32ce45a7 100644
--- a/docs/api/spec.md
+++ b/docs/api/spec.md
@@ -838,7 +838,21 @@ External Secrets meta/v1.SecretKeySelector
 </td>
 <td>
 <em>(Optional)</em>
-<p>The Azure clientId of the service principle used for authentication.</p>
+<p>The Azure clientId of the service principle or managed identity used for authentication.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>tenantId</code></br>
+<em>
+<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
+External Secrets meta/v1.SecretKeySelector
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>The Azure tenantId of the managed identity used for authentication.</p>
 </td>
 </tr>
 <tr>
@@ -911,7 +925,7 @@ string
 </td>
 <td>
 <em>(Optional)</em>
-<p>TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.</p>
+<p>TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type. Optional for WorkloadIdentity.</p>
 </td>
 </tr>
 <tr>
@@ -941,7 +955,7 @@ AzureKVAuth
 </td>
 <td>
 <em>(Optional)</em>
-<p>Auth configures how the operator authenticates with Azure. Required for ServicePrincipal auth type.</p>
+<p>Auth configures how the operator authenticates with Azure. Required for ServicePrincipal auth type. Optional for WorkloadIdentity.</p>
 </td>
 </tr>
 <tr>
diff --git a/docs/provider/azure-key-vault.md b/docs/provider/azure-key-vault.md
index e8a0ad5b7..c7ba7f9ef 100644
--- a/docs/provider/azure-key-vault.md
+++ b/docs/provider/azure-key-vault.md
@@ -90,6 +90,14 @@ You run the controller without service account (effectively without azure permis
 {% include 'azkv-workload-identity.yaml' %}
 ```
 
+In case you don't have the clientId when deploying the SecretStore, such as when deploying a Helm chart that includes instructions for creating a [Managed Identity](https://github.com/Azure/azure-service-operator/blob/main/v2/samples/managedidentity/v1api20181130/v1api20181130_userassignedidentity.yaml) using [Azure Service Operator](https://azure.github.io/azure-service-operator/) next to the SecretStore definition, you may encounter an interpolation problem. Helm lacks dependency management, which means it can create an issue when the clientId is only known after everything is deployed. Although the Service Account can inject `clientId` and `tenantId` into a pod, it doesn't support secretKeyRef/configMapKeyRef. Therefore, you can deliver the clientId and tenantId directly, bypassing the Service Account.
+
+The following example demonstrates using the secretRef field to directly deliver the `clientId` and `tenantId` to the SecretStore while utilizing Workload Identity authentication.
+
+```yaml
+{% include 'azkv-workload-identity-secretref.yaml' %}
+```
+
 ### Update secret store
 Be sure the `azurekv` provider is listed in the `Kind=SecretStore`
 
diff --git a/docs/snippets/azkv-workload-identity-secretref.yaml b/docs/snippets/azkv-workload-identity-secretref.yaml
new file mode 100644
index 000000000..6f222653a
--- /dev/null
+++ b/docs/snippets/azkv-workload-identity-secretref.yaml
@@ -0,0 +1,28 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  # this service account was created by azwi
+  name: workload-identity-sa
+  annotations: {}
+---
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: azure-store
+spec:
+  provider:
+    azurekv:
+      # tenantId spec option #1
+      tenantId: "5a02a20e-xxxx-xxxx-xxxx-0ad5b634c5d8"
+      authType: WorkloadIdentity
+      vaultUrl: "https://xx-xxxx-xx.vault.azure.net"
+      serviceAccountRef:
+        name: workload-identity-sa
+      authSecretRef:
+        clientId:
+          name: umi-secret
+          key: clientId
+        # tenantId spec option #2
+        tenantId:
+          name: umi-secret
+          key: tenantId
diff --git a/pkg/provider/azure/keyvault/keyvault.go b/pkg/provider/azure/keyvault/keyvault.go
index a9bcf8742..70c529c72 100644
--- a/pkg/provider/azure/keyvault/keyvault.go
+++ b/pkg/provider/azure/keyvault/keyvault.go
@@ -73,8 +73,11 @@ const (
 	errDataFromCert          = "cannot get use dataFrom to get certificate secret"
 	errDataFromKey           = "cannot get use dataFrom to get key secret"
 	errMissingTenant         = "missing tenantID in store config"
+	errMissingClient         = "missing clientID: either serviceAccountRef or service account annotation '%s' is missing"
 	errMissingSecretRef      = "missing secretRef in provider config"
 	errMissingClientIDSecret = "missing accessKeyID/secretAccessKey in store config"
+	errMultipleClientID      = "multiple clientID found. Check secretRef and serviceAccountRef"
+	errMultipleTenantID      = "multiple tenantID found. Check secretRef, 'spec.provider.azurekv.tenantId', and serviceAccountRef"
 	errFindSecret            = "could not find secret %s/%s: %w"
 	errFindDataKey           = "no data for %q in secret '%s/%s'"
 
@@ -793,9 +796,10 @@ func getSecretMapProperties(tags map[string]*string, key, property string) map[s
 func (a *Azure) authorizerForWorkloadIdentity(ctx context.Context, tokenProvider tokenProviderFunc) (autorest.Authorizer, error) {
 	aadEndpoint := AadEndpointForType(a.provider.EnvironmentType)
 	kvResource := kvResourceForProviderConfig(a.provider.EnvironmentType)
-	// if no serviceAccountRef was provided
+	// If no serviceAccountRef was provided
 	// we expect certain env vars to be present.
-	// They are set by the azure workload identity webhook.
+	// They are set by the azure workload identity webhook
+	// by adding the label `azure.workload.identity/use: "true"` to the external-secrets pod
 	if a.provider.ServiceAccountRef == nil {
 		clientID := os.Getenv("AZURE_CLIENT_ID")
 		tenantID := os.Getenv("AZURE_TENANT_ID")
@@ -825,13 +829,76 @@ func (a *Azure) authorizerForWorkloadIdentity(ctx context.Context, tokenProvider
 	if err != nil {
 		return nil, err
 	}
-	clientID, ok := sa.ObjectMeta.Annotations[AnnotationClientID]
-	if !ok {
-		return nil, fmt.Errorf(errMissingSAAnnotation, AnnotationClientID)
+	// Extract clientID
+	var clientID string
+	// First check if AuthSecretRef is set and clientID can be fetched from there
+	if a.provider.AuthSecretRef != nil {
+		if a.provider.AuthSecretRef.ClientID == nil {
+			return nil, fmt.Errorf(errMissingClientIDSecret)
+		}
+		clientID, err = resolvers.SecretKeyRef(
+			ctx,
+			a.crClient,
+			a.store.GetKind(),
+			a.namespace, a.provider.AuthSecretRef.ClientID)
+		if err != nil {
+			return nil, err
+		}
 	}
-	tenantID, ok := sa.ObjectMeta.Annotations[AnnotationTenantID]
-	if !ok {
-		return nil, fmt.Errorf(errMissingSAAnnotation, AnnotationTenantID)
+	// If AuthSecretRef is not set, use default (Service Account) implementation
+	// Try to get clientID from Annotations
+	if len(sa.ObjectMeta.Annotations) > 0 {
+		if val, found := sa.ObjectMeta.Annotations[AnnotationClientID]; found {
+			// If clientID is defined in both Annotations and AuthSecretRef, return an error
+			if clientID != "" {
+				return nil, fmt.Errorf(errMultipleClientID)
+			}
+			clientID = val
+		}
+	}
+	// Return an error if clientID is still empty
+	if clientID == "" {
+		return nil, fmt.Errorf(errMissingClient, AnnotationClientID)
+	}
+	// Extract tenantID
+	var tenantID string
+	// First check if AuthSecretRef is set and tenantID can be fetched from there
+	if a.provider.AuthSecretRef != nil {
+		// We may want to set tenantID explicitly in the `spec.provider.azurekv` section of the SecretStore object
+		// So that is okay if it is not there
+		if a.provider.AuthSecretRef.TenantID != nil {
+			tenantID, err = resolvers.SecretKeyRef(
+				ctx,
+				a.crClient,
+				a.store.GetKind(),
+				a.namespace, a.provider.AuthSecretRef.TenantID)
+			if err != nil {
+				return nil, err
+			}
+		}
+	}
+	// Check if spec.provider.azurekv.tenantId is set
+	if tenantID == "" && a.provider.TenantID != nil {
+		tenantID = *a.provider.TenantID
+	}
+	// Try to get tenantID from Annotations first. Default implementation.
+	if len(sa.ObjectMeta.Annotations) > 0 {
+		if val, found := sa.ObjectMeta.Annotations[AnnotationTenantID]; found {
+			// If tenantID is defined in both Annotations and AuthSecretRef, return an error
+			if tenantID != "" {
+				return nil, fmt.Errorf(errMultipleTenantID)
+			}
+			tenantID = val
+		}
+	}
+	// Fallback: use the AZURE_TENANT_ID env var which is set by the azure workload identity webhook
+	// https://azure.github.io/azure-workload-identity/docs/topics/service-account-labels-and-annotations.html#service-account
+	if tenantID == "" {
+		tenantID = os.Getenv("AZURE_TENANT_ID")
+	}
+	// Return an error if tenantID is still empty
+	if tenantID == "" {
+		return nil, errors.New(errMissingTenant)
 	}
 	audiences := []string{AzureDefaultAudience}
 	if len(a.provider.ServiceAccountRef.Audiences) > 0 {
diff --git a/pkg/provider/pulumi/pulumi.go b/pkg/provider/pulumi/pulumi.go
index 756caa60b..f94638150 100644
--- a/pkg/provider/pulumi/pulumi.go
+++ b/pkg/provider/pulumi/pulumi.go
@@ -79,7 +79,7 @@ func (c *client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretD
 		return nil, fmt.Errorf(errGettingSecrets, ref.Key, err)
 	}
 
-	kv := make(map[string]json.RawMessage)
+	kv := make(map[string]interface{})
 	err = json.Unmarshal(data, &kv)
 	if err != nil {
 		return nil, fmt.Errorf(errUnmarshalSecret, err)


Test patch:
diff --git a/pkg/provider/azure/keyvault/keyvault_auth_test.go b/pkg/provider/azure/keyvault/keyvault_auth_test.go
index 76a13f501..3952574de 100644
--- a/pkg/provider/azure/keyvault/keyvault_auth_test.go
+++ b/pkg/provider/azure/keyvault/keyvault_auth_test.go
@@ -76,6 +76,7 @@ func TestGetAuthorizorForWorkloadIdentity(t *testing.T) {
 		saToken       = "FAKETOKEN"
 		saName        = "az-wi"
 		namespace     = "default"
+		secretName    = "mi-spec"
 	)
 
 	// create a temporary file to imitate
@@ -89,6 +90,7 @@ func TestGetAuthorizorForWorkloadIdentity(t *testing.T) {
 	tokenFile := tf.Name()
 
 	authType := esv1beta1.AzureWorkloadIdentity
+
 	defaultProvider := &esv1beta1.AzureKVProvider{
 		VaultURL: &vaultURL,
 		AuthType: &authType,
@@ -136,7 +138,7 @@ func TestGetAuthorizorForWorkloadIdentity(t *testing.T) {
 			},
 		},
 		{
-			name:     "missing sa annotations",
+			name:     "missing sa annotations, tenantID, and clientId/tenantId AuthSecretRef",
 			provider: defaultProvider,
 			k8sObjects: []client.Object{
 				&corev1.ServiceAccount{
@@ -147,10 +149,72 @@ func TestGetAuthorizorForWorkloadIdentity(t *testing.T) {
 					},
 				},
 			},
-			expErr: "missing service account annotation: azure.workload.identity/client-id",
+			expErr: "missing clientID: either serviceAccountRef or service account annotation 'azure.workload.identity/client-id' is missing",
+		},
+		{
+			name: "duplicated clientId",
+			provider: &esv1beta1.AzureKVProvider{
+				VaultURL: &vaultURL,
+				AuthType: &authType,
+				TenantID: pointer.To(tenantID),
+				ServiceAccountRef: &v1.ServiceAccountSelector{
+					Name: saName,
+				},
+				AuthSecretRef: &esv1beta1.AzureKVAuth{
+					ClientID: &v1.SecretKeySelector{Name: secretName, Namespace: pointer.To(namespace), Key: clientID},
+					TenantID: &v1.SecretKeySelector{Name: secretName, Namespace: pointer.To(namespace), Key: tenantID},
+				},
+			},
+			k8sObjects: []client.Object{
+				&corev1.ServiceAccount{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      saName,
+						Namespace: namespace,
+						Annotations: map[string]string{
+							AnnotationClientID: clientID,
+							AnnotationTenantID: tenantID,
+						},
+					},
+				},
+				&corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      secretName,
+						Namespace: namespace,
+					},
+					Data: map[string][]byte{
+						clientID: []byte("clientid"),
+						tenantID: []byte("tenantid"),
+					},
+				},
+			},
+			expErr: "multiple clientID found. Check secretRef and serviceAccountRef",
+		},
+		{
+			name: "duplicated tenantId",
+			provider: &esv1beta1.AzureKVProvider{
+				VaultURL: &vaultURL,
+				AuthType: &authType,
+				TenantID: pointer.To(tenantID),
+				ServiceAccountRef: &v1.ServiceAccountSelector{
+					Name: saName,
+				},
+			},
+			k8sObjects: []client.Object{
+				&corev1.ServiceAccount{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      saName,
+						Namespace: namespace,
+						Annotations: map[string]string{
+							AnnotationClientID: clientID,
+							AnnotationTenantID: tenantID,
+						},
+					},
+				},
+			},
+			expErr: "multiple tenantID found. Check secretRef, 'spec.provider.azurekv.tenantId', and serviceAccountRef",
 		},
 		{
-			name:     "successful case",
+			name:     "successful case #1: ClientID, TenantID from ServiceAccountRef",
 			provider: defaultProvider,
 			k8sObjects: []client.Object{
 				&corev1.ServiceAccount{
@@ -165,6 +229,71 @@ func TestGetAuthorizorForWorkloadIdentity(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "successful case #2: ClientID, TenantID from AuthSecretRef",
+			provider: &esv1beta1.AzureKVProvider{
+				VaultURL: &vaultURL,
+				AuthType: &authType,
+				ServiceAccountRef: &v1.ServiceAccountSelector{
+					Name: saName,
+				},
+				AuthSecretRef: &esv1beta1.AzureKVAuth{
+					ClientID: &v1.SecretKeySelector{Name: secretName, Namespace: pointer.To(namespace), Key: clientID},
+					TenantID: &v1.SecretKeySelector{Name: secretName, Namespace: pointer.To(namespace), Key: tenantID},
+				},
+			},
+			k8sObjects: []client.Object{
+				&corev1.ServiceAccount{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:        saName,
+						Namespace:   namespace,
+						Annotations: map[string]string{},
+					},
+				},
+				&corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      secretName,
+						Namespace: namespace,
+					},
+					Data: map[string][]byte{
+						clientID: []byte("clientid"),
+						tenantID: []byte("tenantid"),
+					},
+				},
+			},
+		},
+		{
+			name: "successful case #3: ClientID from AuthSecretRef, TenantID from provider",
+			provider: &esv1beta1.AzureKVProvider{
+				VaultURL: &vaultURL,
+				AuthType: &authType,
+				TenantID: pointer.To(tenantID),
+				ServiceAccountRef: &v1.ServiceAccountSelector{
+					Name: saName,
+				},
+				AuthSecretRef: &esv1beta1.AzureKVAuth{
+					ClientID: &v1.SecretKeySelector{Name: secretName, Namespace: pointer.To(namespace), Key: clientID},
+				},
+			},
+			k8sObjects: []client.Object{
+				&corev1.ServiceAccount{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:        saName,
+						Namespace:   namespace,
+						Annotations: map[string]string{},
+					},
+				},
+				&corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      secretName,
+						Namespace: namespace,
+					},
+					Data: map[string][]byte{
+						clientID: []byte("clientid"),
+					},
+				},
+			},
+		},
 	} {
 		t.Run(row.name, func(t *testing.T) {
 			store := esv1beta1.SecretStore{
diff --git a/pkg/provider/pulumi/pulumi_test.go b/pkg/provider/pulumi/pulumi_test.go
index 6d11614c1..e1cbfd9de 100644
--- a/pkg/provider/pulumi/pulumi_test.go
+++ b/pkg/provider/pulumi/pulumi_test.go
@@ -18,6 +18,7 @@ import (
 	"encoding/json"
 	"net/http"
 	"net/http/httptest"
+	"reflect"
 	"testing"
 
 	esc2 "github.com/pulumi/esc"
@@ -80,3 +81,70 @@ func TestGetSecret(t *testing.T) {
 		})
 	}
 }
+
+func TestGetSecretMap(t *testing.T) {
+	tests := []struct {
+		name  string
+		ref   esv1beta1.ExternalSecretDataRemoteRef
+		input string
+
+		want    map[string][]byte
+		wantErr bool
+	}{
+		{
+			name: "successful case (basic types)",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key: "mysec",
+			},
+			input: `{"foo": "bar", "foobar": 42, "bar": true}`,
+			want: map[string][]byte{
+				"foo":    []byte("bar"),
+				"foobar": []byte("42"),
+				"bar":    []byte(`true`),
+			},
+			wantErr: false,
+		},
+		{
+			name: "successful case (nested)",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key: "mysec",
+			},
+			input: `{"foo": {"foobar": 42}, "bar": {"foo": "bar"}}`,
+			want: map[string][]byte{
+				"foo": []byte(`{"foobar":42}`),
+				"bar": []byte(`{"foo":"bar"}`),
+			},
+			wantErr: false,
+		},
+		{
+			name: "successful case (basic + nested)",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key: "mysec",
+			},
+			input: `{"foo": "bar", "bar": {"foo": {"bar": false}}}`,
+			want: map[string][]byte{
+				"foo": []byte(`bar`),
+				"bar": []byte(`{"foo":{"bar":false}}`),
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			p := newTestClient(t, http.MethodGet, "/api/preview/environments/foo/bar/open/session", func(w http.ResponseWriter, r *http.Request) {
+				esc2Input, err1 := esc2.FromJSON(tt.input, false)
+				require.NoError(t, err1)
+				err2 := json.NewEncoder(w).Encode(esc2Input)
+				require.NoError(t, err2)
+			})
+			got, err := p.GetSecretMap(context.Background(), tt.ref)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("ProviderPulumi.GetSecretMap() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("ProviderPulumi.GetSecretMap() get = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}