Managing Access Tokens with GitHub Actions and Encrypted Secrets
Tags: jamstack, github, javascript, webdev
Published: 2020-08-12 16:15:06

Integrating 3rd-party APIs into [Jamstack](https://jamstack.org/) apps and websites becomes a _bit_ tricky when the access tokens for these services need refreshing recurrently. Manually refreshing these access tokens and updating the corresponding secrets is one option, but automating the process via a scheduled [GitHub Workflow](https://docs.github.com/en/actions/configuring-and-managing-workflows/configuring-a-workflow)—though adding complexity—gives you one less thing to worry about. Using the [Instagram Basic Display API](https://developers.facebook.com/docs/instagram-basic-display-api/) as a basis, I'm going to walk-through an example of automating this process.

## Create an Encrypted Secret

The first step is to add an existing (valid) access token to your repository as an [encrypted secret](https://docs.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets#creating-encrypted-secrets-for-a-repository). It appears to be a convention to use `UPPER_CASE_SNAKE_CASE` when naming these secrets, so I've assigned my access token for the Instagram Basic Display API to a secret named `INSTAGRAM_ACCESS_TOKEN`.

## Pass a Secret as an Environment Variable

The context in which I'll be using this access token within my Jamstack website is an [axios](https://github.com/axios/axios) request querying the [Instagram User Media endpoint](https://developers.facebook.com/docs/instagram-basic-display-api/reference/user/media/), e.g.:

```js
const response = await axios.get(
  `https://graph.instagram.com/${instagramUserId}/media`,
  {
    params: {
      access_token: process.env.INSTAGRAM_ACCESS_TOKEN,
      fields: options.fields.join(",")
    }
  }
);
```

For `process.env.INSTAGRAM_ACCESS_TOKEN` to be referencing the correct value at runtime the environment variable `INSTAGRAM_ACCESS_TOKEN` must be present at build time. As part of a Continuous Deployment workflow, I assign the encrypted secret `INSTAGRAM_ACCESS_TOKEN` as an environment variable (of the same name) to the build step:

```yaml
# .github/workflows/deploy.yml
name: Continuous Deployment

on:
  push:
    branches:
      - master

jobs:
  deploy:
    timeout-minutes: 5

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - uses: actions/setup-node@v1
        with:
          node-version: 12

      - uses: bahmutov/npm-install@v1

      - run: yarn build
        env:
          NODE_ENV: production
          INSTAGRAM_ACCESS_TOKEN: ${{ secrets.INSTAGRAM_ACCESS_TOKEN }}
```

## Create a Scheduled Workflow

Long-lived Instagram access tokens will last 3 months, but must be refreshing within 2 months of the date they're issued. If we forget to update the `INSTAGRAM_ACCESS_TOKEN` secret on this repository within that time the `yarn build` command will fail. To prevent this from happening we can create an additional GitHub Workflow that refreshes the Instagram access token and updates the corresponding repository secret on a schedule.

In a "here's one I made earlier" fashion, I've already created GitHub Actions for [refreshing Instagram access tokens](https://github.com/saulhardman/github-actions/tree/master/packages/refresh-instagram-access-token) and [updating GitHub secrets](https://github.com/saulhardman/github-actions/tree/master/packages/update-github-secret). Be sure to follow the installation instructions in those packages' respective READMEs. One thing to note is that a [Personal Access Token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) (PAT) with the 'repo' scope is required to create or update GitHub Secrets via the GitHub API.

With the action packages installed and a PAT set to a secret named `PERSONAL_ACCESS_TOKEN` we can now setup a scheduled GitHub Workflow:

```yaml
# .github/workflows/instagram.yml
name: Refresh Instagram Access Token & Update GitHub Secret

on:
  schedule:
    # https://crontab.guru/#0_0_1_*_*
    - cron: "0 0 1 * *"

jobs:
  instagram:
    steps:
      - uses: actions/checkout@v2

      - uses: actions/setup-node@v1
        with:
          node-version: 12

      - uses: bahmutov/npm-install@v1

      - name: Refresh Instagram Access Token
        id: instagram
        uses: ./node_modules/@saulhardman/refresh-instagram-access-token
        with:
          access_token: ${{ secrets.INSTAGRAM_ACCESS_TOKEN }}

      - name: Update GitHub Secret
        uses: ./node_modules/@saulhardman/update-github-secret
        with:
          secret_name: INSTAGRAM_ACCESS_TOKEN
          secret_value: ${{ steps.instagram.outputs.access_token }}
          access_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
```

An bonus step that I often include in workflows like these is to send a [Pushover](https://pushover.net/) notification on success or failure. Passing the access token as part of the success payload gives me the opportunity to update my local development `.env` file, too.

## A Note on Netlify

As far as I'm aware, Netlify doesn't have an API for managing environment variables (in the free tier, at least). If your project is private and you live life fast and dangerous you _could_ read and write an access token from and to a `.env` file that you then commit back to the repository:

```yaml
- uses: falti/dotenv-action@v0.2.4
  id: dotenv

- name: Refresh Instagram Access Token
  id: instagram
  uses: ./node_modules/@saulhardman/refresh-instagram-access-token
  with:
    access_token: ${{ steps.dotenv.outputs.instagram_access_token }}

- uses: TickX/var-to-dotenv@v1.1.1
  with:
    key: INSTAGRAM_ACCESS_TOKEN
    value: ${{ steps.instagram.outputs.access_token }}
    default: ${{ steps.dotenv.outputs.instagram_access_token }}

- name: Commit Updated DotEnv
  uses: EndBug/add-and-commit@v4
  with:
    message: "chore: refresh instagram access token"
    add: .env
  env:
    GITHUB_TOKEN: ${{ github.token }}
```

## Local Testing

I often develop new Workflows on a feature branch and configure them to run on `push` until they're ready to be merged into the primary branch. A less cumbersome option is to use [`act`](https://github.com/nektos/act) to debug Workflows locally.

## Closing Thoughts

Automation can be a double-edged sword, but for processes like this GitHub Workflows are a blessing for a front-end web developer working with the Jamstack. How do you manage access tokens? How are you using GitHub Actions to extend the Jamstack?