GitHub App and OAuth ~ Disjointed flow
Tags: github, tutorial, javascript, webdev
Published: 2022-04-28 08:10:24

In a previous post we saw how it's possible to get information from the GitHub REST API about which repositories have our GitHub App installed. This allowes us to build something like the following component.

![Vercel's Import Git Repository component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ep8jiy1t21yhnvmss0h3.png)

> [Vercel](https://vercel.com/new)'s Import Git Repository component

Since this data is related to a GitHub App, the necessary token is issued by the OAuth included in the GitHub App in question.

_Q: My system is already based on authentication through an OAuth App, I cannot twist it! Also, we it is handled by a third-party (i.g. [Stytch](https://stytch.com/)). But still, the `access_token` issued by an OAuth is not valid for the two endpoints!_

**A: There is a solution. It's slightly more expensive than the previous one, but it works.**

## Index

1. [Create OAuth App and GitHub App](#create-oauth-app-and-github-app)
2. [Authenticate as GitHub App via JWT](#authenticate-as-github-app-via-jwt)
3. [Get `installation_id`s of the GitHub App](get-installation_ids-of-the-github-app)
4. [Get GitHub App's `access_token`s](get-github-app-access-tokens)
5. [Retrieve eligible repositories](retrieve-eligible-repositories)
6. [Related Posts](#related-posts)

***

### Create OAuth App and GitHub App <a href="create-oauth-app-and-github-app"></a>

Obviously it's necessary to [create the OAuth App](https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app) and also [create the GitHub App](https://docs.github.com/en/developers/apps/building-github-apps/creating-a-github-app).

![GitHub OAuth App creation screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1wbmhitw7yg6sdchgln4.png)
- **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_

Finalize the creation. It has been assigned an _Client ID_ and it's possible to generate a _Client Secret_. Please do it and keep them handy.

Similar to how explained in the [previous post](https://dev.to/didof/github-app-and-oauth-practical-kick-starter-4df4), you can get and preserve on the client the `access_token`.

```js
server.get("/oauth/github/login/callback", async (request, reply) => {
  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;

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

  reply.status(302).header("Location", redirectionURL).send();
});
```

Still, if you try to query the two endpoints that return the app installations and their repositories, you'll get a 403.

{% details Endpoints: %} 
1. [/user/installations](https://docs.github.com/en/rest/apps/installations#list-app-installations-accessible-to-the-user-access-token)
2. [/user/installations/{installation_id}/repositories](https://docs.github.com/en/rest/apps/installations#list-repositories-accessible-to-the-user-access-token)
{% enddetails %}

We need another kind of `access_token`, one which is _related to the GitHub App_.

Therefore, create the GitHub App. This time you don't really need to assign any **Callback URL** (but you may want to set a **Setup URL**). For the purposes of this post, you are only interested in keeping track of the **App ID** (About section of your GitHub App configuration page) and generating and storing on your _fs_ the **Private Key** (at the bottom of the page).

<a href="generate-a-private-key"></a>
![Generate a private key](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qtepffu0p1wpw70rg8r7.png)

***

### Authenticate as GitHub App via JWT <a href="authenticate-as-github-app-via-jwt"></a>

As the [official docs](https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps#authenticating-as-a-github-app) expain:

> Authenticating as a GitHub App lets you do a couple of things [...] You can **request access tokens for an installation of the app**. To authenticate as a GitHub App, generate a **private key** in PEM format and download it to your local machine. You'll **use this key to sign** a JSON Web Token (**[JWT](https://jwt.io/)**) and encode it using the **RS256 algorithm**. GitHub checks that the request is authenticated by verifying the token with the app's stored public key.

Thus, the server may have a `/repos` route which in turn **generates the JWT**:

```js
const secret = fs.readFileSync(
  path.resolve(__dirname, ".private-key.pem"),
  "utf-8"
);

server.get("/repos", async (request, reply) => {
  const now = Math.floor(Date.now() / 1000) - 60; // don't just use Date.now()

  const payload = jwt.sign(
    {
      iat: now - 60,
      exp: now + 10 * 60,
      iss: process.env.APP_ID,
    },
    secret,
    {
      algorithm: "RS256",
    }
  );

  // ...
});
```

And once the JWT is created, use it in the authentication header prefixed by `Bearer ` (differently from the majority of GitHub REST API's endpoint, whom request `token `).

***

### Get `installation_id`s of the GitHub App
 <a href="get-installation_ids-of-the-github-app"></a>

The endpoint is [/app/installations](https://docs.github.com/en/rest/apps/apps#list-installations-for-the-authenticated-app):

```js
// still in /repos

const installations = await axios.get(
  `https://api/github.com/app/installations`,
  {
    headers: {
      Authorization: `Bearer ${payload}`,
    },
  }
);
```

A list made of elements like to following is returned:

```json
{
  id: 25061467,
  account: {
    login: '<some-username>',
    ...
  },
  repository_selection: 'selected',
  access_tokens_url: 'https://api.github.com/app/installations/25061467/access_tokens',
  repositories_url: 'https://api.github.com/installation/repositories',
},
```

We need to filter in all items where `.account.login` equals the user's personal account or one of the user's organizations. You may retrieve this infos via the [/user/org](https://docs.github.com/en/rest/orgs/orgs#list-organizations-for-the-authenticated-user), **keeping in mind to pass the `access_token` retrieved via the OAuth app**.

Basically, something like:

```js
const relevantInstallations = installations.data.filter((installation) => {
  return currentUserOrganizations.includes(installation.account.login);
});
```

***

### Get GitHub App's `access_token`s <a href="get-github-app-access-tokens"></a>

For each one of the installations relevant the our user (and all user's organizations), we request a GitHub App authenticated `access_token` via the endpoint [/app/installations/{installation_id}/access_tokens](https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app):

```js
const promises = relevantInstallations.map((installation) => {
    return axios.post(
      `https://api.github.com/app/installations/${installation_id}/access_tokens`,
      null,
      {
        headers: {
          Authorization: `Bearer ${payload}`,
        },
      }
    );
  });

// Parallel
const accessTokens = await axios.all(promises)
```

> Note: instead of manually assembling the url, you could use the premade `installation.access_tokens_url`.

***

### Retrieve eligible repositories <a href="retrieve-eligible-repositories"></a>

Iterate each `access_token` and use it in the _Authorization header_ `token ${access_token}` at the endpoint `/installation/repositories` (`installation.repositories_url`):

```js
// Note: for each access_token!
const response = await axios.get(
  "https://api.github.com/installation/repositories",
  {
    headers: {
      Authorization: `token ${token}`, // not Bearer
    },
  }
);

const repositories = response.data.repositories;
```

Merge or organize all the received repositories and you're back to the first post situation. We got there by a transverse, slightly more strenuous route - nonetheless, we're reached the goal.

***

### Related Posts <a name="related-posts"></a>

- [GitHub App and OAuth ~ Standalone flow](https://dev.to/didof/github-app-and-oauth-practical-kick-starter-4df4)
- [OAuth popup Guide](https://dev.to/didof/oauth-popup-practical-guide-57l9)

- [cover image](https://octodex.github.com/filmtocats/)

### Contacts

- [Twitter](https://twitter.com/did0f)
- [LinkedIn](https://www.linkedin.com/in/didof/)
- [GitHub](https://github.com/didof)