diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..600d2d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode \ No newline at end of file diff --git a/README.md b/README.md index d6e2ee3..23b8bc4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # GitHub App auth for Deno -A minimal Deno library and a CLI app to authenticate as a GitHub App installation. +A minimal Deno library and a CLI app to authenticate as a GitHub App +installation. ## Usage @@ -17,15 +18,31 @@ Then you can use it as `github_app_auth ...`. #### Expected inputs - `app-id`: you can find it in the application settings -- `private-key`: Base64-encoded content of the private key `.pem` file you got when you created the app -- `installation-id`: organization or user installation ID you want to get access to -- `repositories` (optional, trail-arg): list of repositories to give access to; if not provided, it will be all repositories that the installation can access +- `private-key`: Base64-encoded content of the private key `.pem` file you got + when you created the app, converted into pkcs8 format +- `installation-id`: organization or user installation ID you want to get access + to +- `repositories` (optional, trail-arg): list of repositories to give access to; + if not provided, it will be all repositories that the installation can access -See the [API endpoint documentation](https://docs.github.com/en/rest/reference/apps#create-an-installation-access-token-for-an-app) for more info. +See the +[API endpoint documentation](https://docs.github.com/en/rest/reference/apps#create-an-installation-access-token-for-an-app) +for more info. + +#### Converting the GitHub Private Key + +The private key that github generates is a `pkcs1` format key but Deno only +supports `pkcs8`. To convert it you can use openssl: + +```sh +openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in ~/Downloads/example.2024-11-12.private-key.pem -out private-key.key +``` #### GitHub API URL -If you need to change the default GitHub API URL, you can do it by setting the `GITHUB_API_URL` env var and adding `--allow-env=GITHUB_API_URL` when you install the script: +If you need to change the default GitHub API URL, you can do it by setting the +`GITHUB_API_URL` env var and adding `--allow-env=GITHUB_API_URL` when you +install the script: ```shell export GITHUB_API_URL='...' @@ -36,14 +53,16 @@ Or use `--allow-net --allow-env` for simplicity. #### Examples -Given app ID, private key and then installation ID it will create a new installation access token and print it to the standard output: +Given app ID, private key and then installation ID it will create a new +installation access token and print it to the standard output: ```shell $ github_app_auth 123456 $(base64 < private-key.pem) 12345678 ghs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` -If you don't know the installation ID, you can run it with only the first two arguments and get a list of the app installations: +If you don't know the installation ID, you can run it with only the first two +arguments and get a list of the app installations: ```shell $ github_app_auth 123456 $(base64 < private-key.pem) @@ -53,7 +72,9 @@ $ github_app_auth 123456 $(base64 < private-key.pem) ... ``` -You can use generated tokens to make requests on behalf on the app. Here are some examples using GitHub's official CLI `gh`, but you can also do it with `curl` or any other tool by adding the `Authorization: token ...` header. +You can use generated tokens to make requests on behalf on the app. Here are +some examples using GitHub's official CLI `gh`, but you can also do it with +`curl` or any other tool by adding the `Authorization: token ...` header. ```shell $ GITHUB_TOKEN=$(github_app_auth 123456 $(base64 < private-key.pem) 12345678) @@ -73,4 +94,5 @@ $ gh api repos/:owner/repo1/releases ... ``` -Check out [`gh api` docs](https://cli.github.com/manual/gh_api) for more examples and full feature list. +Check out [`gh api` docs](https://cli.github.com/manual/gh_api) for more +examples and full feature list. diff --git a/jwt.ts b/jwt.ts index 67d3f2b..13826c5 100644 --- a/jwt.ts +++ b/jwt.ts @@ -1,6 +1,29 @@ -import { create, getNumericDate } from "https://deno.land/x/djwt@v2.2/mod.ts"; +import { create, getNumericDate } from "jsr:@zaubrik/djwt@^v3.0.2"; +import { decodeBase64 } from "jsr:@std/encoding@^1.0.5"; + +function pemToBinary(pem: string): Uint8Array { + const base64 = pem + .replace(/-----[A-Z ]*-----/g, "") + .replace(/\s+/g, ""); + return decodeBase64(base64); +} + +export async function appJwt( + appId: string, + pkcs8PrivateKey: string, +): Promise { + const buffer = pemToBinary(pkcs8PrivateKey); + const key = await crypto.subtle.importKey( + "pkcs8", + buffer, + { + name: "RSASSA-PKCS1-v1_5", + hash: { name: "SHA-256" }, + }, + true, + ["sign"], + ); -export function appJwt(appId: string, privateKey: string): Promise { return create( { alg: "RS256", typ: "JWT" }, { @@ -8,6 +31,6 @@ export function appJwt(appId: string, privateKey: string): Promise { iat: getNumericDate(0), // issued at time (now) exp: getNumericDate(5 * 60), // expiration time (in 5 minutes) }, - atob(privateKey), + key, ); }