Requesting Strava with Azure Functions
Strava & Azure Functions. Creating a serverless API to request an OAuth2 based API
Matthieu Riegler -
Few days ago, I wanted to add some Strava infos on this site using their API.
Quickly, I stumbled upon a major issue : Strava uses OAuth2 for their tokens. This means I needed a way to generate & refresh a token on a regular basis (every 6 hours).
Since this is a backend-less/static website I had to rely on an external provider. Azure Functions had what I was looking for : A Timer Triggered functions (cron-like).
A First Function : Refresh the tokens
This first function is simple :
- It takes as input the pair clientId/Client secret and a refresh Token
- It returns a new access token & a new refresh token.
A Second Function : Retrieve the data
This second function is even simplier : Using the access tokens I requests the strava API, like you would any other api. For the purpose of this site, my function make multiple calls to return the data structure I needed.
What about the glue between these 2 functions ?
So, now we have 2 standalone functions, but how do I share the tokens ?
That's where Azure Storage enters the game.
I used the table storage as a Key-Value Store to store:
- ClientID
- Client-secret
- Access Token
- Refresh Token
Basic Storage read & write
Read
To read for the table, their is a service.
import azure from "azure-storage";
const tableService = azure.createTableService();
const query = new azure.TableQuery().where("RowKey eq 'ClientID' or RowKey eq 'ClientSecret' or RowKey eq 'RefreshToken'");
tableService.queryEntities <
any >
("my-table-name",
query,
null,
(error, result, response) => {
let clientID, refreshToken, clientSecret;
result.entries.forEach((item) => {
if (item.RowKey._ === "ClientID") {
clientID = item.Value._;
} else if (item.RowKey._ === "ClientSecret") {
clientSecret = item.Value._;
} else if (item.RowKey._ === "RefreshToken") {
refreshToken = item.Value._;
}
});
});
Write
Likewise to write into the table.
const updateRefreshToken = {
PartitionKey: { _: "Token" },
RowKey: { _: "RefreshToken" },
Value: { _: response.refresh_token },
};
const updateAccessToken = {
PartitionKey: { _: "Token" },
RowKey: { _: "AccessToken" },
Value: { _: response.access_token },
};
const batch = new azure.TableBatch();
batch.replaceEntity(updateRefreshToken);
batch.replaceEntity(updateAccessToken);
tableService.executeBatch(tableName, batch, myCallbackFunc);
It gets better
As Azure Function have a great integration with Azure Storage. A table row can be directely used as input of the function.
function.json
"bindings": [
...
{
"type": "table",
"name": "inputTable",
"tableName": "my-values",
"connection": "jeanmechestrava_STORAGE",
"partitionKey": "Token",
"rowKey": "AccessToken",
"direction": "in"
}
],
index.ts
const httpTrigger: AzureFunction = function (context: Context, req: HttpRequest): Promise<void> {
const accessToken = context.bindings.inputTable.Value;
...
};
This way I indicate to the Azure Function framework, I want the AccessToken
field from my table my-values
and I put it into a variable inputTable
.
To bootstrap everything
As you probably don't missed, this only works when there is initial data in the table. There can be new access token without an initial refresh token. It's even more true for the clientId/Client secret pair !
I used Microsoft Azure Storage Explorer to initially put the data in the table
I also went on writing 2 others functions for backup reasons :
- A HTTP Function to request an Authorize to Strava callback
- Another HTTP Function passed as callback to the Strava Auth to trigger a brand new token request (which differ from a token renewal).
Final note
As my examples above are only partial, you will find my Azure Functions on my repo.
Also I would like to thank Dzhavat for sharing with me his implementation of Azure Functions to request the Strava API.