Introduction

This article shows how to impersonate a service account from user account credentials. To understand how to set up everything, read the companion article:

Save the following PowerShell script as a file named impersonate_service_account.ps1. This has been tested on Windows 10 with PowerShell 5.1 and PowerShell 7.0

powershell .\impersonate_service_account.ps1

This example implements a web server for Google OAuth 2 user authentication. The user’s credentials are saved to a file, and the credentials are reused. A good example of saving the OAuth Refresh Token to recreate access tokens. Once the user access token is created, a service account is impersonated the new access token is used to display Compute Engine instances.

I plan to release this code on GitHub to make access easier. Watch for an update with more examples in C#, Python, Go and PowerShell.

PowerShell Example:

# client identifier of your application configured in the Google Console
$clientId = "REPLACE_WITH_CLIENT_ID"

# client secret of your application configured in the Google Console
$clientSecret = "REPLACE_WITH_CLIENT_SECRET"

# File to save the OAuth Refresh Token to
# This should be located in a secure location and not in source code or a project folder
$credFile = "c:/config/credentials.data"

# Service account to impersonate
$sa = "REPLACE_WITH_SERVICE_ACCOUNT_FULL_EMAIL_ADDRESS"

# arbitrary port number for the HTTP web server to listen on
$port = 12345

# Get the default project
$project=gcloud config list project --format "value(core.project)"

# Get the default zone
$zone=gcloud config list compute/zone --format "value(compute.zone)"

function Invoke-WebServer {
	if (-not [System.Net.HttpListener]::IsSupported) {
		"HttpListener is not supported."
		exit 1
	}

	$listener = New-Object System.Net.HttpListener

	$listener.Prefixes.Add("http://localhost:$port/")

	try {
		$listener.Start()
	} catch {
		Write-Host "Error: Unable to start HTTP listener." -ForegroundColor Red
		Write-Host $_.Exception.Message -ForegroundColor Red
		exit 1
	}

	while ($listener.IsListening) {
		Write-Host "HTTP server listening ..."

		$context = $listener.GetContext()

		$q = $context.Request.QueryString

		$uri = "https://www.googleapis.com/oauth2/v4/token"
		$uri += "?client_id=$clientId"
		$uri += "&client_secret=$clientSecret"
		$uri += "&grant_type=authorization_code"
		$uri += "&redirect_uri=http://localhost:$port"

		foreach ($key in $q) {
			if ($key -eq "code") {
				$values = $q.GetValues($key)
				$uri += "&code=$values"
				continue
			}

			if ($key -eq "scope") {
				$values = $q.GetValues($key)
				$uri += "&scope=$values"
				continue
			}
		}

		$authorizationResponse = Invoke-RestMethod -Uri $uri -Method Post

		$j = $authorizationResponse | ConvertTo-Json

		$response = $context.Response

		$response.ContentType = "text/plain"

		$content = [System.Text.Encoding]::UTF8.GetBytes("You can now close this window")

		$response.OutputStream.Write($content, 0, $content.Length)

		$response.Close()

		break
	}

	$listener.Stop()

	return $j
}

function Save-RefreshToken {
	Param(
		[string][Parameter(Position = 0, Mandatory = $true)] $RefreshToken
	)

	Out-File -FilePath $credFile -InputObject $RefreshToken
}

function Get-GoogleAccessTokenFromRefreshToken {
	Param(
		[string][Parameter(Position = 0, Mandatory = $true)] $refreshToken
	)

	$uri = "https://www.googleapis.com/oauth2/v4/token"

	$data = "client_id=$clientId"
	$data += "&client_secret=$clientSecret"
	$data += "&grant_type=refresh_token"
	$data += "&refresh_token=$refreshToken"

	$result = Invoke-RestMethod -Uri $uri -Method Post -Body $data

	return $result.access_token
}

function Get-GoogleAccessToken {
	Param(
		[string][Parameter(Position = 0, Mandatory = $true)] $Scope
	)

	if (Test-Path $credFile -PathType leaf) {
		$refreshToken = Get-Content -Path $credFile

		return Get-GoogleAccessTokenFromRefreshToken $refreshToken
	}

	# URI to start an OAuth authorization flow
	$uri = "https://accounts.google.com/o/oauth2/v2/auth"
	$uri += "?client_id=$clientId"
	$uri += "&redirect_uri=http://localhost:$port"
	$uri += "&response_type=code"
	$uri += "&access_type=offline"
	$uri += "&scope=$Scope email profile"

	# Kick off the default web browser
	Write-Host "Launching web browser to authenticate to Google ..."
	Start-Process $uri

	# Call the web server
	$j = Invoke-WebServer | ConvertFrom-Json

	Save-RefreshToken $j.refresh_token

	return $j.access_token
}

function Impersonate-GoogleServiceAccount {
	Param(
		[string][Parameter(Position = 0, Mandatory = $true)] $AccessToken,
		[string][Parameter(Position = 1, Mandatory = $true)] $ServiceAccount
	)

	$headers = @{
		Authorization = "Bearer $AccessToken"
		"Content-Type" = "application/json"
	}

	$uri = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/" + $sa + ":generateAccessToken?alt=json"

$sbody = @"
{"delegates": [], "lifetime": "3600s", "scope": ["openid", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/appengine.admin", "https://www.googleapis.com/auth/compute"]}
"@

	$result = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body $sbody

	return $result.accessToken
}

function Get-ComputeInstances {
	Param(
		[string][Parameter(Position = 0, Mandatory = $true)] $ServiceAccount
	)

	# Get the user account access token
	$accessToken = Get-GoogleAccessToken -Scope "https://www.googleapis.com/auth/cloud-platform"

	# Using the user account access token impersonate a service account
	$accessToken2 = Impersonate-GoogleServiceAccount $accessToken $sa

	# Build the uri
	$uri = "https://www.googleapis.com/compute/v1/projects/" + $project + "/zones/" + $zone + "/instances"

	# Build the Invoke-RestMethod parameters
	$params = @{
		Headers = @{
			Host = "www.googleapis.com"
			'Authorization' = "Bearer " + $accessToken2
		}
		ContentType = "application/json"
		Method = 'Get'
		Uri = $uri
	}

	# Call Endpoint
	try {
		$output = Invoke-RestMethod @params
		$m = $output.Items
		return $m
	}
	catch {
		Write-Host
		Write-Host "Request failed" -ForegroundColor Red
		Write-Host
		$_.Exception.Message

		return $null
	}
}

$instances = Get-ComputeInstances -ServiceAccount $sa

if ($instances.Count -lt 1) {
	Write-Output "No Compute Engine Instances were found"
	exit
}

# Display as a table
$instances | Select name, status, creationTimeStamp | Format-Table

 

Credits

I write free articles about technology. Recently, I learned about Pexels.com which provides free images. The image in this article is courtesy of Steve at Pexels.