Skip to content

Holding credentials is a posture

This module offers two ambient rungs that do the same work and differ only in what they keep:

azureclient.Ambient(...)   // resolve once, share the result
azureclient.PerCall(...)   // resolve afresh every time, retain nothing

PerCall is not a degraded Ambient. It exists because how long a component holds a credential is a security decision belonging to that component, not to this module.

A signing component may deliberately decline to hold an identity between mints, so that credentials are not resident longer than an operation requires. Offering only a memoising rung would force such a component either to keep its own duplicate resolution — defeating the point of a shared module — or to change its posture to adopt ours.

Both return the same Source, so swapping is one word at the construction site.

Concurrent PerCall calls are deliberately not collapsed into one attempt: collapsing would share a resolution between callers who explicitly asked not to share one.

Why isolation is the default

Two components that each take their ambient default resolve the chain twice, on purpose. A hidden process-wide cache would silently share an identity between components that may deliberately differ — a different tenant, a different managed identity — and would make one component's transient failure everybody's, invisibly.

Sharing is therefore something you do deliberately, by building one source and injecting it.

The footprint boundary

Resolving DefaultAzureCredential costs +7 modulesazidentity, MSAL, golang-jwt, pkg/browser and their graph — measured on the config adapters.

That is why importing this module is an explicit act, and why the Azure-backed config adapters put their ambient rung behind an ambient subpackage. A consumer who never wants ambient credentials never pays for them, and each subpackage carries a test asserting its own cost.

The estate rule: an adapter's root package must not grow the credential graph. If you never import the ambient subpackage, your footprint is exactly what it was before the ladder existed.

The token refreshes; the credential is a factory

An azcore.TokenCredential is not a token — it is a thing that produces tokens. The azcore pipeline calls GetToken again whenever the current one is near expiry, so a long-lived process holding a credential from Ambient does not go stale.

That distinction — a means of obtaining a credential versus a credential — decides whether a value is safe to hold indefinitely. Azure, AWS and GCP all hand out a means. Vault and Consul hand out a token, which is why those go stale and these do not.

Where a value cannot renew, clientlifecycle offers an invalidatable strategy. Azure does not need it, and taking it here would be strictly worse: an invalidation path nobody should call, and a fan-out hazard if anyone does.

Where this is specified

org spec 0003 — isolation as the default (P-5), the non-memoised rung (P-12), the import boundary (P-7), and the typed-nil validation (P-13).