Thanks for this @oilly. Is the snippet still working for you?
I’m getting HTTP/1.1 400 Bad Request
errors with my bash script, which I cobbled together using the bash JWT example in the docs, your post, and the theme upload info in the docs.
I have installed newer versions of CURL (7.81.0 was throwing an error that was fixed in v8.x), different Ghost API versions (the docs reference v3, but I also tried v5.0 since I didn’t actually find anything clearly stating what the current API version is.)
For a while I thought I might be hitting the nginx client_max_body_size filesize limit; when I tested it with https://www.endpoints.dev/, it failed with 413 Payload Too Large status code, but worked if I deleted files out of the theme zip until it was just a few KB.
However, with Ghost it fails even with everything but package.json removed from the zip. I tried editing some Ghost core files (server/web/api/endpoints/admin/app.js
) to increase the limit from 50mb to 500mb (don’t know if it worked).
https://jwt.io/ seems to think my token is valid.
Any insight into what stupid mistake I have made, from someone who is able to successfully upload a theme with bash, would be very much appreciated!
The weird thing is that there are no errors, but also no response from Ghost; nothing shows up in Ghost’s logs, even though curl says it connected (Connected to localhost (127.0.0.1) port 2368 (#0)
).
Thanks!
#!/bin/bash
set -x
set -e
THEME="golden-pro"
SITE_URL="http://localhost:2368"
KEY="[REDACTED]"
API_VERSION="v3.0"
# Split the key into ID and SECRET
TMPIFS=$IFS
IFS=':' read ID SECRET <<< "$KEY"
IFS=$TMPIFS
# Prepare header and payload
NOW=$(date +'%s')
FIVE_MINS=$(($NOW + 300))
HEADER="{\"alg\": \"HS256\",\"typ\": \"JWT\", \"kid\": \"$ID\"}"
PAYLOAD="{\"iat\":$NOW,\"exp\":$FIVE_MINS,\"aud\": \"/admin/\"}"
# Helper function for performing base64 URL encoding
base64_url_encode() {
declare input=${1:-$(</dev/stdin)}
# Use `tr` to URL encode the output from base64.
printf '%s' "${input}" | base64 | tr -d '=' | tr '+' '-' | tr '/' '_'
}
# Prepare the token body
header_base64=$(base64_url_encode "$HEADER")
payload_base64=$(base64_url_encode "$PAYLOAD")
header_payload="${header_base64}.${payload_base64}"
# Create the signature
signature=$(printf '%s' "${header_payload}" | openssl dgst -binary -sha256 -mac HMAC -macopt hexkey:$SECRET | base64_url_encode)
# Concat payload and signature into a valid JWT token
TOKEN="${header_payload}.${signature}"
zip -r "$(date -I)-${THEME}.zip" "$THEME" -x '*git*' '*node_modules*' '*bower_components*'
curl --verbose -X POST -H "Authorization: Ghost ${TOKEN}" -H "Accept-Version: $API_VERSION" -F "file=@/var/www/ghost/content/themes/$(date -I)-${THEME}.zip" $SITE_URL/ghost/api/admin/themes/upload