Lately, I have been working on a few projects that use GitHub Action Workflows. In one project, I tried to use a GitHub Workflow to access and modify the contents of a totally different repo. On the surface, this sounds like an easy problem to solve. But, while trying to solve this, I went down a rabbit-hole of APIs, tokens, and permissions. Pretty soon, my head started to spin.
I think I now have it figured out (mostly). And I'll be trying my best to explain it all in the following blog post. Fair warning, this is going to be a long and technical post.
GitHub Action Workflows
If you enable GitHub Actions on your repo, then GitHub will silently install a special GitHub App to your repo. To my knowledge, there is no way to view this app yourself through the UI, but rest assured it is there. This GitHub App is installed to your repo, and your repo alone. Therefore, the app installation cannot be used to access other repos.
GitHub can automatically generate tokens from this app installation. Specifically, it can generate Installation Access Tokens. In fact, before each job in your GitHub Workflow begins, GitHub generates a new installation access token. This token expires as soon as the job finishes, or after a maximum of 24 hours. Again, this token only allows you to reference this repo and only this repo. Your workflow can use the token in 2 ways:
Using the standard syntax for secrets: ${{ secrets.GITHUB_TOKEN }}
Using the github context: ${{ github.token }}
The next question you may ask is what permissions does this installation access token have to my repo? Well, that depends, as there are 2 different ways you can define the permissions:
One, you can modify your repo's settings, under Actions > General > Workflow Permissions, and pick 1 of 2 different default presets:
Permissive: Read and write permissions
Restrictive: Read repository contents and package permissions
More info about these presets can be found here.
Two, you can also define a "permissions" key inside your Workflow's YAML code:
If you do this, it will override the preset from above.
This allows you to be very granular and precise in the permissions that you grant to the installation access token.
The permissions key can be defined at the top-level of the workflow and will then be inherited by all jobs. Or, you can define the permissions key on each individual job. This is helpful if you need to set different permissions for each job in your Workflow.
More info about this key can be found on my GitHub Workflows Guide.
Tokens, Tokens, Tokens
Now we understand that when using the default GITHUB_TOKEN, we are not allowed to access any other repos. So, knowing that, how do I go about solving my original problem? Well, I need to create a brand new token and use that in my workflow instead of the default GITHUB_TOKEN.
GitHub supports a lot of different types of tokens. We have:
Tokens created by a GitHub User:
Fine-grained Personal Access Tokens (PAT)
Classic Personal Access Tokens (PAT)
Tokens created by a GitHub App:
JSON Web Tokens (JWT)
Installation Access Tokens
User Access Tokens
Tokens created by an OAuth App:
OAuth Tokens
So, which option should I use?
If my own personal GitHub account has the necessary access to both repos, then I could create a Personal Access Token (PAT) and use that in my workflow. But, I really don't like that idea. I don't want anything tied to my personal account if I can avoid it. That's especially true in any type of enterprise environment.
What we really want to use is a token that is generated by an app. This is ideal because, if we do it correctly, it won't be tied directly to anyone's personal user account. As you can see above, GitHub has 2 different kinds of apps: GitHub Apps and OAuth Apps. Tokens created by an OAuth App are delegated / on behalf of a user account, which sort of defeats the purpose here. We don't want anything that depends on permissions from a user account. So, that leaves GitHub Apps as the only remaining option.
GitHub Apps
GitHub Apps are perfect for our use case because they can act as themselves (and therefore won't be tied to a user account), they support fine-grained permissions, they can be installed to a specific list of repos under my account or organization, and they can generate a token that we can use in our workflow. Great, let's create a GitHub App and use it to generate a token.
This post would be way too long if I explained each step in detail. So, you will have to make do with the high-level process, which goes like this:
Create/register a new GitHub App:
This can be done under your account, or it can also be done under a GitHub Organization. This will determine who "owns" the app (either your account or the organization)
Configure the permissions that your new app will require. In our case, in order to modify the files in the other repo, we want the "Repo permissions\Contents: Read and Write" permission. For a full breakdown of all the permissions see this link
After the app is created:
Capture the value for "App ID", we will need this later
Create a new Private Key for your app, we will need this later
Basically, this allows you to pick which repos the app will have access to. You can either pick all repos under your account/your organization, or you can pick a specific list of repos that you choose
This installation will get its own unique "Installation ID". This can easily be grabbed from the URL. For example, the Installation ID for (https://github.com/settings/installations/46454703) would be 46454703.
Capture this Installation ID, we will need it later
Generate a JSON Web Token (JWT) for your app:
We need to generate a JWT using your App ID and Private Key that we captured earlier in step 1
The link above gives you multiple examples of how to do this, including options for Ruby, Python, Bash, and PowerShell
The JWT is only good for a maximum of 10 minutes
Generate a new Installation Access Token:
Call the GitHub API and authenticate with the JWT you just created. Your call should reference the Installation ID that you captured in step 2
The response from the API call will include our coveted Installation Access Token! This token has the same permissions that are assigned to the GitHub App, and it is allowed to talk with the same repos that the GitHub App is installed to
The token is only good for a maximum of 1 hour
Use the new token in your workflow:
The new installation access token can now be used in place of the default GITHUB_TOKEN. Some examples include:
Using it for HTTP-based Git access: git clone https://x-access-token:YOURTOKEN@github.com/owner/repo.git
Using it in the "token" parameter on the actions/checkout Action
Calling the GitHub API and using the token in the "Authorization" header
Since the token is short-lived, it only makes sense to generate it in the same Workflow that you need to use it in.
To make your life easier, GitHub has an official action (actions/create-github-app-token) that will automate steps 3 & 4 for you. All you need to do is supply the App ID (from step 1), the Private Key (also from step 1), and the owner of the installation (whether that's your account or your organization). The action will generate a JWT for you, then it will find the Installation ID, and finally it will generate an Installation Access Token for you. The action has a single output, the token, which you can easily reference from other steps in your workflow. By default, the action will automatically revoke the token as soon as the current job is complete, but this can be turned off, if desired.
GitHub APIs
As part of my journey into solving this problem, I also learned a lot about GitHub's 2 different API's. It can get confusing pretty quickly. GitHub has a REST API as well as a GraphQL API.
The REST API has a lot of different endpoints, and each one can support different forms of authentication. For example, some REST API endpoints only work with JWT tokens, some only work with basic authentication, etc.
On the other hand, the GraphQL API only has 1 endpoint that you send queries to, and the GraphQL API has its own specific authentication requirements.
I tried my best to summarize my findings in the nice graphic that you see below.
Wrap up
Hopefully you got some benefit from this article! There was a lot of investigation, reading, and trial & error that I had to do before I could fully wrap my head around how to properly generate tokens and use them in GitHub Workflows.
Did I miss anything?
Komentáre