|
| 1 | +# Azure Interactive Browser Authentication (`azure/interactive`) |
| 2 | + |
| 3 | +**Status**: Implemented |
| 4 | +**Last Updated**: 2026-08-04 |
| 5 | +**Owners**: Atmos auth subsystem |
| 6 | + |
| 7 | +**Upstream references** (verified via Microsoft docs): |
| 8 | +- MSAL — [Interactive and non-interactive authentication flows](https://learn.microsoft.com/en-us/entra/identity-platform/msal-authentication-flows) |
| 9 | +- MSAL Go — `public.Client.AcquireTokenInteractive`: [pkg.go.dev/github.com/AzureAD/microsoft-authentication-library-for-go/apps/public](https://pkg.go.dev/github.com/AzureAD/microsoft-authentication-library-for-go/apps/public) |
| 10 | +- Microsoft Entra — [Microsoft-managed Conditional Access policies](https://learn.microsoft.com/en-us/entra/identity/conditional-access/managed-policies) (device code flow blocking) |
| 11 | +- Azure CLI — [Sign in with Azure CLI](https://learn.microsoft.com/en-us/cli/azure/authenticate-azure-cli) (the flow this provider mirrors) |
| 12 | + |
| 13 | +**Related Atmos PRDs**: |
| 14 | +- [PRD-Atmos-Auth](../../pkg/auth/docs/PRD/PRD-Atmos-Auth.md) (umbrella) |
| 15 | +- Fix doc: [Azure CLI cache corruption for guest users](../fixes/2026-08-03-azure-cli-cache-corruption-guest-users.md) (#2861 — provides the `AuthMethod`/`HomeAccountID` plumbing this feature builds on) |
| 16 | + |
| 17 | +--- |
| 18 | + |
| 19 | +## 1. Executive Summary |
| 20 | + |
| 21 | +### Problem |
| 22 | + |
| 23 | +Atmos had no one-command human login for Azure that works under modern tenant policy: |
| 24 | + |
| 25 | +- `azure/device-code` performs a self-contained login, but Microsoft-managed Conditional |
| 26 | + Access policies now **block the device code flow** in many tenants (error `AADSTS530035`), |
| 27 | + because device code is a phishing vector. Microsoft is rolling these managed policies out |
| 28 | + broadly, so this failure mode grows over time rather than shrinking. |
| 29 | +- `azure/cli` delegates to an existing `az login` session — it can never be one command, and |
| 30 | + it requires the Azure CLI to be installed and logged in first. |
| 31 | +- `azure/oidc` is CI-only (workload identity federation). |
| 32 | + |
| 33 | +On AWS, `atmos auth login` is a single command (`aws/iam-identity-center`). Azure users had |
| 34 | +no equivalent. |
| 35 | + |
| 36 | +### Solution |
| 37 | + |
| 38 | +A new provider kind **`azure/interactive`** implementing MSAL interactive browser |
| 39 | +authentication — authorization code + PKCE on a localhost redirect, the exact flow |
| 40 | +`az login` uses (and MSAL's own name for it: `AcquireTokenInteractive`). Conditional Access |
| 41 | +allows it because the browser session carries full CA context (MFA, device state, sign-in |
| 42 | +risk). |
| 43 | + |
| 44 | +`atmos auth login` opens the browser, the user signs in, and atmos acquires Management, |
| 45 | +Graph, and Key Vault tokens, persists them to the realm-scoped MSAL cache (refresh tokens |
| 46 | +make repeat logins silent), and writes the Azure CLI-compatible cache files — so Terraform's |
| 47 | +`azurerm`/`azuread` providers authenticate via `ARM_USE_CLI`, and the `az` CLI itself works |
| 48 | +without ever running `az login`. |
| 49 | + |
| 50 | +## 2. Design |
| 51 | + |
| 52 | +### Provider kind |
| 53 | + |
| 54 | +`azure/interactive` — named for the mechanism (MSAL "interactive" flow), consistent with the |
| 55 | +repo convention that kinds name auth mechanisms, not UX (`aws/iam-identity-center`, |
| 56 | +`gcp/workload-identity-federation`). The spec shape is identical to `azure/device-code`: |
| 57 | +`tenant_id` (required), `subscription_id`, `location`, `client_id` (defaults to the Azure |
| 58 | +CLI public client `04b07795-8ddb-461a-bbee-02f9e1bf7b46`, which pre-authorizes localhost |
| 59 | +redirects), `cloud_environment` (public | usgovernment | china). |
| 60 | + |
| 61 | +### Implementation shape |
| 62 | + |
| 63 | +`interactiveProvider` embeds `deviceCodeProvider` and reuses its MSAL client construction, |
| 64 | +silent token acquisition, Graph/Key Vault token fan-out, and Azure CLI cache write-back. |
| 65 | +Only the acquisition step differs (`AcquireTokenInteractive` vs. the device code flow). The |
| 66 | +shared machinery is parameterized by auth method: credentials persist |
| 67 | +`auth_method: interactive`, and the MSAL cache `account_source` mirrors az's own labels |
| 68 | +(`authorization_code` for the browser flow, `device_code` otherwise). |
| 69 | + |
| 70 | +Authentication order: |
| 71 | +1. Silent acquisition from the persisted MSAL cache (refresh tokens survive restarts — no |
| 72 | + browser on repeat logins within the refresh window). |
| 73 | +2. Interactive browser flow, guarded by a TTY check (headless environments get an error |
| 74 | + directing CI/CD to `azure/oidc` and browser-less human sessions to `azure/device-code` |
| 75 | + where tenant policy allows it). |
| 76 | + |
| 77 | +Guest (B2B) users are handled correctly: the MSAL `AuthResult` supplies the real |
| 78 | +`{home-oid}.{home-tenant}` home account ID, which flows into both cache writers (see the |
| 79 | +related fix doc). |
| 80 | + |
| 81 | +### Testability |
| 82 | + |
| 83 | +The MSAL interactive acquisition requires a live identity provider and a browser, so the |
| 84 | +provider exposes two injection seams (`acquireInteractive`, `checkInteractive`) per the |
| 85 | +repo's dependency-injection convention. Tests cover the full success path, acquisition |
| 86 | +failure, and headless refusal against a sandboxed `HOME`. |
| 87 | + |
| 88 | +## 3. Non-goals |
| 89 | + |
| 90 | +- Replacing `azure/device-code` (still valid where a browser cannot run and the tenant |
| 91 | + allows the flow) or `azure/cli` (still valid to piggyback on an existing az session). |
| 92 | +- Embedded/webview sign-in, brokered auth (WAM), or Entra device registration. |
| 93 | +- Tenants requiring a custom app registration: supported via `spec.client_id`, but |
| 94 | + provisioning that registration is out of scope. |
| 95 | + |
| 96 | +## 4. Acceptance |
| 97 | + |
| 98 | +- `atmos auth login` with an `azure/interactive` provider opens the default browser, |
| 99 | + completes SSO (including MFA under Conditional Access), and mints ARM/Graph/Key Vault |
| 100 | + tokens — verified end to end in a tenant where the device code flow is blocked by a |
| 101 | + Microsoft-managed policy and the operator is a B2B guest. |
| 102 | +- Repeat logins are silent via the MSAL refresh token. |
| 103 | +- After login, `az account show` works without `az login`, and the az MSAL cache contains a |
| 104 | + single, correctly-keyed Account entry. |
| 105 | +- Headless environments fail fast with guidance toward `azure/oidc` (CI/CD) or |
| 106 | + `azure/device-code` (browser-less human sessions). |
| 107 | + |
| 108 | +### Verification (manual, 2026-08-04) |
| 109 | + |
| 110 | +Executed end to end in a real Entra tenant where the device code flow is blocked by a |
| 111 | +Microsoft-managed Conditional Access policy (`AADSTS530035`), with an operator who is a |
| 112 | +guest (B2B) user in that tenant. Starting from a fully clean state |
| 113 | +(`az account clear` plus removal of `~/.azure/atmos`, `~/.azure/msal_token_cache.json`, and |
| 114 | +`~/.azure/azureProfile.json`): |
| 115 | + |
| 116 | +1. `atmos auth login` — opened the default browser, SSO completed, tokens minted (~1.5h |
| 117 | + expiry). One command, no device code, no `az login`. |
| 118 | +2. `atmos auth login` again — succeeded **silently** (same token expiry, no browser), |
| 119 | + confirming refresh-token persistence in the realm-scoped MSAL cache. |
| 120 | +3. `atmos auth whoami` — reported provider, identity, subscription principal, tenant, and |
| 121 | + expiry. |
| 122 | +4. `az account show` — worked without ever running `az login`, confirming the drop-in |
| 123 | + write-back. |
| 124 | +5. Cache forensics: exactly one MSAL Account entry, `account_source: authorization_code` |
| 125 | + (matching az's own label for this flow), with a home account ID whose tenant differs |
| 126 | + from the target tenant (the guest case that previously produced the duplicate-account |
| 127 | + corruption); persisted credentials carry `auth_method: interactive` and the home account |
| 128 | + ID. |
| 129 | + |
| 130 | +Known cosmetic limitation: the azureProfile written by the write-back records the |
| 131 | +subscription ID as the subscription's display name (atmos does not query ARM for the |
| 132 | +display name during login), so `az account show` shows the ID in the `name` field until the |
| 133 | +user runs `az login` themselves. Functionality is unaffected. |
0 commit comments