Files
phaseflow/scripts/garmin_auth.py
Petru Paler b221acee40 Implement automatic Garmin token refresh and fix expiry tracking
- Add OAuth1 to OAuth2 token exchange using Garmin's exchange endpoint
- Track refresh token expiry (~30 days) instead of access token expiry (~21 hours)
- Auto-refresh access tokens in cron sync before they expire
- Update Python script to output refresh_token_expires_at
- Add garminRefreshTokenExpiresAt field to User type and database schema
- Fix token input UX: show when warning active, not just when disconnected
- Add Cache-Control headers to /api/user and /api/garmin/status to prevent stale data
- Add oauth-1.0a package for OAuth1 signature generation

The system now automatically refreshes OAuth2 tokens using the stored OAuth1 token,
so users only need to re-run the Python auth script every ~30 days (when refresh
token expires) instead of every ~21 hours (when access token expires).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 20:33:10 +00:00

61 lines
2.0 KiB
Python

#!/usr/bin/env python3
# ABOUTME: PhaseFlow Garmin Token Generator using garth library.
# ABOUTME: Run this locally to authenticate with Garmin (supports MFA).
"""
PhaseFlow Garmin Token Generator
Run this locally to authenticate with Garmin (supports MFA)
Usage:
pip install garth
python3 garmin_auth.py
"""
import json
import sys
from datetime import datetime
from getpass import getpass
try:
import garth
from garth.auth_tokens import OAuth1Token, OAuth2Token
from garth.exc import GarthHTTPError
from pydantic import TypeAdapter
except ImportError:
print("Error: garth library not installed.")
print("Please install it with: pip install garth")
exit(1)
email = input("Garmin email: ")
password = getpass("Garmin password: ")
# MFA handled automatically - prompts if needed
try:
garth.login(email, password)
except GarthHTTPError as e:
if "401" in str(e):
print("\nError: Invalid email or password.", file=sys.stderr)
else:
print(f"\nError: Authentication failed - {e}", file=sys.stderr)
exit(1)
except Exception as e:
print(f"\nError: {e}", file=sys.stderr)
exit(1)
# Serialize Pydantic dataclasses using TypeAdapter
oauth1_adapter = TypeAdapter(OAuth1Token)
oauth2_adapter = TypeAdapter(OAuth2Token)
expires_at_ts = garth.client.oauth2_token.expires_at
refresh_expires_at_ts = garth.client.oauth2_token.refresh_token_expires_at
tokens = {
"oauth1": oauth1_adapter.dump_python(garth.client.oauth1_token, mode='json'),
"oauth2": oauth2_adapter.dump_python(garth.client.oauth2_token, mode='json'),
"expires_at": datetime.fromtimestamp(expires_at_ts).isoformat(),
"refresh_token_expires_at": datetime.fromtimestamp(refresh_expires_at_ts).isoformat()
}
print("\n--- Copy everything below this line ---")
print(json.dumps(tokens, indent=2))
print("--- Copy everything above this line ---")
print(f"\nAccess token expires: {tokens['expires_at']}")
print(f"Refresh token expires: {tokens['refresh_token_expires_at']} (re-run script before this date)")