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
1 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# 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.
I design software for enterprise-class systems and data centers. My background is 30+ years in storage (SCSI, FC, iSCSI, disk arrays, imaging) virtualization. 20+ years in identity, security, and forensics.
For the past 14+ years, I have been working in the cloud (AWS, Azure, Google, Alibaba, IBM, Oracle) designing hybrid and multi-cloud software solutions. I am an MVP/GDE with several.
July 31, 2020 at 2:55 AM
Hello John, great job by doing this, we’re trying to setup a delegation chain between impersonated service accounts and your source is a big help for us, so i really appreciate what you’re doing 🙂