Overview
The Fantasy Basketball Analytics API uses session-based authentication powered by Yahoo OAuth 2.0. Authentication tokens are stored in Flask sessions and automatically refreshed when they expire.
Authentication Flow
1. Yahoo OAuth Setup
The API integrates with Yahoo Fantasy Sports using the Authlib OAuth client:
from authlib.integrations.flask_client import OAuth
oauth = OAuth(app)
yahoo = oauth.register(
name="yahoo",
client_id=os.environ["YAHOO_CLIENT_ID"],
client_secret=os.environ["YAHOO_CLIENT_SECRET"],
authorize_url="https://api.login.yahoo.com/oauth2/request_auth",
access_token_url="https://api.login.yahoo.com/oauth2/get_token",
refresh_token_url="https://api.login.yahoo.com/oauth2/get_token",
api_base_url="https://fantasysports.yahooapis.com/",
client_kwargs={
"scope": "fspt-r",
"token_endpoint_auth_method": "client_secret_basic",
},
)
The API requires read-only access to Yahoo Fantasy Sports data (scope: "fspt-r").
2. OAuth Authorization
Users authenticate through a three-step OAuth flow:
Step 1: Initiate Login
@app.route("/login")
def login():
return yahoo.authorize_redirect(
url_for("callback", _external=True, _scheme="https")
)
This redirects the user to Yahoo’s OAuth authorization page.
Step 2: Handle Callback
@app.route("/callback")
def callback():
session["token"] = yahoo.authorize_access_token()
return redirect(url_for("select"))
Yahoo redirects back with an authorization code, which is exchanged for an access token.
Step 3: Token Storage
The OAuth token is stored in the Flask session:
session["token"] = {
"access_token": "<access_token>",
"refresh_token": "<refresh_token>",
"expires_at": <timestamp>,
"token_type": "Bearer"
}
Never expose YAHOO_CLIENT_ID or YAHOO_CLIENT_SECRET in client-side code. These credentials must remain server-side only.
Token Refresh Mechanism
Access tokens expire periodically. The API implements automatic token refresh:
_refresh_token() Function
def _refresh_token() -> Dict[str, Any]:
old = session.get("token", {})
r = yahoo.refresh_token(
yahoo.refresh_token_url,
refresh_token=old.get("refresh_token")
)
session["token"] = r
log.info("🔑 Yahoo token refreshed at %s", time.strftime("%H:%M:%S"))
return r
Key Points:
- Retrieves the current refresh token from the session
- Calls Yahoo’s token refresh endpoint
- Updates the session with the new access token
- Logs the refresh event for monitoring
Automatic Retry on 401 Errors
The yahoo_api() helper function automatically refreshes tokens when encountering authentication errors:
def yahoo_api(rel_path: str, *, _retry: bool = True) -> Dict[str, Any]:
token = session["token"]
resp = requests.get(
f"https://fantasysports.yahooapis.com/{rel_path}",
headers={"Authorization": f"Bearer {token['access_token']}", "Accept": "application/json"},
params={"format": "json"},
timeout=20,
)
if resp.status_code == 401 and _retry:
_refresh_token()
return yahoo_api(rel_path, _retry=False)
resp.raise_for_status()
try:
return resp.json()
except ValueError:
import xmltodict
return xmltodict.parse(resp.text)
Flow:
- Attempts API call with current access token
- If
401 Unauthorized is returned and _retry=True:
- Calls
_refresh_token() to get a new access token
- Retries the API call with
_retry=False (prevents infinite loops)
- Raises exception if the second attempt fails
The _retry parameter prevents infinite retry loops. Token refresh is attempted only once per API call.
Maintaining Authenticated Sessions
Session Configuration
The API is configured with these session parameters:
app.secret_key = os.getenv("FLASK_SECRET_KEY", "dev-key")
app.config.update(
SESSION_COOKIE_SAMESITE="Lax",
SESSION_COOKIE_SECURE=False,
PERMANENT_SESSION_LIFETIME=60 * 60, # 1 hour
)
Parameters:
SESSION_COOKIE_SAMESITE="Lax" - Allows cookies to be sent on top-level navigation
SESSION_COOKIE_SECURE=False - Allows cookies over HTTP (development mode)
PERMANENT_SESSION_LIFETIME=60 * 60 - Sessions expire after 1 hour
In production, set SESSION_COOKIE_SECURE=True to enforce HTTPS-only cookies and use a strong, randomly generated FLASK_SECRET_KEY.
Session Data Structure
Authenticated sessions store the following data:
session = {
"token": {
"access_token": "<yahoo_access_token>",
"refresh_token": "<yahoo_refresh_token>",
"expires_at": <timestamp>,
"token_type": "Bearer"
},
"league_key": "423.l.54321",
"team_name": "My Team Name"
}
Checking Authentication Status
All protected endpoints verify authentication:
if "token" not in session:
return jsonify({"error": "authentication required"}), 401
League Context Validation
Some endpoints also require league selection:
if "league_key" not in session:
return jsonify({"error": "no league chosen"}), 400
Logout and Session Cleanup
@app.route("/logout")
def logout():
session.clear()
return redirect(url_for("index"))
This clears all session data, including OAuth tokens and league context.
Best Practices
Token Security:
- Never log or expose access tokens in client-side code
- Store tokens only in secure, HTTP-only session cookies
- Implement proper session timeout handling
Refresh Token Expiration:Yahoo refresh tokens can expire if:
- The user revokes access to your application
- The token has not been used for an extended period (typically 90 days)
- The user changes their Yahoo password
When a refresh token expires, the user must re-authenticate through the OAuth flow.
Example: Making an Authenticated Request
@app.route("/api/league_settings")
def api_league_settings():
if "league_key" not in session:
return jsonify({"error": "no league chosen"}), 400
try:
data = yahoo_api(f"fantasy/v2/league/{session['league_key']}/settings")
log.info("Successfully fetched league settings")
return jsonify(data)
except Exception as e:
log.error(f"Error fetching league settings: {e}")
return jsonify({"error": str(e)}), 500
This pattern:
- Validates league context exists
- Calls
yahoo_api() which handles token refresh automatically
- Returns data or appropriate error response