diff --git a/github-app-token/SKILL.md b/github-app-token/SKILL.md index 554d406..52d4528 100644 --- a/github-app-token/SKILL.md +++ b/github-app-token/SKILL.md @@ -5,84 +5,22 @@ description: Generate a GitHub installation access token from a GitHub App PEM k # GitHub App Token Skill -Generate a short-lived GitHub installation access token from a GitHub App's credentials and use it to authenticate the `gh` CLI. +Generate a short-lived GitHub App installation token and authenticate `gh`. -## Prerequisites - -The following environment variables MUST be set before invoking this skill: +## Required Environment Variables | Variable | Description | |---|---| -| `GITHUB_APP_ID` | The numeric App ID from the GitHub App settings page | -| `GITHUB_APP_INSTALLATION_ID` | The numeric Installation ID for the target org/user | -| `GITHUB_APP_PEM_FILE` | Absolute path to the GitHub App's PEM private key file | +| `GITHUB_APP_ID` | Numeric App ID from GitHub App settings | +| `GITHUB_APP_INSTALLATION_ID` | Numeric Installation ID for the target org/user | +| `GITHUB_APP_PEM_FILE` | Absolute path to the App's PEM private key file | -If any variable is missing, stop and tell the user which ones are required. - -Requires `openssl`, `curl`, and `jq`. - -## Generate a Token - -Build a JWT signed with the App's private key, then exchange it for an installation access token and write it to a file: +## Usage ```bash -# Base64url helper -b64enc() { openssl enc -base64 -A | tr '+/' '-_' | tr -d '='; } - -# Build JWT (valid 10 minutes) -NOW=$(date +%s) -HEADER=$(printf '{"alg":"RS256","typ":"JWT"}' | jq -r -c .) -PAYLOAD=$(printf '{"iat":%s,"exp":%s,"iss":"%s"}' "$NOW" "$((NOW + 600))" "$GITHUB_APP_ID" | jq -r -c .) -SIGNED=$(printf '%s' "$HEADER" | b64enc).$(printf '%s' "$PAYLOAD" | b64enc) -SIG=$(printf '%s' "$SIGNED" | openssl dgst -binary -sha256 -sign "$GITHUB_APP_PEM_FILE" | b64enc) -JWT="${SIGNED}.${SIG}" - -# Token file — unique per agent to avoid env-var collisions -GH_TOKEN_FILE="${AGENT_HOME:+${AGENT_HOME}/.gh-token}" -GH_TOKEN_FILE="${GH_TOKEN_FILE:-$(mktemp)}" - -# Exchange JWT for installation token and write to file -curl -s -X POST \ - -H "Authorization: Bearer ${JWT}" \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/app/installations/${GITHUB_APP_INSTALLATION_ID}/access_tokens" \ - | jq -r '.token' > "$GH_TOKEN_FILE" - -chmod 600 "$GH_TOKEN_FILE" +bash github-app-token/scripts/generate-token.sh ``` -The token file path defaults to `$AGENT_HOME/.gh-token` (unique per agent) or a temporary file when `AGENT_HOME` is not set. This avoids env-var collisions when multiple agents generate tokens concurrently. +The script validates env vars, generates a JWT, exchanges it for an installation token, writes the token to `$AGENT_HOME/.gh-token`, and runs `gh auth login`. On success it prints a confirmation line. On failure it exits non-zero with a descriptive error. -## Authenticate the gh CLI - -Read the token from the file and log in: - -```bash -gh auth login --with-token < "$GH_TOKEN_FILE" -``` - -To use `GH_TOKEN` in a single command without polluting the environment: - -```bash -GH_TOKEN=$(cat "$GH_TOKEN_FILE") gh api user -``` - -## Cleanup - -The installation access token expires after 1 hour. To revoke it early and remove the token file: - -```bash -curl -s -X DELETE \ - -H "Authorization: Bearer $(cat "$GH_TOKEN_FILE")" \ - -H "Accept: application/vnd.github+json" \ - "https://api.github.com/installation/token" -rm -f "$GH_TOKEN_FILE" -``` - -## Security Notes - -- Never log or echo the PEM key or installation token to stdout in production. -- The installation token is valid for 1 hour from generation. -- Store the PEM file with restrictive permissions (`chmod 600`) and never check it into git. -- The token file is written with mode `600` and should be cleaned up after use. +Requires `openssl`, `curl`, `jq`, and `gh`. diff --git a/github-app-token/scripts/generate-token.sh b/github-app-token/scripts/generate-token.sh new file mode 100755 index 0000000..0b30761 --- /dev/null +++ b/github-app-token/scripts/generate-token.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +die() { echo "ERROR: $*" >&2; exit 1; } + +# --- Validate required env vars --- +[[ -z "${GITHUB_APP_ID:-}" ]] && die "GITHUB_APP_ID is not set" +[[ -z "${GITHUB_APP_INSTALLATION_ID:-}" ]] && die "GITHUB_APP_INSTALLATION_ID is not set" +[[ -z "${GITHUB_APP_PEM_FILE:-}" ]] && die "GITHUB_APP_PEM_FILE is not set" +[[ ! -f "$GITHUB_APP_PEM_FILE" ]] && die "PEM file not found: $GITHUB_APP_PEM_FILE" + +for cmd in openssl curl jq gh; do + command -v "$cmd" >/dev/null 2>&1 || die "Required command not found: $cmd" +done + +# --- Build JWT (valid 10 minutes) --- +b64url() { openssl enc -base64 -A | tr '+/' '-_' | tr -d '='; } + +NOW=$(date +%s) +HEADER=$(printf '{"alg":"RS256","typ":"JWT"}' | b64url) +PAYLOAD=$(printf '{"iat":%s,"exp":%s,"iss":"%s"}' "$NOW" "$((NOW + 600))" "$GITHUB_APP_ID" | b64url) +SIGNED="${HEADER}.${PAYLOAD}" +SIG=$(printf '%s' "$SIGNED" | openssl dgst -binary -sha256 -sign "$GITHUB_APP_PEM_FILE" | b64url) +JWT="${SIGNED}.${SIG}" + +# --- Exchange JWT for installation access token --- +RESPONSE=$(curl -sf -X POST \ + -H "Authorization: Bearer ${JWT}" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/app/installations/${GITHUB_APP_INSTALLATION_ID}/access_tokens") \ + || die "GitHub API request failed — check App ID, Installation ID, and PEM key" + +TOKEN=$(echo "$RESPONSE" | jq -r '.token // empty') +[[ -z "$TOKEN" ]] && die "No token in GitHub response: $RESPONSE" + +# --- Write token to file --- +GH_TOKEN_FILE="${AGENT_HOME:+${AGENT_HOME}/.gh-token}" +GH_TOKEN_FILE="${GH_TOKEN_FILE:-$(mktemp)}" + +printf '%s' "$TOKEN" > "$GH_TOKEN_FILE" +chmod 600 "$GH_TOKEN_FILE" + +# --- Authenticate gh CLI --- +gh auth login --with-token < "$GH_TOKEN_FILE" + +echo "Authenticated. Token written to $GH_TOKEN_FILE (expires in 1 hour)."