Repository: Azure/azure-sdk-for-rust (rust)

Issue:
Async process abstractions
PR https://github.com/Azure/azure-sdk-for-rust/pull/1560 has brought in the async-process dependency which itself depends on async-io.

As the azure crates already depend on tokio through reqwest, maybe it might be a better idea to switch to tokio instead?

And if that's not possible, maybe it could be hidden behind a feature so that it can be turned off?

Related: #2002

Pull request:
Add async command executor
Resolves #1652. This replaces #2184 since I'm unable to push changes to the topic branch and told @arpad-m I would finish up with changes we want to make to `azure_identity` anyway.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 9af12b753..3edab57ae 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -116,18 +116,6 @@ dependencies = [
  "futures-core",
 ]
 
-[[package]]
-name = "async-channel"
-version = "2.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
-dependencies = [
- "concurrent-queue",
- "event-listener-strategy",
- "futures-core",
- "pin-project-lite",
-]
-
 [[package]]
 name = "async-compression"
 version = "0.4.18"
@@ -141,25 +129,6 @@ dependencies = [
  "tokio",
 ]
 
-[[package]]
-name = "async-io"
-version = "2.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
-dependencies = [
- "async-lock",
- "cfg-if",
- "concurrent-queue",
- "futures-io",
- "futures-lite 2.5.0",
- "parking",
- "polling",
- "rustix",
- "slab",
- "tracing",
- "windows-sys 0.59.0",
-]
-
 [[package]]
 name = "async-lock"
 version = "3.4.0"
@@ -171,43 +140,6 @@ dependencies = [
  "pin-project-lite",
 ]
 
-[[package]]
-name = "async-process"
-version = "2.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb"
-dependencies = [
- "async-channel 2.3.1",
- "async-io",
- "async-lock",
- "async-signal",
- "async-task",
- "blocking",
- "cfg-if",
- "event-listener 5.3.1",
- "futures-lite 2.5.0",
- "rustix",
- "tracing",
-]
-
-[[package]]
-name = "async-signal"
-version = "0.2.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3"
-dependencies = [
- "async-io",
- "async-lock",
- "atomic-waker",
- "cfg-if",
- "futures-core",
- "futures-io",
- "rustix",
- "signal-hook-registry",
- "slab",
- "windows-sys 0.59.0",
-]
-
 [[package]]
 name = "async-stream"
 version = "0.3.6"
@@ -230,12 +162,6 @@ dependencies = [
  "syn",
 ]
 
-[[package]]
-name = "async-task"
-version = "4.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
-
 [[package]]
 name = "async-trait"
 version = "0.1.83"
@@ -247,12 +173,6 @@ dependencies = [
  "syn",
 ]
 
-[[package]]
-name = "atomic-waker"
-version = "1.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
-
 [[package]]
 name = "autocfg"
 version = "1.4.0"
@@ -370,7 +290,6 @@ name = "azure_identity"
 version = "0.23.0"
 dependencies = [
  "async-lock",
- "async-process",
  "async-trait",
  "azure_core",
  "azure_core_test",
@@ -549,19 +468,6 @@ dependencies = [
  "generic-array",
 ]
 
-[[package]]
-name = "blocking"
-version = "1.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
-dependencies = [
- "async-channel 2.3.1",
- "async-task",
- "futures-io",
- "futures-lite 2.5.0",
- "piper",
-]
-
 [[package]]
 name = "bumpalo"
 version = "3.16.0"
@@ -1125,19 +1031,6 @@ dependencies = [
  "waker-fn",
 ]
 
-[[package]]
-name = "futures-lite"
-version = "2.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1"
-dependencies = [
- "fastrand 2.2.0",
- "futures-core",
- "futures-io",
- "parking",
- "pin-project-lite",
-]
-
 [[package]]
 name = "futures-macro"
 version = "0.3.31"
@@ -1231,12 +1124,6 @@ version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
-[[package]]
-name = "hermit-abi"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
-
 [[package]]
 name = "hmac"
 version = "0.12.1"
@@ -1287,9 +1174,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad"
 dependencies = [
  "anyhow",
- "async-channel 1.9.0",
+ "async-channel",
  "base64 0.13.1",
- "futures-lite 1.13.0",
+ "futures-lite",
  "infer",
  "pin-project-lite",
  "rand 0.7.3",
@@ -1939,38 +1826,12 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
-[[package]]
-name = "piper"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
-dependencies = [
- "atomic-waker",
- "fastrand 2.2.0",
- "futures-io",
-]
-
 [[package]]
 name = "pkg-config"
 version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
 
-[[package]]
-name = "polling"
-version = "3.7.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
-dependencies = [
- "cfg-if",
- "concurrent-queue",
- "hermit-abi",
- "pin-project-lite",
- "rustix",
- "tracing",
- "windows-sys 0.59.0",
-]
-
 [[package]]
 name = "powerfmt"
 version = "0.2.0"
diff --git a/Cargo.toml b/Cargo.toml
index b861f650e..900edb41e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -73,7 +73,6 @@ path = "sdk/identity/azure_identity"
 
 [workspace.dependencies]
 async-lock = "3.0"
-async-process = "2.0"
 async-stream = { version = "0.3.5" }
 async-trait = "0.1"
 base64 = "0.22"
diff --git a/eng/dict/crates.txt b/eng/dict/crates.txt
index a7c0e1622..f986b591f 100644
--- a/eng/dict/crates.txt
+++ b/eng/dict/crates.txt
@@ -1,5 +1,4 @@
 async-lock
-async-process
 async-stream
 async-trait
 azure_core
diff --git a/sdk/core/azure_core/CHANGELOG.md b/sdk/core/azure_core/CHANGELOG.md
index 282e558bc..143d0d801 100644
--- a/sdk/core/azure_core/CHANGELOG.md
+++ b/sdk/core/azure_core/CHANGELOG.md
@@ -1,5 +1,18 @@
 # Release History
 
+## 0.23.0 (Unreleased)
+
+### Features Added
+
+- Added `azure_core::process::Executor` to run commands asynchronously.
+  The `tokio_process` feature is disabled by default so `std::process::Command` is used; otherwise, if enabled, `tokio::process::Command` is used.
+
+### Breaking Changes
+
+### Bugs Fixed
+
+### Other Changes
+
 ## 0.22.0 (2025-02-18)
 
 ### Features Added
diff --git a/sdk/core/azure_core/Cargo.toml b/sdk/core/azure_core/Cargo.toml
index 731742d9e..f2a848730 100644
--- a/sdk/core/azure_core/Cargo.toml
+++ b/sdk/core/azure_core/Cargo.toml
@@ -56,6 +56,7 @@ reqwest_gzip = ["typespec_client_core/reqwest_gzip"]
 reqwest_rustls = ["typespec_client_core/reqwest_rustls"]
 test = ["typespec_client_core/test"]
 tokio_fs = ["typespec_client_core/tokio_fs"]
+tokio_process = ["dep:tokio", "tokio/process"]
 tokio_sleep = ["typespec_client_core/tokio_sleep"]
 xml = ["typespec_client_core/xml"]
 
@@ -68,6 +69,7 @@ features = [
   "reqwest_gzip",
   "reqwest_rustls",
   "tokio_fs",
+  "tokio_process",
   "tokio_sleep",
   "xml",
 ]
diff --git a/sdk/core/azure_core/src/lib.rs b/sdk/core/azure_core/src/lib.rs
index 0ff2781af..09d4e2068 100644
--- a/sdk/core/azure_core/src/lib.rs
+++ b/sdk/core/azure_core/src/lib.rs
@@ -19,6 +19,7 @@ mod policies;
 pub mod credentials;
 pub mod headers;
 pub mod lro;
+pub mod process;
 pub mod request_options;
 
 #[cfg(feature = "test")]
diff --git a/sdk/core/azure_core/src/process/mod.rs b/sdk/core/azure_core/src/process/mod.rs
new file mode 100644
index 000000000..e3e1a1231
--- /dev/null
+++ b/sdk/core/azure_core/src/process/mod.rs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+use async_trait::async_trait;
+use std::{ffi::OsStr, fmt, io, process::Output, sync::Arc};
+
+mod standard;
+#[cfg(feature = "tokio_process")]
+mod tokio;
+
+pub use standard::StdExecutor;
+#[cfg(feature = "tokio_process")]
+pub use tokio::TokioExecutor;
+
+/// Creates a new [`Executor`].
+///
+/// This returns a [`StdExecutor`] that spawns a [`std::process::Command`] in a separate thread unless `tokio_process` was enabled,
+/// in which case an executor is returned that spawns a `tokio::process::Command`.
+pub fn new_executor() -> Arc<dyn Executor> {
+    #[cfg(not(feature = "tokio_process"))]
+    {
+        Arc::new(StdExecutor)
+    }
+    #[cfg(feature = "tokio_process")]
+    {
+        Arc::new(TokioExecutor)
+    }
+}
+
+/// An async command runner.
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
+pub trait Executor: Send + Sync + fmt::Debug {
+    /// Run a program with the given arguments until it terminates, returning the output.
+    async fn run(&self, program: &OsStr, args: &[&OsStr]) -> io::Result<Output>;
+}
diff --git a/sdk/core/azure_core/src/process/standard.rs b/sdk/core/azure_core/src/process/standard.rs
new file mode 100644
index 000000000..57a0e0da5
--- /dev/null
+++ b/sdk/core/azure_core/src/process/standard.rs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+use super::Executor;
+use async_trait::async_trait;
+use futures::channel::oneshot;
+use std::{ffi::OsStr, io, process::Output, thread};
+
+/// An [`Executor`] using [`tokio::process::Command`].
+#[derive(Debug)]
+pub struct StdExecutor;
+
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
+impl Executor for StdExecutor {
+    async fn run(&self, program: &OsStr, args: &[&OsStr]) -> io::Result<Output> {
+        let (tx, rx) = oneshot::channel();
+        let mut cmd = std::process::Command::new(program);
+        cmd.args(args);
+        thread::spawn(move || {
+            let output = cmd.output();
+            tx.send(output)
+        });
+        let output = rx
+            .await
+            .map_err(|err| io::Error::new(io::ErrorKind::Other, err))??;
+        Ok(output)
+    }
+}
diff --git a/sdk/core/azure_core/src/process/tokio.rs b/sdk/core/azure_core/src/process/tokio.rs
new file mode 100644
index 000000000..419cec878
--- /dev/null
+++ b/sdk/core/azure_core/src/process/tokio.rs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+use super::Executor;
+use async_trait::async_trait;
+use std::{ffi::OsStr, io, process::Output};
+
+/// An [`Executor`] using [`tokio::process::Command`].
+#[derive(Debug)]
+pub struct TokioExecutor;
+
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
+impl Executor for TokioExecutor {
+    async fn run(&self, program: &OsStr, args: &[&OsStr]) -> io::Result<Output> {
+        ::tokio::process::Command::new(program)
+            .args(args)
+            .output()
+            .await
+    }
+}
diff --git a/sdk/identity/azure_identity/CHANGELOG.md b/sdk/identity/azure_identity/CHANGELOG.md
index 763d54f84..52f7b3815 100644
--- a/sdk/identity/azure_identity/CHANGELOG.md
+++ b/sdk/identity/azure_identity/CHANGELOG.md
@@ -5,10 +5,16 @@
 ### Features Added
 
 - Added `AzurePipelinesCredential`.
+- `AzureCliCredentialOptions` (new) accepts a `azure_core::process::Executor` to run the Azure CLI asynchronously.
+  The `tokio_process` feature is disabled by default so `std::process::Command` is used; otherwise, if enabled, `tokio::process::Command` is used.
+  Callers can also implement the trait themselves to use a different asynchronous runtime.
 
 ### Breaking Changes
 
+- Added `Option<AzureCliCredentialOptions>` to `AzureCliCredential::new`.
+- `AzureCliCredential` authenticates only against the first scope passed as a resource to support both v1 and v2 CLI versions.
 - `ClientAssertionCredential` constructors moved some parameters to an `Option<ClientAssertionCredentialOptions>` parameter.
+- Removed `get_subscription()` and `get_tenant()` from `AzureCliCredential`.
 - `WorkloadIdentityCredential` constructors moved some parameters to an `Option<ClientAssertionCredentialOptions>` parameter.
 
 ### Bugs Fixed
diff --git a/sdk/identity/azure_identity/Cargo.toml b/sdk/identity/azure_identity/Cargo.toml
index 741379a64..db7b318bb 100644
--- a/sdk/identity/azure_identity/Cargo.toml
+++ b/sdk/identity/azure_identity/Cargo.toml
@@ -26,9 +26,6 @@ tracing.workspace = true
 typespec_client_core = { workspace = true, features = ["derive"] }
 url.workspace = true
 
-[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-async-process.workspace = true
-
 [target.'cfg(unix)'.dependencies]
 tz-rs = { workspace = true, optional = true }
 
@@ -46,6 +43,7 @@ tracing-subscriber.workspace = true
 default = ["reqwest", "old_azure_cli"]
 reqwest = ["azure_core/reqwest"]
 reqwest_rustls = ["azure_core/reqwest_rustls"]
+tokio_process = ["azure_core/tokio_process"]
 client_certificate = ["openssl"]
 
 # If you are using and Azure CLI version older than 2.54.0 from November 2023,
@@ -56,4 +54,9 @@ client_certificate = ["openssl"]
 old_azure_cli = ["time/local-offset", "tz-rs"]
 
 [package.metadata.docs.rs]
-features = ["reqwest", "reqwest_rustls", "client_certificate"]
+features = [
+  "client_certificate",
+  "reqwest",
+  "reqwest_rustls",
+  "tokio_process",
+]
diff --git a/sdk/identity/azure_identity/examples/azure_cli_credentials.rs b/sdk/identity/azure_identity/examples/azure_cli_credentials.rs
index d02bffa40..7a798fd2c 100644
--- a/sdk/identity/azure_identity/examples/azure_cli_credentials.rs
+++ b/sdk/identity/azure_identity/examples/azure_cli_credentials.rs
@@ -8,20 +8,21 @@ use url::Url;
 
 #[tokio::main]
 async fn main() -> Result<(), Box<dyn Error>> {
-    let sub_id = AzureCliCredential::get_subscription().await?;
-    println!("Azure cli subscription: {sub_id}");
+    let subscription_id =
+        std::env::var("AZURE_SUBSCRIPTION_ID").expect("AZURE_SUBSCRIPTION_ID required");
 
-    let credentials = AzureCliCredential::new()?;
+    let credentials = AzureCliCredential::new(None)?;
     let res = credentials
         .get_token(&["https://management.azure.com/.default"])
         .await?;
-    println!("Azure cli response == {res:?}");
+    eprintln!("Azure CLI response == {res:?}");
+
     // Let's enumerate the Azure storage accounts
     // in the subscription. Note: this way of calling the REST API
     // will be different (and easier) using other Azure Rust SDK
     // crates, this is just an example.
     let url = Url::parse(&format!(
-                 "https://management.azure.com/subscriptions/{sub_id}/providers/Microsoft.Storage/storageAccounts?api-version=2019-06-01"
+                 "https://management.azure.com/subscriptions/{subscription_id}/providers/Microsoft.Storage/storageAccounts?api-version=2019-06-01"
              ))?;
 
     let resp = reqwest::Client::new()
diff --git a/sdk/identity/azure_identity/examples/specific_credential.rs b/sdk/identity/azure_identity/examples/specific_credential.rs
index 2cf3419c9..54ae2e328 100644
--- a/sdk/identity/azure_identity/examples/specific_credential.rs
+++ b/sdk/identity/azure_identity/examples/specific_credential.rs
@@ -127,7 +127,7 @@ impl SpecificAzureCredential {
                     )
                 }
                 #[cfg(not(target_arch = "wasm32"))]
-                azure_credential_kinds::AZURE_CLI => AzureCliCredential::new()
+                azure_credential_kinds::AZURE_CLI => AzureCliCredential::new(Some(options.into()))
                     .map(SpecificAzureCredentialKind::AzureCli)
                     .with_context(ErrorKind::Credential, || {
                         format!(
diff --git a/sdk/identity/azure_identity/src/credentials/azure_cli_credentials.rs b/sdk/identity/azure_identity/src/credentials/azure_cli_credentials.rs
index 4240436f9..9d48e7d14 100644
--- a/sdk/identity/azure_identity/src/credentials/azure_cli_credentials.rs
+++ b/sdk/identity/azure_identity/src/credentials/azure_cli_credentials.rs
@@ -1,15 +1,18 @@
 // Copyright (c) Microsoft Corporation. All rights reserved.
 // Licensed under the MIT License.
 
-use crate::credentials::cache::TokenCache;
-use async_process::Command;
+use crate::{
+    credentials::{cache::TokenCache, TokenCredentialOptions},
+    validate_scope, validate_subscription, validate_tenant_id,
+};
 use azure_core::{
     credentials::{AccessToken, Secret, TokenCredential},
     error::{Error, ErrorKind, ResultExt},
     json::from_json,
+    process::{new_executor, Executor},
 };
 use serde::Deserialize;
-use std::{str, sync::Arc};
+use std::{ffi::OsStr, fmt::Debug, str, sync::Arc};
 use time::OffsetDateTime;
 use tracing::trace;
 
@@ -116,8 +119,6 @@ struct CliTokenResponse {
     /// The token's expiry time in seconds since the epoch, a unix timestamp.
     /// Available in Azure CLI 2.54.0 or newer.
     pub expires_on: Option<i64>,
-    pub subscription: String,
-    pub tenant: String,
     #[allow(unused)]
     #[serde(rename = "tokenType")]
     pub token_type: String,
@@ -151,21 +152,69 @@ impl CliTokenResponse {
 #[derive(Debug)]
 pub struct AzureCliCredential {
     cache: TokenCache,
+    options: AzureCliCredentialOptions,
+}
+
+/// Options for constructing an [`AzureCliCredential`].
+#[derive(Clone, Debug, Default)]
+pub struct AzureCliCredentialOptions {
+    /// Specifies tenants to which the credential may authenticate, in addition to [`Self::tenant_id`].
+    ///
+    /// When `tenant_id` is `None` this option has no effect and the credential will authenticate to any requested tenant.
+    /// Add the wildcard value "*" to allow the credential to authenticate to any tenant.
+    pub additionally_allowed_tenants: Vec<String>,
+
+    /// The name or ID of a subscription
+    ///
+    /// Set this to acquire tokens for an account other than the Azure CLI's current account.
+    pub subscription: Option<String>,
+
+    /// Identifies the tenant the credential should authenticate in.
+    ///
+    /// Defaults to the CLI's default tenant, which is typically the home tenant of the logged in user.
+    pub tenant_id: Option<String>,
+
+    /// An implementation of [`Executor`] to run commands asynchronously.
+    ///
+    /// If `None`, one is created using [`new_executor`]; alternatively,
+    /// you can supply your own implementation using a different asynchronous runtime.
+    pub executor: Option<Arc<dyn Executor>>,
 }
 
 impl AzureCliCredential {
     /// Create a new `AzureCliCredential`.
-    pub fn new() -> azure_core::Result<Arc<Self>> {
-        // TODO check `az version` to see if it's installed
+    pub fn new(options: Option<AzureCliCredentialOptions>) -> azure_core::Result<Arc<Self>> {
+        let mut options = options.unwrap_or_default();
+        if let Some(ref tenant_id) = options.tenant_id {
+            validate_tenant_id(tenant_id)?;
+        }
+        if let Some(ref subscription) = options.subscription {
+            validate_subscription(subscription)?;
+        }
+        if options.executor.is_none() {
+            options.executor = Some(new_executor());
+        }
+
         Ok(Arc::new(Self {
             cache: TokenCache::new(),
+            options,
         }))
     }
 
     /// Get an access token for an optional resource
-    async fn get_access_token(scopes: Option<&[&str]>) -> azure_core::Result<CliTokenResponse> {
-        // on window az is a cmd and it should be called like this
-        // see https://doc.rust-lang.org/nightly/std/process/struct.Command.html
+    async fn get_access_token(&self, scopes: &[&str]) -> azure_core::Result<CliTokenResponse> {
+        if scopes.is_empty() {
+            return Err(Error::new(
+                ErrorKind::Credential,
+                "exactly one scope required",
+            ));
+        }
+        // Pass the CLI a Microsoft Entra ID v1 resource because we don't know which CLI version is installed and older ones don't support v2 scopes.
+        let resource = scopes[0].trim_end_matches("/.default");
+        validate_scope(resource)?;
+
+        // On Windows az is a cmd and it should be called like this.
+        // See https://doc.rust-lang.org/nightly/std/process/struct.Command.html
         let program = if cfg!(target_os = "windows") {
             "cmd"
         } else {
@@ -180,12 +229,16 @@ impl AzureCliCredential {
         args.push("get-access-token");
         args.push("--output");
         args.push("json");
+        args.push("--resource");
+        args.push(resource);
 
-        let scopes = scopes.map(|x| x.join(" "));
-
-        if let Some(scopes) = &scopes {
-            args.push("--scope");
-            args.push(scopes);
+        if let Some(ref tenant_id) = self.options.tenant_id {
+            args.push("--tenant");
+            args.push(tenant_id);
+        }
+        if let Some(ref subscription) = self.options.subscription {
+            args.push("--subscription");
+            args.push(subscription);
         }
 
         trace!(
@@ -193,7 +246,17 @@ impl AzureCliCredential {
             args.join(" "),
         );
 
-        match Command::new(program).args(args).output().await {
+        let args = args.iter().map(|arg| arg.as_ref()).collect::<Vec<&OsStr>>();
+
+        let status = self
+            .options
+            .executor
+            .as_ref()
+            // It's okay to call unwrap() here because new() ensures it's initialized.
+            .unwrap()
+            .run(OsStr::new(program), &args)
+            .await;
+        match status {
             Ok(az_output) if az_output.status.success() => {
                 let output = str::from_utf8(&az_output.stdout)?;
 
@@ -217,20 +280,8 @@ impl AzureCliCredential {
         }
     }
 
-    /// Returns the current subscription ID from the Azure CLI.
-    pub async fn get_subscription() -> azure_core::Result<String> {
-        let tr = Self::get_access_token(None).await?;
-        Ok(tr.subscription)
-    }
-
-    /// Returns the current tenant ID from the Azure CLI.
-    pub async fn get_tenant() -> azure_core::Result<String> {
-        let tr = Self::get_access_token(None).await?;
-        Ok(tr.tenant)
-    }
-
     async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
-        let tr = Self::get_access_token(Some(scopes)).await?;
+        let tr = self.get_access_token(scopes).await?;
         let expires_on = tr.expires_on()?;
         Ok(AccessToken::new(tr.access_token, expires_on))
     }
@@ -249,6 +300,15 @@ impl TokenCredential for AzureCliCredential {
     }
 }
 
+impl From<TokenCredentialOptions> for AzureCliCredentialOptions {
+    fn from(options: TokenCredentialOptions) -> Self {
+        Self {
+            executor: Some(options.executor.clone()),
+            ..Default::default()
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -309,10 +369,7 @@ mod tests {
           }
         "#;
         let token_response: CliTokenResponse = from_json(json)?;
-        assert_eq!(
-            token_response.tenant,
-            "065e9f5e-870d-4ed1-af2b-1b58092353f3"
-        );
+        assert_eq!(token_response.expires_on, None);
         Ok(())
     }
 
@@ -330,10 +387,6 @@ mod tests {
         }
         "#;
         let token_response: CliTokenResponse = from_json(json)?;
-        assert_eq!(
-            token_response.tenant,
-            "065e9f5e-870d-4ed1-af2b-1b58092353f3"
-        );
         assert_eq!(token_response.expires_on, Some(1704158596));
         assert_eq!(token_response.expires_on()?.unix_timestamp(), 1704158596);
         Ok(())
diff --git a/sdk/identity/azure_identity/src/credentials/cache.rs b/sdk/identity/azure_identity/src/credentials/cache.rs
index f60945b81..161aca9d3 100644
--- a/sdk/identity/azure_identity/src/credentials/cache.rs
+++ b/sdk/identity/azure_identity/src/credentials/cache.rs
@@ -61,6 +61,12 @@ impl TokenCache {
     }
 }
 
+impl Default for TokenCache {
+    fn default() -> Self {
+        TokenCache::new()
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/sdk/identity/azure_identity/src/credentials/default_azure_credentials.rs b/sdk/identity/azure_identity/src/credentials/default_azure_credentials.rs
index 24ad72838..32ca3e2d8 100644
--- a/sdk/identity/azure_identity/src/credentials/default_azure_credentials.rs
+++ b/sdk/identity/azure_identity/src/credentials/default_azure_credentials.rs
@@ -74,7 +74,9 @@ impl DefaultAzureCredentialBuilder {
             match source {
                 #[cfg(not(target_arch = "wasm32"))]
                 DefaultAzureCredentialType::AzureCli => {
-                    if let Ok(credential) = AzureCliCredential::new() {
+                    if let Ok(credential) =
+                        AzureCliCredential::new(Some(self.options.clone().into()))
+                    {
                         sources.push(DefaultAzureCredentialKind::AzureCli(credential));
                     }
                 }
diff --git a/sdk/identity/azure_identity/src/credentials/options.rs b/sdk/identity/azure_identity/src/credentials/options.rs
index 366efa2c1..6f1369b65 100644
--- a/sdk/identity/azure_identity/src/credentials/options.rs
+++ b/sdk/identity/azure_identity/src/credentials/options.rs
@@ -18,6 +18,7 @@ pub struct TokenCredentialOptions {
     pub(crate) env: Env,
     pub(crate) http_client: Arc<dyn azure_core::HttpClient>,
     pub(crate) authority_host: String,
+    pub(crate) executor: Arc<dyn azure_core::process::Executor>,
 }
 
 /// The default token credential options.
@@ -34,6 +35,7 @@ impl Default for TokenCredentialOptions {
             env: Env::default(),
             http_client: azure_core::new_http_client(),
             authority_host,
+            executor: azure_core::process::new_executor(),
         }
     }
 }
@@ -44,18 +46,25 @@ impl TokenCredentialOptions {
         self.authority_host = authority_host;
     }
 
-    /// The authority host to use for authentication requests.  The default is
-    /// `https://login.microsoftonline.com`.
+    /// The authority host to use for authentication requests.
+    ///
+    /// The default is `https://login.microsoftonline.com`.
     pub fn authority_host(&self) -> Result<Url> {
         Url::parse(&self.authority_host).with_context(ErrorKind::DataConversion, || {
             format!("invalid authority host URL {}", &self.authority_host)
         })
     }
 
+    /// The [`azure_core::HttpClient`] to make requests.
     pub fn http_client(&self) -> Arc<dyn azure_core::HttpClient> {
         self.http_client.clone()
     }
 
+    /// The [`azure_core::process::Executor`] to run commands.
+    pub fn executor(&self) -> Arc<dyn azure_core::process::Executor> {
+        self.executor.clone()
+    }
+
     pub(crate) fn env(&self) -> &Env {
         &self.env
     }
diff --git a/sdk/identity/azure_identity/src/lib.rs b/sdk/identity/azure_identity/src/lib.rs
index 078fe841b..090f7ae80 100644
--- a/sdk/identity/azure_identity/src/lib.rs
+++ b/sdk/identity/azure_identity/src/lib.rs
@@ -37,13 +37,64 @@ fn test_validate_not_empty() {
     assert!(validate_not_empty("not empty", "it's not empty").is_ok());
 }
 
+#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
+fn validate_scope(scope: &str) -> Result<()> {
+    if scope.is_empty()
+        || !scope.chars().all(|c| {
+            c.is_alphanumeric() || c == '.' || c == '-' || c == '_' || c == ':' || c == '/'
+        })
+    {
+        return Err(Error::message(
+            ErrorKind::Credential,
+            format!("invalid scope {scope}"),
+        ));
+    }
+
+    Ok(())
+}
+
+#[test]
+fn test_validate_scope() {
+    assert!(validate_scope("").is_err());
+    assert!(validate_scope("invalid_scope@id").is_err());
+    assert!(validate_scope("A-1b_2c:3d/4.z").is_ok());
+    assert!(validate_scope("http://vault.azure.net").is_ok());
+}
+
+#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
+fn validate_subscription(subscription: &str) -> Result<()> {
+    if subscription.is_empty()
+        || !subscription
+            .chars()
+            .all(|c| c.is_alphanumeric() || c == '.' || c == '-' || c == '_' || c == ' ')
+    {
+        return Err(Error::message(
+            ErrorKind::Credential,
+            format!("invalid subscription {subscription}. If this is the name of a subscription, use its ID instead"),
+        ));
+    }
+
+    Ok(())
+}
+
+#[test]
+fn test_validate_subscription() {
+    assert!(validate_subscription("").is_err());
+    assert!(validate_subscription("invalid_subscription@id").is_err());
+    assert!(validate_subscription("A-1b_2c 3.z").is_ok());
+    assert!(validate_subscription("7b795fb9-09d3-42f4-a494-38864f99ba3c").is_ok());
+}
+
 fn validate_tenant_id(tenant_id: &str) -> Result<()> {
     if tenant_id.is_empty()
         || !tenant_id
             .chars()
             .all(|c| c.is_alphanumeric() || c == '.' || c == '-')
     {
-        return Err(Error::message(ErrorKind::Credential, "invalid tenantID. You can locate your tenantID by following the instructions listed here: https://learn.microsoft.com/partner-center/find-ids-and-domain-names"));
+        return Err(Error::message(
+            ErrorKind::Credential,
+            format!("invalid tenant ID {tenant_id}. You can locate your tenantID by following the instructions listed here: https://learn.microsoft.com/partner-center/find-ids-and-domain-names"),
+        ));
     }
 
     Ok(())


Test patch:
diff --git a/eng/common/TestResources/New-TestResources.ps1 b/eng/common/TestResources/New-TestResources.ps1
index 0b1fa603c..fce3bd020 100755
--- a/eng/common/TestResources/New-TestResources.ps1
+++ b/eng/common/TestResources/New-TestResources.ps1
@@ -194,6 +194,18 @@ try {
         -serviceDirectoryName $serviceName `
         -CI $CI
 
+    if ($wellKnownTMETenants.Contains($TenantId)) {
+        # Add a prefix to the resource group name to avoid flagging the usages of local auth
+        # See details at https://eng.ms/docs/products/onecert-certificates-key-vault-and-dsms/key-vault-dsms/certandsecretmngmt/credfreefaqs#how-can-i-disable-s360-reporting-when-testing-customer-facing-3p-features-that-depend-on-use-of-unsafe-local-auth
+        $ResourceGroupName = "SSS3PT_" + $ResourceGroupName
+    }
+
+    if ($ResourceGroupName.Length -gt 90) {
+        # See limits at https://docs.microsoft.com/azure/architecture/best-practices/resource-naming
+        Write-Warning -Message "Resource group name '$ResourceGroupName' is too long. So pruning it to be the first 90 characters."
+        $ResourceGroupName = $ResourceGroupName.Substring(0, 90)
+    }
+
     # Make sure pre- and post-scripts are passed formerly required arguments.
     $PSBoundParameters['BaseName'] = $BaseName