NIFI-9266 Added Azure Key Vault Secret SPP

Signed-off-by: Pierre Villard <pierre.villard.fr@gmail.com>

This closes #5435.
diff --git a/nifi-commons/nifi-sensitive-property-provider/pom.xml b/nifi-commons/nifi-sensitive-property-provider/pom.xml
index 5ef4a43..0fb83e35 100644
--- a/nifi-commons/nifi-sensitive-property-provider/pom.xml
+++ b/nifi-commons/nifi-sensitive-property-provider/pom.xml
@@ -73,8 +73,31 @@
         </dependency>
         <dependency>
             <groupId>com.azure</groupId>
+            <artifactId>azure-security-keyvault-secrets</artifactId>
+            <version>4.3.3</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.azure</groupId>
+                    <artifactId>azure-core-http-netty</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.fasterxml.woodstox</groupId>
+                    <artifactId>woodstox-core</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.codehaus.woodstox</groupId>
+                    <artifactId>stax2-api</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>io.netty</groupId>
+                    <artifactId>netty-tcnative-boringssl-static</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.azure</groupId>
             <artifactId>azure-security-keyvault-keys</artifactId>
-            <version>4.3.1</version>
+            <version>4.3.3</version>
             <exclusions>
                 <exclusion>
                     <groupId>com.azure</groupId>
@@ -144,6 +167,12 @@
             <version>2.10.0</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-inline</artifactId>
+            <version>${mockito.version}</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
     <build>
         <!-- Required to run Groovy tests without any Java tests -->
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/AzureKeyVaultSecretSensitivePropertyProvider.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/AzureKeyVaultSecretSensitivePropertyProvider.java
new file mode 100644
index 0000000..ac9e1da
--- /dev/null
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/AzureKeyVaultSecretSensitivePropertyProvider.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *     http://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 org.apache.nifi.properties;
+
+import com.azure.core.exception.ResourceNotFoundException;
+import com.azure.security.keyvault.secrets.SecretClient;
+import com.azure.security.keyvault.secrets.models.KeyVaultSecret;
+
+import java.util.Objects;
+
+/**
+ * Microsoft Azure Key Vault Secret implementation of Sensitive Property Provider for externalized storage of properties
+ */
+public class AzureKeyVaultSecretSensitivePropertyProvider implements SensitivePropertyProvider {
+    private static final String FORWARD_SLASH = "/";
+
+    private static final String PERIOD = "\\.";
+
+    private static final String DASH = "-";
+
+    private SecretClient secretClient;
+
+    AzureKeyVaultSecretSensitivePropertyProvider(final SecretClient secretClient) {
+        this.secretClient = secretClient;
+    }
+
+    /**
+     * Get Provider name using Protection Scheme
+     *
+     * @return Provider name
+     */
+    @Override
+    public String getName() {
+        return PropertyProtectionScheme.AZURE_KEYVAULT_SECRET.getName();
+    }
+
+    /**
+     * Get Identifier key using Protection Scheme
+     *
+     * @return Identifier key
+     */
+    @Override
+    public String getIdentifierKey() {
+        return PropertyProtectionScheme.AZURE_KEYVAULT_SECRET.getIdentifier();
+    }
+
+    /**
+     * Is Provider supported returns status based on configuration of Secret Client
+     *
+     * @return Supported status
+     */
+    @Override
+    public boolean isSupported() {
+        return secretClient != null;
+    }
+
+    /**
+     * Protect value stores a Secret in Azure Key Vault using the Property Context Key as the Secret Name
+     *
+     * @param unprotectedValue Value to be stored
+     * @param context Property Context containing Context Key used to store Secret
+     * @return Key Vault Secret Identifier
+     * @throws SensitivePropertyProtectionException Thrown when storing Secret failed
+     */
+    @Override
+    public String protect(final String unprotectedValue, final ProtectedPropertyContext context) throws SensitivePropertyProtectionException {
+        Objects.requireNonNull(unprotectedValue, "Value required");
+        final String secretName = getSecretName(context);
+        try {
+            final KeyVaultSecret keyVaultSecret = secretClient.setSecret(secretName, unprotectedValue);
+            return keyVaultSecret.getId();
+        } catch (final RuntimeException e) {
+            throw new SensitivePropertyProtectionException(String.format("Set Secret [%s] failed", secretName), e);
+        }
+    }
+
+    /**
+     * Unprotect value retrieves a Secret from Azure Key Vault using Property Context Key
+     *
+     * @param protectedValue Key Vault Secret Identifier is not used
+     * @param context Property Context containing Context Key used to retrieve Secret
+     * @return Secret Value
+     * @throws SensitivePropertyProtectionException Thrown when Secret not found or retrieval failed
+     */
+    @Override
+    public String unprotect(final String protectedValue, final ProtectedPropertyContext context) throws SensitivePropertyProtectionException {
+        final String secretName = getSecretName(context);
+        try {
+            final KeyVaultSecret keyVaultSecret = secretClient.getSecret(secretName);
+            return keyVaultSecret.getValue();
+        } catch (final ResourceNotFoundException e) {
+            throw new SensitivePropertyProtectionException(String.format("Secret [%s] not found", secretName), e);
+        } catch (final RuntimeException e) {
+            throw new SensitivePropertyProtectionException(String.format("Secret [%s] request failed", secretName), e);
+        }
+    }
+
+    /**
+     * Clean up not implemented
+     */
+    @Override
+    public void cleanUp() {
+
+    }
+
+    private String getSecretName(final ProtectedPropertyContext context) {
+        final String contextKey = Objects.requireNonNull(context, "Context required").getContextKey();
+        // Replace forward slash and period with dash since Azure Key Vault Secret Names do not support certain characters
+        return contextKey.replaceAll(FORWARD_SLASH, DASH).replaceAll(PERIOD, DASH);
+    }
+}
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/PropertyProtectionScheme.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/PropertyProtectionScheme.java
index 157fc81..7d71004 100644
--- a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/PropertyProtectionScheme.java
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/PropertyProtectionScheme.java
@@ -27,6 +27,7 @@
     AES_GCM("aes/gcm/(128|192|256)", "aes/gcm/%s", "AES Sensitive Property Provider", true),
     AWS_KMS("aws/kms", "aws/kms", "AWS KMS Sensitive Property Provider", false),
     AZURE_KEYVAULT_KEY("azure/keyvault/key", "azure/keyvault/key", "Azure Key Vault Key Sensitive Property Provider", false),
+    AZURE_KEYVAULT_SECRET("azure/keyvault/secret", "azure/keyvault/secret", "Azure Key Vault Secret Sensitive Property Provider", false),
     GCP_KMS("gcp/kms", "gcp/kms", "GCP Cloud KMS Sensitive Property Provider", false),
     HASHICORP_VAULT_KV("hashicorp/vault/kv/[a-zA-Z0-9_-]+", "hashicorp/vault/kv/%s", "HashiCorp Vault Key/Value Engine Sensitive Property Provider", false),
     HASHICORP_VAULT_TRANSIT("hashicorp/vault/transit/[a-zA-Z0-9_-]+", "hashicorp/vault/transit/%s", "HashiCorp Vault Transit Engine Sensitive Property Provider", false);
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/StandardSensitivePropertyProviderFactory.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/StandardSensitivePropertyProviderFactory.java
index 1564e3f..1d7e12f 100644
--- a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/StandardSensitivePropertyProviderFactory.java
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/StandardSensitivePropertyProviderFactory.java
@@ -17,10 +17,12 @@
 package org.apache.nifi.properties;
 
 import com.azure.security.keyvault.keys.cryptography.CryptographyClient;
+import com.azure.security.keyvault.secrets.SecretClient;
 import com.google.cloud.kms.v1.KeyManagementServiceClient;
 import org.apache.nifi.properties.BootstrapProperties.BootstrapPropertyKey;
 import org.apache.nifi.properties.configuration.AwsKmsClientProvider;
 import org.apache.nifi.properties.configuration.AzureCryptographyClientProvider;
+import org.apache.nifi.properties.configuration.AzureSecretClientProvider;
 import org.apache.nifi.properties.configuration.ClientProvider;
 import org.apache.nifi.properties.configuration.GoogleKeyManagementServiceClientProvider;
 import org.apache.nifi.util.NiFiBootstrapUtils;
@@ -142,6 +144,13 @@
                     final Optional<CryptographyClient> cryptographyClient = clientProvider.getClient(clientProperties);
                     return new AzureKeyVaultKeySensitivePropertyProvider(cryptographyClient.orElse(null), clientProperties);
                 });
+            case AZURE_KEYVAULT_SECRET:
+                return providerMap.computeIfAbsent(protectionScheme, s -> {
+                    final AzureSecretClientProvider clientProvider = new AzureSecretClientProvider();
+                    final Properties clientProperties = getClientProperties(clientProvider);
+                    final Optional<SecretClient> secretClient = clientProvider.getClient(clientProperties);
+                    return new AzureKeyVaultSecretSensitivePropertyProvider(secretClient.orElse(null));
+                });
             case GCP_KMS:
                 return providerMap.computeIfAbsent(protectionScheme, s -> {
                     final GoogleKeyManagementServiceClientProvider clientProvider = new GoogleKeyManagementServiceClientProvider();
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/configuration/AzureClientProvider.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/configuration/AzureClientProvider.java
new file mode 100644
index 0000000..61561e7
--- /dev/null
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/configuration/AzureClientProvider.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *     http://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 org.apache.nifi.properties.configuration;
+
+import com.azure.core.credential.TokenCredential;
+import com.azure.identity.DefaultAzureCredentialBuilder;
+import org.apache.nifi.properties.BootstrapProperties;
+import org.apache.nifi.util.StringUtils;
+
+import java.util.Optional;
+import java.util.Properties;
+import java.util.Set;
+
+/**
+ * Abstract Microsoft Azure Client Provider
+ */
+public abstract class AzureClientProvider<T> extends BootstrapPropertiesClientProvider<T> {
+    public AzureClientProvider() {
+        super(BootstrapProperties.BootstrapPropertyKey.AZURE_KEYVAULT_SENSITIVE_PROPERTY_PROVIDER_CONF);
+    }
+
+    /**
+     * Get Client using Client Properties
+     *
+     * @param clientProperties Client Properties can be null
+     * @return Configured Client or empty when Client Properties object is null
+     */
+    @Override
+    public Optional<T> getClient(final Properties clientProperties) {
+        return isMissingProperties(clientProperties) ? Optional.empty() : Optional.of(getConfiguredClient(clientProperties));
+    }
+
+    /**
+     * Get Default Azure Token Credential using Default Credentials Builder for environment variables and system properties
+     *
+     * @return Token Credential
+     */
+    protected TokenCredential getDefaultTokenCredential() {
+        return new DefaultAzureCredentialBuilder().build();
+    }
+
+    /**
+     * Get Property Names required for initializing client in order to perform initial validation
+     *
+     * @return Set of required client property names
+     */
+    protected abstract Set<String> getRequiredPropertyNames();
+
+    private boolean isMissingProperties(final Properties clientProperties) {
+        return clientProperties == null || getRequiredPropertyNames().stream().anyMatch(propertyName ->
+            StringUtils.isBlank(clientProperties.getProperty(propertyName))
+        );
+    }
+}
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/configuration/AzureCryptographyClientProvider.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/configuration/AzureCryptographyClientProvider.java
index 643d02e..adf974e 100644
--- a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/configuration/AzureCryptographyClientProvider.java
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/configuration/AzureCryptographyClientProvider.java
@@ -16,23 +16,22 @@
  */
 package org.apache.nifi.properties.configuration;
 
-import com.azure.identity.DefaultAzureCredentialBuilder;
 import com.azure.security.keyvault.keys.cryptography.CryptographyClient;
 import com.azure.security.keyvault.keys.cryptography.CryptographyClientBuilder;
-import org.apache.nifi.properties.BootstrapProperties;
 import org.apache.nifi.properties.SensitivePropertyProtectionException;
 
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.Properties;
+import java.util.Set;
 
 /**
  * Microsoft Azure Cryptography Client Provider
  */
-public class AzureCryptographyClientProvider extends BootstrapPropertiesClientProvider<CryptographyClient> {
-    private static final String KEY_ID_PROPERTY = "azure.keyvault.key.id";
+public class AzureCryptographyClientProvider extends AzureClientProvider<CryptographyClient> {
+    protected static final String KEY_ID_PROPERTY = "azure.keyvault.key.id";
 
-    public AzureCryptographyClientProvider() {
-        super(BootstrapProperties.BootstrapPropertyKey.AZURE_KEYVAULT_SENSITIVE_PROPERTY_PROVIDER_CONF);
-    }
+    private static final Set<String> REQUIRED_PROPERTY_NAMES = new HashSet<>(Collections.singletonList(KEY_ID_PROPERTY));
 
     /**
      * Get Configured Client using Default Azure Credentials Builder and configured Key Identifier
@@ -47,11 +46,21 @@
 
         try {
             return new CryptographyClientBuilder()
-                    .credential(new DefaultAzureCredentialBuilder().build())
+                    .credential(getDefaultTokenCredential())
                     .keyIdentifier(keyIdentifier)
                     .buildClient();
         } catch (final RuntimeException e) {
             throw new SensitivePropertyProtectionException("Azure Cryptography Builder Client Failed using Default Credentials", e);
         }
     }
+
+    /**
+     * Get required property names for Azure Cryptography Client
+     *
+     * @return Required client property names
+     */
+    @Override
+    protected Set<String> getRequiredPropertyNames() {
+        return REQUIRED_PROPERTY_NAMES;
+    }
 }
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/configuration/AzureSecretClientProvider.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/configuration/AzureSecretClientProvider.java
new file mode 100644
index 0000000..65432fa
--- /dev/null
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/configuration/AzureSecretClientProvider.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *     http://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 org.apache.nifi.properties.configuration;
+
+import com.azure.core.credential.TokenCredential;
+import com.azure.identity.DefaultAzureCredentialBuilder;
+import com.azure.security.keyvault.secrets.SecretClient;
+import com.azure.security.keyvault.secrets.SecretClientBuilder;
+import org.apache.nifi.properties.SensitivePropertyProtectionException;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+
+/**
+ * Microsoft Azure Secret Client Provider
+ */
+public class AzureSecretClientProvider extends AzureClientProvider<SecretClient> {
+    protected static final String URI_PROPERTY = "azure.keyvault.uri";
+
+    private static final Set<String> REQUIRED_PROPERTY_NAMES = new HashSet<>(Collections.singletonList(URI_PROPERTY));
+
+    /**
+     * Get Secret Client using Default Azure Credentials Builder and default configuration from environment variables
+     *
+     * @param clientProperties Client Properties
+     * @return Secret Client
+     */
+    @Override
+    protected SecretClient getConfiguredClient(final Properties clientProperties) {
+        final String uri = clientProperties.getProperty(URI_PROPERTY);
+        logger.debug("Azure Secret Client with URI [{}]", uri);
+
+        try {
+            final TokenCredential credential = new DefaultAzureCredentialBuilder().build();
+            return new SecretClientBuilder()
+                    .credential(credential)
+                    .vaultUrl(uri)
+                    .buildClient();
+        } catch (final RuntimeException e) {
+            throw new SensitivePropertyProtectionException("Azure Secret Builder Client Failed using Default Credentials", e);
+        }
+    }
+
+    /**
+     * Get required property names for Azure Secret Client
+     *
+     * @return Required client property names
+     */
+    @Override
+    protected Set<String> getRequiredPropertyNames() {
+        return REQUIRED_PROPERTY_NAMES;
+    }
+}
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/AzureKeyVaultSecretSensitivePropertyProviderTest.java b/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/AzureKeyVaultSecretSensitivePropertyProviderTest.java
new file mode 100644
index 0000000..3ec2d4a
--- /dev/null
+++ b/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/AzureKeyVaultSecretSensitivePropertyProviderTest.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *     http://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 org.apache.nifi.properties;
+
+import com.azure.core.exception.AzureException;
+import com.azure.core.exception.ResourceNotFoundException;
+import com.azure.core.http.HttpResponse;
+import com.azure.security.keyvault.secrets.SecretClient;
+import com.azure.security.keyvault.secrets.models.KeyVaultSecret;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class AzureKeyVaultSecretSensitivePropertyProviderTest {
+    private static final String PROPERTY_NAME = "property.name";
+
+    private static final String SECRET_NAME = "default-property-name";
+
+    private static final String PROPERTY = String.class.getName();
+
+    private static final String PROTECTED_PROPERTY = KeyVaultSecret.class.getSimpleName();
+
+    private static final String ID = KeyVaultSecret.class.getName();
+
+    @Mock
+    private SecretClient secretClient;
+
+    @Mock
+    private KeyVaultSecret keyVaultSecret;
+
+    @Mock
+    private HttpResponse httpResponse;
+
+    private AzureKeyVaultSecretSensitivePropertyProvider provider;
+
+    @BeforeEach
+    public void setProvider() {
+        provider = new AzureKeyVaultSecretSensitivePropertyProvider(secretClient);
+    }
+
+    @Test
+    public void testClientNull() {
+        final AzureKeyVaultSecretSensitivePropertyProvider provider = new AzureKeyVaultSecretSensitivePropertyProvider(null);
+        assertNotNull(provider);
+        assertFalse(provider.isSupported());
+    }
+
+    @Test
+    public void testProtect() {
+        when(secretClient.setSecret(eq(SECRET_NAME), eq(PROPERTY))).thenReturn(keyVaultSecret);
+        when(keyVaultSecret.getId()).thenReturn(ID);
+
+        final ProtectedPropertyContext context = ProtectedPropertyContext.defaultContext(PROPERTY_NAME);
+        final String protectedProperty = provider.protect(PROPERTY, context);
+        assertEquals(ID, protectedProperty);
+    }
+
+    @Test
+    public void testProtectException() {
+        final ProtectedPropertyContext context = ProtectedPropertyContext.defaultContext(PROPERTY_NAME);
+        final String secretName = context.getContextKey();
+        when(secretClient.setSecret(eq(secretName), eq(PROPERTY))).thenThrow(new AzureException());
+
+        assertThrows(SensitivePropertyProtectionException.class, () -> provider.protect(PROPERTY, context));
+    }
+
+    @Test
+    public void testUnprotect() {
+        when(secretClient.getSecret(eq(SECRET_NAME))).thenReturn(keyVaultSecret);
+        when(keyVaultSecret.getValue()).thenReturn(PROPERTY);
+
+        final ProtectedPropertyContext context = ProtectedPropertyContext.defaultContext(PROPERTY_NAME);
+        final String property = provider.unprotect(PROTECTED_PROPERTY, context);
+        assertEquals(PROPERTY, property);
+    }
+
+    @Test
+    public void testUnprotectResourceNotFoundException() {
+        when(secretClient.getSecret(eq(SECRET_NAME))).thenThrow(new ResourceNotFoundException(SECRET_NAME, httpResponse));
+
+        final ProtectedPropertyContext context = ProtectedPropertyContext.defaultContext(PROPERTY_NAME);
+        assertThrows(SensitivePropertyProtectionException.class, () -> provider.unprotect(PROTECTED_PROPERTY, context));
+    }
+}
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/configuration/AzureCryptographyClientProviderTest.java b/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/configuration/AzureCryptographyClientProviderTest.java
new file mode 100644
index 0000000..e898ea4
--- /dev/null
+++ b/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/configuration/AzureCryptographyClientProviderTest.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *     http://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 org.apache.nifi.properties.configuration;
+
+import com.azure.security.keyvault.keys.cryptography.CryptographyClient;
+import org.apache.nifi.util.StringUtils;
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+public class AzureCryptographyClientProviderTest {
+
+    @Test
+    public void testClientPropertiesNull() {
+        final AzureCryptographyClientProvider provider = new AzureCryptographyClientProvider();
+        final Optional<CryptographyClient> optionalClient = provider.getClient(null);
+        assertFalse(optionalClient.isPresent());
+    }
+
+    @Test
+    public void testClientPropertiesKeyIdBlank() {
+        final AzureCryptographyClientProvider provider = new AzureCryptographyClientProvider();
+        final Properties clientProperties = new Properties();
+        clientProperties.setProperty(AzureCryptographyClientProvider.KEY_ID_PROPERTY, StringUtils.EMPTY);
+        final Optional<CryptographyClient> optionalClient = provider.getClient(clientProperties);
+        assertFalse(optionalClient.isPresent());
+    }
+}
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/configuration/AzureSecretClientProviderTest.java b/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/configuration/AzureSecretClientProviderTest.java
new file mode 100644
index 0000000..6f312df
--- /dev/null
+++ b/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/configuration/AzureSecretClientProviderTest.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *     http://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 org.apache.nifi.properties.configuration;
+
+import com.azure.security.keyvault.secrets.SecretClient;
+import org.apache.nifi.util.StringUtils;
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+public class AzureSecretClientProviderTest {
+
+    @Test
+    public void testClientPropertiesNull() {
+        final AzureSecretClientProvider provider = new AzureSecretClientProvider();
+        final Optional<SecretClient> optionalClient = provider.getClient(null);
+        assertFalse(optionalClient.isPresent());
+    }
+
+    @Test
+    public void testClientPropertiesUriBlank() {
+        final AzureSecretClientProvider provider = new AzureSecretClientProvider();
+        final Properties clientProperties = new Properties();
+        clientProperties.setProperty(AzureSecretClientProvider.URI_PROPERTY, StringUtils.EMPTY);
+        final Optional<SecretClient> optionalClient = provider.getClient(clientProperties);
+        assertFalse(optionalClient.isPresent());
+    }
+}
diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index 4f64041..8d2c58e 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -1858,8 +1858,21 @@
 |`aws.secret.access.key`|The secret access key used to access AWS KMS.|_none_
 |===
 
-=== Azure Key Vault Key provider
-This protection scheme uses keys managed by https://docs.microsoft.com/en-us/azure/key-vault/keys/about-keys[Azure Key Vault Keys] for encryption and decryption. Azure Key Vault configuration properties can be stored in the `bootstrap-azure.conf` file, as referenced in the `bootstrap.conf` of NiFi or NiFi Registry. The provider will use the Azure default credentials provider chain as described in the https://docs.microsoft.com/en-us/java/api/overview/azure/security-keyvault-keys-readme?view=azure-java-stable[Azure Key Vault Key client library for Java] documentation.
+[[azure-key-vault-key-provider]]
+=== Azure Key Vault Key Provider
+This protection scheme uses keys managed by
+https://docs.microsoft.com/en-us/azure/key-vault/keys/about-keys[Azure Key Vault Keys] for encryption and decryption.
+
+Azure Key Vault configuration properties can be stored in the `bootstrap-azure.conf` file, as referenced in the
+`bootstrap.conf` of NiFi or NiFi Registry.
+The provider will use the
+https://docs.microsoft.com/en-us/java/api/com.azure.identity.defaultazurecredential[DefaultAzureCredential]
+for authentication.
+The https://docs.microsoft.com/en-us/java/api/overview/azure/identity-readme#key-concepts[Azure Identity] client library
+describes the process for credentials resolution, which leverages environment variables, system properties, and falls
+back to
+https://docs.microsoft.com/en-us/java/api/overview/azure/identity-readme#managed-identity-support[Managed Identity]
+authentication.
 
 ==== Required properties
 [options="header,footer"]
@@ -1869,6 +1882,39 @@
 |`azure.keyvault.encryption.algorithm`|The encryption algorithm that the Azure Key Vault client uses for encryption and decryption.|_none_
 |===
 
+[[azure-key-vault-secret-provider]]
+=== Azure Key Vault Secret Provider
+This protection scheme uses secrets managed by
+https://docs.microsoft.com/en-us/azure/key-vault/secrets/about-secrets[Azure Key Vault Secrets] for storing and
+retrieving protected properties.
+
+Azure Key Vault configuration properties can be stored in the `bootstrap-azure.conf` file, as referenced in the
+`bootstrap.conf` of NiFi or NiFi Registry.
+The provider will use the
+https://docs.microsoft.com/en-us/java/api/com.azure.identity.defaultazurecredential[DefaultAzureCredential]
+for authentication.
+The https://docs.microsoft.com/en-us/java/api/overview/azure/identity-readme#key-concepts[Azure Identity] client library
+describes the process for credentials resolution, which leverages environment variables, system properties, and falls
+back to
+https://docs.microsoft.com/en-us/java/api/overview/azure/identity-readme#managed-identity-support[Managed Identity]
+authentication.
+
+Names of secrets stored in Azure Key Vault support alphanumeric and dash characters, but do not support characters such as `/` or `.`.
+For this reason, NiFi replaces these characters with `-` when storing and retrieving secrets. The following table provides an example property name mapping:
+
+[options="header,footer"]
+|===
+|Property Context|Property Name|Secret Name
+|`default`|`nifi.security.keystorePasswd`|`default-nifi-security-keystorePasswd`
+|===
+
+==== Required properties
+[options="header,footer"]
+|===
+|Property Name|Description|Default
+|`azure.keyvault.uri`|URI for the Azure Key Vault service such as `https://{value-name}.vault.azure.net/` |_none_
+|===
+
 === Google Cloud KMS provider
 This protection scheme uses Google Cloud Key Management Service (https://cloud.google.com/security-key-management[Google Cloud Key Management Service]) for encryption and decryption. Google Cloud KMS configuration properties are to be stored in the `bootstrap-gcp.conf` file, as referenced in the `bootstrap.conf` of NiFi or NiFi Registry. Credentials must be configured as per the following documentation: https://cloud.google.com/kms/docs/reference/libraries[Google Cloud KMS documentation]
 
diff --git a/nifi-docs/src/main/asciidoc/toolkit-guide.adoc b/nifi-docs/src/main/asciidoc/toolkit-guide.adoc
index be4eb79..d1c74d9 100644
--- a/nifi-docs/src/main/asciidoc/toolkit-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/toolkit-guide.adoc
@@ -435,7 +435,7 @@
 * `-f`,`--flowXml <file>`                       The _flow.xml.gz_ file currently protected with old password (will be overwritten unless `-g` is specified)
 * `-g`,`--outputFlowXml <file>`                 The destination _flow.xml.gz_ file containing protected config values (will not modify input _flow.xml.gz_)
 * `-b`,`--bootstrapConf <file>`                 The bootstrap.conf file to persist root key and to optionally provide any configuration for the protection scheme.
-* `-S`,`--protectionScheme <protectionScheme>`  Selects the protection scheme for encrypted properties.  Valid values are: [<<AES_GCM>>, <<HASHICORP_VAULT_TRANSIT>>, <<HASHICORP_VAULT_KV>>, <<AWS_KMS>>, <<AZURE_KEYVAULT_KEY>>, <<GCP_KMS>>] (default is AES_GCM)
+* `-S`,`--protectionScheme <protectionScheme>`  Selects the protection scheme for encrypted properties.  Valid values are: [<<AES_GCM>>, <<HASHICORP_VAULT_TRANSIT>>, <<HASHICORP_VAULT_KV>>, <<AWS_KMS>>, <<AZURE_KEYVAULT_KEY>>, <<AZURE_KEYVAULT_SECRET>>, <<GCP_KMS>>] (default is AES_GCM)
 * `-k`,`--key <keyhex>`                         The raw hexadecimal key to use to encrypt the sensitive properties
 * `-e`,`--oldKey <keyhex>`                      The old raw hexadecimal key to use during key migration
 * `-H`,`--oldProtectionScheme <protectionScheme>` The old protection scheme to use during encryption migration (see --protectionScheme for possible values).  Default is AES_GCM
@@ -456,7 +456,7 @@
 * `-v`,`--verbose`                              Sets verbose mode (default false)
 * `-p`,`--password <password>`                  Protect the files using a password-derived key. If an argument is not provided to this flag, interactive mode will be triggered to prompt the user to enter the password.
 * `-k`,`--key <keyhex>`                         Protect the files using a raw hexadecimal key. If an argument is not provided to this flag, interactive mode will be triggered to prompt the user to enter the key.
-* `-S`,`--protectionScheme <protectionScheme>`  Selects the protection scheme for encrypted properties.  Valid values are: [<<AES_GCM>>, <<HASHICORP_VAULT_TRANSIT>>, <<HASHICORP_VAULT_KV>>, <<AWS_KMS>>, <<AZURE_KEYVAULT_KEY>>, <<GCP_KMS>>]  (default is AES_GCM)
+* `-S`,`--protectionScheme <protectionScheme>`  Selects the protection scheme for encrypted properties.  Valid values are: [<<AES_GCM>>, <<HASHICORP_VAULT_TRANSIT>>, <<HASHICORP_VAULT_KV>>, <<AWS_KMS>>, <<AZURE_KEYVAULT_KEY>>, <<AZURE_KEYVAULT_SECRET>>, <<GCP_KMS>>]  (default is AES_GCM)
 * `--oldPassword <password>`                    If the input files are already protected using a password-derived key, this specifies the old password so that the files can be unprotected before re-protecting.
 * `--oldKey <keyhex>`                           If the input files are already protected using a key, this specifies the raw hexadecimal key so that the files can be unprotected before re-protecting.
 * `-H`,`--oldProtectionScheme <protectionScheme>`The old protection scheme to use during encryption migration (see --protectionScheme for possible values).  Default is AES_GCM.
@@ -485,8 +485,40 @@
 ==== AWS_KMS [[AWS_KMS]]
 This protection scheme uses https://aws.amazon.com/kms/[AWS Key Management] Service for encryption and decryption. AWS KMS configuration properties can be stored in the `bootstrap-aws.conf` file, as referenced in the `bootstrap.conf` of NiFi or NiFi Registry. If the configuration properties are not specified in `bootstrap-aws.conf`, then the provider will attempt to use the AWS default credentials provider, which checks standard environment variables and system properties.  Therefore, when using the AWS_KMS protection scheme, the `nifi(.registry)?.bootstrap.protection.aws.kms.conf` property in the `bootstrap.conf` specified using the `-b` flag must be available to the Encrypt Configuration Tool and must be configured as described in the <<administration-guide.adoc#_aws_kms_provider, AWS KMS provider>> section in the link:administration-guide.html[NiFi Administration Guide].
 
-==== AZURE_KEYVAULT_KEY [[AZURE_KEYVAULT_KEY]]
-This protection scheme uses keys managed by https://docs.microsoft.com/en-us/azure/key-vault/keys/about-keys[Azure Key Vault Keys] for encryption and decryption. Azure Key Vault configuration properties can be stored in the `bootstrap-azure.conf` file, as referenced in the `bootstrap.conf` of NiFi or NiFi Registry. The provider will utilize the Azure default credentials provider chain as described in the https://docs.microsoft.com/en-us/java/api/overview/azure/security-keyvault-keys-readme?view=azure-java-stable[Azure Key Vault Key client library for Java] documentation. Therefore, when using the AZURE_KEYVAULT_KEY protection scheme, the `nifi(.registry)?.bootstrap.protection.azure.keyvault.conf` property in the `bootstrap.conf` specified using the `-b` flag must be available to the Encrypt Configuration Tool and must be configured as described in the <<administration-guide.adoc#_azure_key_vault_key_provider, Azure Key Vault Key provider>> section in the link:administration-guide.html[NiFi Administration Guide].
+==== Microsoft Azure Key Vault Sensitive Property Providers
+
+Azure Key Vault configuration properties can be stored in the `bootstrap-azure.conf` file, as referenced in the
+`bootstrap.conf` of NiFi or NiFi Registry.
+
+Azure Key Vault providers will use the
+https://docs.microsoft.com/en-us/java/api/com.azure.identity.defaultazurecredential[DefaultAzureCredential]
+for authentication.
+The https://docs.microsoft.com/en-us/java/api/overview/azure/identity-readme#key-concepts[Azure Identity] client library
+describes the process for credentials resolution, which leverages environment variables, system properties, and falls
+back to
+https://docs.microsoft.com/en-us/java/api/overview/azure/identity-readme#managed-identity-support[Managed Identity]
+authentication.
+
+When using Azure Key Vault providers, `bootstrap.conf` must contain the
+`nifi.bootstrap.protection.azure.keyvault.conf` property. The `bootstrap.conf` file location must be specified using the
+`-b` argument when running the Encrypt Config Tool.
+
+===== AZURE_KEYVAULT_KEY [[AZURE_KEYVAULT_KEY]]
+
+This protection scheme uses keys managed by
+https://docs.microsoft.com/en-us/azure/key-vault/keys/about-keys[Azure Key Vault Keys] for encryption and decryption.
+
+See <<administration-guide.adoc#azure-key-vault-key-provider,Azure Key Vault Key Provider>> in the NiFi System
+Administrator's Guide for required properties.
+
+===== AZURE_KEYVAULT_SECRET [[AZURE_KEYVAULT_SECRET]]
+
+This protection scheme uses secrets managed by
+https://docs.microsoft.com/en-us/azure/key-vault/secrets/about-secrets[Azure Key Vault Secrets] for storing and
+retrieving sensitive property values.
+
+See <<administration-guide.adoc#azure-key-vault-secret-provider,Azure Key Vault Secret Provider>> in the NiFi System
+Administrator's Guide for required properties.
 
 ==== GCP_KMS [[GCP_KMS]]
 This protection scheme uses Google Cloud Key Management Service (https://cloud.google.com/security-key-management[Google Cloud Key Management Service]) for encryption and decryption. Google Cloud KMS configuration properties are to be stored in the `bootstrap-gcp.conf` file, as referenced in the `bootstrap.conf` of NiFi or NiFi Registry. Credentials must be configured as per the following documentation: https://cloud.google.com/kms/docs/reference/libraries[Google Cloud KMS documentation]. Therefore, when using the GCP_KMS protection scheme, the `nifi(.registry)?.bootstrap.protection.gcp.kms.conf` property in the `bootstrap.conf` specified using the `-b` flag must be available to the Encrypt Configuration Tool and must be configured as described in the <<administration-guide.adoc#_google_cloud_kms_provider,Google Cloud KMS provider>> section in the link:administration-guide.html[NiFi Administration Guide].
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/bootstrap-azure.conf b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/bootstrap-azure.conf
index f7cc47e..49e318e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/bootstrap-azure.conf
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/bootstrap-azure.conf
@@ -15,6 +15,10 @@
 # limitations under the License.
 #
 
-# Azure KeyVault Key ID and Encryption Algorithm is required to be configured for Azure KeyVault Sensitive Property Provider
+# Key Identifier for Azure Key Vault Key Sensitive Property Provider
 azure.keyvault.key.id=
-azure.keyvault.encryption.algorithm=
\ No newline at end of file
+# Encryption Algorithm for Azure Key Vault Key Sensitive Property Provider
+azure.keyvault.encryption.algorithm=
+
+# Vault URI for Azure Key Vault Secret Sensitive Property Provider
+azure.keyvault.uri=
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/bootstrap-azure.conf b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/bootstrap-azure.conf
index f7cc47e..49e318e 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/bootstrap-azure.conf
+++ b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/bootstrap-azure.conf
@@ -15,6 +15,10 @@
 # limitations under the License.
 #
 
-# Azure KeyVault Key ID and Encryption Algorithm is required to be configured for Azure KeyVault Sensitive Property Provider
+# Key Identifier for Azure Key Vault Key Sensitive Property Provider
 azure.keyvault.key.id=
-azure.keyvault.encryption.algorithm=
\ No newline at end of file
+# Encryption Algorithm for Azure Key Vault Key Sensitive Property Provider
+azure.keyvault.encryption.algorithm=
+
+# Vault URI for Azure Key Vault Secret Sensitive Property Provider
+azure.keyvault.uri=
diff --git a/pom.xml b/pom.xml
index 54ebe3c..2fc31dd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -111,6 +111,7 @@
         <aspectj.version>1.9.6</aspectj.version>
         <jersey.version>2.33</jersey.version>
         <logback.version>1.2.6</logback.version>
+        <mockito.version>2.28.2</mockito.version>
     </properties>
 
     <repositories>
@@ -286,12 +287,12 @@
             <dependency>
                 <groupId>org.mockito</groupId>
                 <artifactId>mockito-core</artifactId>
-                <version>2.28.2</version>
+                <version>${mockito.version}</version>
             </dependency>
             <dependency>
                 <groupId>org.mockito</groupId>
                 <artifactId>mockito-junit-jupiter</artifactId>
-                <version>2.28.2</version>
+                <version>${mockito.version}</version>
             </dependency>
             <dependency>
                 <groupId>org.codehaus.groovy</groupId>