microsoft/aspire · error · InvalidOperationException

Unable to determine the Azure identity to provision as: the…

Error message

Unable to determine the Azure identity to provision as: the access token returned by the credential does not contain a valid 'oid' (object id) claim.

What it means

After decoding the JWT payload, GetPrincipalAsync extracts the 'oid' (object id) claim that identifies the Azure identity to provision as. If the claim is missing or not a parseable Guid, the provider throws because ARM provisioning and role assignment need a concrete principal id. This means the token is a JWT but not an ARM-appropriate access token bearing an object id.

Solutions

  1. Ensure the credential requests tokens for the Azure ARM audience (https://management.azure.com/) so Entra ID includes the oid claim.
  2. Verify the identity is a real Entra ID object (user, service principal, or managed identity); check with `az ad signed-in-user show` that an id exists.
  3. Set AZURE_TENANT_ID/AZURE_CLIENT_ID/AZURE_CLIENT_SECRET (or use DefaultAzureCredential) so a valid Entra token is issued.
  4. In tests, emit a payload with a valid Guid 'oid' claim, e.g. {"oid":"00000000-0000-0000-0000-000000000000"}.

Example fix

// before
var token = await cred.GetTokenAsync(new TokenRequestContext(new[] { "https://graph.microsoft.com/.default" }), ct); // wrong audience
// after
var token = await cred.GetTokenAsync(new TokenRequestContext(new[] { "https://management.azure.com/.default" }), ct); // ARM audience includes oid
Defensive patterns

Strategy: validation

Validate before calling

var payload = DecodeJwtPayload(token); // base64url decode middle segment
using var doc = JsonDocument.Parse(payload);
var ok = doc.RootElement.TryGetProperty("oid", out var oid)
         && Guid.TryParse(oid.GetString(), out _);
if (!ok) throw new InvalidOperationException("Token has no valid 'oid' claim; request a token for the ARM audience.");

Type guard

bool HasValidOidClaim(string jwt)
{
    try
    {
        var payload = jwt.Split('.')[1].Replace('-', '+').Replace('_', '/').PadRight(4 * ((jwt.Split('.')[1].Length + 3) / 4), '=');
        using var doc = JsonDocument.Parse(Convert.FromBase64String(payload));
        return doc.RootElement.TryGetProperty("oid", out var oid) && Guid.TryParse(oid.GetString(), out _);
    }
    catch { return false; }
}

Try / catch

try
{
    var principal = await principalProvider.GetPrincipalAsync(ct);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("'oid'") && ex.Message.Contains("claim"))
{
    // request an ARM-audience token (https://management.azure.com/.default) with a valid Entra identity
}

Prevention

When it happens

Trigger: GetPrincipalAsync decodes a token whose payload lacks 'oid' or has a non-Guid value — e.g. using a credential scoped to a non-ARM audience (Graph-only or opaque tokens), a token minted by a non-Entra ID STS, or a fabricated test token with an invalid oid.

Common situations: Developers hit this with custom credentials fetching tokens for the wrong resource/audience, tokens from non-Entra identity providers, tenant-mismatched service principals, or test doubles whose payload omits oid.

Related errors


AI-assisted analysis of microsoft/aspire@25830f84bd (2026-09-16). Data as JSON: /api/errors/985709c22be39040. Report an issue: GitHub.

Appendix: source

Thrown at src/Aspire.Hosting.Azure/Provisioning/Internal/DefaultAzurePrincipalProvider.cs:79

            var bytes = Convert.FromBase64String(convertedToken);

            // Read claims from the root object only. JWT claims are top-level by definition, but a
            // claim's *value* can itself be an object or array — Entra emits `_claim_sources` that
            // way for the groups-overage case, and RFC 8693 delegation tokens nest identity claims
            // under `act`. A streaming reader that walks every token would treat a nested "oid" or
            // "idtyp" as if it were a real claim, and last-write-wins would silently swap the
            // principal these values describe. That matters here because they become the
            // principalId/principalType of an ARM role assignment, so picking up the wrong one
            // would grant access to the wrong identity. Microsoft also documents that new claims
            // may be added without notice, so scope the lookup structurally rather than relying on
            // today's payloads happening to be flat.
            using var document = JsonDocument.Parse(bytes);
            var root = document.RootElement;

            var oid = GetRootString(root, "oid");
            if (!Guid.TryParse(oid, out var principalId))
            {
                throw new InvalidOperationException(
                    "Unable to determine the Azure identity to provision as: the access token returned by " +
                    "the credential does not contain a valid 'oid' (object id) claim.");
            }

            // Default to "User" so older tokens — and any flow that omits `idtyp` — keep the
            // historical behavior of a hardcoded "User" principalType instead of regressing to an
            // empty value. `idtyp` is an optional claim that Entra only emits for app-only tokens
            // unless the resource opts in via `include_user_token`, so absence is not evidence of
            // a user identity; it just means we can't tell and fall back to the previous default.
            // The comparison is case-insensitive for resilience against future producers that emit
            // different casing than the lower-case values Entra documents.
            var isAppOnly = string.Equals(GetRootString(root, "idtyp"), IdTypApp, StringComparison.OrdinalIgnoreCase);

            var principalType = isAppOnly
                ? PrincipalTypeServicePrincipal
                : PrincipalTypeUser;

            return new AzurePrincipal(principalId, ResolvePrincipalName(root, principalId, isAppOnly), principalType);

View on GitHub (pinned to 25830f84bd)