Skip to main content

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:
  1. Attempts API call with current access token
  2. 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)
  3. 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:
  1. Validates league context exists
  2. Calls yahoo_api() which handles token refresh automatically
  3. Returns data or appropriate error response

Build docs developers (and LLMs) love