What's in the bag? Behind the scenes at vBrownBag.com
- What’s in the bag? Behind the scenes at vBrownBag.com
- Part 2 of “What’s in the bag?” Behind the scenes at vBrownBag.com
- Part 3 of “What’s in the bag?” Behind the scenes at vBrownBag.com
- Part 4 of “What’s in the bag?” Behind the scenes at vBrownBag.com
- Part 5 of “What’s in the bag?” Behind the scenes at vBrownBag.com
- Part 6 of “What’s in the bag?” Behind the scenes at vBrownBag.com
Part 4 of this series covered orchestration of the AWS Lambda function using AWS Step Functions & EventBridge. In this post, I’d like to cover Google OAuth with PSAuthClient, decrypting AWS Lambda environment variables, and more.
Google OAuth with PSAuthClient
Some quick introductory info: there are two ways of authenticating with the YouTube data API. The first is through an API key created through https://console.cloud.google.com. It’s an alpha-numeric string that can be appended to REST API calls against endpoints such as https://www.googleapis.com/youtube/v3/videos. However, the API key doesn’t allow privileged access to an account, and for that you need to use OAuth.
There’s a ton of helpful documentation on Google OAuth, but my goal is not to repeat all of that here, nor talk about creating an app in Google’s Cloud console. Rather, I’d like to talk about how I used PSAuthClient to meet my OAuth needs using PowerShell. The two most important pieces of information needed to get started are the client_id
and client_secret
. The first uniquely identifies the Google app, and the second is a client secret string.
After installing PSAuthClient using Install-Module
, I used Invoke-OAuth2AuthorizationEndpoint
to make the initial OAuth request in order to begin building my $authorization
object. The customParameters
hashtable is important as I need to be able to refresh the OAuth token without requiring human interaction every time, since this is running in a Lambda function. Google OAuth also needs the client_secret
in the $authorization
object, so I add it to the object. Finally, I use the Invoke-OAuth2TokenEndpoint
cmdlet to get the $token
object by splatting the $authorization
object (passing a collection of parameter values to a command as a unit).
$authorization = Invoke-OAuth2AuthorizationEndpoint -uri $uri -client_id $client_id -redirect_uri $redirect_uri -response_type "code" -scope $scope -customParameters @{ access_type = "offline"; include_granted_scopes = "true" }
$authorization.Add("client_secret", $client_secret)
$token = Invoke-OAuth2TokenEndpoint -uri $uri @authorization
$token
is a PSCustom object that contains access_token
and refresh_token
strings. The access_token
expires in about an hour, which means that I need to get a new one whenever I’m making an OAuth request. This is accomplished through Invoke-OAuth2TokenEndpoint
.
$token = Invoke-OAuth2TokenEndpoint -uri $uri -refresh_token $refresh_token -client_id $client_id -client_secret $client_secret
Now I can begin building a new $youtubeService
object for uploading new videos, assigning them to playlists, searching, etc.
$googleCredential = [Google.Apis.Auth.OAuth2.GoogleCredential]::FromAccessToken($token.access_token).CreateScoped($token.scope)
# Create a new YouTube service
$youtubeService = New-Object Google.Apis.YouTube.v3.YouTubeService(
New-Object Google.Apis.Services.BaseClientService+Initializer -Property @{
HttpClientInitializer = $googleCredential
ApplicationName = "YouTube Uploader"
}
)
I’d like to note for the purposes of clarity, that I had to use Invoke-OAuth2AuthorizationEndpoint
from my desktop, as it uses WebView2 to handle web interaction/approving the credentials. PSAuthClient is a PowerShell Core & Desk module which throws errors when I require the entire module in my Lambda script. However, Invoke-OAuth2TokenEndpoint
is a PSCore cmdlet, so I’m able to include the .ps1 file directly in my Lambda zip file.
Encrypted environment variables in Lambda
As you can see above, I’m storing a lot of sensitive information in variables. I decided during the early stages of working on Meatgrinder to use Lambda environment variables instead of storing those values directly in the script, or a secrets file, etc. The variables & their values are defined in the AWS Lambda configuration panel. When the Lambda function runs, those variables are available to the script with the $env
prefix, so the value of oauth_client_id
is available as $env:oauth_client_id
, and so on. For the sensitive variables, I chose to create an AWS KMS symmetric key to encrypt & decrypt my environment variables, and require the AWS.Tools.KeyManagementService
module in my script, which includes the Invoke-KMSDecrypt
cmdlet. Now you might be thinking, “oh, I can just use that cmdlet to directly decrypt my variable!” and you’d be wrong just like I was. There’s actually a few extra steps required.
First, the encryption context needs to be defined, and in this case, it’s the environment variable AWS_LAMBDA_FUNCTION_NAME
. (The encryption context is defined in the execution role policy that allows the function the kms:decrypt
action using the resource arn of the KMS key.) Then the ciphertext value stored in the encrypted environment variable needs to be converted from a base64-encoded string to a byte array. The context and the encrypted byte array are passed to the Invoke-KMSDecrypt
cmdlet with the -Select Plaintext
parameter which asks for the plaintext value instead of the whole service response (which is the default), and a decrypted byte array is returned. That byte array is converted to a string which is the decrypted plaintext value. That’s a lot, right? If you were setting up the encryption configuration for environment variables chose the “enable helpers” checkbox, AWS would present you with popup containing a .NET 8 snippet that does all of this, but it doesn’t show it in PowerShell. So, here’s the PowerShell equivalent:
<#
.SYNOPSIS
Helper function to decrypt a ciphertext using the AWS Key Management Service (KMS).
.PARAMETER ciphertext
The ciphertext to decrypt.
.OUTPUTS
Returns the plain text
.EXAMPLE
decrypt -ciphertext "ciphertext"
#>
function decrypt {
param (
[Parameter(Mandatory = $true)]
[string]$ciphertext
)
$encryptionContext = @{"LambdaFunctionName" = $env:AWS_LAMBDA_FUNCTION_NAME }
$cipherbytes = [System.Convert]::FromBase64String($ciphertext)
$response = Invoke-KMSDecrypt -CiphertextBlob $cipherbytes -EncryptionContext $encryptionContext -Select Plaintext
return [System.Text.Encoding]::UTF8.GetString($response.ToArray())
}
Well, if you read this far, congratulations! Or, hi Mom, whichever the case may be. 🙂 In the next post, I’d like to cover WordPress REST API and RSS XML updates. Cheers!