Here is how you can emulate Azure managed identity on your local system by running an instance metadata service identity endpoint and simulating the token API to return an access token from Azure CLI. You can use this for example to log into SQL Server Management studio using your Azure CLI identity, or locally test code that uses the "Azure only" ManagedIdentityCredential.
Warning
Azure AD tokens are security sensitive material. Even though this script only exposes tokens on a local loopback interface, ensure you understand the consequences of doing this.
Note
The below script is for Windows and has been tested on Windows 11 and PowerShell 7.3. The same mechanism should work on other platforms with some modifications.
You must first bind the well-known and link-local (non-routable) instance metadata service IP address 169.254.169.254
to your loopback interface. Using the loopback interface ensures the tokens can only be acquired from processes on your local system. On Windows in an elevated terminal:
# configure imds well known ip address on loopback interface in the active (until next reboot) store
netsh interface ipv4 add address name=1 address=169.254.169.254 mask=255.255.255.255 store=active skipassource=true
# show the loopback interface ip address configuration
netsh interface ipv4 show addresses name=1
Ensure you are logged into Azure CLI.
Run the following script in an elevated terminal, which will start up an HttpListener that emulates the IMDS identity endpoint and returns tokens using your Azure CLI logged in identity. The terminal needs to be elevated for HttpListener to bind to the IP address without a namespace reservation.
& {
clear-host
$account = az account show|convertfrom-json
if($null -eq $account) {write-warning 'Azure CLI not logged into any accounts';break;}
write-host -f green "Returning tokens from account $($account.name) ($($account.id)) for principal $($account.user.name) of type $($account.user.type)"
$listener = new-object net.httplistener
$listener.prefixes.add("http://169.254.169.254:80/")
$listener.start()
write-host -f green "Started IMDS token endpoint on $($listener.prefixes)"
$cache = [runtime.caching.memorycache]::new('tokens')
try {
while($listener.islistening) {
$task = $listener.getcontextasync()
$taskid = $task.id
$startdate = get-date
while(-not $task.wait(1000)){write-progress -id $taskid -activity "Waiting for connection $taskid (CTRL-C to stop)" -status "$([int]((get-date)-$startdate).totalseconds) seconds"}
write-progress -id $taskid -activity "Waiting for connection $taskid" -completed
if ($task.iscompletedsuccessfully) {
$context = $task.result
$request = $context.request
$response = $context.response
write-host -f cyan "$($taskid): received request from $($request.remoteendpoint) by $($request.useragent)"
if ($request.headers['Metadata'] -eq $true -and $request.url.absolutepath -eq '/metadata/identity/oauth2/token') {
$request_returnid = $request.headers['x-ms-return-client-request-id']
$request_requestid = $request.headers['x-ms-client-request-id']
if ($request_returnid -eq $true) {
$response.headers['x-ms-client-request-id'] = $request_requestid
}
$request_resource = $request.querystring['resource']
$imds_token = $cache.get($request_resource)
if ($null -eq $imds_token) {
write-host -f cyan "$($taskid): getting token for resource $request_resource using Azure CLI"
$imds_token = az account get-access-token --subscription $account.id --resource $request_resource|convertfrom-json
$imds_token_expireson = [datetimeoffset]::parseexact($imds_token.expiresOn, 'yyyy-MM-dd HH:mm:ss.ffffff', [globalization.datetimeformatinfo]::invariantinfo)
$imds_token_cache_expireson = $imds_token_expireson.addminutes(-5)
write-host -f cyan "$($taskid): got token for resource $request_resource expiring on $($imds_token.expireson) from Azure CLI"
$cache.add($request_resource, $imds_token, $imds_token_cache_expireson)|out-null
} else {write-host -f cyan "$($taskid): got token for resource $request_resource expiring $($imds_token.expireson) from cache"}
$access_token = $imds_token.accesstoken
$access_token_payload_raw = $access_token.split('.')[1].replace('-','+').replace('_','/')
$access_token_payload_json = [text.encoding]::ascii.getstring([convert]::frombase64string($access_token_payload_raw.padright($access_token_payload_raw.length+((4-($access_token_payload_raw.length%4))%4),'=')))
$access_token_payload = $access_token_payload_json|convertfrom-json
$response_obj = @{
token_type = $imds_token.tokentype
resource = $access_token_payload.aud
not_before = $access_token_payload.nbf
expires_on = $access_token_payload.exp
expires_in = $access_token_payload.exp - $access_token_payload.iat
refresh_token = ''
access_token = $access_token
}
$response_json = $response_obj|convertto-json -compress
$response_bytes = [text.encoding]::utf8.getbytes($response_json)
$response.statuscode = 200
$response.contenttype = 'application/json'
$response.contentlength64 = $response_bytes.length
write-host -f cyan "$($taskid): returning token for resource $($response_obj.resource)`n"
$response.outputstream.write($response_bytes, 0, $response_bytes.length)
} else {
write-host -f red "$($taskid): returning 401 since metadata header '$($request.headers['Metadata'])' or path '$($request.url.absolutepath)' did not match`n"
$response.statuscode = 401
}
$response.close()
} else {
write-host -f red "$($taskid): error handling connection $($task.exception)`n"
}
}
} finally {
write-host -f green "Clearing cache of $($cache.getcount()) items"
$cache.dispose()
write-host -f green "Stopping IMDS token endpoint on $($listener.prefixes)"
$listener.stop()
write-host -f green "Stopped IMDS token endpoint on $($listener.prefixes)"
}
}
Finally if desired you can cleanup by removing the IP address, although it will be automatically removed on next Windows restart.
# remove the ip address if you want to do so before a system restart
netsh interface ipv4 delete address name=1 address=169.254.169.254 store=active
Very nice! I made a version that doesn't require admin access, since most tools like func and whatnot look for the MSI_ENDPOINT or IDENTITY_ENDPOINT environment variables.
https://gist.github.com/JustinGrote/2b7ff3c08b38ba459f9079e456e49563