Session and Cookie Settings
docsfy supports two authentication paths: Bearer tokens for API clients and cookies for browser sessions.
src/docsfy/main.py
# 1. Check Authorization header (API clients)
auth_header = request.headers.get("authorization", "")
if auth_header.startswith("Bearer "):
token = auth_header[7:]
if token == settings.admin_key:
is_admin = True
username = "admin"
else:
user = await get_user_by_key(token)
# 2. Check session cookie (browser) -- opaque session token
if not user and not is_admin:
session_token = request.cookies.get("docsfy_session")
if session_token:
session = await get_session(session_token)
Secure Cookie Defaults
SECURE_COOKIES is enabled by default, and session cookies are set as HttpOnly with SameSite=Strict.
src/docsfy/config.py
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
admin_key: str = "" # Required — validated at startup
ai_provider: str = "claude"
ai_model: str = "claude-opus-4-6[1m]" # [1m] = 1 million token context window
ai_cli_timeout: int = Field(default=60, gt=0)
log_level: str = "INFO"
data_dir: str = "/data"
secure_cookies: bool = True # Set to False for local HTTP dev
src/docsfy/main.py
response.set_cookie(
"docsfy_session",
session_token,
httponly=True,
samesite="strict",
secure=settings.secure_cookies,
max_age=SESSION_TTL_SECONDS,
)
src/docsfy/main.py
response.delete_cookie(
"docsfy_session",
httponly=True,
samesite="strict",
secure=settings.secure_cookies,
)
Warning: With default settings, browsers do not send
Securecookies over plain HTTP. If you run docsfy onhttp://localhostand keepSECURE_COOKIES=true, login may appear to work but follow-up requests can redirect back to/login.
SameSite Behavior
docsfy explicitly uses SameSite=Strict for session cookies, which blocks cookie sending in cross-site requests and helps reduce CSRF risk.
src/docsfy/main.py
response.set_cookie(
"docsfy_session",
session_token,
httponly=True,
samesite="strict",
secure=settings.secure_cookies,
max_age=SESSION_TTL_SECONDS,
)
tests/test_auth.py
async def test_login_cookie_has_samesite_strict(
unauthed_client: AsyncClient,
) -> None:
"""Login cookie should have SameSite=strict."""
response = await unauthed_client.post(
"/login",
data={"username": "admin", "api_key": TEST_ADMIN_KEY},
follow_redirects=False,
)
set_cookie = response.headers.get("set-cookie", "")
assert "samesite=strict" in set_cookie.lower()
Tip: For cross-origin integrations, use
Authorization: Bearer <api_key>rather than relying on browser cookies.
TTL and Session Expiration
Session lifetime is 8 hours, enforced both in the cookie (max_age) and in server-side session lookup (expires_at > now).
src/docsfy/storage.py
SESSION_TTL_SECONDS = 28800 # 8 hours
SESSION_TTL_HOURS = SESSION_TTL_SECONDS // 3600
src/docsfy/storage.py
async def create_session(
username: str, is_admin: bool = False, ttl_hours: int = SESSION_TTL_HOURS
) -> str:
"""Create an opaque session token."""
token = secrets.token_urlsafe(32)
token_hash = _hash_session_token(token)
expires_at = datetime.now(timezone.utc) + timedelta(hours=ttl_hours)
expires_str = expires_at.strftime("%Y-%m-%d %H:%M:%S")
async with aiosqlite.connect(DB_PATH) as db:
await db.execute(
"INSERT INTO sessions (token, username, is_admin, expires_at) VALUES (?, ?, ?, ?)",
(token_hash, username, 1 if is_admin else 0, expires_str),
)
src/docsfy/storage.py
async def get_session(token: str) -> dict[str, str | int | None] | None:
"""Look up a session. Returns None if expired or not found."""
token_hash = _hash_session_token(token)
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute(
"SELECT * FROM sessions WHERE token = ? AND expires_at > datetime('now')",
(token_hash,),
)
src/docsfy/main.py
await cleanup_expired_sessions()
src/docsfy/storage.py
async def cleanup_expired_sessions() -> None:
"""Remove expired sessions.
NOTE: This is called during application startup (lifespan) only.
"""
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("DELETE FROM sessions WHERE expires_at <= datetime('now')")
await db.commit()
Note: Expired sessions are rejected even before cleanup runs, because
get_session()filters byexpires_aton every lookup.
Opaque Session Tokens (Not API Keys)
Browser cookies carry a random session token, not the raw user/admin API key.
tests/test_auth.py
async def test_session_cookie_is_opaque_token(unauthed_client: AsyncClient) -> None:
"""The session cookie should NOT contain the raw API key."""
response = await unauthed_client.post(
"/login",
data={"username": "admin", "api_key": TEST_ADMIN_KEY},
follow_redirects=False,
)
assert "docsfy_session" in response.cookies
cookie_value = response.cookies["docsfy_session"]
assert cookie_value != TEST_ADMIN_KEY
assert len(cookie_value) > 20
Local HTTP Development Adjustments
For local non-TLS development, explicitly disable secure cookies in .env.
.env.example
# Set to false for local HTTP development
# SECURE_COOKIES=false
docker-compose.yaml
services:
docsfy:
build: .
ports:
- "8000:8000"
env_file: .env
Set this in your local .env:
SECURE_COOKIES=false
Then restart the app/container so Settings reloads the value.
Warning: Do not use
SECURE_COOKIES=falseoutside local HTTP development.
Test/Automation Coverage for Cookie Rules
Cookie/session behavior is covered in unit tests and executed via tox.
tox.toml
envlist = ["unittests"]
[env.unittests]
deps = ["uv"]
commands = [["uv", "run", "--extra", "dev", "pytest", "-n", "auto", "tests"]]
This includes tests for:
- SameSite=strict cookie headers
- opaque session cookie values
- session invalidation on logout
- expired-session cleanup behavior