74 lines
3.6 KiB
MySQL
74 lines
3.6 KiB
MySQL
|
|
-- 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');
|