As I’m working on my new AI startup Rivets, I’ve been setting up a GitHub App.

That’s not an OAuth app, but the second type they provide, where the app can operate as itself as opposed to operating as you on your behalf.

There’s an excellent PHP library lcobucci/jwt available to handle the meaty parts of generating JWTs, but I had some trouble getting it working with GitHub Apps specifically, as there are some quirks, and not a huge range of examples.

And when we look at the examples GitHub does have, they don’t help us over in PHP. But they dropped some clues about formatting that were useful as I dug into the library.

GitHub apps require a couple of specific things, which the library already helpfully makes really easy!, but it wasn’t clear to me how to get things going.

Here’s how to generate your JWT - you’ll need your app’s client_id, a private key, and for the user to have already installed it so you have an installation_id (and naturally, first let’s composer install lcobucci/jwt!).

<?php
use Lcobucci\JWT\Token\Builder;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Encoding\ChainedFormatter;

$github_app_client_id = "123456789"; // Get your client ID from the GitHub App settings
$github_app_private_key = ""; // Download your private key .pem file, base64-encode it, and provide it here

$tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::withUnixTimestampDates()));
$algorithm = new Sha256();
$signingKey = InMemory::base64Encoded($github_app_private_key);
$now = new \DateTimeImmutable();

$token = $tokenBuilder
    ->issuedBy($github_app_client_id) // iss
    ->issuedAt($now) // iat
    ->expiresAt($now->modify('+10 minutes')) // exp
    ->getToken($algorithm, $signingKey);

$tokenString = $token->toString();

You’ll note four key points here:

  1. in my example the private key is base64-encoded, but you can pass in your private key as a file directly if you prefer
  2. the exp claim (expiresAt()) can only be a maximum of 10 minutes from right now
  3. you must use the RS256 algorithm as provided by Lcobucci\JWT\Signer\Rsa\Sha256
  4. most importantly, you’ll want to specify ChainedFormatter::withUnixTimestampDates() as your formatter

By default, using ChainedFormatter::default() as per all the examples in lcobucci/jwt’s documentation, your ‘issued at’ and ’expires at’ claims will be full dates, but GitHub wants them to be timestamps.

In the end, it’s very simple, and hopefully this example is useful for anyone else looking into GitHub Apps.