Add scoped tokens, L2-side revocation, and security docs
Security improvements: - Tokens now include optional l1_server claim for scoping - /auth/verify checks token scope matches requesting L1 - L2 maintains revoked_tokens table - even if L1 ignores revoke, token fails - Logout revokes token in L2 db before notifying L1s - /renderers/attach creates scoped tokens (not embedded in HTML) - Add get_token_claims() to auth.py Database: - Add revoked_tokens table with token_hash, username, expires_at - Add db functions: revoke_token, is_token_revoked, cleanup_expired_revocations Documentation: - Document security features in README Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
49
db.py
49
db.py
@@ -109,6 +109,14 @@ CREATE TABLE IF NOT EXISTS user_renderers (
|
||||
UNIQUE(username, l1_url)
|
||||
);
|
||||
|
||||
-- Revoked tokens (for federated logout)
|
||||
CREATE TABLE IF NOT EXISTS revoked_tokens (
|
||||
token_hash VARCHAR(64) PRIMARY KEY,
|
||||
username VARCHAR(255) NOT NULL,
|
||||
revoked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_assets_content_hash ON assets(content_hash);
|
||||
@@ -120,6 +128,7 @@ CREATE INDEX IF NOT EXISTS idx_activities_published ON activities(published DESC
|
||||
CREATE INDEX IF NOT EXISTS idx_activities_anchor ON activities(anchor_root);
|
||||
CREATE INDEX IF NOT EXISTS idx_anchors_created ON anchors(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_followers_username ON followers(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_revoked_tokens_expires ON revoked_tokens(expires_at);
|
||||
"""
|
||||
|
||||
|
||||
@@ -761,3 +770,43 @@ async def detach_renderer(username: str, l1_url: str) -> bool:
|
||||
username, l1_url
|
||||
)
|
||||
return "DELETE 1" in result
|
||||
|
||||
|
||||
# ============ Token Revocation ============
|
||||
|
||||
async def revoke_token(token_hash: str, username: str, expires_at) -> bool:
|
||||
"""Revoke a token. Returns True if newly revoked."""
|
||||
async with get_connection() as conn:
|
||||
try:
|
||||
await conn.execute(
|
||||
"""INSERT INTO revoked_tokens (token_hash, username, expires_at)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (token_hash) DO NOTHING""",
|
||||
token_hash, username, expires_at
|
||||
)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
async def is_token_revoked(token_hash: str) -> bool:
|
||||
"""Check if a token has been revoked."""
|
||||
async with get_connection() as conn:
|
||||
row = await conn.fetchrow(
|
||||
"SELECT 1 FROM revoked_tokens WHERE token_hash = $1 AND expires_at > NOW()",
|
||||
token_hash
|
||||
)
|
||||
return row is not None
|
||||
|
||||
|
||||
async def cleanup_expired_revocations() -> int:
|
||||
"""Remove expired revocation entries. Returns count removed."""
|
||||
async with get_connection() as conn:
|
||||
result = await conn.execute(
|
||||
"DELETE FROM revoked_tokens WHERE expires_at < NOW()"
|
||||
)
|
||||
# Extract count from "DELETE N"
|
||||
try:
|
||||
return int(result.split()[-1])
|
||||
except (ValueError, IndexError):
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user