PythonにてStrava APIのOAuth認証を簡略化したい🏃♂️💨
はじめに
みなさんはStravaは利用されていますでしょうか?
運動されている方はよく使われているかもしれません。ランニングやサイクリングなどのアクティビティを記録するアプリになります。
今回はStrava社が提供しているStrava APIを利用するにあたって認証の部分で少し躓いたためどのように対処、簡略化したかを記します。
本記事の流れ
- 認証からデータ取得までを一通り試す
- 躓いたポイント
- 対処法の実装
注意
以下より説明される.env
や strava_tokens.json
には機密情報が含まれるため、Gitにコミットしないよう .gitignore
に追加しましょう。
認証からデータ取得までを一通り試す
今回Stravaのデータ取得にあたってStrava自体のユーザー登録は済んでいることは前提に進めます。
上記記事を参考に進めます。
アクセストークンの作成
上記リンクより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