mcp connection
This commit is contained in:
86
migrations/000012_oauth_clients.up.sql
Normal file
86
migrations/000012_oauth_clients.up.sql
Normal file
@@ -0,0 +1,86 @@
|
||||
-- ============================================================================
|
||||
-- Krow — OAuth 2.1 clients
|
||||
--
|
||||
-- Phase 3, migration 1 of 3. This table holds the clients that may ask for a
|
||||
-- token: in practice, one row per Claude installation that has connected.
|
||||
--
|
||||
-- Registered DYNAMICALLY (RFC 7591), not seeded. An MCP client discovers this
|
||||
-- server, registers itself, and gets a client_id back. There is deliberately no
|
||||
-- pre-provisioned row and no fixture: a seeded client is a credential in the
|
||||
-- repository, and the whole point of dynamic registration is that nobody has to
|
||||
-- put one there.
|
||||
--
|
||||
-- PUBLIC CLIENTS ONLY, and that is why there is no client_secret column.
|
||||
-- Claude Desktop and Claude Web are public clients — they run on a machine the
|
||||
-- user controls, so any secret shipped to them is a secret the user has. OAuth
|
||||
-- 2.1 handles this with PKCE instead, which is why code_challenge is mandatory
|
||||
-- in 000013 rather than optional. A column for a secret that must never be
|
||||
-- trusted is a column somebody will eventually trust.
|
||||
--
|
||||
-- Target schema: public. No system schema is read or written.
|
||||
-- ============================================================================
|
||||
|
||||
SET search_path = public;
|
||||
|
||||
CREATE TABLE oauth_clients (
|
||||
-- The client_id handed back at registration and presented on every
|
||||
-- authorization and token request. Opaque and server-generated: a client
|
||||
-- that could choose its own id could impersonate one already registered.
|
||||
client_id text PRIMARY KEY,
|
||||
|
||||
-- What the client calls itself, for the consent screen. Untrusted display
|
||||
-- text — it is whatever the registering client sent, so it is shown as a
|
||||
-- name and never used for a decision.
|
||||
client_name text NOT NULL DEFAULT '',
|
||||
|
||||
-- Exact-match redirect targets. An array because a client may legitimately
|
||||
-- register more than one (a desktop loopback port and a hosted callback),
|
||||
-- and the authorization endpoint matches the presented redirect_uri against
|
||||
-- these byte-for-byte. No prefix matching, no wildcards, no normalisation:
|
||||
-- every one of those has been an open-redirect CVE somewhere.
|
||||
redirect_uris text[] NOT NULL,
|
||||
|
||||
-- Recorded for auditing which client asked for what. Constrained rather than
|
||||
-- free text so an unexpected value is a failed insert instead of a row
|
||||
-- nobody notices.
|
||||
grant_types text[] NOT NULL DEFAULT ARRAY['authorization_code', 'refresh_token'],
|
||||
|
||||
-- The scopes this client may request. Held per-client so tightening the
|
||||
-- global policy later does not silently widen an existing registration.
|
||||
scopes text[] NOT NULL DEFAULT ARRAY['krow.read'],
|
||||
|
||||
created_date timestamptz NOT NULL DEFAULT now(),
|
||||
last_used_at timestamptz,
|
||||
|
||||
-- A client can be disabled without deleting it, so its tokens can be
|
||||
-- revoked and its history kept.
|
||||
disabled_at timestamptz,
|
||||
|
||||
-- At least one redirect URI, or the client can never complete a flow. Caught
|
||||
-- here so a malformed registration fails at the point of registration rather
|
||||
-- than at the point a person is staring at a broken consent screen.
|
||||
CONSTRAINT oauth_clients_redirect_uris_present
|
||||
CHECK (array_length(redirect_uris, 1) >= 1),
|
||||
|
||||
-- Bound so a registration cannot be used to store bulk data.
|
||||
CONSTRAINT oauth_clients_redirect_uris_bounded
|
||||
CHECK (array_length(redirect_uris, 1) <= 10),
|
||||
|
||||
CONSTRAINT oauth_clients_name_bounded
|
||||
CHECK (length(client_name) <= 200)
|
||||
);
|
||||
|
||||
-- Listing a user's connected clients is not a query this table answers — that
|
||||
-- comes from oauth_tokens, which carries user_id. The only index here is the
|
||||
-- primary key, which is also the lookup path: every request arrives with a
|
||||
-- client_id and probes exactly that column.
|
||||
|
||||
COMMENT ON TABLE oauth_clients IS
|
||||
'OAuth 2.1 public clients, registered dynamically per RFC 7591. No secrets '
|
||||
'are stored: public clients authenticate with PKCE, not with a credential.';
|
||||
|
||||
COMMENT ON COLUMN oauth_clients.redirect_uris IS
|
||||
'Exact-match redirect targets. Never prefix-matched or normalised.';
|
||||
|
||||
COMMENT ON COLUMN oauth_clients.disabled_at IS
|
||||
'Set to disable a client without losing its registration or audit history.';
|
||||
Reference in New Issue
Block a user