GitHub App generating JWT
Some of GitHub API like status checks requires GitHub App
In general it is almost the same as using REST API but there is one caveat - generating an access token - it is like a night mare
Things becomes worse because it is needed not often and each time become pain
That's why going to put some notes after yet another time going thru seven rounds of hell trying to figure out hot it should be done
Let's pretend he have created and installend an app, as well as granted required permissions to it
We should have following info (in all next examples we expect this is accessible as environment variables)
APP_ID
- can be found at url like this:https://github.com/organizations/{ORG}/settings/apps/{APP_NAME}
INSTALLATION_ID
- can be found at installations pagehttps://github.com/organizations/{ORG}/settings/apps/{APP_NAME}/installations
, just navigate to intallation and grab your id from an urlPRIVATE_KEY
- path to downloaded private key, it will have name likemy-awesome-app.2023-12-14.private-key.pem
and conctent like-----BEGIN RSA PRIVATE KEY-----.......
Example: Using Python to generate a JWT
Before doing anything else we need an baseline to check if things works at all
So I am taking an example from docs for python
Here is an script:
import jwt
import time
import os
import requests
private_key = jwt.jwk_from_pem(open(os.getenv("PRIVATE_KEY"), 'rb').read())
app_id = os.getenv("APP_ID")
installation_id = os.getenv("INSTALLATION_ID")
payload = {
'iat': int(time.time()) - 10,
'exp': int(time.time()) + 600,
'iss': app_id
}
encoder = jwt.JWT()
encoded = encoder.encode(payload, private_key, alg='RS256')
print('jwt:', encoded)
token = requests.post(f"https://api.github.com/app/installations/{installation_id}/access_tokens", headers={"Authorization": f"Bearer {encoded}"}).json()['token']
print('token:', token)
meta = requests.get("https://api.github.com/meta", headers={"Authorization": f"Bearer {token}"})
print('meta:', meta.status_code)
python3 -m venv
source ./venv/bin/activate
pip install jwt
python sample.py
APP_ID=123 INSTALLATION_ID=456 PRIVATE_KEY=privatekey.pem python sample.py
If everything fine, output should be:
jwt: <token signed by private key>
token: <exchanged access token>
meta: 200
Note that
https://api.github.com/meta
is accessible by anonymous users, but if you will try to access it with wrong token you will receive an bad credentials error
Example: Using JavaScript to generate a JWT
Not it become little bit easy, here is an notjs example of the same test
import crypto from 'crypto'
import fs from 'fs'
var app_id = process.env.APP_ID
var installation_id = process.env.INSTALLATION_ID
var private_key = process.env.PRIVATE_KEY
var header = Buffer.from(
JSON.stringify({
typ: 'JWT',
alg: 'RS256',
})
).toString('base64url')
var payload = Buffer.from(
JSON.stringify({
iat: Math.floor(Date.now() / 1000) - 10,
exp: Math.floor(Date.now() / 1000) + 600,
iss: app_id,
})
).toString('base64url')
const signature = crypto.createSign('RSA-SHA256').update(`${header}.${payload}`).sign(fs.readFileSync(private_key, 'utf8'), 'base64url')
var jwt = `${header}.${payload}.${signature}`
console.log('jwt:', jwt)
var { token } = await fetch(`https://api.github.com/app/installations/${installation_id}/access_tokens`, { method: 'POST', headers: { authorization: `Bearer ${jwt}` } }).then(r => r.json())
console.log('token:', token)
const meta = await fetch('https://api.github.com/meta', { headers: { authorization: `Bearer ${token}` } })
console.log('meta:', meta.status)
And its usage:
APP_ID=123 INSTALLATION_ID=456 PRIVATE_KEY=privatekey.pem node sample.mjs
Note: there is no need to install any 3rd party dependencies so it should work out of the box
Example: Using C# to generate a JWT
Before proceeding to next example, here is one more for C#
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
var app_id = Environment.GetEnvironmentVariable("APP_ID");
var private_key = Environment.GetEnvironmentVariable("PRIVATE_KEY");
var installation_id = Environment.GetEnvironmentVariable("INSTALLATION_ID");
var head = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new
{
alg = "RS256",
typ = "JWT"
}))).TrimEnd('=').Replace('+', '-').Replace('/', '_');
var payload = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new
{
iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
exp = DateTimeOffset.UtcNow.AddMinutes(10).ToUnixTimeSeconds(),
iss = app_id
}))).TrimEnd('=').Replace('+', '-').Replace('/', '_');
var rsa = RSA.Create();
rsa.ImportFromPem(File.ReadAllText(private_key));
var signature = Convert.ToBase64String(rsa.SignData(Encoding.UTF8.GetBytes($"{head}.{payload}"), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)).TrimEnd('=').Replace('+', '-').Replace('/', '_');
var jwt = $"{head}.{payload}.{signature}";
Console.WriteLine($"jwt: {jwt}");
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", jwt);
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (compatible; AcmeInc/1.0)");
var response = await client.PostAsync($"https://api.github.com/app/installations/{installation_id}/access_tokens", null);
var token = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement.GetProperty("token").GetString();
Console.WriteLine($"token: {token}");
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
response = await client.GetAsync("https://api.github.com/meta");
Console.WriteLine($"meta: {response.StatusCode}");
Usage:
APP_ID=123 INSTALLATION_ID=456 PRIVATE_KEY=privatekey.pem dotnet run
and finaly
Example: Using PowerShell to generate a JWT
$app_id = $env:APP_ID
$installation_id = $env:INSTALLATION_ID
$private_key = $env:PRIVATE_KEY
$header = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @{
alg = "RS256"
typ = "JWT"
}))).TrimEnd('=').Replace('+', '-').Replace('/', '_');
$payload = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @{
iat = [System.DateTimeOffset]::UtcNow.AddSeconds(-10).ToUnixTimeSeconds()
exp = [System.DateTimeOffset]::UtcNow.AddMinutes(10).ToUnixTimeSeconds()
iss = $app_id
}))).TrimEnd('=').Replace('+', '-').Replace('/', '_');
$rsa = [System.Security.Cryptography.RSA]::Create()
$rsa.ImportFromPem((Get-Content $private_key -Raw))
$signature = [Convert]::ToBase64String($rsa.SignData([System.Text.Encoding]::UTF8.GetBytes("$header.$payload"), [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)).TrimEnd('=').Replace('+', '-').Replace('/', '_')
$jwt = "$header.$payload.$signature"
Write-Host "jwt: $jwt"
$token = Invoke-RestMethod -Method Post "https://api.github.com/app/installations/$installation_id/access_tokens" -Headers @{ Authorization = "Bearer $jwt" } | Select-Object -ExpandProperty token
Write-Host "token: $token"
$meta = Invoke-WebRequest "https://api.github.com/meta" -Headers @{ Authorization = "Bearer $token" }
Write-Host "meta: $($meta.StatusCode)"
With this in place, not it should be pretty straight forward to move further
Wondering if powershell example will be good enought to be added to dcos, meanwhile saving for myself in future