From 17ae8a7c2606136ac59f6bc0a6d2ad7d6936fa9c Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Thu, 28 May 2026 00:03:23 +0000 Subject: [PATCH 1/8] Add --reset flag to azd auth login to clear stale auth cache When users encounter AADSTS700082 (expired refresh token) errors after re-authenticating with azd auth login, the stale MSAL cache and credential files prevent the new login from taking effect. The --reset flag clears all cached authentication data before performing the login flow: - MSAL token cache (auth/msal/) - Credential cache files - auth.json (current user config) - auth.claims - Subscription cache Fixes #7541 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/cmd/auth_login.go | 19 ++++++ cli/azd/cmd/testdata/TestFigSpec.ts | 4 ++ .../testdata/TestUsage-azd-auth-login.snap | 1 + cli/azd/pkg/auth/manager.go | 37 +++++++++++ cli/azd/pkg/auth/manager_coverage_test.go | 63 +++++++++++++++++++ 5 files changed, 124 insertions(+) diff --git a/cli/azd/cmd/auth_login.go b/cli/azd/cmd/auth_login.go index 9c9ca9cb99c..c452c61d441 100644 --- a/cli/azd/cmd/auth_login.go +++ b/cli/azd/cmd/auth_login.go @@ -72,6 +72,7 @@ type loginFlags struct { onlyCheckStatus bool browser bool managedIdentity bool + reset bool useDeviceCode boolPtr tenantID string clientID string @@ -138,6 +139,12 @@ const ( func (lf *loginFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) { local.BoolVar(&lf.onlyCheckStatus, "check-status", false, "Checks the log-in status instead of logging in.") + local.BoolVar( + &lf.reset, + "reset", + false, + "Clear all cached authentication data before logging in.", + ) f := local.VarPF( &lf.useDeviceCode, "use-device-code", @@ -214,6 +221,8 @@ func newLoginCmd(parent string) *cobra.Command { To log in using a managed identity, pass --managed-identity, which will use the system assigned managed identity. To use a user assigned managed identity, pass --client-id in addition to --managed-identity with the client id of the user assigned managed identity you wish to use. + + To clear all cached authentication data (such as stale tokens) before logging in, pass --reset. `), Annotations: map[string]string{ loginCmdParentAnnotation: parent, @@ -255,6 +264,16 @@ func newAuthLoginAction( } func (la *loginAction) Run(ctx context.Context) (*actions.ActionResult, error) { + if la.flags.reset { + if err := la.authManager.CleanAllAuthCache(); err != nil { + return nil, fmt.Errorf("clearing auth cache: %w", err) + } + if err := la.accountSubManager.ClearSubscriptions(ctx); err != nil { + return nil, fmt.Errorf("clearing subscriptions cache: %w", err) + } + la.console.Message(ctx, "Authentication data cleared.") + } + loginMode, err := la.authManager.Mode() if err != nil { return nil, err diff --git a/cli/azd/cmd/testdata/TestFigSpec.ts b/cli/azd/cmd/testdata/TestFigSpec.ts index 076ab4c0d02..cea5c0d0f86 100644 --- a/cli/azd/cmd/testdata/TestFigSpec.ts +++ b/cli/azd/cmd/testdata/TestFigSpec.ts @@ -3645,6 +3645,10 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['--reset'], + description: 'Clear all cached authentication data before logging in.', + }, { name: ['--tenant-id'], description: 'The tenant id or domain name to authenticate with.', diff --git a/cli/azd/cmd/testdata/TestUsage-azd-auth-login.snap b/cli/azd/cmd/testdata/TestUsage-azd-auth-login.snap index 91c46723c0f..98aa9b7a654 100644 --- a/cli/azd/cmd/testdata/TestUsage-azd-auth-login.snap +++ b/cli/azd/cmd/testdata/TestUsage-azd-auth-login.snap @@ -12,6 +12,7 @@ Flags --federated-credential-provider string : The provider to use to acquire a federated token to authenticate with. Supported values: github, azure-pipelines, oidc --managed-identity : Use a managed identity to authenticate. --redirect-port int : Choose the port to be used as part of the redirect URI during interactive login. + --reset : Clear all cached authentication data before logging in. --tenant-id string : The tenant id or domain name to authenticate with. --use-device-code : When true, log in by using a device code instead of a browser. diff --git a/cli/azd/pkg/auth/manager.go b/cli/azd/pkg/auth/manager.go index f91566c8c3c..2f018e58956 100644 --- a/cli/azd/pkg/auth/manager.go +++ b/cli/azd/pkg/auth/manager.go @@ -1093,6 +1093,43 @@ func (m *Manager) Logout(ctx context.Context) error { return nil } +// CleanAllAuthCache removes all cached authentication data, including MSAL token cache files, +// credential cache files, auth config, and claims. This provides a clean slate for re-authentication, +// which resolves stale token issues (e.g. AADSTS700082 expired refresh tokens). +func (m *Manager) CleanAllAuthCache() error { + cfgRoot, err := config.GetUserConfigDir() + if err != nil { + return fmt.Errorf("getting config dir: %w", err) + } + + // Remove the entire auth directory (contains msal/ cache and credential files) + authRoot := filepath.Join(cfgRoot, "auth") + if err := os.RemoveAll(authRoot); err != nil { + return fmt.Errorf("removing auth directory: %w", err) + } + + // Remove auth.json (current user config) + authCfgFile := filepath.Join(cfgRoot, authConfigFileName) + if err := os.Remove(authCfgFile); err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("removing auth config: %w", err) + } + + // Remove claims file + if claimsFile, err := claimsFilePath(); err == nil { + if err := os.Remove(claimsFile); err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("removing claims file: %w", err) + } + } + + // Recreate the auth/msal directory structure so subsequent login can proceed + cacheRoot := filepath.Join(authRoot, "msal") + if err := os.MkdirAll(cacheRoot, osutil.PermissionDirectoryOwnerOnly); err != nil { + return fmt.Errorf("recreating msal cache directory: %w", err) + } + + return nil +} + func (m *Manager) UseExternalAuth() bool { return m.externalAuthCfg.Endpoint != "" && m.externalAuthCfg.Key != "" } diff --git a/cli/azd/pkg/auth/manager_coverage_test.go b/cli/azd/pkg/auth/manager_coverage_test.go index ea7700ff8cd..85d58f2ef43 100644 --- a/cli/azd/pkg/auth/manager_coverage_test.go +++ b/cli/azd/pkg/auth/manager_coverage_test.go @@ -581,6 +581,69 @@ func TestLogout_NotLoggedIn(t *testing.T) { require.NoError(t, err) } +// --- CleanAllAuthCache --- + +func TestCleanAllAuthCache(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("AZD_CONFIG_DIR", tempDir) + + // Create auth directory structure with files + authDir := tempDir + "/auth" + msalDir := authDir + "/msal" + require.NoError(t, os.MkdirAll(msalDir, 0700)) + + // Create MSAL cache file + require.NoError(t, os.WriteFile(msalDir+"/cache.json", []byte(`{"tokens":"stale"}`), 0600)) + + // Create credential cache file + require.NoError(t, os.WriteFile(authDir+"/credtenant.client.json", []byte(`{"secret":"old"}`), 0600)) + + // Create auth.json + require.NoError(t, os.WriteFile(tempDir+"/auth.json", []byte(`{"auth.account.currentUser":{}}`), 0600)) + + // Create auth.claims + require.NoError(t, os.WriteFile(tempDir+"/auth.claims", []byte(`claims-data`), 0600)) + + m := &Manager{} + err := m.CleanAllAuthCache() + require.NoError(t, err) + + // auth.json should be removed + _, err = os.Stat(tempDir + "/auth.json") + assert.True(t, os.IsNotExist(err), "auth.json should be deleted") + + // auth.claims should be removed + _, err = os.Stat(tempDir + "/auth.claims") + assert.True(t, os.IsNotExist(err), "auth.claims should be deleted") + + // Old MSAL cache files should be gone + _, err = os.Stat(msalDir + "/cache.json") + assert.True(t, os.IsNotExist(err), "MSAL cache should be deleted") + + // Old credential files should be gone + _, err = os.Stat(authDir + "/credtenant.client.json") + assert.True(t, os.IsNotExist(err), "credential cache should be deleted") + + // auth/msal directory should be recreated (empty) + info, err := os.Stat(msalDir) + require.NoError(t, err, "msal directory should be recreated") + assert.True(t, info.IsDir()) +} + +func TestCleanAllAuthCache_NoExistingFiles(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("AZD_CONFIG_DIR", tempDir) + + m := &Manager{} + err := m.CleanAllAuthCache() + require.NoError(t, err, "should succeed even when no auth files exist") + + // auth/msal directory should still be created + info, err := os.Stat(tempDir + "/auth/msal") + require.NoError(t, err) + assert.True(t, info.IsDir()) +} + // --- EnsureLoggedInCredential --- func TestEnsureLoggedInCredential_Success(t *testing.T) { From a34f8aa2fb8fb5eb206091bc9a9c75c9c97ef400 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Thu, 28 May 2026 00:12:12 +0000 Subject: [PATCH 2/8] Add authentication documentation for azd auth login Documents all supported authentication methods including interactive browser, device code, service principal (secret/certificate), federated credentials (GitHub Actions, Azure Pipelines, generic OIDC), managed identity, delegated auth via Azure CLI, and external authentication. Also documents the new --reset flag, when to use it, what files it clears, and how authentication state is stored on disk. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/docs/authentication.md | 237 +++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 cli/azd/docs/authentication.md diff --git a/cli/azd/docs/authentication.md b/cli/azd/docs/authentication.md new file mode 100644 index 00000000000..269eb317d65 --- /dev/null +++ b/cli/azd/docs/authentication.md @@ -0,0 +1,237 @@ +# Authentication + +This document covers the authentication methods supported by `azd auth login` and how `azd` manages +authentication state. + +## Authentication methods + +### Interactive browser login (default) + +The default method. Running `azd auth login` opens a browser window for you to sign in with your +Microsoft Entra ID (Azure AD) account. + +```bash +azd auth login +``` + +To target a specific tenant: + +```bash +azd auth login --tenant-id +``` + +To choose the local port used for the redirect URI during the browser flow: + +```bash +azd auth login --redirect-port 8080 +``` + +### Device code login + +Use device code flow when a browser is not available on the current machine (e.g. SSH sessions, +containers, Codespaces in a browser). + +```bash +azd auth login --use-device-code +``` + +### Service principal with client secret + +Authenticate as a service principal using a client secret. Both `--client-id` and `--tenant-id` are +required. + +```bash +azd auth login \ + --client-id \ + --tenant-id \ + --client-secret +``` + +If you pass `--client-secret` with an empty value, `azd` prompts you to enter the secret +interactively (useful to avoid leaking secrets in shell history). + +### Service principal with client certificate + +Authenticate using a PEM-encoded certificate file. + +```bash +azd auth login \ + --client-id \ + --tenant-id \ + --client-certificate /path/to/cert.pem +``` + +### Federated credentials (OIDC) + +Federated token providers allow authentication without secrets in CI/CD environments using +OpenID Connect (OIDC). + +#### GitHub Actions + +```bash +azd auth login \ + --client-id \ + --tenant-id \ + --federated-credential-provider github +``` + +The `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN` environment variables must be +available (GitHub sets these automatically when `id-token: write` is granted in the workflow). + +#### Azure Pipelines + +```bash +azd auth login \ + --client-id \ + --tenant-id \ + --federated-credential-provider azure-pipelines +``` + +When using `azure-pipelines`, the following environment variables are read automatically if +`--client-id` or `--tenant-id` are not provided: + +| Variable | Description | +|---|---| +| `AZURESUBSCRIPTION_CLIENT_ID` | Client ID of the service connection | +| `AZURESUBSCRIPTION_TENANT_ID` | Tenant ID of the service connection | +| `AZURESUBSCRIPTION_SERVICE_CONNECTION_ID` | Service connection ID (required) | +| `SYSTEM_ACCESSTOKEN` | Pipeline system access token (must be mapped via `env`) | + +#### Generic OIDC + +For other OIDC-compatible providers: + +```bash +azd auth login \ + --client-id \ + --tenant-id \ + --federated-credential-provider oidc +``` + +### Managed identity + +Authenticate using a managed identity when running on an Azure compute resource (VMs, App Service, +Container Apps, etc.). + +```bash +# System-assigned managed identity +azd auth login --managed-identity + +# User-assigned managed identity +azd auth login --managed-identity --client-id +``` + +### Delegated authentication (Azure CLI) + +You can configure `azd` to delegate authentication to the Azure CLI (`az`) instead of managing +credentials itself. This is useful when `azd` does not yet support your preferred authentication +method. + +```bash +azd config set auth.useAzCliAuth true +``` + +When this is set, `azd auth login` detects the delegated mode and offers to switch back to +built-in authentication. To authenticate, use `az login` directly. + +### External authentication + +When `azd` is launched by a host tool (e.g. the VS Code extension), the host can provide +authentication by setting the `AZD_AUTH_ENDPOINT` and `AZD_AUTH_KEY` environment variables. In this +mode, `azd` proxies all token requests to the host process. + +For full details on the external authentication protocol, see +[External Authentication](external-authentication.md). + +## Checking login status + +To verify whether you are currently logged in without triggering a new login flow: + +```bash +azd auth login --check-status +``` + +This prints the current authentication status and exits. Use `--output json` for machine-readable +output that includes the token expiration time. + +## Logging out + +To sign out and remove cached authentication data: + +```bash +azd auth logout +``` + +This removes the current user from the MSAL cache, deletes stored service principal credentials, +and clears the subscriptions cache. + +## Resetting authentication state (`--reset`) + +> **Added in:** [#7541](https://github.com/Azure/azure-dev/issues/7541) + +In rare cases, `azd` may report an expired or invalid token error (e.g. `AADSTS700082: The refresh +token has expired due to inactivity`) even immediately after a successful `azd auth login`. This +happens because stale data in the local MSAL token cache or credential files can interfere with +the new login session. + +The `--reset` flag performs a complete cleanup of all locally cached authentication data **before** +logging in, giving you a clean slate: + +```bash +azd auth login --reset +``` + +### What `--reset` clears + +| Item | Path | Description | +|---|---|---| +| MSAL token cache | `~/.azd/auth/msal/` | Cached access and refresh tokens from MSAL | +| Credential cache | `~/.azd/auth/` | Stored service principal secrets and certificates | +| Auth config | `~/.azd/auth.json` | Current user identity metadata | +| Claims file | `~/.azd/auth.claims` | Cached claims from previous login | +| Subscription cache | `~/.azd/subscriptions.cache` | Cached list of accessible subscriptions | + +After clearing, the directory structure is recreated and the normal login flow proceeds. This is +equivalent to manually deleting these files and then running `azd auth login`. + +### When to use `--reset` + +Use `--reset` when: + +- You see `AADSTS700082` or similar stale-token errors right after logging in successfully +- `azd` commands fail with authentication errors that persist across multiple `azd auth login` + attempts +- You want to ensure a completely fresh authentication state (e.g. after switching tenants + or accounts) + +The flag can be combined with any other login method: + +```bash +# Reset and log in interactively +azd auth login --reset + +# Reset and log in with device code +azd auth login --reset --use-device-code + +# Reset and log in as a service principal +azd auth login --reset --client-id --tenant-id --client-secret +``` + +## How authentication state is stored + +`azd` stores authentication data under the user configuration directory (default `~/.azd/`, +overridable via `AZD_CONFIG_DIR`): + +```text +~/.azd/ +├── auth.json # Current user identity (home account ID, client/tenant IDs) +├── auth.claims # Cached claims for re-login +├── subscriptions.cache # Cached Azure subscriptions +└── auth/ + └── msal/ + ├── cache*.json # MSAL token cache (access/refresh tokens) + └── cred*.json # Service principal credential cache +``` + +On Windows, the MSAL cache is encrypted using `CryptProtectData` and stored as `.bin` files instead +of `.json`. On all platforms, auth files are ACL'd to be readable only by the current user. From 353c17b82151749143481e92f2c99096c7e0d846 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Thu, 28 May 2026 00:27:30 +0000 Subject: [PATCH 3/8] fix: address Copilot review feedback (iteration 1) - Reject --reset + --check-status flag combination - Use cfgRoot directly for claims path instead of ignoring claimsFilePath errors - Use filepath.Join and osutil permission constants in tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/cmd/auth_login.go | 4 +++ cli/azd/pkg/auth/manager.go | 7 +++--- cli/azd/pkg/auth/manager_coverage_test.go | 30 ++++++++++++++--------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/cli/azd/cmd/auth_login.go b/cli/azd/cmd/auth_login.go index c452c61d441..12533f27c6e 100644 --- a/cli/azd/cmd/auth_login.go +++ b/cli/azd/cmd/auth_login.go @@ -264,6 +264,10 @@ func newAuthLoginAction( } func (la *loginAction) Run(ctx context.Context) (*actions.ActionResult, error) { + if la.flags.reset && la.flags.onlyCheckStatus { + return nil, errors.New("cannot use --reset with --check-status") + } + if la.flags.reset { if err := la.authManager.CleanAllAuthCache(); err != nil { return nil, fmt.Errorf("clearing auth cache: %w", err) diff --git a/cli/azd/pkg/auth/manager.go b/cli/azd/pkg/auth/manager.go index 2f018e58956..59be302404f 100644 --- a/cli/azd/pkg/auth/manager.go +++ b/cli/azd/pkg/auth/manager.go @@ -1115,10 +1115,9 @@ func (m *Manager) CleanAllAuthCache() error { } // Remove claims file - if claimsFile, err := claimsFilePath(); err == nil { - if err := os.Remove(claimsFile); err != nil && !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("removing claims file: %w", err) - } + claimsFile := filepath.Join(cfgRoot, "auth.claims") + if err := os.Remove(claimsFile); err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("removing claims file: %w", err) } // Recreate the auth/msal directory structure so subsequent login can proceed diff --git a/cli/azd/pkg/auth/manager_coverage_test.go b/cli/azd/pkg/auth/manager_coverage_test.go index 85d58f2ef43..7e90636fde2 100644 --- a/cli/azd/pkg/auth/manager_coverage_test.go +++ b/cli/azd/pkg/auth/manager_coverage_test.go @@ -7,6 +7,7 @@ import ( "context" "errors" "os" + "path/filepath" "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore" @@ -14,6 +15,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/AzureAD/microsoft-authentication-library-for-go/apps/public" "github.com/azure/azure-dev/cli/azd/pkg/cloud" + "github.com/azure/azure-dev/cli/azd/pkg/osutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -588,40 +590,44 @@ func TestCleanAllAuthCache(t *testing.T) { t.Setenv("AZD_CONFIG_DIR", tempDir) // Create auth directory structure with files - authDir := tempDir + "/auth" - msalDir := authDir + "/msal" - require.NoError(t, os.MkdirAll(msalDir, 0700)) + authDir := filepath.Join(tempDir, "auth") + msalDir := filepath.Join(authDir, "msal") + require.NoError(t, os.MkdirAll(msalDir, osutil.PermissionDirectoryOwnerOnly)) // Create MSAL cache file - require.NoError(t, os.WriteFile(msalDir+"/cache.json", []byte(`{"tokens":"stale"}`), 0600)) + require.NoError(t, os.WriteFile( + filepath.Join(msalDir, "cache.json"), []byte(`{"tokens":"stale"}`), osutil.PermissionFileOwnerOnly)) // Create credential cache file - require.NoError(t, os.WriteFile(authDir+"/credtenant.client.json", []byte(`{"secret":"old"}`), 0600)) + require.NoError(t, os.WriteFile( + filepath.Join(authDir, "credtenant.client.json"), []byte(`{"secret":"old"}`), osutil.PermissionFileOwnerOnly)) // Create auth.json - require.NoError(t, os.WriteFile(tempDir+"/auth.json", []byte(`{"auth.account.currentUser":{}}`), 0600)) + require.NoError(t, os.WriteFile( + filepath.Join(tempDir, "auth.json"), []byte(`{"auth.account.currentUser":{}}`), osutil.PermissionFileOwnerOnly)) // Create auth.claims - require.NoError(t, os.WriteFile(tempDir+"/auth.claims", []byte(`claims-data`), 0600)) + require.NoError(t, os.WriteFile( + filepath.Join(tempDir, "auth.claims"), []byte(`claims-data`), osutil.PermissionFileOwnerOnly)) m := &Manager{} err := m.CleanAllAuthCache() require.NoError(t, err) // auth.json should be removed - _, err = os.Stat(tempDir + "/auth.json") + _, err = os.Stat(filepath.Join(tempDir, "auth.json")) assert.True(t, os.IsNotExist(err), "auth.json should be deleted") // auth.claims should be removed - _, err = os.Stat(tempDir + "/auth.claims") + _, err = os.Stat(filepath.Join(tempDir, "auth.claims")) assert.True(t, os.IsNotExist(err), "auth.claims should be deleted") // Old MSAL cache files should be gone - _, err = os.Stat(msalDir + "/cache.json") + _, err = os.Stat(filepath.Join(msalDir, "cache.json")) assert.True(t, os.IsNotExist(err), "MSAL cache should be deleted") // Old credential files should be gone - _, err = os.Stat(authDir + "/credtenant.client.json") + _, err = os.Stat(filepath.Join(authDir, "credtenant.client.json")) assert.True(t, os.IsNotExist(err), "credential cache should be deleted") // auth/msal directory should be recreated (empty) @@ -639,7 +645,7 @@ func TestCleanAllAuthCache_NoExistingFiles(t *testing.T) { require.NoError(t, err, "should succeed even when no auth files exist") // auth/msal directory should still be created - info, err := os.Stat(tempDir + "/auth/msal") + info, err := os.Stat(filepath.Join(tempDir, "auth", "msal")) require.NoError(t, err) assert.True(t, info.IsDir()) } From 485c410927210e23816a471f0396e4e65d22f0a8 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Thu, 28 May 2026 05:16:13 +0000 Subject: [PATCH 4/8] fix: use typed sentinel error for flag conflict Replace bare errors.New with fmt.Errorf wrapping internal.ErrInvalidFlagCombination to satisfy Test_RunMethodsNoBareErrors CI check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/cmd/auth_login.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/cmd/auth_login.go b/cli/azd/cmd/auth_login.go index 12533f27c6e..cda5ba7daed 100644 --- a/cli/azd/cmd/auth_login.go +++ b/cli/azd/cmd/auth_login.go @@ -265,7 +265,7 @@ func newAuthLoginAction( func (la *loginAction) Run(ctx context.Context) (*actions.ActionResult, error) { if la.flags.reset && la.flags.onlyCheckStatus { - return nil, errors.New("cannot use --reset with --check-status") + return nil, fmt.Errorf("cannot use --reset with --check-status: %w", internal.ErrInvalidFlagCombination) } if la.flags.reset { From 3fe4524c922a69988ab6dc8d5e83492d737d06a7 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Tue, 2 Jun 2026 20:42:40 +0000 Subject: [PATCH 5/8] refactor: auto-clear auth cache on re-login instead of --reset flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the explicit --reset flag with automatic detection: when the user is already logged in, azd auth login now clears all cached authentication data (MSAL tokens, credentials, auth.json, claims, subscriptions cache) before re-authenticating. This makes the fix for stale token issues transparent — no extra flags needed. If the user is not logged in, the normal login flow proceeds without clearing anything. Closes #7541 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/cmd/auth_login.go | 37 ++++++++----------- cli/azd/cmd/testdata/TestFigSpec.ts | 4 -- .../testdata/TestUsage-azd-auth-login.snap | 1 - cli/azd/docs/authentication.md | 35 +++++------------- 4 files changed, 25 insertions(+), 52 deletions(-) diff --git a/cli/azd/cmd/auth_login.go b/cli/azd/cmd/auth_login.go index cda5ba7daed..a8023743d66 100644 --- a/cli/azd/cmd/auth_login.go +++ b/cli/azd/cmd/auth_login.go @@ -72,7 +72,6 @@ type loginFlags struct { onlyCheckStatus bool browser bool managedIdentity bool - reset bool useDeviceCode boolPtr tenantID string clientID string @@ -139,12 +138,6 @@ const ( func (lf *loginFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) { local.BoolVar(&lf.onlyCheckStatus, "check-status", false, "Checks the log-in status instead of logging in.") - local.BoolVar( - &lf.reset, - "reset", - false, - "Clear all cached authentication data before logging in.", - ) f := local.VarPF( &lf.useDeviceCode, "use-device-code", @@ -222,7 +215,9 @@ func newLoginCmd(parent string) *cobra.Command { To use a user assigned managed identity, pass --client-id in addition to --managed-identity with the client id of the user assigned managed identity you wish to use. - To clear all cached authentication data (such as stale tokens) before logging in, pass --reset. + When already logged in, azd automatically clears cached authentication data (such as stale tokens) + before re-authenticating. This ensures a clean login state and prevents issues with expired or + corrupted cached credentials. `), Annotations: map[string]string{ loginCmdParentAnnotation: parent, @@ -264,20 +259,6 @@ func newAuthLoginAction( } func (la *loginAction) Run(ctx context.Context) (*actions.ActionResult, error) { - if la.flags.reset && la.flags.onlyCheckStatus { - return nil, fmt.Errorf("cannot use --reset with --check-status: %w", internal.ErrInvalidFlagCombination) - } - - if la.flags.reset { - if err := la.authManager.CleanAllAuthCache(); err != nil { - return nil, fmt.Errorf("clearing auth cache: %w", err) - } - if err := la.accountSubManager.ClearSubscriptions(ctx); err != nil { - return nil, fmt.Errorf("clearing subscriptions cache: %w", err) - } - la.console.Message(ctx, "Authentication data cleared.") - } - loginMode, err := la.authManager.Mode() if err != nil { return nil, err @@ -386,6 +367,18 @@ func (la *loginAction) Run(ctx context.Context) (*actions.ActionResult, error) { } } + // When already logged in, clear cached auth data before re-authenticating. + // This prevents issues with stale MSAL tokens or corrupted credential cache files + // that can cause AADSTS700082 errors even after a successful login. + if _, err := la.authManager.LogInDetails(ctx); err == nil { + if err := la.authManager.CleanAllAuthCache(); err != nil { + return nil, fmt.Errorf("clearing auth cache: %w", err) + } + if err := la.accountSubManager.ClearSubscriptions(ctx); err != nil { + return nil, fmt.Errorf("clearing subscriptions cache: %w", err) + } + } + if err := la.login(ctx); err != nil { return nil, err } diff --git a/cli/azd/cmd/testdata/TestFigSpec.ts b/cli/azd/cmd/testdata/TestFigSpec.ts index cea5c0d0f86..076ab4c0d02 100644 --- a/cli/azd/cmd/testdata/TestFigSpec.ts +++ b/cli/azd/cmd/testdata/TestFigSpec.ts @@ -3645,10 +3645,6 @@ const completionSpec: Fig.Spec = { }, ], }, - { - name: ['--reset'], - description: 'Clear all cached authentication data before logging in.', - }, { name: ['--tenant-id'], description: 'The tenant id or domain name to authenticate with.', diff --git a/cli/azd/cmd/testdata/TestUsage-azd-auth-login.snap b/cli/azd/cmd/testdata/TestUsage-azd-auth-login.snap index 98aa9b7a654..91c46723c0f 100644 --- a/cli/azd/cmd/testdata/TestUsage-azd-auth-login.snap +++ b/cli/azd/cmd/testdata/TestUsage-azd-auth-login.snap @@ -12,7 +12,6 @@ Flags --federated-credential-provider string : The provider to use to acquire a federated token to authenticate with. Supported values: github, azure-pipelines, oidc --managed-identity : Use a managed identity to authenticate. --redirect-port int : Choose the port to be used as part of the redirect URI during interactive login. - --reset : Clear all cached authentication data before logging in. --tenant-id string : The tenant id or domain name to authenticate with. --use-device-code : When true, log in by using a device code instead of a browser. diff --git a/cli/azd/docs/authentication.md b/cli/azd/docs/authentication.md index 269eb317d65..eab7dcd150c 100644 --- a/cli/azd/docs/authentication.md +++ b/cli/azd/docs/authentication.md @@ -165,7 +165,7 @@ azd auth logout This removes the current user from the MSAL cache, deletes stored service principal credentials, and clears the subscriptions cache. -## Resetting authentication state (`--reset`) +## Automatic authentication state cleanup on re-login > **Added in:** [#7541](https://github.com/Azure/azure-dev/issues/7541) @@ -174,14 +174,12 @@ token has expired due to inactivity`) even immediately after a successful `azd a happens because stale data in the local MSAL token cache or credential files can interfere with the new login session. -The `--reset` flag performs a complete cleanup of all locally cached authentication data **before** -logging in, giving you a clean slate: +To prevent this, `azd auth login` automatically detects when you are already logged in and clears +all locally cached authentication data before re-authenticating. This gives you a clean slate +without requiring any extra flags. If you are not currently logged in, `azd auth login` proceeds +directly with the normal login flow without clearing anything. -```bash -azd auth login --reset -``` - -### What `--reset` clears +### What is cleared on re-login | Item | Path | Description | |---|---|---| @@ -194,28 +192,15 @@ azd auth login --reset After clearing, the directory structure is recreated and the normal login flow proceeds. This is equivalent to manually deleting these files and then running `azd auth login`. -### When to use `--reset` +### When automatic cleanup helps -Use `--reset` when: +The automatic cleanup resolves situations where: - You see `AADSTS700082` or similar stale-token errors right after logging in successfully - `azd` commands fail with authentication errors that persist across multiple `azd auth login` attempts -- You want to ensure a completely fresh authentication state (e.g. after switching tenants - or accounts) - -The flag can be combined with any other login method: - -```bash -# Reset and log in interactively -azd auth login --reset - -# Reset and log in with device code -azd auth login --reset --use-device-code - -# Reset and log in as a service principal -azd auth login --reset --client-id --tenant-id --client-secret -``` +- You are switching tenants or accounts and want to ensure a completely fresh authentication + state ## How authentication state is stored From c65807f26b8fb8c9047a3cde8f70983d98770996 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Tue, 2 Jun 2026 22:19:10 +0000 Subject: [PATCH 6/8] feat: add AADSTS700082 error suggestion rule and pipeline tests - Add specific AADSTS700082 rule in error_suggestions.yaml with guidance about automatic cache clearing on re-login - Update generic AADSTS rule to mention auto-clearing behavior - Add end-to-end pipeline tests for both the specific and generic rules Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/pkg/errorhandler/pipeline_test.go | 30 +++++++++++++++++++++++ cli/azd/resources/error_suggestions.yaml | 13 +++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/cli/azd/pkg/errorhandler/pipeline_test.go b/cli/azd/pkg/errorhandler/pipeline_test.go index 5a0a8179d14..b16b804862d 100644 --- a/cli/azd/pkg/errorhandler/pipeline_test.go +++ b/cli/azd/pkg/errorhandler/pipeline_test.go @@ -714,3 +714,33 @@ func TestErrorSuggestionsYaml_LoadPipelineConfig(t *testing.T) { require.NotNil(t, pipeline) assert.NotEmpty(t, pipeline.rules, "pipeline must load rules from embedded YAML") } + +func TestErrorSuggestionsYaml_AADSTS700082_SpecificRule(t *testing.T) { + t.Parallel() + pipeline := NewErrorHandlerPipeline(nil) + + // AADSTS700082 should match the specific rule, not just the generic AADSTS one + result := pipeline.Process( + t.Context(), + errors.New("AADSTS700082: The refresh token has expired due to inactivity"), + ) + require.NotNil(t, result) + assert.Equal(t, "The refresh token has expired or been revoked.", result.Message) + assert.Contains(t, result.Suggestion, "azd auth login") + assert.Contains(t, result.Suggestion, "automatically clears stale cached tokens") +} + +func TestErrorSuggestionsYaml_AADSTS_GenericRule(t *testing.T) { + t.Parallel() + pipeline := NewErrorHandlerPipeline(nil) + + // A generic AADSTS error should match the broad rule and mention auto-clearing + result := pipeline.Process( + t.Context(), + errors.New("AADSTS530084: some other auth error"), + ) + require.NotNil(t, result) + assert.Equal(t, "Authentication with Azure failed.", result.Message) + assert.Contains(t, result.Suggestion, "azd auth login") + assert.Contains(t, result.Suggestion, "automatically clears cached authentication data") +} diff --git a/cli/azd/resources/error_suggestions.yaml b/cli/azd/resources/error_suggestions.yaml index df571c9b8c9..86b2ac75844 100644 --- a/cli/azd/resources/error_suggestions.yaml +++ b/cli/azd/resources/error_suggestions.yaml @@ -604,10 +604,21 @@ rules: # Text Pattern Rules — Broad/generic patterns (least specific, must be last) # ============================================================================ + - patterns: + - "AADSTS700082" + message: "The refresh token has expired or been revoked." + suggestion: >- + Run 'azd auth login' to sign in again. + When already logged in, azd automatically clears stale cached tokens before + re-authenticating. + - patterns: - "AADSTS" message: "Authentication with Azure failed." - suggestion: "Run 'azd auth login' to sign in again." + suggestion: >- + Run 'azd auth login' to sign in again. + When already logged in, azd automatically clears cached authentication data before + re-authenticating to prevent stale token issues. links: - url: "https://learn.microsoft.com/azure/developer/azure-developer-cli/reference#azd-auth-login" title: "azd auth login reference" From 80f64c79ce5d7d9e2f0b0335fa88cb2de45aaf5f Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Thu, 4 Jun 2026 00:23:02 +0000 Subject: [PATCH 7/8] fix: address review feedback from hemarina - Add links field to AADSTS700082 error suggestion rule - Broaden login-state gate: only skip cleanup on ErrNoCurrentUser, proceed with cleanup for any other result (including corrupted cache) - Add telemetry attribute for cache-clear failures - Update test to verify links in AADSTS700082 rule Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/cmd/auth_login.go | 12 ++++++++---- cli/azd/pkg/errorhandler/pipeline_test.go | 2 ++ cli/azd/resources/error_suggestions.yaml | 3 +++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/cli/azd/cmd/auth_login.go b/cli/azd/cmd/auth_login.go index a8023743d66..fe26fccd379 100644 --- a/cli/azd/cmd/auth_login.go +++ b/cli/azd/cmd/auth_login.go @@ -33,6 +33,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/tools/github" "github.com/spf13/cobra" "github.com/spf13/pflag" + "go.opentelemetry.io/otel/attribute" ) // The parent of the login command. @@ -367,14 +368,17 @@ func (la *loginAction) Run(ctx context.Context) (*actions.ActionResult, error) { } } - // When already logged in, clear cached auth data before re-authenticating. - // This prevents issues with stale MSAL tokens or corrupted credential cache files - // that can cause AADSTS700082 errors even after a successful login. - if _, err := la.authManager.LogInDetails(ctx); err == nil { + // When already logged in (or when login state cannot be determined due to corrupted cache), + // clear cached auth data before re-authenticating. This prevents issues with stale MSAL + // tokens or corrupted credential cache files that can cause AADSTS700082 errors even after + // a successful login. Only skip cleanup when definitively not logged in. + if _, err := la.authManager.LogInDetails(ctx); !errors.Is(err, auth.ErrNoCurrentUser) { if err := la.authManager.CleanAllAuthCache(); err != nil { + tracing.SetUsageAttributes(attribute.String("auth.cache_clear_failed", "auth")) return nil, fmt.Errorf("clearing auth cache: %w", err) } if err := la.accountSubManager.ClearSubscriptions(ctx); err != nil { + tracing.SetUsageAttributes(attribute.String("auth.cache_clear_failed", "subscriptions")) return nil, fmt.Errorf("clearing subscriptions cache: %w", err) } } diff --git a/cli/azd/pkg/errorhandler/pipeline_test.go b/cli/azd/pkg/errorhandler/pipeline_test.go index b16b804862d..6c242dd303c 100644 --- a/cli/azd/pkg/errorhandler/pipeline_test.go +++ b/cli/azd/pkg/errorhandler/pipeline_test.go @@ -728,6 +728,8 @@ func TestErrorSuggestionsYaml_AADSTS700082_SpecificRule(t *testing.T) { assert.Equal(t, "The refresh token has expired or been revoked.", result.Message) assert.Contains(t, result.Suggestion, "azd auth login") assert.Contains(t, result.Suggestion, "automatically clears stale cached tokens") + require.Len(t, result.Links, 1) + assert.Contains(t, result.Links[0].URL, "azd-auth-login") } func TestErrorSuggestionsYaml_AADSTS_GenericRule(t *testing.T) { diff --git a/cli/azd/resources/error_suggestions.yaml b/cli/azd/resources/error_suggestions.yaml index 86b2ac75844..02991ac6f44 100644 --- a/cli/azd/resources/error_suggestions.yaml +++ b/cli/azd/resources/error_suggestions.yaml @@ -611,6 +611,9 @@ rules: Run 'azd auth login' to sign in again. When already logged in, azd automatically clears stale cached tokens before re-authenticating. + links: + - url: "https://learn.microsoft.com/azure/developer/azure-developer-cli/reference#azd-auth-login" + title: "azd auth login reference" - patterns: - "AADSTS" From 7636d5721752eb1db7f085dc1f2eb9fa4b23ef94 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Fri, 5 Jun 2026 17:22:20 +0000 Subject: [PATCH 8/8] fix: skip auth cache cleanup for MI/SP re-logins MI and SP auth don't use MSAL refresh tokens, so the stale-token issue (AADSTS700082) doesn't apply to them. Skip the cleanup to avoid unnecessary cache clearing that could block an otherwise valid login. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/cmd/auth_login.go | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/cli/azd/cmd/auth_login.go b/cli/azd/cmd/auth_login.go index fe26fccd379..853d6a239c8 100644 --- a/cli/azd/cmd/auth_login.go +++ b/cli/azd/cmd/auth_login.go @@ -368,18 +368,23 @@ func (la *loginAction) Run(ctx context.Context) (*actions.ActionResult, error) { } } - // When already logged in (or when login state cannot be determined due to corrupted cache), - // clear cached auth data before re-authenticating. This prevents issues with stale MSAL - // tokens or corrupted credential cache files that can cause AADSTS700082 errors even after - // a successful login. Only skip cleanup when definitively not logged in. - if _, err := la.authManager.LogInDetails(ctx); !errors.Is(err, auth.ErrNoCurrentUser) { - if err := la.authManager.CleanAllAuthCache(); err != nil { - tracing.SetUsageAttributes(attribute.String("auth.cache_clear_failed", "auth")) - return nil, fmt.Errorf("clearing auth cache: %w", err) - } - if err := la.accountSubManager.ClearSubscriptions(ctx); err != nil { - tracing.SetUsageAttributes(attribute.String("auth.cache_clear_failed", "subscriptions")) - return nil, fmt.Errorf("clearing subscriptions cache: %w", err) + // When already logged in with interactive/device-code auth (or when login state cannot be + // determined due to corrupted cache), clear cached auth data before re-authenticating. This + // prevents issues with stale MSAL refresh tokens that can cause AADSTS700082 errors even + // after a successful login. + // Skip cleanup for MI and SP re-logins: they don't use refresh tokens, so the stale-token + // issue doesn't apply, and unnecessary cleanup could block an otherwise valid login. + isServicePrincipalOrMI := la.flags.managedIdentity || la.flags.clientID != "" + if !isServicePrincipalOrMI { + if _, err := la.authManager.LogInDetails(ctx); !errors.Is(err, auth.ErrNoCurrentUser) { + if err := la.authManager.CleanAllAuthCache(); err != nil { + tracing.SetUsageAttributes(attribute.String("auth.cache_clear_failed", "auth")) + return nil, fmt.Errorf("clearing auth cache: %w", err) + } + if err := la.accountSubManager.ClearSubscriptions(ctx); err != nil { + tracing.SetUsageAttributes(attribute.String("auth.cache_clear_failed", "subscriptions")) + return nil, fmt.Errorf("clearing subscriptions cache: %w", err) + } } }