Skip to main content

⚡ fastapi-steam

PyPI version PyPI downloads Python 3.10+ License MIT Status Ruff

fastapi-steam - async Steam OpenID 2.0 authentication for FastAPI / Starlette

(─‿‿─)

 ,---.                ,--.                 ,--. 
/  .-' ,--,--. ,---.,-'  '-. ,--,--. ,---. `--' 
|  `-,' ,-.  |(  .-''-.  .-'' ,-.  || .-. |,--. 
|  .-'\ '-'  |.-'  `) |  |  \ '-'  || '-' '|  | 
`--'   `--`--'`----'  `--'   `--`--'|  |-' `--' 
                                    `--'        
                                        
        ,--.                            
 ,---.,-'  '-. ,---.  ,--,--.,--,--,--. 
(  .-''-.  .-'| .-. :' ,-.  ||        | 
.-'  `) |  |  \   --.\ '-'  ||  |  |  | 
`----'  `--'   `----' `--`--'`--`--`--' 

📦 installation

pip install fastapi-steam
pip install fastapi-steam[fastapi]  # with the FastAPI integration (SteamAuth)
pip install "fastapi-steam[fastapi]" uvicorn  # to run examples/

Full working examples are in the examples/ directory - a complete app with sessions and a SQLite user table.


📑 quick start

from fastapi import FastAPI, Request
from fastapi_steam import SteamAuth, SteamUser

steam = SteamAuth(
    api_key="...",  # Steam Web API key
    redirect_uri="http://localhost:8000/auth/callback",
)

app = FastAPI()


@app.get("/login")
async def login(request: Request):
    token = steam.generate_state()
    request.session["steam_state"] = token
    return steam.redirect(state=token)


@app.get("/auth/callback")
async def callback(request: Request) -> SteamUser:
    user: SteamUser = await steam.authenticate(
        request,
        state=request.session["steam_state"],
    )
    return user

🧩 features

  • 🚀 async-native - single reusable httpx.AsyncClient, no thread-pool blocking
  • 🎯 OpenID 2.0 done right - 9 spec checks before the callback is trusted
  • 🛡️ CSRF state token - OpenID 2.0 has no state; bind an unguessable token via return_to
  • 🧩 framework-agnostic core - SteamOpenIDClient has zero web-framework dependencies
  • ⚡ FastAPI integration - SteamAuth shipped as an optional [fastapi] extra (lazy import, no hard dependency)
  • 📦 player summaries - ISteamUser/GetPlayerSummaries/v0002 mapped with Pydantic v2
  • ✅ fully typed - py.typed marker, mypy --strict clean
  • 🐍 Python 3.10+

⚖️ comparison with alternatives

Feature fastapi-steam social-auth-core authlib manual (DIY)
Steam OpenID 2.0 ✅ ✅ ❌ (OAuth 2.0 / OIDC only) 🔧
Async (asyncio / httpx) ✅ ❌ (requests) ✅ 🔧
Spec response validation (9 checks) ✅ partial ❌ 🔧
CSRF state token ✅ ❌ ❌ 🔧
Framework-agnostic core ✅ ⚠️ needs strategy/storage glue ✅ -
FastAPI-native integration ✅ ⚠️ manual wiring ✅ 🔧
Steam player summary fetch ✅ built-in ✅ ❌ 🔧
Pydantic v2 models ✅ ❌ ❌ -
py.typed, mypy --strict clean ✅ ❌ ❌ -
Runtime deps (basic mode) httpx + pydantic requests + many httpx -
Python versions 3.10+ 3.10+ 3.8+ -

Each library has its own strengths - choose what fits your use case.


📖 usage

SteamAuth (FastAPI integration)

state is mandatory for every SteamAuth call - generate a token with generate_state(), pass it to redirect() and hand it to authenticate():

from fastapi import FastAPI, Request
from fastapi_steam import SteamAuth, SteamUser

steam = SteamAuth(
    api_key="...",
    redirect_uri="http://localhost:8000/auth/callback",
)


@app.get("/login")
async def login(request: Request):
    token = steam.generate_state()
    request.session["steam_state"] = token
    return steam.redirect(state=token)  # 307 to the Steam login page


@app.get("/auth/callback")
async def callback(request: Request) -> SteamUser:
    return await steam.authenticate(
        request,
        state=request.session["steam_state"],
    )

Lifecycle: SteamAuth owns an httpx.AsyncClient. Use it as an async context manager or close it explicitly:

async with steam:  # closes the internal client on exit
    user = await steam.authenticate(request, state=request.session["steam_state"])
# or, for a long-lived instance:
await steam.aclose()

Why is state required?

OpenID 2.0 has no state field (unlike OAuth 2.0) and Steam does not issue per-login nonces you can verify locally. Without an unguessable token bound to the session, an attacker can launch a login CSRF attack: they start a Steam login in your app and trick the victim into finishing it, logging the victim into the attacker's account.

The equivalent protection is a random token embedded in return_to, which binds the callback to the session that started the login. SteamAuth refuses to run without it - authenticate() and redirect() raise ValueError if the token is missing, and authenticate() also requires redirect_uri so the callback can be verified against the exact URL used at login time.

Framework-agnostic usage

SteamOpenIDClient is independent of any web framework:

import httpx
from fastapi_steam import SteamOpenIDClient

async def main() -> None:
    client = SteamOpenIDClient(api_key="...")

    login_url = client.get_login_url("https://example.com/auth/callback")
    # redirect the user's browser to `login_url`...

    # after Steam redirects back, pass the raw query parameters:
    steam_id = await client.verify_response(
        {"openid.claimed_id": "...", "openid.mode": "id_res", ...}
    )

    user = await client.get_user_summary(steam_id)
    print(user.personaname, user.profile_url)

The core does not import fastapi; importing it never pulls in a web framework.

Custom httpx client

A custom httpx.AsyncClient can be injected; the library will never close it:

transport = httpx.AsyncHTTPTransport(retries=3, pool_connections=20)
async with httpx.AsyncClient(transport=transport) as http:
    steam = SteamOpenIDClient(api_key="...", client=http)

🛡️ security

verify_response performs the following checks on every callback, mirroring OpenID Authentication 2.0:

  1. openid.ns must be http://specs.openid.net/auth/2.0.
  2. openid.mode must be id_res (a cancel response raises OpenIDError).
  3. openid.sig and openid.signed must be present.
  4. openid.claimed_id must match ^https://steamcommunity\.com/openid/id/(\d+)$.
  5. openid.identity must equal openid.claimed_id.
  6. openid.op_endpoint must point to Steam.
  7. openid.return_to must match the return_to used when the login was initiated.
  8. The CSRF state token (constant-time compared) must match the one issued at login.
  9. The parameters are re-posted to https://steamcommunity.com/openid/login with openid.mode=check_authentication and the response must contain is_valid:true.

⚠️ steam's own limits

Steam does not implement OpenID 2.0 fully, so two spec checks are impossible against it:

  • local verification of openid.sig - Steam always issues a dummy assoc_handle;
  • re-verification of the signed fields echoed by check_authentication - Steam only returns ns and is_valid.

is_valid:true from the provider is the authoritative check.


📝 models

SteamUser mirrors ISteamUser/GetPlayerSummaries/v0002 (API key aliases handled transparently - steamid, profileurl, avatarfull, ...).

Field Type Description
steam_id str SteamID64 (always a string, never an int)
personaname str profile display name
profile_url HttpUrl Steam community profile URL
avatar HttpUrl small avatar (32px)
avatar_full HttpUrl full-size avatar
community_visibility_state int profile visibility (1 = private, 2 = friends only, 3 = public)
profilestate int | None profile configured (1 = yes)
lastlogoff int | None unix timestamp of last logoff
realname str | None real name
timecreated int | None account creation timestamp
loccountrycode str | None two-letter country code

🔗 API Reference

SteamAuth(api_key, *, redirect_uri, client, timeout)

FastAPI integration. Requires the [fastapi] extra.

Parameter Type Default Description
api_key str | None None Steam Web API key for GetPlayerSummaries
redirect_uri str | None None absolute URL of the /auth/callback route (required by login_url/authenticate)
client httpx.AsyncClient | None None inject your own client (never closed)
timeout httpx.Timeout | float 10.0 request timeout

Methods (all CSRF methods require state):

  • generate_state() → str - fresh CSRF token
  • login_url(state) → str - Steam login URL (raises ValueError without redirect_uri)
  • redirect(state) → RedirectResponse - 307 to Steam
  • await authenticate(request, *, state) → SteamUser - validate callback + fetch summary (raises ValueError without redirect_uri or state)
  • await aclose() / async with steam: - lifecycle

SteamOpenIDClient(api_key, *, client, timeout)

Framework-agnostic core. No web-framework dependency.

Parameter Type Default Description
api_key str | None None Steam Web API key for GetPlayerSummaries
client httpx.AsyncClient | None None inject your own client (never closed)
timeout httpx.Timeout | float 10.0 request timeout

Methods:

  • get_login_url(return_to, *, realm=None, state=None) → str
  • with_state(return_to, state) → str (static)
  • generate_state() → str (static)
  • await verify_response(params, *, return_to=None, state=None) → str (SteamID64)
  • await get_user_summary(steam_id) → SteamUser
  • await aclose()

Exceptions

Exception Description
SteamAuthError base class for all library errors
InvalidSignatureError malformed/forged response or is_valid:false
OpenIDError provider reported openid_error or the user cancelled the login (mode=cancel)
SteamAPIError Steam Web API / transport failures or malformed payloads (carries .status_code)
UserNotFoundError no player profile for the given steam_id

📜 license

MIT

Metadata

Release files for fastapi-steam 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fastapi-steam 0.1.0
File Size Uploaded
fastapi_steam-0.1.0.tar.gz 19.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fastapi-steam 0.1.0
File Interpreter ABI Platform
fastapi_steam-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 33.8 kB

Release files / fastapi_steam-0.1.0.tar.gz

Download URL fastapi_steam-0.1.0.tar.gz
Size 19.6 kB
Tags Source
SHA-256 checksum
How to use checksums
83ed5450b88e4acbd4b9743a0267a7e3e26dc565c311eac81b60a31958d8e117
BLAKE2b-256 checksum
How to use checksums
ade40c3e9e47a75caa203e34bed3024885c194668a7b253181c0c13d5a6e760f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release files / fastapi_steam-0.1.0-py3-none-any.whl

Download URL fastapi_steam-0.1.0-py3-none-any.whl
Size 14.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5a36877463ffede797ab81ee4123c43e2f231d764cf28fcf58b9b7b8d02c0c0a
BLAKE2b-256 checksum
How to use checksums
7689c7a41ecd22c2ab2d706ae131151d67564492a2bbc6f79b7940ae8571229d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page