Metadata-Version: 2.4
Name: fastapi-steam
Version: 0.1.0
Summary: async Steam OpenID 2.0 authentication for FastAPI / Starlette.
Project-URL: Repository, https://github.com/drawiks/fastapi-steam
Project-URL: Issues, https://github.com/drawiks/fastapi-steam/issues
Author: drawiks
License: MIT License
        
        Copyright (c) 2026 drawiks
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: async,authentication,fastapi,openid,openid 2.0,pydantic,starlette,steam
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP :: Session
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.5
Provides-Extra: dev
Requires-Dist: build>=1.0; extra == 'dev'
Requires-Dist: fastapi>=0.110; extra == 'dev'
Requires-Dist: hatchling>=1.25; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.110; extra == 'fastapi'
Description-Content-Type: text/markdown

<div align="center">
    <h1>⚡ fastapi-steam</h1>
    <a href="https://pypi.org/project/fastapi-steam/">
        <img alt="PyPI version" src="https://img.shields.io/pypi/v/fastapi-steam?color=blue">
    </a>
    <a href="https://pypi.org/project/fastapi-steam/">
        <img alt="PyPI downloads" src="https://img.shields.io/pypi/dm/fastapi-steam?color=blue">
    </a>
    <img height="20" alt="Python 3.10+" src="https://img.shields.io/badge/python-3.10+-blue">
    <img height="20" alt="License MIT" src="https://img.shields.io/badge/license-MIT-green">
    <img height="20" alt="Status" src="https://img.shields.io/badge/status-beta-orange">
    <a href="https://github.com/drawiks/fastapi-steam">
        <img alt="Ruff" src="https://img.shields.io/badge/code%20style-ruff-000000">
    </a>
    <p><strong>fastapi-steam</strong> - async Steam OpenID 2.0 authentication for FastAPI / Starlette</p>
    <blockquote>(─‿‿─)</blockquote>
</div>

---

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

---

## **📦 installation**

```bash
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/`](examples/) directory - a complete app
> with sessions and a SQLite user table.

---

## **📑 quick start**

```python
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()`:

```python
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:

```python
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:

```python
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:

```python
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](https://openid.net/specs/openid-authentication-2_0.html):

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](https://github.com/drawiks/fastapi-steam/blob/main/LICENSE)
