Skip to content

Instantly share code, notes, and snippets.

@JustinGrote
Last active August 29, 2024 14:53
Show Gist options
  • Save JustinGrote/2b7ff3c08b38ba459f9079e456e49563 to your computer and use it in GitHub Desktop.
Save JustinGrote/2b7ff3c08b38ba459f9079e456e49563 to your computer and use it in GitHub Desktop.
A Managed Identity Emulator for testing Managed Identities locally. Returns a token from your currently logged in Azure PowerShell context
#requires -Module Az.Accounts
$verbosePreference = 'continue'
function ConvertFrom-JWTtoken {
<#
.NOTES
Lovingly borrowed from: https://www.michev.info/blog/post/2140/decode-jwt-access-and-id-tokens-via-powershell
#>
[cmdletbinding()]
param([Parameter(Mandatory = $true)][string]$token)
#Validate as per https://tools.ietf.org/html/rfc7519
#Access and ID tokens are fine, Refresh tokens will not work
if (!$token.Contains('.') -or !$token.StartsWith('eyJ')) { Write-Error 'Invalid token' -ErrorAction Stop }
#Header
$tokenheader = $token.Split('.')[0].Replace('-', '+').Replace('_', '/')
#Fix padding as needed, keep adding "=" until string length modulus 4 reaches 0
while ($tokenheader.Length % 4) { Write-Verbose 'Invalid length for a Base-64 char array or string, adding ='; $tokenheader += '=' }
Write-Verbose 'Base64 encoded (padded) header:'
Write-Verbose $tokenheader
#Convert from Base64 encoded string to PSObject all at once
Write-Verbose 'Decoded header:'
[System.Text.Encoding]::ASCII.GetString([system.convert]::FromBase64String($tokenheader)) | ConvertFrom-Json | Format-List | Out-Default
#Payload
$tokenPayload = $token.Split('.')[1].Replace('-', '+').Replace('_', '/')
#Fix padding as needed, keep adding "=" until string length modulus 4 reaches 0
while ($tokenPayload.Length % 4) { Write-Verbose 'Invalid length for a Base-64 char array or string, adding ='; $tokenPayload += '=' }
Write-Verbose 'Base64 encoded (padded) payoad:'
Write-Verbose $tokenPayload
#Convert to Byte array
$tokenByteArray = [System.Convert]::FromBase64String($tokenPayload)
#Convert to string array
$tokenArray = [System.Text.Encoding]::ASCII.GetString($tokenByteArray)
Write-Verbose 'Decoded array in JSON format:'
Write-Verbose $tokenArray
#Convert from JSON to PSObject
$tokobj = $tokenArray | ConvertFrom-Json
Write-Verbose 'Decoded Payload:'
return $tokobj
}
function Start-Listener ([int]$Port = 42069, $ListenIP = 'localhost', $Scope = 'User') {
$path = '/oauth2/token'
$listenEndpoint = "http://${ListenIP}:${Port}"
$msiEndpoint = "${listenEndpoint}${path}"
$ENV:IDENTITY_ENDPOINT = $msiEndpoint
$ENV:IDENTITY_HEADER = (New-Guid)
$ENV:MSI_ENDPOINT = $ENV:IDENTITY_ENDPOINT
$ENV:MSI_SECRET = $ENV:IDENTITY_HEADER
Write-Host -f green "Setting up $Scope MSI environment variables"
[Environment]::SetEnvironmentVariable('IDENTITY_ENDPOINT', $ENV:IDENTITY_ENDPOINT, $Scope)
[Environment]::SetEnvironmentVariable('IDENTITY_HEADER', $ENV:IDENTITY_HEADER, $Scope)
[Environment]::SetEnvironmentVariable('MSI_ENDPOINT', $ENV:MSI_ENDPOINT, $Scope)
[Environment]::SetEnvironmentVariable('MSI_SECRET', $ENV:MSI_SECRET, $Scope)
Write-Host -f green "Completed $Scope MSI environment variables"
Write-Host -f green "Starting IMDS token endpoint on $listenEndpoint"
$listener = [Net.HttpListener]::new()
$listener.prefixes.add("$listenEndpoint/") #Thanks Ben
$listener.start()
Write-Host -f green "Started IMDS token endpoint on $listenEndpoint"
$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
$context = $task.result
$request = $context.request
$response = $context.response
Write-Host -f cyan "$($taskid): received request from $($request.remoteendpoint) by $($request.useragent) for $($request.url.absolutepath)"
if ($request.headers['Metadata'] -ne $true) {
Write-Warning "Metadata Header was not set to true. The Az PowerShell sometimes does this and it's technically incorrect. Value:$($request.headers['Metadata'])"
}
if ($request.url.absolutepath -ne $path) {
Write-Warning "Unknown Request Path $($request.url.absolutepath)"
$response.statuscode = 401
$response.close()
continue
}
$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']
if (-not $request_resource) {
Write-Warning "No resource specified in request"
$response.statuscode = 400
$response.close()
continue
}
$imds_token = $cache.get($request_resource)
if (-not $imds_token) {
Write-Verbose "$($taskid): getting token for resource $request_resource using Azure PowerShell"
$imds_token = Get-AzAccessToken -AsSecureString -ResourceUrl $request_resource -WarningAction SilentlyContinue
$imds_token_expireson = [datetimeoffset]$imds_token.ExpiresOn
$imds_token_cache_expireson = $imds_token_expireson.addminutes(-5)
$cache.add($request_resource, $imds_token, $imds_token_cache_expireson) | Out-Null
}
Write-Host -f cyan "$($taskid): got token for resource $request_resource expiring on $($imds_token.expireson) from Azure PowerShell"
$access_token = $imds_token.Token | ConvertFrom-SecureString -AsPlainText
$access_token_payload = ConvertFrom-JWTtoken -token $access_token -Verbose:$false
$response_obj = @{
token_type = $imds_token.Type
resource = $access_token_payload.aud
not_before = [string]$access_token_payload.nbf
expires_on = [string]($access_token_payload.exp)
expires_in = [string]($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)
$response.close()
}
} finally {
Write-Host -f green "Removing User MSI environment variables"
[Environment]::SetEnvironmentVariable('IDENTITY_ENDPOINT', $null, 'User')
[Environment]::SetEnvironmentVariable('IDENTITY_HEADER', $null, 'User')
[Environment]::SetEnvironmentVariable('MSI_ENDPOINT', $null, 'User')
[Environment]::SetEnvironmentVariable('MSI_SECRET', $null, 'User')
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)"
}
}
Start-Listener
@JustinGrote
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment