Adds migrations/0002_oauth_tables.sql per design.md §4.2: clients, auth_codes, access_tokens, refresh_tokens. Cascading foreign keys guarantee that revoking a client tears down every dependent row, and that a refresh token can never outlive its access token. Storage choices: - Broker access/refresh tokens stored as hex-encoded SHA-256 hashes; plaintext leaves the broker exactly once (when handed to the MCP client). Lookups by hash are O(log n) via the PK index. - Forgejo tokens stored cleartext (subprocess spawning needs them). At-rest protection is the volume permissions + optional encrypted volume; application-layer encryption is tracked as backlog item -sd4. - Timestamps are unix epoch INTEGERs, set by the application — keeps deadline comparisons trivial and lets phase 5c inject a test clock. - Tables are not STRICT to stay consistent with the phase-1 broker_meta table; converting both is a future cleanup if we want it. Tests verify column sets via PRAGMA table_info, expected indexes are present, the FK CASCADE works in both directions (client → tokens, and access_token → refresh_token), and the oauth_schema_version marker is written. Existing migration-count assertions parameterised on embeddedMigrationCount so adding a third migration only needs that constant bumped. Closes forgejo-mcp-broker-cpb. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
74 lines
3.6 KiB
SQL
74 lines
3.6 KiB
SQL
-- 0002_oauth_tables.sql
|
|
-- OAuth 2.1 authorization-server state per design.md §4.2.
|
|
--
|
|
-- Storage rules:
|
|
-- * Broker tokens (access, refresh) are stored as hex-encoded SHA-256 hashes.
|
|
-- Plaintext leaves the broker exactly once, when handed to the MCP client.
|
|
-- * Forgejo tokens are stored cleartext — the broker has to be able to use
|
|
-- them to spawn forgejo-mcp subprocesses. The SQLite file is therefore a
|
|
-- sensitive secret at rest; protect it via volume permissions and (in
|
|
-- production) an encrypted underlying volume.
|
|
-- * Timestamps are unix epoch seconds (INTEGER). Comparing deadlines as
|
|
-- ints is dramatically simpler than parsing ISO-8601 strings, and SQLite's
|
|
-- type affinity makes mixed-type rows easy to misuse.
|
|
-- * No DEFAULT clauses for timestamps — the application supplies them so a
|
|
-- test clock can be injected later (phase 5c).
|
|
|
|
CREATE TABLE clients (
|
|
client_id TEXT PRIMARY KEY,
|
|
client_secret TEXT, -- NULL for public clients (PKCE-only)
|
|
redirect_uris TEXT NOT NULL, -- JSON array of strings
|
|
metadata_json TEXT, -- raw RFC 7591 client-metadata blob, optional
|
|
created_at INTEGER NOT NULL,
|
|
last_used INTEGER -- bumped on successful authorize/token use
|
|
);
|
|
|
|
CREATE INDEX idx_clients_last_used ON clients(last_used);
|
|
|
|
CREATE TABLE auth_codes (
|
|
code TEXT PRIMARY KEY,
|
|
client_id TEXT NOT NULL REFERENCES clients(client_id) ON DELETE CASCADE,
|
|
redirect_uri TEXT NOT NULL,
|
|
code_challenge TEXT NOT NULL,
|
|
code_challenge_method TEXT NOT NULL, -- only 'S256' accepted by the AS
|
|
scopes TEXT NOT NULL, -- space-separated
|
|
forgejo_access_token TEXT NOT NULL,
|
|
forgejo_refresh_token TEXT,
|
|
forgejo_token_expires_at INTEGER NOT NULL,
|
|
forgejo_user_id INTEGER NOT NULL,
|
|
forgejo_username TEXT NOT NULL,
|
|
expires_at INTEGER NOT NULL, -- short TTL (~600s); reaper sweeps expired rows
|
|
used_at INTEGER -- non-NULL after exchange; replay defense
|
|
);
|
|
|
|
CREATE INDEX idx_auth_codes_expires_at ON auth_codes(expires_at);
|
|
|
|
CREATE TABLE access_tokens (
|
|
token_hash TEXT PRIMARY KEY, -- hex(sha256(plaintext_token))
|
|
client_id TEXT NOT NULL REFERENCES clients(client_id) ON DELETE CASCADE,
|
|
forgejo_user_id INTEGER NOT NULL,
|
|
forgejo_username TEXT NOT NULL,
|
|
scopes TEXT NOT NULL,
|
|
forgejo_access_token TEXT NOT NULL,
|
|
forgejo_refresh_token TEXT,
|
|
forgejo_token_expires_at INTEGER NOT NULL,
|
|
expires_at INTEGER NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
revoked_at INTEGER -- NULL = active
|
|
);
|
|
|
|
CREATE INDEX idx_access_tokens_expires_at ON access_tokens(expires_at);
|
|
CREATE INDEX idx_access_tokens_forgejo_uid ON access_tokens(forgejo_user_id);
|
|
|
|
CREATE TABLE refresh_tokens (
|
|
token_hash TEXT PRIMARY KEY, -- hex(sha256(plaintext_token))
|
|
access_token_hash TEXT NOT NULL REFERENCES access_tokens(token_hash) ON DELETE CASCADE,
|
|
client_id TEXT NOT NULL REFERENCES clients(client_id) ON DELETE CASCADE,
|
|
expires_at INTEGER NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
revoked_at INTEGER
|
|
);
|
|
|
|
CREATE INDEX idx_refresh_tokens_expires_at ON refresh_tokens(expires_at);
|
|
|
|
INSERT INTO broker_meta (key, value) VALUES ('oauth_schema_version', '1');
|