diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs
index c0dcf99b..d0754def 100644
--- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs
@@ -465,7 +465,7 @@ await CreateBlueprintClientSecretAsync(
logger.LogWarning("Setup will continue to configure Bot API permissions");
logger.LogWarning("");
logger.LogWarning("To resolve endpoint registration issues:");
- logger.LogWarning(" 1. Delete existing endpoint: a365 cleanup blueprint");
+ logger.LogWarning(" 1. Delete existing endpoint: a365 cleanup blueprint --endpoint-only");
logger.LogWarning(" 2. Register endpoint again: a365 setup blueprint --endpoint-only");
logger.LogWarning(" Or rerun full setup: a365 setup blueprint");
logger.LogWarning("");
diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/AuthenticationService.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/AuthenticationService.cs
index 2c592f74..55d7e64f 100644
--- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/AuthenticationService.cs
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/AuthenticationService.cs
@@ -12,7 +12,29 @@
namespace Microsoft.Agents.A365.DevTools.Cli.Services;
///
-/// Service for handling authentication to Agent 365 Tools
+/// Service for handling authentication to Agent 365 Tools and Microsoft Graph API.
+///
+/// AUTHENTICATION STRATEGY:
+/// - Uses interactive authentication by default (no device code flow)
+/// - Implements comprehensive token caching to minimize authentication prompts
+/// - Typical user experience: 1-2 authentication prompts for entire CLI workflow
+///
+/// TOKEN CACHING:
+/// - Cache Location: %LocalApplicationData%\Agent365\token-cache.json (Windows)
+/// - Cache Key Format: {resourceUrl}:tenant:{tenantId}
+/// - Cache Expiration: Validated with 5-minute buffer before token expiry
+/// - Reuse Across Commands: All CLI commands share the same token cache
+///
+/// AUTHENTICATION FLOW:
+/// 1. Check cache for valid token (tenant-specific)
+/// 2. If cache miss or expired: Prompt for interactive authentication
+/// 3. Cache new token for future CLI command invocations
+/// 4. Token persists across CLI sessions until expiration
+///
+/// MULTI-COMMAND WORKFLOW:
+/// - First command (e.g., 'setup all'): 1-2 authentication prompts
+/// - Subsequent commands: 0 prompts (uses cached tokens)
+/// - Token refresh: Automatic when within 5 minutes of expiration
///
public class AuthenticationService
{
@@ -37,12 +59,12 @@ public AuthenticationService(ILogger logger)
/// Optional client ID for authentication. If not provided, uses PowerShell client ID
/// Optional explicit scopes to request. If not provided, uses .default scope pattern
public async Task GetAccessTokenAsync(
- string resourceUrl,
- string? tenantId = null,
- bool forceRefresh = false,
+ string resourceUrl,
+ string? tenantId = null,
+ bool forceRefresh = false,
string? clientId = null,
IEnumerable? scopes = null,
- bool useInteractiveBrowser = false)
+ bool useInteractiveBrowser = true)
{
// Build cache key based on resource and tenant only
// Azure AD returns tokens with all consented scopes regardless of which scopes are requested,
@@ -338,12 +360,12 @@ private bool IsTokenExpired(TokenInfo token)
/// Optional client ID for authentication. If not provided, uses PowerShell client ID
/// Access token with the requested scopes
public async Task GetAccessTokenWithScopesAsync(
- string resourceAppId,
- IEnumerable scopes,
- string? tenantId = null,
+ string resourceAppId,
+ IEnumerable scopes,
+ string? tenantId = null,
bool forceRefresh = false,
string? clientId = null,
- bool useInteractiveBrowser = false)
+ bool useInteractiveBrowser = true)
{
if (string.IsNullOrWhiteSpace(resourceAppId))
throw new ArgumentException("Resource App ID cannot be empty", nameof(resourceAppId));
diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/AzureWebAppCreator.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/AzureWebAppCreator.cs
index dcf3c354..41f284d0 100644
--- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/AzureWebAppCreator.cs
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/AzureWebAppCreator.cs
@@ -33,13 +33,15 @@ public async Task CreateWebAppAsync(
try
{
ArmClient armClient;
- // Use DefaultAzureCredential with InteractiveBrowserCredential excluded to avoid
- // Windows Authentication Broker (WAM) issues in console apps.
- // Users should run 'az login' before using this command.
- // See GitHub issues #146 and #151.
+ // Use DefaultAzureCredential which tries credentials in this order:
+ // 1. Environment variables
+ // 2. Managed Identity
+ // 3. Visual Studio / VS Code
+ // 4. Azure CLI (az login)
+ // 5. Interactive Browser (if needed)
var credentialOptions = new DefaultAzureCredentialOptions
{
- ExcludeInteractiveBrowserCredential = true
+ ExcludeInteractiveBrowserCredential = false
};
if (!string.IsNullOrWhiteSpace(tenantId))
diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/GraphApiService.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/GraphApiService.cs
index 6e49d8a5..d1608d52 100644
--- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/GraphApiService.cs
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/GraphApiService.cs
@@ -182,20 +182,47 @@ public GraphApiService(ILogger logger, CommandExecutor executor
private async Task EnsureGraphHeadersAsync(string tenantId, CancellationToken ct = default, IEnumerable? scopes = null)
{
- // When specific scopes are required, token provider must be configured
- if (scopes != null && _tokenProvider == null)
+ // Authentication Strategy:
+ // 1. If specific scopes required AND token provider configured: Use interactive browser auth (cached)
+ // 2. Otherwise: Use Azure CLI token (requires 'az login')
+ // This dual approach minimizes auth prompts while supporting special scope requirements
+
+ string? token;
+
+ if (scopes != null && _tokenProvider != null)
+ {
+ // Use token provider with delegated scopes (interactive browser auth with caching)
+ _logger.LogDebug("Acquiring Graph token with specific scopes via token provider: {Scopes}", string.Join(", ", scopes));
+ token = await _tokenProvider.GetMgGraphAccessTokenAsync(tenantId, scopes, false, CustomClientAppId, ct);
+
+ if (string.IsNullOrWhiteSpace(token))
+ {
+ _logger.LogError("Failed to acquire Graph token with scopes: {Scopes}", string.Join(", ", scopes));
+ return false;
+ }
+
+ _logger.LogDebug("Successfully acquired Graph token with specific scopes (cached or new)");
+ }
+ else if (scopes != null && _tokenProvider == null)
{
+ // Scopes required but no token provider - this is a configuration issue
_logger.LogError("Token provider is not configured, but specific scopes are required: {Scopes}", string.Join(", ", scopes));
return false;
}
+ else
+ {
+ // Use Azure CLI token (default fallback for operations that don't need special scopes)
+ _logger.LogDebug("Acquiring Graph token via Azure CLI (no specific scopes required)");
+ token = await GetGraphAccessTokenAsync(tenantId, ct);
- // When specific scopes are required, use custom client app if configured
- // CustomClientAppId should be set by callers who have access to config
- var token = (scopes != null && _tokenProvider != null)
- ? await _tokenProvider.GetMgGraphAccessTokenAsync(tenantId, scopes, false, CustomClientAppId, ct)
- : await GetGraphAccessTokenAsync(tenantId, ct);
-
- if (string.IsNullOrWhiteSpace(token)) return false;
+ if (string.IsNullOrWhiteSpace(token))
+ {
+ _logger.LogError("Failed to acquire Graph token via Azure CLI. Ensure 'az login' is completed.");
+ return false;
+ }
+
+ _logger.LogDebug("Successfully acquired Graph token via Azure CLI");
+ }
// Trim token to remove any newline characters that may cause header validation errors
token = token.Trim();
diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/InteractiveGraphAuthService.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/InteractiveGraphAuthService.cs
index 7112c1fe..38c9d62b 100644
--- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/InteractiveGraphAuthService.cs
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/InteractiveGraphAuthService.cs
@@ -83,28 +83,24 @@ public Task GetAuthenticatedGraphClientAsync(
_logger.LogInformation("Please sign in with an account that has Global Administrator or similar privileges.");
_logger.LogInformation("");
- // Try browser authentication first
- GraphServiceClient? graphClient = null;
- bool shouldTryDeviceCode = false;
-
+ // Use browser authentication (MsalBrowserCredential handles WAM on Windows and browser on other platforms)
try
{
- // Use MsalBrowserCredential which handles WAM on Windows and browser on other platforms
// Pass null for redirectUri to let MsalBrowserCredential decide based on platform
var browserCredential = new MsalBrowserCredential(
_clientAppId,
tenantId,
redirectUri: null, // Let MsalBrowserCredential use WAM on Windows
_logger);
-
+
_logger.LogInformation("Authenticating to Microsoft Graph...");
_logger.LogInformation("IMPORTANT: You must grant consent for all required permissions.");
_logger.LogInformation("Required permissions are defined in AuthenticationConstants.RequiredClientAppPermissions.");
_logger.LogInformation($"See {ConfigConstants.Agent365CliDocumentationUrl} for the complete list.");
_logger.LogInformation("");
-
+
// Create GraphServiceClient with the credential
- graphClient = new GraphServiceClient(browserCredential, RequiredScopes);
+ var graphClient = new GraphServiceClient(browserCredential, RequiredScopes);
_logger.LogInformation("Successfully authenticated to Microsoft Graph!");
_logger.LogInformation("");
@@ -112,24 +108,26 @@ public Task GetAuthenticatedGraphClientAsync(
// Cache the client for reuse
_cachedClient = graphClient;
_cachedTenantId = tenantId;
-
+
return Task.FromResult(graphClient);
}
catch (MsalAuthenticationFailedException ex) when (ex.Message.Contains("invalid_grant"))
{
- // Most specific: permissions issue - don't try fallback
+ // Permissions issue
ThrowInsufficientPermissionsException(ex);
throw; // Unreachable but required for compiler
}
catch (MsalAuthenticationFailedException ex) when (
- ex.Message.Contains("localhost") ||
+ ex.Message.Contains("localhost") ||
ex.Message.Contains("connection") ||
ex.Message.Contains("redirect_uri"))
{
- // Infrastructure issue - try device code fallback
- _logger.LogWarning("Browser authentication failed due to connectivity issue, falling back to device code flow...");
- _logger.LogInformation("");
- shouldTryDeviceCode = true;
+ // Infrastructure/connectivity issue
+ _logger.LogError("Browser authentication failed due to connectivity issue: {Message}", ex.Message);
+ throw new GraphApiException(
+ "Browser authentication",
+ $"Authentication failed due to connectivity issue: {ex.Message}. Please ensure you have network connectivity.",
+ isPermissionIssue: false);
}
catch (Microsoft.Identity.Client.MsalServiceException ex) when (ex.ErrorCode == "access_denied")
{
@@ -147,75 +145,6 @@ public Task GetAuthenticatedGraphClientAsync(
$"Authentication failed: {ex.Message}",
isPermissionIssue: false);
}
-
- // DeviceCodeCredential fallback safety net:
- // If browser authentication fails due to infrastructure issues (localhost connectivity,
- // redirect URI problems, etc.), this fallback provides an alternative authentication path.
- // The device code flow displays a code that users can enter at microsoft.com/devicelogin,
- // which works even in environments where browser-based OAuth redirects fail.
- // This fallback is preserved even after the WAM fix (GitHub issues #146, #151).
- if (shouldTryDeviceCode)
- {
- try
- {
- var deviceCodeCredential = new DeviceCodeCredential(new DeviceCodeCredentialOptions
- {
- TenantId = tenantId,
- ClientId = _clientAppId,
- AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
- TokenCachePersistenceOptions = new TokenCachePersistenceOptions
- {
- Name = AuthenticationConstants.ApplicationName
- },
- DeviceCodeCallback = (code, cancellation) =>
- {
- _logger.LogInformation("");
- _logger.LogInformation("=============================================================");
- _logger.LogInformation("DEVICE CODE AUTHENTICATION");
- _logger.LogInformation("=============================================================");
- _logger.LogInformation("");
- _logger.LogInformation("To sign in, use a web browser to open the page:");
- _logger.LogInformation(" {0}", code.VerificationUri);
- _logger.LogInformation("");
- _logger.LogInformation("And enter the code:");
- _logger.LogInformation(" {0}", code.UserCode);
- _logger.LogInformation("");
- _logger.LogInformation("=============================================================");
- _logger.LogInformation("");
- return Task.CompletedTask;
- }
- });
-
- graphClient = new GraphServiceClient(deviceCodeCredential, RequiredScopes);
-
- _logger.LogInformation("Successfully authenticated to Microsoft Graph!");
- _logger.LogInformation("");
-
- // Cache the client for reuse
- _cachedClient = graphClient;
- _cachedTenantId = tenantId;
-
- return Task.FromResult(graphClient);
- }
- catch (Azure.Identity.AuthenticationFailedException ex) when (ex.Message.Contains("invalid_grant"))
- {
- // Permissions issue in device code flow
- ThrowInsufficientPermissionsException(ex);
- throw; // Unreachable but required for compiler
- }
- catch (Exception ex)
- {
- _logger.LogError("Device code authentication failed: {Message}", ex.Message);
- throw new GraphApiException(
- "Device code authentication",
- $"Authentication failed: {ex.Message}. Ensure you have required permissions and completed authentication flow.");
- }
- }
-
- // If browser auth succeeded, we already returned at line 83
- // If device code was attempted and succeeded, we already returned above
- // This line is truly unreachable in normal flow
- throw new InvalidOperationException("Authentication failed unexpectedly.");
}
private void ThrowInsufficientPermissionsException(Exception innerException)
diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/Internal/MicrosoftGraphTokenProvider.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/Internal/MicrosoftGraphTokenProvider.cs
index f808925f..40af220f 100644
--- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/Internal/MicrosoftGraphTokenProvider.cs
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/Internal/MicrosoftGraphTokenProvider.cs
@@ -17,6 +17,20 @@ namespace Microsoft.Agents.A365.DevTools.Cli.Services;
///
/// Implements Microsoft Graph token acquisition via PowerShell Microsoft.Graph module.
+///
+/// AUTHENTICATION METHOD:
+/// - Uses Connect-MgGraph (PowerShell) for Graph API authentication
+/// - Default: Interactive browser authentication (useDeviceCode=false)
+/// - Device Code Flow: Available but NOT used by default (DCF discouraged in production)
+///
+/// TOKEN CACHING:
+/// - In-memory cache per CLI process: Tokens cached by (tenant + clientId + scopes)
+/// - Persistent cache: PowerShell module manages its own session cache
+/// - Reduces repeated Connect-MgGraph prompts during multi-step operations
+///
+/// USAGE:
+/// - Called by GraphApiService when specific scopes are required
+/// - Integrates with overall CLI authentication strategy (1-2 total prompts)
///
public sealed class MicrosoftGraphTokenProvider : IMicrosoftGraphTokenProvider, IDisposable
{
@@ -63,7 +77,7 @@ public MicrosoftGraphTokenProvider(
public async Task GetMgGraphAccessTokenAsync(
string tenantId,
IEnumerable scopes,
- bool useDeviceCode = true,
+ bool useDeviceCode = false,
string? clientAppId = null,
CancellationToken ct = default)
{
@@ -180,7 +194,8 @@ private static string BuildPowerShellScript(string tenantId, string[] scopes, bo
var escapedTenantId = CommandStringHelper.EscapePowerShellString(tenantId);
var scopesArray = BuildScopesArray(scopes);
- // Use -UseDeviceCode for CLI-friendly authentication (no browser popup/download)
+ // Use interactive browser auth by default (useDeviceCode=false)
+ // If useDeviceCode=true, use device code flow instead
var authMethod = useDeviceCode ? "-UseDeviceCode" : "";
// Include -ClientId parameter if provided (ensures authentication uses the custom client app)