Managing Access Tokens with GitHub Actions and Encrypted Secrets

Managing Access Tokens with GitHub Actions and Encrypted Secrets

Integrating 3rd-party APIs into Jamstack 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—though adding complexity—gives you one less thing to worry about. Using the 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. 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 request querying the Instagram User Media endpoint, e.g.:

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:

# .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 and updating GitHub secrets. Be sure to follow the installation instructions in those packages' respective READMEs. One thing to note is that 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:

# .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 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:

- 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 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?