Github App

Github Apps are a way to give programmatic access to Github repositories. Instead of having to set up a 'robot' user, or worse use your own credentials when scripting you can install a Github App to your repository.

A Github App acts as a middle man between some code you've written and yours or someone elses repositories. Github users and organisations can install your Github App and then your code can run on their repositories. During the creation of the Github App you specify the permissions you'd like on the repository (read/write contents, read metadata, write webhooks) and then during installation the user can see these and specify which repositories of theirs they would like to grant your app access to.

A Github App as you create it on Github has no logic or code in it. The App and installations are just a way to get permission to access a repository. In order to do anything with that permission you would need to write some code and host it somewhere. The name in my opinion is awkward and so in this post I will make the distinction between the Github App (the thing on Github) and the code needed to actually make an app, which I will refer to as code or the application.

Types of authentication

When you have set up your App and it is installed on a repository you can authenticate with it. In practice this means generating access tokens that you can use on the Github API. There are two types of token you can use as a Github App; user tokens, and installation tokens.

Installation token

If your App is installed on a repository then you can generate an installation token. Theis token would allow you to access the API with the permissions that the installation has. After installation no user interaction is required, your application can generate and use an installation token whenever it needs to. This token can use any of the endpoints provided by the API.

User Token

If your App is installed on a repository then you can generate user tokens. In this case you set up a flow in your app that authenticates a user (see below). You will receive a user token. This token will allow you to interact with the API with the intersection of permissions that you and a user have. i.e. the user must have access to a repository and the App must be installed to the repository in order to use the token. User tokens can't yet use all of the endpoints, but are restricted to a subset. You can still do quite a lot with them but there are a couple of quite annoying gaps.

Creation and Installation flow

A summary of how to create an App and how a user installs an app:

  1. Host some code somewhere that will interact with the Github api to perform actions on repositories.
  2. Create the Github App link .
    • You will need to give a redirect url.
    • Specify permissions that will be requested.
    • Given client ID and client secret, with option to generate a private key.
  3. Add the client ID and secret to your application code to allow it to authenticate.
  4. A user or organisation installs your App.
    • Asked to install on a repository, shown permissions you have requested.
    • This is an 'installation'
  5. A user can authenticate with the App to perform actions or your application code can authenticate with the App as an installation to perform actions on repositories that it has access to via installations.

Authentication Flow

Authenticating as a user

When a user uses your application you will want to identify them and see what access they have. This is detailed on Github here. To summarise:

  1. User goes to your applications landing page
  2. User is redirected to Github login screen
  3. User logs in and authorises your app to check what permissions they have
  4. User is redirected back to your app, with a 'code' parameter in the url
  5. Your application combines the code and App secret to generate a user token
  6. Your application uses this user token to check permissions or access a number of other endpoints

Some javascript to do the redirect might look like this:

// Get the code parameter from the url if it exists
const code = extractParam(document.location.search, "code");

// Check if you've stored a token
// We don't want to keep remaking tokens
const storedToken = localStorage.getItem("token");

if (storedToken) {
    // Do stuff with token
} else {
    if (code) {
        // We have already redirected and been given a code
        getToken(code)
    } else {
        // No code therefore do redirect auth
        window.location.href = 
            "https://github.com/login/oauth/authorize?client_id=" +
            clientId + // This is your app's client id
            "&redirect_uri=" +
            redirectURI // This is the URL the user will get redirected to after auth
    }
}

So what does that getToken method do? This is client side javascript so one thing we can't do is have our App's secret in the code anywhere. But we need to use the secret to exchange the code for a user token. One way around this is to host this exhange logic on a server and have the server side code do this exchange and pass back the user token. Gatekeeper is an easy to use program to do this.

Our getToken function might then look a bit like this:

getToken(code) {
    var req = new XMLHttpRequest();
    req.addEventListener("load", function() {
        token = JSON.parse(this.response).token
        // Do something with token
    });
    req.open("GET", gatekeeperURL + "/" + code);
    req.send();
}

Now we've got a token and can start doing stuff! The intended use of a user token is to check what repositories a user can access and what their access level is. For example:

var req = new XMLHttpRequest();
req.addEventListener("load", function() {
    // The response will include a list of all the installation of your App a user has     access to
});
// Use our shiny new token to authenticate
req.setRequestHeader("Authorization", "token " + theUserToken);
// This accept header is necessary whilst Apps are in Early Access preview
req.setRequestHeader("Accept", "application/vnd.github.machine-man-preview+json");
req.open("GET", "https://api.github.com/user/installations");
req.send();

As linked above you can access quite a few endpoints with the user token, but not all. For that you will need to generate an installation token.

Authenticating as an installation

The details of how to authenticate and get an installation token are here. To summarise:

  1. If you need to, authenticate a user as above and using their user token check their permissions. If you think they have sufficient permission; continue.
  2. Use your App's private key to generate a token to authenticate as your App. This token includes some expiry details as well.
  3. Use this App token to ask for an installation token
  4. Finally start using the installation token to make requests from the API

In my case I made a Python Flask application that takes requests from a web app that can't be fulfilled using the user token (getting the contents of a repository for example). It takes the requested url and the user token, checks the permissions that the user has, and if they have the correct permissions makes the request for them with an installation token and returns the response from the API.

Here is a simplified version of that code:

import sys
import json
import os
import logging
import datetime
import requests
import time
from flask import Flask, request
from flask_cors import CORS
import jwt

app = Flask(__name__)
CORS(app)

# Config
INSTALLATION_ID = "00000"
REPO_ID = "0000000"
PEM_FILE_LOCATION = "my-pem-file.pem"
URL_PREFIX = 'repos/someorg/somerepo/contents/'
ROUTE_URL = '/relay-github/'

@app.route(ROUTE_URL,methods=['POST'])
def relay():
    str_response = request.data.decode('utf-8')
    data = json.loads(str_response)

    # Takes two posted strings
    # The user token and the requested url
    if 'token' in data:
        token = data['token']
        if 'url' in data:
            url = data['url']
        else:
            return "", 404
          # Check the installation
        response = requests.get('https://api.github.com/user/installations/' + INSTALLATION_ID + '/repositories', headers={'Authorization': 'token ' + token, 'Content-Type': 'application/json', 'Accept': 'application/vnd.github.machine-man-preview+json'})
        try:
            # Check that they have the correct permissions on the repo
            for repo in response.json()["repositories"]:
                if repo['id'] == REPO_ID and repo['permissions']['push']:
                    # If they do create an App token
                    pem_file = open(PEM_FILE_LOCATION, "r")
                    encoded_jwt = jwt.encode({'iat': int(round(time.time())), 'exp': int(round(time.time()) + 60), 'iss': '6509'}, pem_file.read(), algorithm='RS256')
                    # Send the App token off in exhange for an installation token
                    response = requests.post('https://api.github.com/installations/' + INSTALLATION_ID '/access_tokens', headers={'Authorization': 'Bearer ' + str(encoded_jwt.decode("utf-8")), 'Content-Type': 'application/json', 'Accept': 'application/vnd.github.machine-man-preview+json'})
                    instoken = response.json()['token']
                    # Make their request with the installation token and return the reponse
                    response = requests.get('https://api.github.com/' + URL_PREFIX + url, headers={'Authorization': 'token ' + instoken, 'Content-Type': 'application/json', 'Accept': 'application/vnd.github.machine-man-preview+json'})
                    return response.text, response.status_code
        except Exception as error:
            print(error)
            return "Bad token", 401

    return "No token", 401

if __name__ == "__main__":
    app.run(host='0.0.0.0')

This is quite a simple example (the exception handling leaves something to be desired :P) but it works quite well. Once you get this installation token you have full access to the Gihub API and your application can do whatever you want it to.

Github app vs OAuth app

Just a quick note on this. Confusingly there are two types of app. OAuth Apps and Github Apps.

An OAuth App will be given the permissions of a user. There is some control over what actions it can perform (via 'scopes') but you can not specify particular repositories. The app is essentially acting as the user. This is fine but if your app needs read/write permissions then the user will be asked to grant you read/write permissions to all of their repo's which is understandably a little bit worrying.

A Github App then provides a bit more reassurance. In addition to some scoping of permissions (like the OAuth App) a user 'installs' your app to a particular repository or organisation and the app only gets permissions for that repo/org. This is why in general I would choose to use a Github App