From e17e3fd1d64978b76e2184a5510736a7dd98de15 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:43:36 -0700 Subject: [PATCH 1/2] Add package-specific skill for azure-identity Create SKILL.md with architecture overview, token API guidance (get_token vs get_token_info), MSAL three-tier integration model, sync/async split rules, DefaultAzureCredential chain details, managed identity backends, and common pitfalls. Includes reference docs for credential taxonomy, authentication flows, cross-cutting behaviors, and testing patterns. Registers azure-identity in the find-package-skill registry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/find-package-skill/SKILL.md | 3 +- .../.github/skills/azure-identity/SKILL.md | 108 +++++++++++++ .../azure-identity/references/architecture.md | 149 ++++++++++++++++++ .../azure-identity/references/testing.md | 55 +++++++ 4 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 sdk/identity/azure-identity/.github/skills/azure-identity/SKILL.md create mode 100644 sdk/identity/azure-identity/.github/skills/azure-identity/references/architecture.md create mode 100644 sdk/identity/azure-identity/.github/skills/azure-identity/references/testing.md diff --git a/.github/skills/find-package-skill/SKILL.md b/.github/skills/find-package-skill/SKILL.md index 514cef4492e4..6bfaec79be64 100644 --- a/.github/skills/find-package-skill/SKILL.md +++ b/.github/skills/find-package-skill/SKILL.md @@ -22,4 +22,5 @@ you already know the package well. | Package | Path | | ------------------------- | --------------------------------------------------------------------------- | -| `azure-search-documents` | `sdk/search/azure-search-documents/.github/skills/azure-search-documents/SKILL.md` | \ No newline at end of file +| `azure-search-documents` | `sdk/search/azure-search-documents/.github/skills/azure-search-documents/SKILL.md` | +| `azure-identity` | `sdk/identity/azure-identity/.github/skills/azure-identity/SKILL.md` | \ No newline at end of file diff --git a/sdk/identity/azure-identity/.github/skills/azure-identity/SKILL.md b/sdk/identity/azure-identity/.github/skills/azure-identity/SKILL.md new file mode 100644 index 000000000000..5e21fa58a8bb --- /dev/null +++ b/sdk/identity/azure-identity/.github/skills/azure-identity/SKILL.md @@ -0,0 +1,108 @@ +--- +name: azure-identity +description: 'Domain knowledge for azure-identity. Covers architecture, credential chain, MSAL integration, sync/async split, managed identity, token APIs, and common pitfalls. WHEN: modify azure-identity; fix azure-identity bug; add azure-identity credential; azure-identity feature; change DefaultAzureCredential; update managed identity.' +--- + +# azure-identity — Package Skill + +`azure-identity` is **entirely hand-authored** — there is no TypeSpec, no `_generated/` directory, no `_patch.py` files, and no code generation step. Every file is owned and maintained directly. + +## Architecture + +``` +azure/identity/ +├── __init__.py # Public API re-exports; __all__ is the contract +├── _version.py # VERSION string +├── _constants.py # EnvironmentVariables, AzureAuthorityHosts, KnownAuthorities +├── _enums.py # RegionalAuthority, TokenRefreshStatus +├── _auth_record.py # AuthenticationRecord (serializable identity snapshot) +├── _bearer_token_provider.py # get_bearer_token_provider() helper (sync + async variants) +├── _persistent_cache.py # TokenCachePersistenceOptions, platform-specific cache loading +├── _exceptions.py # CredentialUnavailableError, AuthenticationRequiredError +├── _credentials/ # Sync credential implementations (one file per credential) +│ ├── default.py # DefaultAzureCredential — the chain +│ ├── chained.py # ChainedTokenCredential — base chain logic +│ ├── managed_identity.py # ManagedIdentityCredential — env-sniffing dispatcher +│ ├── environment.py # EnvironmentCredential — reads env vars +│ ├── broker.py # BrokerCredential (WAM, Windows/WSL only) +│ └── ... # certificate, client_secret, browser, device_code, etc. +├── _internal/ # Shared plumbing (NOT public API) +│ ├── get_token_mixin.py # GetTokenMixin — unified get_token/get_token_info flow +│ ├── msal_credentials.py # MSAL app wrapper (per-tenant, CAE/non-CAE separation) +│ ├── msal_client.py # Low-level MSAL HTTP adapter +│ ├── aad_client.py / aad_client_base.py # In-house OAuth client (raw HTTP, uses msal.TokenCache) +│ ├── managed_identity_client.py # In-house MI client (Cloud Shell uses this) +│ ├── msal_managed_identity_client.py # MSAL-based MI client (all other MI sources) +│ ├── client_credential_base.py # Base for confidential client credentials +│ ├── interactive.py # Base for interactive credentials +│ ├── decorators.py # Logging decorators (log_get_token, log_get_token_async) +│ └── utils.py # Authority normalization, within_dac ContextVar, DAC helpers +└── aio/ # Async mirror (see Async Parity section) + ├── __init__.py # Async public API re-exports (fewer than sync) + ├── _credentials/ # Async credential implementations + └── _internal/ # Async plumbing — raw OAuth via azure-core async pipeline +``` + +**Extension package:** `azure-identity-broker` provides `InteractiveBrowserBrokerCredential` (sync only) for native broker auth (WAM/Company Portal). `VisualStudioCodeCredential` requires this package and only works on Windows/WSL. + +## Token APIs + +Two methods exist on every credential — understanding the distinction is critical: + +- **`get_token()`** — **deprecated**. Returns `AccessToken(token, expires_on)`. Keyword set frozen at `claims` / `tenant_id` / `enable_cae`. Cannot do PoP tokens, `refresh_on` hints, or non-Bearer `token_type`. +- **`get_token_info()`** — **preferred going forward**. Returns `AccessTokenInfo(token, expires_on, refresh_on, token_type)`. Required for CAE, Proof-of-Possession (PoP/mTLS/SHR), refresh-on hints. A credential signals support via `SupportsTokenInfo`. + +Both are implemented in `GetTokenMixin` (`_internal/get_token_mixin.py`). `get_token` internally builds `TokenRequestOptions` and delegates to `_get_token_base`. New features go on `get_token_info` / `TokenRequestOptions` only. + +## Common Pitfalls + +1. **Three sync-only credentials.** `DeviceCodeCredential`, `InteractiveBrowserCredential`, and `UsernamePasswordCredential` are intentionally sync-only — they prompt a human and there is no benefit to an async surface. They are NOT in `aio/__init__.py` or the async DAC chain. Do not add async versions. + +2. **`__all__` in both `__init__.py` files.** New public symbols must be added to BOTH `azure/identity/__init__.py` AND `azure/identity/aio/__init__.py` (if async-applicable). Missing entries silently hide the class. + +3. **MSAL is sync-only.** No async credential ever instantiates `msal.PublicClientApplication`, `msal.ConfidentialClientApplication`, or `msal.ManagedIdentityClient`. Async token acquisition uses the in-house `AadClient` (raw OAuth HTTP via azure-core async pipeline). MSAL's `TokenCache` data structure IS reused by async for cache compatibility. See `references/architecture.md` for the three tiers of MSAL usage. + +4. **DAC chain order matters and differs sync vs async.** Sync DAC has 10 entries (including `InteractiveBrowserCredential` excluded by default, and `BrokerCredential`). Async DAC has only 8 (omits those two). New credentials must be inserted at the correct position in BOTH `_credentials/default.py` and `aio/_credentials/default.py`, with matching `exclude_*` kwargs. + +5. **`within_dac` ContextVar.** When DAC is iterating its chain, `within_dac` is set so child credentials suppress noisy errors and exit fast on `CredentialUnavailableError`. Log level depends on this context — don't bypass it. + +6. **Multi-tenant: `additionally_allowed_tenants`.** Credentials reject cross-tenant token requests unless the target tenant is in `additionally_allowed_tenants` or `*` is specified. Forgetting this causes `ClientAuthenticationError`. + +7. **Managed identity has 7 backends + an exception.** `ManagedIdentityCredential` auto-detects App Service, IMDS, Azure Arc, Service Fabric, Cloud Shell, Azure ML, Workload Identity. Most use `MsalManagedIdentityClient` (wraps `msal.ManagedIdentityClient`), but **`CloudShellCredential` is the exception** — it uses the in-house `ManagedIdentityClient` and talks to the Cloud Shell MSI endpoint directly. + +8. **Don't hardcode authority URLs.** Use `AzureAuthorityHosts` constants and `get_default_authority()` / `normalize_authority()` from `_internal/utils.py`. Hardcoded URLs break sovereign clouds. + +9. **`AZURE_TOKEN_CREDENTIALS` env var.** Narrows DAC before `exclude_*` flags: `prod` keeps only Environment/WorkloadIdentity/ManagedIdentity; `dev` keeps developer credentials + broker; a specific credential name selects one. `SharedTokenCacheCredential` and `BrokerCredential` have no `env_name` and cannot be selected individually. + +10. **CAE requires separate caches.** `AadClient` and `MsalCredential` maintain separate token caches for CAE vs non-CAE so tokens don't collide. When adding a credential that supports CAE, ensure it respects this separation. + +## Key Internal Flow: `GetTokenMixin` + +Most credentials inherit `GetTokenMixin`, which unifies both `get_token()` and `get_token_info()`: + +1. Validate at least one scope is provided +2. Build `TokenRequestOptions` from kwargs (for `get_token`) or use options directly (for `get_token_info`) +3. Try `_acquire_token_silently()` (cache hit / refresh token) +4. Check `get_refresh_status()` → `REQUIRED` | `RECOMMENDED` | `NOT_NEEDED` +5. If `REQUIRED` or `RECOMMENDED`, call `_request_token()` (full auth flow) +6. Log result — log level depends on `within_credential_chain` / `within_dac` context + +Each credential implements `_acquire_token_silently()` and `_request_token()`. + +## Async Parity + +Async credentials live under `aio/` and mirror sync, with key differences: + +- **MSAL is never used directly in async.** Async `AadClient` performs raw OAuth HTTP via azure-core's async pipeline. Async MI sources hit IMDS/app-host endpoints directly. +- **Three credentials are sync-only** (see pitfall #1). +- **Async DAC has 8 entries** vs sync's 10 (no `InteractiveBrowserCredential`, no `BrokerCredential`). +- Shared helpers should live in sync `_internal/` and be imported by async — don't duplicate. + +## Testing + +See `references/testing.md` for test commands and patterns. + +## References + +- Credential taxonomy, MSAL tiers, managed identity, DAC details: `references/architecture.md` +- Test commands and patterns: `references/testing.md` diff --git a/sdk/identity/azure-identity/.github/skills/azure-identity/references/architecture.md b/sdk/identity/azure-identity/.github/skills/azure-identity/references/architecture.md new file mode 100644 index 000000000000..a5ce11857674 --- /dev/null +++ b/sdk/identity/azure-identity/.github/skills/azure-identity/references/architecture.md @@ -0,0 +1,149 @@ +# Architecture Reference for azure-identity + +## Credential Taxonomy + +### By OAuth Application Type + +This is the OAuth/MSAL distinction that drives most of the credential design. + +**Public client application** — cannot keep a secret; authenticates a _user_ interactively. Backed by `msal.PublicClientApplication`: +- `InteractiveBrowserCredential`, `DeviceCodeCredential`, `UsernamePasswordCredential` +- `AuthorizationCodeCredential`, `SharedTokenCacheCredential` +- `VisualStudioCodeCredential` (delegates to broker) +- `InteractiveBrowserBrokerCredential` (broker package) + +**Confidential client application** — runs on a server, holds a secret/certificate/assertion. Backed by `msal.ConfidentialClientApplication`: +- `ClientSecretCredential`, `CertificateCredential`, `ClientAssertionCredential` +- `OnBehalfOfCredential` +- `WorkloadIdentityCredential` (built on `ClientAssertionCredential`) +- `AzurePipelinesCredential` (composes `ClientAssertionCredential`) +- `EnvironmentCredential` (resolves to one of the above) + +**Neither (managed identity)** — authenticates the compute resource via local IMDS/metadata endpoint: +- `ManagedIdentityCredential` and backends: `ImdsCredential`, `AppServiceCredential`, `AzureMLCredential`, `AzureArcCredential`, `ServiceFabricCredential`, `CloudShellCredential` + +**Subprocess-based** — shells out to a developer tool that has already authenticated (no MSAL): +- `AzureCliCredential`, `AzureDeveloperCliCredential`, `AzurePowerShellCredential` + +### By Production vs Developer (DAC Classification) + +Used by `DefaultAzureCredential` and the `AZURE_TOKEN_CREDENTIALS` env var: + +| Category | Credentials | +|----------|------------| +| **Production** | `EnvironmentCredential`, `WorkloadIdentityCredential`, `ManagedIdentityCredential` | +| **Developer** | `SharedTokenCacheCredential`, `VisualStudioCodeCredential`, `AzureCliCredential`, `AzurePowerShellCredential`, `AzureDeveloperCliCredential`, `BrokerCredential` | +| **Other** (explicit use only) | `ClientSecretCredential`, `CertificateCredential`, `ClientAssertionCredential`, `OnBehalfOfCredential`, `AuthorizationCodeCredential`, `DeviceCodeCredential`, `UsernamePasswordCredential`, `AzurePipelinesCredential`, `InteractiveBrowserCredential` (DAC-excluded by default) | + +## MSAL Integration — Three Tiers + +Understanding the three tiers of MSAL usage is critical for making changes: + +### Tier 1: Full MSAL client app (sync only) + +These instantiate `msal.PublicClientApplication`, `msal.ConfidentialClientApplication`, or `msal.ManagedIdentityClient`: +- `ClientSecretCredential`, `CertificateCredential` → `ConfidentialClientApplication` +- `OnBehalfOfCredential` → `ConfidentialClientApplication.acquire_token_on_behalf_of` +- `InteractiveBrowserCredential`, `DeviceCodeCredential`, `UsernamePasswordCredential` → `PublicClientApplication` +- `ManagedIdentityCredential` (most backends) → `MsalManagedIdentityClient` → `msal.ManagedIdentityClient` + - **Exception:** `CloudShellCredential` does NOT use `msal.ManagedIdentityClient` — uses in-house `ManagedIdentityClient` + +### Tier 2: MSAL TokenCache only + +The in-house `AadClient` performs raw OAuth HTTP requests but stores results in `msal.TokenCache` for cache compatibility: +- `AuthorizationCodeCredential`, `ClientAssertionCredential`, `WorkloadIdentityCredential`, `AzurePipelinesCredential` +- `SharedTokenCacheCredential` — reads MSAL's persisted cache, redeems refresh tokens via `AadClient` + +### Tier 3: No MSAL at all + +Subprocess-based credentials that parse stdout JSON: +- `AzureCliCredential` (`az account get-access-token`) +- `AzureDeveloperCliCredential` (`azd auth token`) +- `AzurePowerShellCredential` (`Get-AzAccessToken` via `pwsh`) + +### Sync vs Async — The Big Rule + +**MSAL is a sync-only library.** No async credential ever instantiates a MSAL client application. Async credentials inherit from sync base classes that import MSAL, but token acquisition on the async side is implemented in-house: +- Async `AadClient` performs raw OAuth HTTP requests via azure-core's async pipeline +- Async managed-identity sources hit IMDS/app-host endpoints directly +- MSAL's `TokenCache` data structure IS reused for cache compatibility + +So every Tier 1 sync credential drops to Tier 2 in its async counterpart. + +## Managed Identity Backends + +`ManagedIdentityCredential` sniffs the environment and picks ONE backend: + +| Environment | Detection | Uses MSAL MI Client? | +|------------|-----------|---------------------| +| App Service | `IDENTITY_ENDPOINT` + `IDENTITY_HEADER` | Yes | +| Service Fabric | `IDENTITY_ENDPOINT` + `IDENTITY_SERVER_THUMBPRINT` | Yes | +| Azure Arc | `IDENTITY_ENDPOINT` + `IMDS_ENDPOINT` | Yes | +| Azure ML | `MSI_ENDPOINT` + `MSI_SECRET` | Yes | +| Cloud Shell | `MSI_ENDPOINT` (no secret) | **No** — uses in-house client | +| Workload Identity | `AZURE_FEDERATED_TOKEN_FILE` + `AZURE_TENANT_ID` + `AZURE_CLIENT_ID` | Yes | +| IMDS (fallback) | None of the above | Yes | + +## DefaultAzureCredential Chain + +### Sync chain (10 entries) + +1. `EnvironmentCredential` +2. `WorkloadIdentityCredential` +3. `ManagedIdentityCredential` +4. `SharedTokenCacheCredential` +5. `VisualStudioCodeCredential` +6. `AzureCliCredential` +7. `AzurePowerShellCredential` +8. `AzureDeveloperCliCredential` +9. `InteractiveBrowserCredential` (**excluded by default** — `exclude_interactive_browser_credential=True`) +10. `BrokerCredential` (no-op unless `azure-identity-broker` installed + Windows/WSL) + +### Async chain (8 entries) + +Same as sync entries 1–8. **No** `InteractiveBrowserCredential` or `BrokerCredential`. + +### `FailedDACCredential` pattern + +If a credential fails to **initialize** (not at token time), it's replaced with `FailedDACCredential` (sync) or `AsyncFailedDACCredential` (async). This lets the chain continue and reports the init error only if ALL credentials fail. + +### `AZURE_TOKEN_CREDENTIALS` env var + +Narrows DAC before `exclude_*` flags: +- `prod` → keeps only Environment, WorkloadIdentity, ManagedIdentity +- `dev` → keeps developer credentials + broker +- Specific credential name → selects one (but `SharedTokenCacheCredential` and `BrokerCredential` have no `env_name` and can't be selected this way) +- `require_envvar=True` + unset var → `ValueError` + +### `within_dac` ContextVar + +Set while DAC iterates its chain. Child credentials check this to: +- Suppress noisy error logging (use DEBUG instead of WARNING) +- Exit fast on `CredentialUnavailableError` + +## Authentication Flows + +| Flow | Credentials | +|------|------------| +| Client credentials — secret | `ClientSecretCredential`, `EnvironmentCredential` (with `AZURE_CLIENT_SECRET`) | +| Client credentials — certificate | `CertificateCredential`, `EnvironmentCredential` (with `AZURE_CLIENT_CERTIFICATE_PATH`) | +| Client credentials — federated assertion | `ClientAssertionCredential` | +| Workload identity federation | `WorkloadIdentityCredential` | +| Azure Pipelines OIDC | `AzurePipelinesCredential` | +| On-Behalf-Of | `OnBehalfOfCredential` | +| IMDS / app-host metadata | `ManagedIdentityCredential` (auto-detects source) | +| Authorization code + PKCE | `InteractiveBrowserCredential`, `InteractiveBrowserBrokerCredential` | +| Authorization code redemption | `AuthorizationCodeCredential` | +| Device code | `DeviceCodeCredential` | +| ROPC (discouraged) | `UsernamePasswordCredential` | +| Silent / refresh-token from cache | `SharedTokenCacheCredential`, silent step in interactive credentials | +| Native broker (WAM) | `InteractiveBrowserBrokerCredential`, `VisualStudioCodeCredential` | +| Subprocess delegation | `AzureCliCredential`, `AzurePowerShellCredential`, `AzureDeveloperCliCredential` | + +## Cross-Cutting Behaviors + +- **CAE**: Credentials that proxy MSAL pass `enable_cae` through. `AadClient` keeps a separate `cae_cache`. CAE and non-CAE tokens must not collide. +- **Multi-tenant**: Per-call `tenant_id` overrides constructor tenant, subject to `additionally_allowed_tenants` allow-list. +- **Token caching**: In-memory by default. Persistent via `TokenCachePersistenceOptions` → `msal-extensions` (DPAPI/Keychain/libsecret, plaintext fallback only if explicitly allowed). +- **Authorities/clouds**: Controlled by `authority=` or `AZURE_AUTHORITY_HOST`. `AzureAuthorityHosts` enumerates Public, US Gov, China. +- **Logging**: Every credential logs through `azure.identity` logger. `TROUBLESHOOTING.md` is the canonical troubleshooting reference. diff --git a/sdk/identity/azure-identity/.github/skills/azure-identity/references/testing.md b/sdk/identity/azure-identity/.github/skills/azure-identity/references/testing.md new file mode 100644 index 000000000000..2ad7d4ea9000 --- /dev/null +++ b/sdk/identity/azure-identity/.github/skills/azure-identity/references/testing.md @@ -0,0 +1,55 @@ +# Testing Guide for azure-identity + +## Running Tests + +```bash +cd sdk/identity/azure-identity + +# Run unit tests (playback mode, no credentials needed) +azpysdk pytest . + +# Run a specific test file +azpysdk pytest tests/test_default.py + +# Run async tests +azpysdk pytest tests/test_default_async.py + +# Run with keyword filter +azpysdk pytest tests/ -k "test_default_credential" +``` + +## Test Structure + +Tests generally mirror the credential layout, but sync-only credentials do not have async test pairs: + +``` +tests/ +├── test_default.py / test_default_async.py +├── test_managed_identity.py / test_managed_identity_async.py +├── test_certificate_credential.py / test_certificate_credential_async.py +├── test_cli_credential.py / test_cli_credential_async.py +├── test_browser_credential.py # sync-only credential +├── test_chained_credential.py / test_chained_credential_async.py +├── managed-identity-live/ # Live MI tests (require Azure environment) +├── helpers.py # Shared test utilities +└── conftest.py # Fixtures, sanitizers, env-gated marks +``` + +## Key Testing Patterns + +- **`time.sleep` and `asyncio.sleep` are patched to no-op** in non-live mode (`conftest.py`). Don't rely on timing in unit tests. +- **Custom markers**: `@pytest.mark.manual` (requires human interaction), `@pytest.mark.prints` (outputs to terminal). +- **Live tests are env-gated**: Service principal creds, certificate paths, and username/password must be in env vars. Tests skip if vars are missing. +- **Managed identity live tests** are in a separate `managed-identity-live/` directory with their own `conftest.py`. They require Key Vault access and specific environment configuration. +- **MSAL app behavior is often mocked** with `unittest.mock.patch`, while HTTP transports are also used where request/response behavior matters (for example, token endpoint flows and managed identity clients). +- **Async tests** use `pytest-asyncio`. The event loop policy is overridden on Windows to `WindowsSelectorEventLoopPolicy` in `conftest.py`. + +## Validation + +```bash +# Run package checks +azpysdk pytest . +azpysdk pylint . +azpysdk mypy . +azpysdk sphinx . +``` From e80a711fe00d778c0a2f78213fb8475d1183971f Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:53:12 -0700 Subject: [PATCH 2/2] Refine azure-identity package skill Align the skill with create-package-skill guidance by keeping the top-level instructions concise, correcting identity-specific details, and adding package validation guidance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../.github/skills/azure-identity/SKILL.md | 109 +++++------------- .../azure-identity/references/architecture.md | 8 +- .../azure-identity/references/testing.md | 8 +- 3 files changed, 38 insertions(+), 87 deletions(-) diff --git a/sdk/identity/azure-identity/.github/skills/azure-identity/SKILL.md b/sdk/identity/azure-identity/.github/skills/azure-identity/SKILL.md index 5e21fa58a8bb..d3a0d62b359d 100644 --- a/sdk/identity/azure-identity/.github/skills/azure-identity/SKILL.md +++ b/sdk/identity/azure-identity/.github/skills/azure-identity/SKILL.md @@ -1,106 +1,51 @@ --- name: azure-identity -description: 'Domain knowledge for azure-identity. Covers architecture, credential chain, MSAL integration, sync/async split, managed identity, token APIs, and common pitfalls. WHEN: modify azure-identity; fix azure-identity bug; add azure-identity credential; azure-identity feature; change DefaultAzureCredential; update managed identity.' +description: 'Domain knowledge for azure-identity. Covers architecture, credential chain, MSAL integration, sync/async split, managed identity, token APIs, and common pitfalls. WHEN: modify azure-identity; fix azure-identity bug; add azure-identity credential; add azure-identity feature; change azure-identity DefaultAzureCredential; update azure-identity managed identity.' --- # azure-identity — Package Skill -`azure-identity` is **entirely hand-authored** — there is no TypeSpec, no `_generated/` directory, no `_patch.py` files, and no code generation step. Every file is owned and maintained directly. - -## Architecture +`azure-identity` is **entirely hand-authored**: no TypeSpec, no `_generated/`, no `_patch.py`, and no regeneration step. Every changed file is production source, so first verify public API exports, sync/async parity, credential-chain behavior, and tests. ``` azure/identity/ -├── __init__.py # Public API re-exports; __all__ is the contract -├── _version.py # VERSION string -├── _constants.py # EnvironmentVariables, AzureAuthorityHosts, KnownAuthorities -├── _enums.py # RegionalAuthority, TokenRefreshStatus -├── _auth_record.py # AuthenticationRecord (serializable identity snapshot) -├── _bearer_token_provider.py # get_bearer_token_provider() helper (sync + async variants) -├── _persistent_cache.py # TokenCachePersistenceOptions, platform-specific cache loading -├── _exceptions.py # CredentialUnavailableError, AuthenticationRequiredError -├── _credentials/ # Sync credential implementations (one file per credential) -│ ├── default.py # DefaultAzureCredential — the chain -│ ├── chained.py # ChainedTokenCredential — base chain logic -│ ├── managed_identity.py # ManagedIdentityCredential — env-sniffing dispatcher -│ ├── environment.py # EnvironmentCredential — reads env vars -│ ├── broker.py # BrokerCredential (WAM, Windows/WSL only) -│ └── ... # certificate, client_secret, browser, device_code, etc. -├── _internal/ # Shared plumbing (NOT public API) -│ ├── get_token_mixin.py # GetTokenMixin — unified get_token/get_token_info flow -│ ├── msal_credentials.py # MSAL app wrapper (per-tenant, CAE/non-CAE separation) -│ ├── msal_client.py # Low-level MSAL HTTP adapter -│ ├── aad_client.py / aad_client_base.py # In-house OAuth client (raw HTTP, uses msal.TokenCache) -│ ├── managed_identity_client.py # In-house MI client (Cloud Shell uses this) -│ ├── msal_managed_identity_client.py # MSAL-based MI client (all other MI sources) -│ ├── client_credential_base.py # Base for confidential client credentials -│ ├── interactive.py # Base for interactive credentials -│ ├── decorators.py # Logging decorators (log_get_token, log_get_token_async) -│ └── utils.py # Authority normalization, within_dac ContextVar, DAC helpers -└── aio/ # Async mirror (see Async Parity section) - ├── __init__.py # Async public API re-exports (fewer than sync) - ├── _credentials/ # Async credential implementations - └── _internal/ # Async plumbing — raw OAuth via azure-core async pipeline +├── __init__.py # public sync exports; __all__ is the contract +├── _credentials/ # sync credentials, DAC, managed identity dispatcher +├── _internal/ # shared auth plumbing; not public API +└── aio/ # async mirror, with fewer public credentials ``` -**Extension package:** `azure-identity-broker` provides `InteractiveBrowserBrokerCredential` (sync only) for native broker auth (WAM/Company Portal). `VisualStudioCodeCredential` requires this package and only works on Windows/WSL. - -## Token APIs - -Two methods exist on every credential — understanding the distinction is critical: - -- **`get_token()`** — **deprecated**. Returns `AccessToken(token, expires_on)`. Keyword set frozen at `claims` / `tenant_id` / `enable_cae`. Cannot do PoP tokens, `refresh_on` hints, or non-Bearer `token_type`. -- **`get_token_info()`** — **preferred going forward**. Returns `AccessTokenInfo(token, expires_on, refresh_on, token_type)`. Required for CAE, Proof-of-Possession (PoP/mTLS/SHR), refresh-on hints. A credential signals support via `SupportsTokenInfo`. - -Both are implemented in `GetTokenMixin` (`_internal/get_token_mixin.py`). `get_token` internally builds `TokenRequestOptions` and delegates to `_get_token_base`. New features go on `get_token_info` / `TokenRequestOptions` only. +**Extension package:** `azure-identity-broker` provides `InteractiveBrowserBrokerCredential` (sync only) for broker auth on Windows, macOS, Linux, and WSL, with non-Windows/WSL fallback behavior. `VisualStudioCodeCredential` requires this package and only works on Windows/WSL. ## Common Pitfalls -1. **Three sync-only credentials.** `DeviceCodeCredential`, `InteractiveBrowserCredential`, and `UsernamePasswordCredential` are intentionally sync-only — they prompt a human and there is no benefit to an async surface. They are NOT in `aio/__init__.py` or the async DAC chain. Do not add async versions. - -2. **`__all__` in both `__init__.py` files.** New public symbols must be added to BOTH `azure/identity/__init__.py` AND `azure/identity/aio/__init__.py` (if async-applicable). Missing entries silently hide the class. - -3. **MSAL is sync-only.** No async credential ever instantiates `msal.PublicClientApplication`, `msal.ConfidentialClientApplication`, or `msal.ManagedIdentityClient`. Async token acquisition uses the in-house `AadClient` (raw OAuth HTTP via azure-core async pipeline). MSAL's `TokenCache` data structure IS reused by async for cache compatibility. See `references/architecture.md` for the three tiers of MSAL usage. - -4. **DAC chain order matters and differs sync vs async.** Sync DAC has 10 entries (including `InteractiveBrowserCredential` excluded by default, and `BrokerCredential`). Async DAC has only 8 (omits those two). New credentials must be inserted at the correct position in BOTH `_credentials/default.py` and `aio/_credentials/default.py`, with matching `exclude_*` kwargs. - -5. **`within_dac` ContextVar.** When DAC is iterating its chain, `within_dac` is set so child credentials suppress noisy errors and exit fast on `CredentialUnavailableError`. Log level depends on this context — don't bypass it. +1. **Public symbols require `__all__`.** Add new sync public symbols to `azure/identity/__init__.py`; add async-applicable symbols to `azure/identity/aio/__init__.py`. +2. **Three credentials are sync-only.** Do not add async `DeviceCodeCredential`, `InteractiveBrowserCredential`, or `UsernamePasswordCredential`. +3. **MSAL is sync-only.** Async credentials may reuse MSAL `TokenCache`, but never instantiate `msal.PublicClientApplication`, `ConfidentialClientApplication`, or `ManagedIdentityClient`. +4. **DAC chain order is contract-sensitive.** Sync DAC has Environment, WorkloadIdentity, ManagedIdentity, SharedTokenCache, VS Code, CLI, PowerShell, Developer CLI, InteractiveBrowser (excluded by default), Broker. Async DAC stops after Developer CLI. +5. **`within_dac` is intentional.** DAC sets this `ContextVar` while iterating so child credentials suppress noisy errors and exit fast on `CredentialUnavailableError`. +6. **Managed identity dispatch is environment-driven.** Cloud Shell uses the in-house `ManagedIdentityClient`; most other managed-identity hosts use `MsalManagedIdentityClient`; Workload Identity dispatches to `WorkloadIdentityCredential`. +7. **Never hardcode authorities.** Use `AzureAuthorityHosts`, `get_default_authority()`, and `normalize_authority()` so sovereign clouds work. -6. **Multi-tenant: `additionally_allowed_tenants`.** Credentials reject cross-tenant token requests unless the target tenant is in `additionally_allowed_tenants` or `*` is specified. Forgetting this causes `ClientAuthenticationError`. +## Token API Rule -7. **Managed identity has 7 backends + an exception.** `ManagedIdentityCredential` auto-detects App Service, IMDS, Azure Arc, Service Fabric, Cloud Shell, Azure ML, Workload Identity. Most use `MsalManagedIdentityClient` (wraps `msal.ManagedIdentityClient`), but **`CloudShellCredential` is the exception** — it uses the in-house `ManagedIdentityClient` and talks to the Cloud Shell MSI endpoint directly. +Every credential supports `get_token()` and `get_token_info()` through `GetTokenMixin`. `get_token()` is legacy but supported; new token-response features such as PoP, `refresh_on`, or non-Bearer token types belong on `get_token_info()` / `TokenRequestOptions`. -8. **Don't hardcode authority URLs.** Use `AzureAuthorityHosts` constants and `get_default_authority()` / `normalize_authority()` from `_internal/utils.py`. Hardcoded URLs break sovereign clouds. +## Change Checklist -9. **`AZURE_TOKEN_CREDENTIALS` env var.** Narrows DAC before `exclude_*` flags: `prod` keeps only Environment/WorkloadIdentity/ManagedIdentity; `dev` keeps developer credentials + broker; a specific credential name selects one. `SharedTokenCacheCredential` and `BrokerCredential` have no `env_name` and cannot be selected individually. +1. Mirror sync changes under `aio/` unless the credential is one of the three sync-only credentials. +2. Keep shared helpers in sync `_internal/` and import them from async; do not duplicate logic. +3. If DAC changes, update both sync and async defaults, exclude kwargs, `AZURE_TOKEN_CREDENTIALS` behavior, and tests. +4. If token acquisition changes, preserve CAE/non-CAE cache separation and multi-tenant `additionally_allowed_tenants` checks. +5. Update `CHANGELOG.md`, README snippets, and troubleshooting links when public behavior changes. -10. **CAE requires separate caches.** `AadClient` and `MsalCredential` maintain separate token caches for CAE vs non-CAE so tokens don't collide. When adding a credential that supports CAE, ensure it respects this separation. +After code changes, run the package validation tool: -## Key Internal Flow: `GetTokenMixin` - -Most credentials inherit `GetTokenMixin`, which unifies both `get_token()` and `get_token_info()`: - -1. Validate at least one scope is provided -2. Build `TokenRequestOptions` from kwargs (for `get_token`) or use options directly (for `get_token_info`) -3. Try `_acquire_token_silently()` (cache hit / refresh token) -4. Check `get_refresh_status()` → `REQUIRED` | `RECOMMENDED` | `NOT_NEEDED` -5. If `REQUIRED` or `RECOMMENDED`, call `_request_token()` (full auth flow) -6. Log result — log level depends on `within_credential_chain` / `within_dac` context - -Each credential implements `_acquire_token_silently()` and `_request_token()`. - -## Async Parity - -Async credentials live under `aio/` and mirror sync, with key differences: - -- **MSAL is never used directly in async.** Async `AadClient` performs raw OAuth HTTP via azure-core's async pipeline. Async MI sources hit IMDS/app-host endpoints directly. -- **Three credentials are sync-only** (see pitfall #1). -- **Async DAC has 8 entries** vs sync's 10 (no `InteractiveBrowserCredential`, no `BrokerCredential`). -- Shared helpers should live in sync `_internal/` and be imported by async — don't duplicate. - -## Testing +``` +azsdk_package_run_check with packagePath="sdk/identity/azure-identity" and checkType="All" +``` -See `references/testing.md` for test commands and patterns. +For targeted local checks from `sdk/identity/azure-identity`, use `azpysdk pytest .`, `azpysdk pylint .`, `azpysdk mypy .`, and `azpysdk sphinx .`. ## References diff --git a/sdk/identity/azure-identity/.github/skills/azure-identity/references/architecture.md b/sdk/identity/azure-identity/.github/skills/azure-identity/references/architecture.md index a5ce11857674..fe1bd32daa38 100644 --- a/sdk/identity/azure-identity/.github/skills/azure-identity/references/architecture.md +++ b/sdk/identity/azure-identity/.github/skills/azure-identity/references/architecture.md @@ -6,13 +6,13 @@ This is the OAuth/MSAL distinction that drives most of the credential design. -**Public client application** — cannot keep a secret; authenticates a _user_ interactively. Backed by `msal.PublicClientApplication`: +**Public client application** — cannot keep a secret; authenticates a _user_. This is the OAuth category; implementation varies by credential, so check the MSAL tiers below before changing internals: - `InteractiveBrowserCredential`, `DeviceCodeCredential`, `UsernamePasswordCredential` - `AuthorizationCodeCredential`, `SharedTokenCacheCredential` - `VisualStudioCodeCredential` (delegates to broker) - `InteractiveBrowserBrokerCredential` (broker package) -**Confidential client application** — runs on a server, holds a secret/certificate/assertion. Backed by `msal.ConfidentialClientApplication`: +**Confidential client application** — runs on a server, holds a secret/certificate/assertion. This is the OAuth category; not every credential here instantiates `msal.ConfidentialClientApplication` directly: - `ClientSecretCredential`, `CertificateCredential`, `ClientAssertionCredential` - `OnBehalfOfCredential` - `WorkloadIdentityCredential` (built on `ClientAssertionCredential`) @@ -68,7 +68,7 @@ Subprocess-based credentials that parse stdout JSON: - Async managed-identity sources hit IMDS/app-host endpoints directly - MSAL's `TokenCache` data structure IS reused for cache compatibility -So every Tier 1 sync credential drops to Tier 2 in its async counterpart. +So Tier 1 sync credentials do not stay Tier 1 in async: their async counterparts either use `AadClient` / `TokenCache` compatibility or direct async managed-identity HTTP. ## Managed Identity Backends @@ -81,7 +81,7 @@ So every Tier 1 sync credential drops to Tier 2 in its async counterpart. | Azure Arc | `IDENTITY_ENDPOINT` + `IMDS_ENDPOINT` | Yes | | Azure ML | `MSI_ENDPOINT` + `MSI_SECRET` | Yes | | Cloud Shell | `MSI_ENDPOINT` (no secret) | **No** — uses in-house client | -| Workload Identity | `AZURE_FEDERATED_TOKEN_FILE` + `AZURE_TENANT_ID` + `AZURE_CLIENT_ID` | Yes | +| Workload Identity | `AZURE_FEDERATED_TOKEN_FILE` + `AZURE_TENANT_ID` + `AZURE_CLIENT_ID` | No — dispatches to `WorkloadIdentityCredential` / `AadClient` | | IMDS (fallback) | None of the above | Yes | ## DefaultAzureCredential Chain diff --git a/sdk/identity/azure-identity/.github/skills/azure-identity/references/testing.md b/sdk/identity/azure-identity/.github/skills/azure-identity/references/testing.md index 2ad7d4ea9000..1e2827e11b0d 100644 --- a/sdk/identity/azure-identity/.github/skills/azure-identity/references/testing.md +++ b/sdk/identity/azure-identity/.github/skills/azure-identity/references/testing.md @@ -47,9 +47,15 @@ tests/ ## Validation ```bash -# Run package checks +# Run targeted local checks azpysdk pytest . azpysdk pylint . azpysdk mypy . azpysdk sphinx . ``` + +For full package validation through the Azure SDK release tooling, run: + +``` +azsdk_package_run_check with packagePath="sdk/identity/azure-identity" and checkType="All" +```