`az login`: intermittent "No subscriptions found" with OIDC service-principal login — transient empty ARM subscription list (HTTP 200)
- Dominant language
- Python
- Stars
- 4.6k
- Forks
- 3.5k
- Avg merge
- 3d 2h
- Merged PRs (30d)
- 60
Description
### Describe the bug
When authenticating a service principal via OIDC/federated token (no secret), `az login` intermittently fails with:
```
Attempting Azure CLI login by using OIDC...
Error: No subscriptions found for ***.
```
Re-running the exact same command, unchanged, succeeds. We see this in ~10–20% of CI runs. Configuration (SP, tenant, subscription, RBAC) is stable and correct.
This is the CLI-side counterpart of the widely-reported GitHub Actions symptom in Azure/login#592 (where @isra-fel is investigating) and the retry request in Azure/login#591. Since `azure/login` just shells out to `az login`, I believe the behaviour originates here, in `Profile.login()`.
### Root cause analysis
In `src/azure-cli-core/azure/cli/core/_profile.py`, `Profile.login()` calls `SubscriptionFinder.find_using_specific_tenant()` → a single `client.subscriptions.list()`. I believe ARM transiently returns a **successful HTTP 200 with an empty `value[]`** (not an error), because newly-effective access can lag due to ARM/RBAC propagation ("up to 10 minutes," with ARM caching — see [Troubleshoot Azure RBAC](https://learn.microsoft.com/en-us/azure/role-based-access-control/troubleshooting)). The empty list then trips:
```python
if not subscriptions and not allow_no_subscriptions:
...
raise CLIError("No subscriptions found for {}.".format(username))
```
Crucially, the `azure-core` `RetryPolicy` **cannot** mitigate this: `is_retry()` returns `False` for any `status_code < 400` and never inspects the body, so a 200-with-empty-list is terminal to the HTTP retry layer. The retry has to live at the application layer, where the empty *result* is evaluated.
> Caveat: I've verified each link in the chain — empty-200 semantics ([Subscriptions - List](https://learn.microsoft.com/en-us/rest/api/resources/subscriptions/list)), the `RetryPolicy` behaviour ([azure-core `_retry.py`](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/core/azure-core/azure/core/pipeline/policies/_retry.py)), and the documented RBAC propagation latency — from primary sources, but I have not found an MS doc attributing this exact intermittent-OIDC symptom to the race verbatim; it is inferred and corroborated by Azure/login#592.
### Proposed fix
A bounded retry of subscription discovery, firing **only** on the exact path that would otherwise raise (`not subscriptions and not is_bare_mode and not allow_no_subscriptions`), with exponential backoff + jitter and an environment variable to tune/disable it. This adds **zero** latency for `--allow-no-subscriptions` and `--skip-subscription-discovery` logins (excluded by construction). I have a worked-out patch design and unit tests ready to implement, and will open a PR as soon as the approach/layer is confirmed.
### Question for maintainers
Before I open the PR: do you agree the fix belongs in `azure-cli` (`Profile.login`) rather than in `azure/login`? And is a small, env-tunable retry-on-empty acceptable here, or would you prefer a different approach (e.g. surfacing a clearer transient error)?
### Expected behavior
A transient empty subscription response from ARM immediately after token issuance should not fail `az login`; it should be retried briefly before giving up, so legitimate logins do not fail ~10–20% of the time.
### Environment summary
- Reproduced via `azure/login@v3` (commit `532459e`) on GitHub-hosted `ubuntu-latest`, which invokes the runner's bundled `az`. Azure/login#592 reports it across the `az` shipped with login `v2.2.0` and `v3`.
- Auth: service principal, OIDC federated token, `auth-type: SERVICE_PRINCIPAL`, explicit `subscription-id` / `tenant-id`, `environment: azurecloud`.
Contributor guide
Assessment
This issue has not been assessed yet.