Update Ghost blogs and pages with PowerShell

In order to remove a bit of repetitive manual work from my schedule I wrote a little sync between my Scrum.org class schedule to this blog.

Update Ghost blogs and pages with PowerShell
The Ghost Key. Turns out it's hard to get the right one.

The samples provided by Ghost are in JavaScript, Curl and Python, all languages I'm not fluent in, so I set out to do the same from PowerShell or C#.

The hardest part turned out to be the code to create the correct JWT token to authenticate against Ghost. In their admin panel they provide a set of tokens, but it turns out you can't just use these tokens as-is.

API Key provided by Ghost

The API key essentially makes up 2 parts:

  1. The Key Identifier, that's the part up to the :
  2. The Secret, that's the part after the :

In order to use this key, we first have to split it into its two parts:

$parts = $adminApiKey -split ":"
$id = $parts[0]
$secret = $parts[1]

We then need to construct a JWT token from this key. I relied on the Posh-JWT package for that part:

Install-Module -Name JWT -force

$key = [Convert]::FromHexString($secret) # only works in PowerShell Core

$jwttoken = New-Jwt `
  -Header (
    @{
      "alg" = "HS256"
      "kid" = $id
      "typ" = "JWT"
    } | ConvertTo-Json
  ) `
  -PayloadJson (
    @{
      "exp" = ([DateTimeOffset](Get-Date).AddMinutes(2)).ToUnixTimeSeconds()
      "iat" = ([DateTimeOffset](Get-Date)).ToUnixTimeSeconds()
      "aud" = "/admin/"
    } | ConvertTo-Json) 
  -Secret $key

With this token, you can then invoke the Ghost API.

$headers = @{
  Authorization  = "Ghost $jwttoken"
  "Content-Type" = "application/json"
}

$baseUri = "https://scrumbug.ghost.io/ghost/api"
        
$result = Invoke-RestMethod `
  -Uri "$baseUri/admin/pages/" `
  -Method GET `
  -Headers $headers     

As I said, I used this code to automatically sync my Scrum.org classes to this blog. You can find the (private) Ghost update GitHub Action here. To keep my secrets secure I added a couple of extra lines of code to register my secrets with the runner:

Write-Output "::add-mask::$id"
...
Write-Output "::add-mask::$secret"
...
Write-Output "::add-mask::$key"
...
Write-Output "::add-mask::$jwttoken"

Make sure to always register your secrets every time they change their shape or representation to prevent your secrets from leaking into the GitHub Action logs.

End result? My classes are automatically updated 🎉:

Github actions workflow runs successfully