Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions cli/azd/cmd/auth_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type loginFlags struct {
onlyCheckStatus bool
browser bool
managedIdentity bool
reset bool
useDeviceCode boolPtr
tenantID string
clientID string
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -255,6 +264,20 @@ 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)
}
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
Expand Down
4 changes: 4 additions & 0 deletions cli/azd/cmd/testdata/TestFigSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
1 change: 1 addition & 0 deletions cli/azd/cmd/testdata/TestUsage-azd-auth-login.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
237 changes: 237 additions & 0 deletions cli/azd/docs/authentication.md
Original file line number Diff line number Diff line change
@@ -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 <tenant-id-or-domain>
```

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 <app-id> \
--tenant-id <tenant-id> \
--client-secret <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 <app-id> \
--tenant-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 <app-id> \
--tenant-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 <app-id> \
--tenant-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 <app-id> \
--tenant-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 <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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also update our error suggestions accordingly? I think we already have some logic to detect AADSTS700082

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestion! Done — I've added a specific AADSTS700082 rule in error_suggestions.yaml and updated the generic AADSTS rule to mention that re-running azd auth login now automatically clears stale cached tokens. Also added end-to-end pipeline tests for both rules.

Additionally, the approach has changed: instead of the --reset flag, azd auth login now automatically clears all cached auth data when it detects you're already logged in, making the fix fully transparent.


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 <app-id> --tenant-id <tenant-id> --client-secret <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.
36 changes: 36 additions & 0 deletions cli/azd/pkg/auth/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -1093,6 +1093,42 @@ 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
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
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 != ""
}
Expand Down
Loading
Loading