GitHub App Authentication (JWT Generation in Java, Bash and Ruby)

Personal Note

There’s been a loooonnnnngggg gap in my posting, part of that is life related (bought a house, etc) and part of that is professionally related (I left ShoppingGives in Sept 2021 to be a Principal Engineer at DELL Technologies). Either way, I’m sorry but I’ve switched from C# as my professional language (it’ll still be my #1 personal project language, to Java so that’s why there’s Java snippits rather than C#.

The difference between OAuth App and GitHub Apps

The first thing to talk about is the difference between GitHub Apps and OAuth Apps and it all kind of boils down to this: OAuth Apps are used to access resources on behalf of a user, while GitHub Apps are able to do that as well, but in addition are installed on repositories and can work without a user’s authentication.

The Official Documentation

https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps

I’m sorry I had to subjugate you to that, but I will probably link to pieces of it anyways, I just feel like it does a bad job with the big picture.

The TLDR

There are 3 steps to authenticating:

  1. Private Key Generation: Generating Private Key
  2. JWT Generation: JWT Generation
  3. Access Code Retrieval: Authenticating as an installation

NOTE: The JWT ONLY GIVES ACCESS TO GET THE ACCESS CODE. You cannot use it to perform actions.

Refined Info

1. Private Key Generation

There’s no missing information here, good job GitHub docs.

2. JWT Generation

So for JWT Generation they only give 1 example and it’s in Ruby. So here’s other ways:

Java

For Java I wasn’t able to figure out how to get the private key without a little OpenSSL magic so the first step you need to do is to convert the .PEM that’s downloaded from the private key generation into der format by doing:

openssl pkcs8 -topk8 -inform PEM -outform DER -in **KEY** -out github-private-key.der -nocrypt

Then load it as a private key and use it to sign the jwt:

   public String createJWT(String issuer, long ttlMillis) throws Exception {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RS256;
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        Key key = getPrivateKey(**private key location**);

        //Let's set the JWT Claims
        JwtBuilder builder = Jwts.builder()
                .setIssuedAt(now)
                .setIssuer(issuer)
                .signWith(signatureAlgorithm, key);

        //if it has been specified, let's add the expiration
        if (ttlMillis > 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }

        //Builds the JWT and serializes it to a compact, URL-safe string
        return builder.compact();
    }

    public static PrivateKey getPrivateKey(String filename) throws Exception {

        File f = new File(filename);
        FileInputStream fis = new FileInputStream(f);
        DataInputStream dis = new DataInputStream(fis);
        byte[] keyBytes = new byte[(int) f.length()];
        dis.readFully(keyBytes);
        dis.close();

        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        return kf.generatePrivate(spec);
    }
Bash

Found this gist: https://gist.github.com/carestad/bed9cb8140d28fe05e67e15f667d98ad

Copied here for posterity. NOTE: YOU NEED TO CHANGE THE VARIABLES IN HERE TO THE APP ID AND LOCATION OF THE PEM Key

#!/usr/bin/env bash

# Generate JWT for Github App
#
# Inspired by implementation by Will Haley at:
#   http://willhaley.com/blog/generate-jwt-with-bash/
# From:
#   https://stackoverflow.com/questions/46657001/how-do-you-create-an-rs256-jwt-assertion-with-bash-shell-scripting

thisdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
set -o pipefail

# Change these variables:
app_id=1337
app_private_key="$(< $thisdir/app.key)"

# Shared content to use as template
header='{
    "alg": "RS256",
    "typ": "JWT"
}'
payload_template='{}'

build_payload() {
        jq -c \
                --arg iat_str "$(date +%s)" \
                --arg app_id "${app_id}" \
        '
        ($iat_str | tonumber) as $iat
        | .iat = $iat
        | .exp = ($iat + 300)
        | .iss = ($app_id | tonumber)
        ' <<< "${payload_template}" | tr -d '\n'
}

b64enc() { openssl enc -base64 -A | tr '+/' '-_' | tr -d '='; }
json() { jq -c . | LC_CTYPE=C tr -d '\n'; }
rs256_sign() { openssl dgst -binary -sha256 -sign <(printf '%s\n' "$1"); }

sign() {
    local algo payload sig
    algo=${1:-RS256}; algo=${algo^^}
    payload=$(build_payload) || return
    signed_content="$(json <<<"$header" | b64enc).$(json <<<"$payload" | b64enc)"
    sig=$(printf %s "$signed_content" | rs256_sign "$app_private_key" | b64enc)
    printf '%s.%s\n' "${signed_content}" "${sig}"
}

sign

3. Access Code Retrieval

Pretty straightforward in the docs.

Prologue

Hopefully this is more succinct and easier to understand than the GitHub docs and saves someone time!