DEV Community

Cover image for GitHub App and OAuth ~ Practical Kick-Starter
Francesco Di Donato
Francesco Di Donato

Posted on • Edited on

GitHub App and OAuth ~ Practical Kick-Starter

What can we build

An interface that gives a GitHub-authenticated user (or any user's organization) the ability to see which of his repositories have a given GitHub App installed.

Vercel's Import Git Repository component

Vercel's Import Git Repository component


Premise

I found two different ways to achieve the goal.

  1. Standalone provider (GitHub App)
  2. Disjointed flow (OAuth App, GitHub App).

If you know of other modalities, please contact me (twitter).

In this post we are implementing the first modality.


Index

  1. Create GitHub App
  2. GitHub OAuth
  3. Query the GitHub REST API
  4. Related Posts

Create GitHub App

In order for us to find the list of repos that have the GitHub App installed, we must first create one.

For the purposes of this post, we just need to know that to authenticate a user through GitHub, you need to register your own OAuth app; however, every GitHub App has an OAuth inside it.

That's why I (arbitrarily) call this method standalone - we only use a single GitHub App.

Register GitHub App configuration

  • Homepage URL: http://localhost:3000
  • Callback URL: Where the provider should send back the user once the authentication flow is completed. You can pick any route, I'm using /oauth/github/login/callback
  • Setup URL: Where the provider should send back the user once the GitHub has been installed/uninstalled/permission-changed. You can pick any route, I'm using /new.

Contents read-only permission

Configure the app so that it has the right to read the content. Otherwise, it won't be able to be installed on specific repos.

You'll also find a Webhook section. For the purposes of this post we don't care, so to continue you can just mark it as inactive.

Finally create the GitHub App. It has been assigned an App ID, a Client ID and it's possible to generate a Client Secret - All the stuff we need, but in a little while.


GitHub OAuth

Authenticating a user with GitHub is straight forward.

If something isn't clear kindly refer to companion repo.

First, the frontend presents a CTA which points to a route on the server.

<a id="oauth-github">Authenticate via GitHub</a>
<script>
    const CLIENT_ID = "Iv1.395930440f268143";

    const url = new URL("/login/oauth/authorize", "https://github.com");
    url.searchParams.set("client_id", CLIENT_ID);

    document.getElementById("oauth-github")
      .setAttribute("href", url);
</script>
Enter fullscreen mode Exit fullscreen mode

Note: security

To remain concise, I'm only reporting client_id since it's required; however in a production context be sure to review the official documentation, especially regarding scope and state.

Make it pretty, add the GitHub icon to it - the user presses it and is taken to the following page:

GitHub authentication screen

Now, when the green Authorize button is pressed, the user is redirected to Callback URL set during GitHub App creation.

Let's make sure that the server is able to listen to /oauth/github/login/callback. Watch out for this key step: GitHub redirects and adds a query param, a code needed to authenticate the user.

server.get("/oauth/github/login/callback", async (request, reply) => {
  const { code } = request.query;

  // ...
});
Enter fullscreen mode Exit fullscreen mode

code is supposed to be exchanged with an access_token, which will be stored in the client and associated with requests to the GitHub REST API.

Now, before moving on, please go back to your GitHub App Configuration page and Generate a new Client Secret. Thus, dotenv it.

const { code } = request.query;

const exchangeURL = new URL("login/oauth/access_token", "https://github.com");
exchangeURL.searchParams.set("client_id", process.env.CLIENT_ID);
exchangeURL.searchParams.set("client_secret", process.env.CLIENT_SECRET);
exchangeURL.searchParams.set("code", code);

const response = await axios.post(exchangeURL.toString(), null, {
  headers: {
    Accept: "application/json",
  },
});

const { access_token } = response.data;
Enter fullscreen mode Exit fullscreen mode

For the sake of brevity, the example does not report error handling. It is left to the common sense of the reader.

Thus, the token is delivered to the client whom ultimately is redirected to some /new route. But - here's the hero of our story - also receives the access_token as query param.

const { access_token } = response.data;

const redirectionURL = new URL("new", "http://localhost:3000");
redirectionURL.searchParams.set("access_token", access_token);

reply.status(302).header("Location", redirectionURL).send();
Enter fullscreen mode Exit fullscreen mode

Extract it on the client:

<!-- new.html, sent when GET /new -->
<script>
  const access_token = new URL(window.location).searchParams.get(
    "access_token"
  );

  localStorage.setItem("access_token", access_token);
</script>
Enter fullscreen mode Exit fullscreen mode

You can store it any Web Storage, in-memory, really depends on how your interface is to be used. Although an HttpOnly and Secure cookie would help mitigate XSS attacks.

The client of the authenticated user can finally associate the access_token when it legitimately queries the GitHub REST API. Almost there.

Enhancement: popup instead of redirection

Usually other OAuth in the wild do not hard redirect the current page. Suppose to be working on a SPA, think how sad it is to find out that this flow empties your in-memory store. Easy to solve, the redirection will still happen but in a spawned popup (post) that, before dissolving will pass the access_token back to main tab.


Query the GitHub REST API

The data we need is returned from two endpoints.

  1. /user/installations
  2. /user/installations/{installation_id}/repositories

The latter is called with information received from the former.

The documentation states:

You must use a user-to-server OAuth access token, created for a user who has authorized your GitHub App, to access this endpoint.

The access_token returned by a "simple" OAuth App is not valid for these endpoints. However, there's a way to make it work using an OAuth and three other different endpoints. This is shown in the second post.

const githubAPI = axios.create({
  baseURL: "https://api.github.com",
  headers: {
    Accept: "application/vnd.github.v3+json",
  },
});

const authorizationConf = {
  headers: {
    authorization: `token ${access_token}`,
  },
};

(async () => {
  const installationResponse = await githubAPI.get(
    "user/installations",
    authorizationConf
  );
})();
Enter fullscreen mode Exit fullscreen mode

Setting to application/vnd.github.v3+json is recommended by the official docs. It's the GitHub API custom media type.

We receive back a list containing... wait, it's empty. That's because the GitHub App has not yet been installed anywhere.

Let's make it easy to install:

<a
 href="https://github.com/apps/<github-app-name>/installations/new"
>
  Change permissions
</a>
Enter fullscreen mode Exit fullscreen mode

That once is clicked shows the following screen:

GitHub App change permission screen

Either pick you personal account or one of your organizations. Or both. Install it somewhere.

Repository access screen

Now /user/installations returns a list of installations. One for each account (personal account || organization) that has at least one repo with the github app installed in it.

Each item has the property id. That's the installation_id required for the next endpoint.

const promises = installationsResponse.data.installations.map(
  (installation) => {
    return githubAPI.get(
      `user/installations/${installation.id}/repositories`,
      authorizationConf
    );
  }
);

// parallel
const responses = await axios.all(promises);

const repositories = responses.map((response) => {
  return response.data.repositories;
});
Enter fullscreen mode Exit fullscreen mode

And there you have all the repositories of the authenticated user or one of his organizations that have the GitHub App installed.


Epilogue

To recap, you now can:

  • Authenticate users via GitHub OAuth authentication
  • Redirect users to install your GitHub App
  • Show them a dropdown containing all the organization in addition the the personal account
  • Know which repositories to show

I'll leave it up to you all the fun of implementing the UI with your favorite framework. If you need to look at the full tour, in the companion repo I used technologies that anyone knows.


Related Posts

Contacts

Top comments (0)