mcp connection
This commit is contained in:
117
migrations/000013_oauth_grants.up.sql
Normal file
117
migrations/000013_oauth_grants.up.sql
Normal file
@@ -0,0 +1,117 @@
|
||||
-- ============================================================================
|
||||
-- Krow — OAuth 2.1 authorization codes
|
||||
--
|
||||
-- Phase 3, migration 2 of 3. One row per authorization code issued: the short
|
||||
-- window between a person clicking Approve and the client exchanging the code
|
||||
-- for a token.
|
||||
--
|
||||
-- A row here is a bearer credential with a fuse. Three properties make it safe,
|
||||
-- and all three are enforced by this schema rather than by the code that uses
|
||||
-- it:
|
||||
--
|
||||
-- SINGLE-USE consumed_at, set by the same UPDATE that reads the row. A
|
||||
-- code redeemed twice is an attacker replaying a code they
|
||||
-- intercepted, and the second attempt must fail.
|
||||
-- SHORT-LIVED expires_at, minutes not hours. The code is in transit through
|
||||
-- a browser redirect, which is the least trustworthy hop in the
|
||||
-- flow.
|
||||
-- BOUND to client, redirect_uri, user, scope, resource and PKCE
|
||||
-- challenge. Every one of those is re-verified at the token
|
||||
-- endpoint, so a code stolen from one context cannot be spent
|
||||
-- in another.
|
||||
--
|
||||
-- THE CODE ITSELF IS NEVER STORED. code_hash holds SHA-256, exactly as
|
||||
-- sessions.token_hash does, so a dump of this table cannot be replayed.
|
||||
--
|
||||
-- Target schema: public. No system schema is read or written.
|
||||
-- ============================================================================
|
||||
|
||||
SET search_path = public;
|
||||
|
||||
CREATE TABLE oauth_grants (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- SHA-256 of the authorization code, lowercase hex. The CHECK pins the
|
||||
-- format so a caller cannot accidentally store a raw code here: a raw code is
|
||||
-- base64url of random bytes and fails this pattern. Same guard, same
|
||||
-- reasoning as sessions_token_hash_sha256 in 000004.
|
||||
code_hash text NOT NULL,
|
||||
|
||||
client_id text NOT NULL REFERENCES oauth_clients (client_id) ON DELETE CASCADE,
|
||||
|
||||
-- Who approved. ON DELETE CASCADE: a deleted user must not leave a code
|
||||
-- behind that could still be exchanged for a token authenticating as them.
|
||||
user_id uuid NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
|
||||
-- The tenant, denormalised from the user row at issue time. Carried for
|
||||
-- auditing only. It is NEVER read back as the authority on tenancy —
|
||||
-- identity is rebuilt from the live user row at every token validation, so a
|
||||
-- user who moved organisation does not keep the old one. See
|
||||
-- oauth.Authenticator.
|
||||
org_id uuid NOT NULL REFERENCES organizations (id) ON DELETE CASCADE,
|
||||
|
||||
-- Re-verified at the token endpoint. RFC 6749 requires the redirect_uri
|
||||
-- presented at exchange to match the one presented at authorization; without
|
||||
-- this column there is nothing to match against.
|
||||
redirect_uri text NOT NULL,
|
||||
|
||||
scopes text[] NOT NULL,
|
||||
|
||||
-- RFC 8707. The MCP server this code is being obtained for. Carried into the
|
||||
-- access token's audience, which is what makes a token issued for one
|
||||
-- resource unusable at another.
|
||||
resource text NOT NULL,
|
||||
|
||||
-- PKCE. Mandatory — the column is NOT NULL, so a code without a challenge
|
||||
-- cannot exist. OAuth 2.1 requires PKCE for public clients and this is where
|
||||
-- that requirement stops being advisory.
|
||||
code_challenge text NOT NULL,
|
||||
code_challenge_method text NOT NULL,
|
||||
|
||||
created_date timestamptz NOT NULL DEFAULT now(),
|
||||
expires_at timestamptz NOT NULL,
|
||||
|
||||
-- Set on redemption, in the same statement that reads the row. NULL means
|
||||
-- unspent.
|
||||
consumed_at timestamptz,
|
||||
|
||||
CONSTRAINT oauth_grants_code_hash_key UNIQUE (code_hash),
|
||||
CONSTRAINT oauth_grants_code_hash_sha256 CHECK (code_hash ~ '^[0-9a-f]{64}$'),
|
||||
|
||||
-- S256 only. `plain` is permitted by RFC 7636 and forbidden by OAuth 2.1 for
|
||||
-- public clients, because it makes the verifier recoverable from the
|
||||
-- challenge — which is the entire attack PKCE exists to stop. Refused at the
|
||||
-- schema level so no code path can relax it.
|
||||
CONSTRAINT oauth_grants_pkce_s256_only CHECK (code_challenge_method = 'S256'),
|
||||
|
||||
-- A challenge is base64url of a 32-byte SHA-256 digest: 43 characters, no
|
||||
-- padding. Anything else is malformed.
|
||||
CONSTRAINT oauth_grants_challenge_shape CHECK (code_challenge ~ '^[A-Za-z0-9_-]{43}$'),
|
||||
|
||||
CONSTRAINT oauth_grants_expires_after_created CHECK (expires_at > created_date),
|
||||
CONSTRAINT oauth_grants_scopes_present CHECK (array_length(scopes, 1) >= 1)
|
||||
);
|
||||
|
||||
-- The redemption path: look up by hash, check unspent and unexpired. The UNIQUE
|
||||
-- constraint above already provides this index.
|
||||
|
||||
-- The sweep of dead rows.
|
||||
CREATE INDEX oauth_grants_expires_idx ON oauth_grants (expires_at);
|
||||
|
||||
-- Revoking every outstanding code for a user, and the FK's own cascade check.
|
||||
CREATE INDEX oauth_grants_user_idx ON oauth_grants (user_id);
|
||||
|
||||
COMMENT ON TABLE oauth_grants IS
|
||||
'OAuth authorization codes: single-use, short-lived, and bound to client, '
|
||||
'redirect_uri, user, scope, resource and PKCE challenge. The raw code is '
|
||||
'never stored — only SHA-256 of it.';
|
||||
|
||||
COMMENT ON COLUMN oauth_grants.code_hash IS
|
||||
'Lowercase hex SHA-256 of the authorization code. Never the code.';
|
||||
|
||||
COMMENT ON COLUMN oauth_grants.consumed_at IS
|
||||
'Set by the redemption UPDATE itself, so a code cannot be spent twice.';
|
||||
|
||||
COMMENT ON COLUMN oauth_grants.org_id IS
|
||||
'The tenant at issue time, for audit only. Tenancy is re-read from the live '
|
||||
'user row on every token validation and is never taken from here.';
|
||||
Reference in New Issue
Block a user