Files
StarPunk/starpunk/database.py
Phil Skentelbery 5e50330bdf feat: Implement PKCE authentication for IndieLogin.com
This fixes critical IndieAuth authentication by implementing PKCE (Proof Key
for Code Exchange) as required by IndieLogin.com API specification.

Added:
- PKCE code_verifier and code_challenge generation (RFC 7636)
- Database column: auth_state.code_verifier for PKCE support
- Issuer validation for authentication callbacks
- Comprehensive PKCE unit tests (6 tests, all passing)
- Database migration script for code_verifier column

Changed:
- Corrected IndieLogin.com API endpoints (/authorize and /token)
- State token validation now returns code_verifier for token exchange
- Authentication flow follows IndieLogin.com API specification exactly
- Enhanced logging with code_verifier redaction

Removed:
- OAuth metadata endpoint (/.well-known/oauth-authorization-server)
  Added in v0.7.0 but not required by IndieLogin.com
- h-app microformats markup from templates
  Modified in v0.7.1 but not used by IndieLogin.com
- indieauth-metadata link from HTML head

Security:
- PKCE prevents authorization code interception attacks
- Issuer validation prevents token substitution attacks
- Code verifier securely stored, redacted in logs, and single-use

Documentation:
- Version: 0.8.0
- CHANGELOG updated with v0.8.0 entry and v0.7.x notes
- ADR-016 and ADR-017 marked as superseded by ADR-019
- Implementation report created in docs/reports/
- Test update guide created in TODO_TEST_UPDATES.md

Breaking Changes:
- Users mid-authentication will need to restart login after upgrade
- Database migration required before deployment

Related: ADR-019

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 15:43:38 -07:00

110 lines
2.9 KiB
Python

"""
Database initialization and operations for StarPunk
SQLite database for metadata, sessions, and tokens
"""
import sqlite3
from pathlib import Path
# Database schema
SCHEMA_SQL = """
-- Notes metadata (content is in files)
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT UNIQUE NOT NULL,
file_path TEXT UNIQUE NOT NULL,
published BOOLEAN DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP,
content_hash TEXT
);
CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_notes_published ON notes(published);
CREATE INDEX IF NOT EXISTS idx_notes_slug ON notes(slug);
CREATE INDEX IF NOT EXISTS idx_notes_deleted_at ON notes(deleted_at);
-- Authentication sessions (IndieLogin)
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_token_hash TEXT UNIQUE NOT NULL,
me TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
last_used_at TIMESTAMP,
user_agent TEXT,
ip_address TEXT
);
CREATE INDEX IF NOT EXISTS idx_sessions_token_hash ON sessions(session_token_hash);
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
CREATE INDEX IF NOT EXISTS idx_sessions_me ON sessions(me);
-- Micropub access tokens
CREATE TABLE IF NOT EXISTS tokens (
token TEXT PRIMARY KEY,
me TEXT NOT NULL,
client_id TEXT,
scope TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_tokens_me ON tokens(me);
-- CSRF state tokens (for IndieAuth flow)
CREATE TABLE IF NOT EXISTS auth_state (
state TEXT PRIMARY KEY,
code_verifier TEXT NOT NULL DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
redirect_uri TEXT
);
CREATE INDEX IF NOT EXISTS idx_auth_state_expires ON auth_state(expires_at);
"""
def init_db(app=None):
"""
Initialize database schema
Args:
app: Flask application instance (optional, for config access)
"""
if app:
db_path = app.config["DATABASE_PATH"]
else:
# Fallback to default path
db_path = Path("./data/starpunk.db")
# Ensure parent directory exists
db_path.parent.mkdir(parents=True, exist_ok=True)
# Create database and schema
conn = sqlite3.connect(db_path)
try:
conn.executescript(SCHEMA_SQL)
conn.commit()
print(f"Database initialized: {db_path}")
finally:
conn.close()
def get_db(app):
"""
Get database connection
Args:
app: Flask application instance
Returns:
sqlite3.Connection
"""
db_path = app.config["DATABASE_PATH"]
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row # Return rows as dictionaries
return conn