Fetching Authenticated API Data with GitHub Actions 🔑
Tags: github, githubactions, typescript, githubpages
Published: 2020-03-08 14:07:29


I've been running a lot lately. I'm training for my first ever marathon and so far It's been tough. I'm proud of how far I've come in such a seemingly short period of time. It was only in October of 2019 that I did [my first half marathon in Baltimore](https://www.thebaltimoremarathon.com/) and before then I didn't run or workout at all.

![Runners](https://dev-to-uploads.s3.amazonaws.com/i/2c3l38tgnud6k8vzcgc8.png)

I track all of my runs on a website called [Strava](https://www.strava.com/) and thought it would be kind of cool to show my latest run on my website. In order to make an API request to Strava you must provide an access token with the request. These access tokens expire every six hours and a new one can be requested using the `oauth/token` endpoint. This is not ideal for me for a few reasons:

- I can't expose my access token as it allows people to make authorized requests on my account.
- Requesting a new token requires you to provide even more credentials such as a client id and secret.
- This page is hosted on [GitHub Pages](https://pages.github.com/) which doesn't have a backend, so I can't proxy the API request at all.

I needed to find a way to securely request this data. Sure I could create an [AWS Lambda](https://aws.amazon.com/lambda/) or something similar but that seemed like more hassle than it was worth.

## Enter GitHub Actions

[I have some familiarity already with GitHub Actions](https://github.com/JamesIves/github-pages-deploy-action), and thought this project would be a great use-case for them as I didn't care how up-to-date the data is. If it's a couple hours behind that's no big deal.

[GitHub Actions](https://github.com/features/actions) like other CI tools out there allows you to run some code on [any event that GitHub supports](https://help.github.com/en/articles/events-that-trigger-workflows) such as pushing, pull requests, tagging and even on a schedule. I thought a great way to retrieve this data would be to run a job every day that saves the running data as a `.json` file. I could then combine this workflow with the [GitHub Pages Deploy Action](https://github.com/JamesIves/github-pages-deploy-action) that I built to push that data into my base branch. Once that base branch detects a change it automatically re-builds the page that ingests the `.json` file.

## Building the Action

The action itself is going to be quite simple. It's going to make a `POST` request for the token, and then afterwards a `GET` request to get the running data. At its core the [TypeScript](http://www.typescriptlang.org/) action code looked something like the following.

```javascript
import { getInput } from '@actions/core'

// Retrieves the tokens for the data request.
export async function retrieveTokens(): Promise<object> {
  const configuration = JSON.parse(getInput('CONFIGURATION'))

  if (configuration.body) {
    configuration.body = JSON.stringify(configuration.body)
  }

  const response = await fetch(
    'https://strava.com/api/v3/oauth/token',
    configuration
  )
  return await response.json()
}

// Retrieves the data
export async function retrieveData({
  action,
}: ActionInterface): Promise<object> {
  try {
    const tokens = await retrieveTokens(action)
    const response = await fetch('https://www.strava.com/api/v3/athlete/activities', {
      headers: {
        Authorization: `Bearer ${tokens.access_token}`,
      },
    })

    const data = await response.json()

    await fs.writeFile('fetch-api-data-action/data.json', data, 'utf8')
  } catch (error) {
    throw new Error(`There was an error fetching from the API: ${error}`)
  }
}
```

The function call to `getInput` allows you to retrieve data passed through the `with` parameter [when invoking an action in a workflow](https://help.github.com/en/actions/configuring-and-managing-workflows/configuring-a-workflow). These configurations are great because you can store secrets in the repositories `Settings/Secrets` menu and replace parts in a string with the `${{ secrets.secret_name }}` syntax. GitHub will automatically hide these strings if they get exposed in your logs.

```yml
name: Refresh Feed
on:
  schedule:
    - cron: 25 17 * * 0-6
jobs:
  refresh-feed:
    runs-on: ubuntu-latest
    steps:
      - name: Fetch API Data 📦
        uses: JamesIves/fetch-api-data-action@releases/v1
        with:
          CONFIGURATION: '{ "method": "POST", "body": {"client_id": "${{ secrets.client_id }}", "client_secret": "${{ secrets.client_secret }}"} }'
```

With that said and done I was able to retrieve my running data on a scheduled job without exposing my credentials. But why stop there?

## Making the Action Reusable

With some minor re-factors I figured this action could be used to retrieve data from _just about any_ REST API. In order to achieve that I'm going to need to figure out how to get the following information:

* What endpoint does the user want to retrieve the data from?
* Do they need to make a token request first like the Strava API? If so how do I know what information from that request is needed in the data request?

I made some changes to my workflow configuration. The `TOKEN_ENDPOINT` request with its accompanying configuration is now made first (if specified) followed by the `ENDPOINT` data request. The only thing left to figure out is how to get the access token into the `athlete/activities` configuration.

```yml
name: Refresh Feed
on:
  schedule:
    - cron: 25 17 * * 0-6
jobs:
  refresh-feed:
    runs-on: ubuntu-latest
    steps:
      - name: Fetch API Data 📦
        uses: JamesIves/fetch-api-data-action@releases/v1
        with:
          TOKEN_ENDPOINT: https://www.strava.com/api/v3/oauth/token
          TOKEN_CONFIGURATION: '{ "method": "POST", "body": {"client_id": "${{ secrets.client_id }}", "client_secret": "${{ secrets.client_secret }}"} }'
          ENDPOINT: https://www.strava.com/api/v3/athlete/activities
          CONFIGURATION: '...'
```

In order to get the keys from the first request in the second [I decided to use mustache.js](https://github.com/janl/mustache.js/) to replace strings formatted with triple brackets. In my workflow configuration I can now use triple brackets with keys from the token request to populate the data request. 

```javascript 
import {render} from 'mustache'

const tokens = {
  data: {
    access_token: '123',
  }
}

const formatted = render('{ "method": "GET", "headers": {"Authorization": "Bearer {{{ data.access_token }}}"} }', tokens)

console.log(formatted)
// '{ "method": "GET", "headers": {"Authorization": "Bearer 123"} }'
```

With this setup the workflow can look something like this:

```yml
name: Refresh Feed
on:
  schedule:
    - cron: 25 17 * * 0-6
jobs:
  refresh-feed:
    runs-on: ubuntu-latest
    steps:
      - name: Fetch API Data 📦
        uses: JamesIves/fetch-api-data-action@releases/v1
        with:
          # The token endpoint is requested first. This retrieves the access token for the other endpoint.
          TOKEN_ENDPOINT: https://www.strava.com/api/v3/oauth/token
          # The configuration contains secrets held in the Settings/Secrets menu of the repository.
          TOKEN_CONFIGURATION: '{ "method": "POST", "body": {"client_id": "${{ secrets.client_id }}", "client_secret": "${{ secrets.client_secret }}"} }'
          # Once the token endpoint has fetched then this endpoint is requested.
          ENDPOINT: https://www.strava.com/api/v3/athlete/activities
          # The bearer token here is returned from the TOKEN_ENDPOINT call. The returned data looks like so: {data: {access_token: '123'}}, meaning it can be accessed using the triple bracket syntax.
          CONFIGURATION: '{ "method": "GET", "headers": {"Authorization": "Bearer {{{ data.access_token }}}"} }'
```

Looking good! 

![Cool cat](https://dev-to-uploads.s3.amazonaws.com/i/58zfxh53r7hubuzyi57o.gif)

## Using the Action

In order to use the action on my own site all I needed to do was create a `feed.yml` file in the `.github/workflows` directory. This [workflow](https://help.github.com/en/actions/configuring-and-managing-workflows/configuring-a-workflow) is setup to download the data on a schedule from the Strava API, and then it uses the [GitHub Pages Deploy action](https://github.com/JamesIves/github-pages-deploy-action) to push the data into the correct folder in my repository.

```yml
name: Refresh Feed
on: 
  schedule:
    - cron: 10 15 * * 0-6
jobs:
  refresh-feed:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout 🛎️
        uses: actions/checkout@v2
        with:
          persist-credentials: false
  
      - name: Fetch API Data 📦
        uses: JamesIves/fetch-api-data-action@releases/v1
        with:
          TOKEN_ENDPOINT: https://www.strava.com/api/v3/oauth/token
          TOKEN_CONFIGURATION: '{ "method": "POST", "body": {"client_id": "${{ secrets.client_id }}", "client_secret": "${{ secrets.client_secret }}"} }'
          ENDPOINT: https://www.strava.com/api/v3/athlete/activities
          CONFIGURATION: '{ "method": "GET", "headers": {"Authorization": "Bearer {{{ data.access_token }}}"} }'
          SAVE_NAME: strava

      - name: Build and Deploy 🚀
        uses: JamesIves/github-pages-deploy-action@releases/v3
        with:
          ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
          BRANCH: master # Pushes the updates to the master branch.
          FOLDER: fetch-api-data-action # The location of the data.json file saved by the Fetch API Data action.
          TARGET_FOLDER: src/data # Saves the data into the 'data' directory on the master branch.
```

I have another workflow setup called `build.yml` that builds and deploys my page whenever a push is made to the master branch. This means whenever the `feed.yml` workflow finishes running it will trigger the next one, making the updates automatic.

```yml
name: Build and Deploy
on:
 branches:
  - master
jobs:
 build-and-deploy:
   runs-on: ubuntu-latest
   steps:
     - name: Checkout 🛎️
       uses: actions/checkout@v2
       with:
         persist-credentials: false

     - name: Install 🔧
       run: |
         npm install
         npm run-script build

     - name: Build and Deploy 🚀
       uses: JamesIves/github-pages-deploy-action@releases/v3
       with:
         ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
         BRANCH: gh-pages
         FOLDER: build
```

I can then use the data for my website by simply importing it. Depending on your project structure you may need to use [Webpack](https://webpack.js.org/) or something similar to load the `.json` file type.

```javascript
import data from '../data/strava.json'

const latestRun = data.filter(item => item.type === 'Run')[0]
```

## Hackathon

This was my entry to the [2020 GitHub Actions Hackathon](https://github.community/t5/Events/Featured-Event-GitHub-Actions-Hackathon/td-p/48206), and [it's available for you to use on the GitHub marketplace](https://github.com/marketplace/actions/fetch-api-data). If you have any questions please feel free to reach out via my [contact form](https://jamesiv.es) or on [Twitter](https://twitter.com/_JamesIves). I had a ton of fun making this and I hope other people find it equally as useful. If you encounter any problems using it [please create an issue](https://github.com/JamesIves/fetch-api-data-action).

![Medal](https://dev-to-uploads.s3.amazonaws.com/i/24kfrjqg125iqnrru97s.jpg)