😸

PythonにてStrava APIのOAuth認証を簡略化したい🏃‍♂️💨

に公開

はじめに

みなさんはStravaは利用されていますでしょうか?
運動されている方はよく使われているかもしれません。ランニングやサイクリングなどのアクティビティを記録するアプリになります。
今回はStrava社が提供しているStrava APIを利用するにあたって認証の部分で少し躓いたためどのように対処、簡略化したかを記します。

本記事の流れ

  1. 認証からデータ取得までを一通り試す
  2. 躓いたポイント
  3. 対処法の実装

注意

以下より説明される.envstrava_tokens.jsonには機密情報が含まれるため、Gitにコミットしないよう .gitignore に追加しましょう。

認証からデータ取得までを一通り試す

今回Stravaのデータ取得にあたってStrava自体のユーザー登録は済んでいることは前提に進めます。

https://qiita.com/tatsuki-tsuchiyama/items/fb15145029e5e7318bec

上記記事を参考に進めます。

アクセストークンの作成

https://www.strava.com/settings/api

上記リンクよりMy APIアプリケーションの作成を行います。
ウェブサイト、認証コールバックドメインは以下にしました。

ウェブサイト: http://localhost/
認証コールバックドメイン: localhost

My APIアプリケーションが作成できたらシークレット(STRAVA_CLIENT_SECRET)とクラインアントIDを控えます。

以下のようなコードを用意し実行します。
.env
上記にて入手したものを.envに格納

STRAVA_CLIENT_SECRET=

get_token.py

import requests
from dotenv import load_dotenv
import os
import json

load_dotenv()
client_id = {クライアントID}
client_secret = os.environ["STRAVA_CLIENT_SECRET"]
redirect_uri = "http://localhost/exchange_token"

request_url = (
    f"https://www.strava.com/oauth/authorize?client_id={client_id}"
    f"&response_type=code&redirect_uri={redirect_uri}"
    f"&approval_prompt=force"
    f"&scope=profile:read_all,activity:read_all"
)

print("クリックしてください:", request_url)
print("アプリを認証し、生成されたコードを以下のURLにコピー&ペーストしてください。")
code = input("URLからコードを貼り付ける: ")

# http://localhost/exchange_token?state=&code=ed7c4bc34a971e83bbccb194c7023d7f4c57ca19&scope=read,activity:read_all,profile:read_all

token = requests.post(
    url="https://www.strava.com/api/v3/oauth/token",
    data={
        "client_id": client_id,
        "client_secret": client_secret,
        "code": code,
        "grant_type": "authorization_code",
    },
)

※このスクリプトはトークンを取得するのみで、保存は後述します。

実行すると以下のような表示が出ます。

$ python3 get_token.py 
クリックしてください: https://www.strava.com/oauth/authorize?client_id={クライアントID}&response_type=code&redirect_uri=http://localhost/exchange_token&approval_prompt=force&scope=profile:read_all,activity:read_all
アプリを認証し、生成されたコードを以下のURLにコピー&ペーストしてください。
URLからコードを貼り付ける: 

URLにアクセスし、

認証するとリダイレクトします

スクリーンショットにてURLの赤くマスクした部分が必要なのでコピーし、ターミナルにて貼り付けます。
するとアクセストークンを取得できます。

認証が通っているか確認

.envを以下を追加

STRAVA_TOKEN = xxx # 上記にて入手したトークン
REFRESH_TOKEN = xxx # StaravaAPIのホームより入手
STRAVA_CLIENT_ID = xxx # StaravaAPIのホームより入手

認証を試すコードを作成し実行

import os
import requests
from dotenv import load_dotenv

def check_strava_auth() -> None:
    """Strava API 認証確認"""
    load_dotenv()

    access_token = os.getenv("STRAVA_TOKEN")
    refresh_token = os.getenv("REFRESH_TOKEN")
    client_id = os.getenv("STRAVA_CLIENT_ID")
    client_secret = os.getenv("STRAVA_CLIENT_SECRET")

    # 1️現在の access_token で認証テスト
    url = "https://www.strava.com/api/v3/athlete"
    headers = {"Authorization": f"Bearer {access_token}"}
    response = requests.get(url, headers=headers)

    if response.status_code == 200:
        athlete = response.json()
        print("認証成功!")
        print(f"アスリート名: {athlete.get('firstname')} {athlete.get('lastname')}")
    else:
        print("現在のトークンは無効です。リフレッシュを試みます...")
        print(f"status_code: {response.status_code}")
        print("レスポンス:", response.text)

        # 2️トークンをリフレッシュ
        refresh_url = "https://www.strava.com/api/v3/oauth/token"
        data = {
            "client_id": client_id,
            "client_secret": client_secret,
            "grant_type": "refresh_token",
            "refresh_token": refresh_token,
        }
        refresh_res = requests.post(refresh_url, data=data)

        if refresh_res.status_code == 200:
            new_data = refresh_res.json()
            new_token = new_data["access_token"]
            print("新しいトークンを取得しました:", new_token)

            # 3️新しいトークンで再度確認
            headers = {"Authorization": f"Bearer {new_token}"}
            retry = requests.get(url, headers=headers)
            if retry.status_code == 200:
                athlete = retry.json()
                print("再認証成功!")
                print(f"アスリート名: {athlete.get('firstname')} {athlete.get('lastname')}")
            else:
                print("新しいトークンでも認証失敗:", retry.status_code, retry.text)
        else:
            print("トークン更新失敗:", refresh_res.status_code, refresh_res.text)


if __name__ == "__main__":
    check_strava_auth()
認証成功!
アスリート名: {自分のユーザー名}

躓いたポイント

「認証からデータ取得までを一通り試す」の章にてAPIアプリケーション作成からPythonを通して認証を通すところまで行いました。

一度きりの実行であればこのままでよいかもしれません。しかし自分がStarava APIを利用する目的として、定期的にAPIに自動的に認証を通し、情報を取得しに行きたいというニーズがありました。

現状ではStaravaAPIはアクセストークンの有効期限が6時間で切れるため、その都度手動でリンクへアクセスし認証を通してアクセストークンを入手しという作業が発生します。
これを自動化するのはかなり面倒です。色々調べていたところ解決策があったため、そのアプローチを以下に記します。

対処法の実装

今回Pythonのパッケージとして提供されているstravalibを利用します。

pip install stravalib

StravaのAPIはOAuth2を使用しますが、Stravalibはアクセストークンの自動更新をサポートしています。これにより、「トークンが期限切れになるたびに自分で更新する」必要がありません。
ライブラリが自動的にリフレッシュトークンを使って新しいアクセストークンを取得します。

またPythonオブジェクトとしてデータ取得する際に、APIレスポンスをそのままJSONで扱うのではなく、Activity や Athlete などのPythonクラスに変換して扱えます。

stravalibを用いた実装

同じディレクトリ内にstrava_tokens.jsonというファイルを作成します。(中身を空で構いません。)
まず先ほど紹介したget_token.pyの最下行に以下処理を追加します。
以下を再度実行することでトークン情報をstrava_tokens.jsonへ保存します。
get_token.py

strava_token = token.json()

with open("strava_tokens.json", "w") as f:
    json.dump(strava_token, f, indent=2)

以下がstravalibを用いた実装になります。
client.get_athlete() のようにAPIを呼ぶ際、トークンが期限切れであれば stravalib が自動で更新処理を行います。その結果、新しい access_token が ``client.access_token に格納され,ファイルへ保存されます。

auth.py

import os
import json
import requests
from typing import Dict, Any, List
from stravalib import Client


def load_tokens(json_path: str) -> Dict[str, Any]:
    """strava_tokens.json からトークン情報を読み込む"""
    with open(json_path, "r") as f:
        return json.load(f)


def save_tokens(json_path: str, token_data: Dict[str, Any]) -> None:
    """最新のトークン情報を JSON に保存する"""
    with open(json_path, "w") as f:
        json.dump(token_data, f, indent=2)


def create_client(token_data: Dict[str, Any]) -> Client:
    """stravalib クライアントを生成"""
    client = Client(
        access_token=token_data["access_token"],
        refresh_token=token_data["refresh_token"],
        token_expires=token_data["expires_at"],
    )
    return client


def refresh_and_save_tokens(client: Client, json_path: str) -> Dict[str, Any]:
    """
    stravalib が自動リフレッシュしたトークンを保存。
    (有効期限が切れていた場合に備えて)
    """
    new_token = {
        "access_token": client.access_token,
        "refresh_token": client.refresh_token,
        "expires_at": client.token_expires,
    }
    save_tokens(json_path, new_token)
    return new_token

def main() -> None:
    """エントリーポイント"""
    # ファイルパス
    json_path = os.path.join(os.getcwd(), "strava_tokens.json")

    # 1.トークン読み込み
    token_data = load_tokens(json_path)
    print("Current expires_at:", token_data["expires_at"])

    # 2.クライアント生成
    client = create_client(token_data)

    # 3.ユーザー確認
    athlete = client.get_athlete()
    print(f"Hi, {athlete.firstname} Welcome to stravalib!")

    # 4.トークン更新(必要なら)&保存
    new_token = refresh_and_save_tokens(client, json_path)

if __name__ == "__main__":
    main()

終わりに

stravalibを用いいることで手動を介さないとできなかった認証を簡略化でき、自動化のハードルを下げることができました。
AWSなどであればLambdaで認証スクリプトをデプロイし、S3にstrava_tokens.jsonを保存し更新していくのが良さそうです。

Discussion