Managed Identity
Compatible with the azure-identity ManagedIdentityCredential (Java, Python, Node.js), the
Microsoft.ManagedIdentity ARM management plane, and any HTTP client. Covers user-assigned
identities (with federated identity credentials) and the IMDS token endpoint that
applications use to acquire tokens without secrets.
HTTP-only: no Docker. ARM CRUD state follows
storage.mode; IMDS tokens are minted with the same RSA signing key as the Entra ID emulation, so they verify against the emulator JWKS.
Features
- User-assigned identities: CreateOrUpdate, Get, Update (tags), Delete, List (by resource
group and by subscription);
principalId/clientId/tenantIdare server-generated GUIDs that stay stable across updates (msi spec2024-11-30) - Identity references: a
PUTorPATCHof any resource whoseidentity.userAssignedIdentitiesnames an identity that doesn't exist answers400 FailedIdentityOperationand stores nothing; a null entry, which removes an identity, needs no existing identity - Federated identity credentials: CreateOrUpdate, Get, Delete, List with
issuer/subject/audiences(as used byazurerm_federated_identity_credential) - System-assigned identity read:
GET {scope}/providers/Microsoft.ManagedIdentity/identities/defaultreturns deterministic per-scope GUIDs - IMDS token endpoint:
GET /metadata/identity/oauth2/token(imds spec2023-07-01): requires theMetadata: trueheader, acceptsresourceplus an optionalclient_id/object_id/msi_res_idselector, and returns the all-string IMDS response shape. Anyapi-versionis accepted (SDKs send2018-02-01) - App Service token endpoint:
GET /msi/token(theIDENTITY_ENDPOINT/IDENTITY_HEADERprotocol used by Goazidentityand Rustazure_identityin App Service mode): requires theX-IDENTITY-HEADERheader andresource, acceptsclient_id/object_id/mi_res_id/msi_res_idselectors, and returns an all-string response (including a quotedexpires_on) - Verifiable tokens: v1.0 JWTs (
appid,oid,idtyp=app, andxms_miridnaming the identity's ARM id) signed by the Entra key; validate againstGET /common/discovery/v2.0/keys
Endpoints
PUT /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{name}
GET /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{name}
PATCH /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{name}
DELETE /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{name}
GET /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ManagedIdentity/userAssignedIdentities
GET /subscriptions/{sub}/providers/Microsoft.ManagedIdentity/userAssignedIdentities
PUT .../userAssignedIdentities/{name}/federatedIdentityCredentials/{ficName}
GET .../userAssignedIdentities/{name}/federatedIdentityCredentials/{ficName}
DELETE .../userAssignedIdentities/{name}/federatedIdentityCredentials/{ficName}
GET .../userAssignedIdentities/{name}/federatedIdentityCredentials
GET /{scope}/providers/Microsoft.ManagedIdentity/identities/default
GET /metadata/identity/oauth2/token?resource={resource}[&client_id=...] # header: Metadata: true
GET /msi/token?resource={resource}[&client_id=...] # header: X-IDENTITY-HEADER: {secret}
Quickstart
1: Create a user-assigned identity
curl -s -X PUT \
"http://localhost:4577/subscriptions/my-sub/resourceGroups/my-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/my-identity?api-version=2024-11-30" \
-H "Content-Type: application/json" \
-d '{"location":"eastus"}'
{
"name": "my-identity",
"type": "Microsoft.ManagedIdentity/userAssignedIdentities",
"properties": {
"tenantId": "00000000-0000-0000-0000-000000000002",
"principalId": "e3b0c442-…",
"clientId": "9f86d081-…"
}
}
2: Acquire a token via IMDS (raw HTTP)
curl -s -H "Metadata: true" \
"http://localhost:4577/metadata/identity/oauth2/token?resource=https://management.azure.com/&api-version=2018-02-01&client_id={clientId}"
Omit client_id for a system-assigned token. All response values are strings, per the real
IMDS contract:
{
"access_token": "eyJ0…",
"client_id": "9f86d081-…",
"expires_in": "3599",
"expires_on": "1767225599",
"ext_expires_in": "3599",
"not_before": "1767221999",
"resource": "https://management.azure.com/",
"token_type": "Bearer"
}
3: Use ManagedIdentityCredential from the SDKs
The azure-identity SDKs target http://169.254.169.254 by default. Point them at the emulator
with AZURE_POD_IDENTITY_AUTHORITY_HOST (honored by the Java, Python, and Node.js SDKs):
4: Acquire a token via App Service Managed Identity (raw HTTP)
The Go (azidentity) and Rust (azure_identity) credential chains use a different protocol in
App Service mode. They read IDENTITY_ENDPOINT and IDENTITY_HEADER, then GET the endpoint
with the X-IDENTITY-HEADER header. floci-az serves that protocol natively at /msi/token:
curl -s -H "X-IDENTITY-HEADER: floci-az-msi-secret" \
"http://localhost:4577/msi/token?resource=https://vault.azure.net&api-version=2019-08-01"
The consumer, not floci-az, sets IDENTITY_ENDPOINT and IDENTITY_HEADER. For the defaults
above:
The request contract:
X-IDENTITY-HEADER(required) must equal the configured secret (app-service-header-secret).resource(required) is echoed back in the response.api-versionis accepted but any value is allowed; the canonical value is2019-08-01.- Selectors are optional and mutually exclusive:
client_id,object_id, and the ARM-resource-id selector which has two spellings (mi_res_idandmsi_res_id; both are accepted, and supplying both is treated as the single resource-id selector). Omitting all selectors yields a system-assigned token.
The response is all strings, and expires_on is a quoted epoch-seconds string (never a JSON
number, never a datetime): the Rust azure_identity SDK parses it with a custom deserializer that
accepts only a JSON string of integer epoch-seconds.
{
"access_token": "eyJ0…",
"client_id": "9f86d081-…",
"expires_in": "3599",
"expires_on": "1767225599",
"resource": "https://vault.azure.net",
"token_type": "Bearer"
}
Configuration
floci-az:
services:
managed-identity:
enabled: true
system-assigned-scope: subscriptions/${floci-az.services.arm.default-subscription-id}
app-service-enabled: true
app-service-path: msi/token
app-service-header-secret: floci-az-msi-secret
| Property | Env var | Default | Description |
|---|---|---|---|
enabled |
FLOCI_AZ_SERVICES_MANAGED_IDENTITY_ENABLED |
true |
Enables the ARM provider and both token endpoints |
system-assigned-scope |
FLOCI_AZ_SERVICES_MANAGED_IDENTITY_SYSTEM_ASSIGNED_SCOPE |
subscriptions/ + the ARM default-subscription-id |
ARM scope that seeds the system-assigned IMDS identity's principalId/clientId. Set it to your resource's scope (e.g. subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/virtualMachines/{vm}) so IMDS tokens match GET {scope}/.../identities/default reads |
app-service-enabled |
FLOCI_AZ_SERVICES_MANAGED_IDENTITY_APP_SERVICE_ENABLED |
true |
Enables the App Service Managed Identity token endpoint at app-service-path |
app-service-path |
FLOCI_AZ_SERVICES_MANAGED_IDENTITY_APP_SERVICE_PATH |
msi/token |
Data-plane token path for App Service Managed Identity, stored with no leading slash |
app-service-header-secret |
FLOCI_AZ_SERVICES_MANAGED_IDENTITY_APP_SERVICE_HEADER_SECRET |
floci-az-msi-secret |
Secret value the X-IDENTITY-HEADER header must carry |
Token tenant, issuer, and lifetime follow the Entra ID settings
(floci-az.services.entra.*).
Intentional deviations
clientSecretUrlis synthetic: real Azure returns a Key Vault-backed credential renewal URL foridentities/default; the emulator returns a placeholder URL under the emulator base.- Any
api-versionis accepted on both planes; real IMDS rejects unknown versions. isolationScopeis not modeled on user-assigned identities.- Federated identity credentials are CRUD-only: no token-exchange semantics.
- Unknown identities return
400 {"error":"invalid_request","error_description":"Identity not found"}, matching real IMDS behavior for unassigned user identities. - The system-assigned IMDS identity is not tied to a caller resource: on real Azure, IMDS
runs on the resource and returns that resource's own identity. The emulator is not attached to
a resource, so system-assigned tokens are seeded from the configured
system-assigned-scope(default: the default subscription).identities/defaultreads for other scopes return different (deterministic) GUIDs; setsystem-assigned-scopeto your resource's scope when your code compares an ARM-readprincipalIdagainst the token'soid. - Identity state follows
storage.mode: identities and federated credentials are kept in themanagedidentitystore, so under a persistent mode they survive a restart with the same client and principal ids. An identity addressed in another casing (resourcegroups/RG,MyIdentity) is the same identity, and responses keep the casing of the request that created it. - Resource-group deletion does not cascade: deleting a resource group leaves its identities
behind (consistent with the other ARM resources in the emulator). Identities do appear in
GET .../resourceGroups/{rg}/resources, soprevent_deletion_if_contains_resourcesworks.