Purpose: Fixes P0 race condition in refresh token reuse detectionChanges:
Adds refresh_token_locked_at column to mcp_server_oauth_tokens
Creates indexes for efficient lock queries
Enables optimistic locking pattern
SQL:
Copy
ALTER TABLE mcp_server_oauth_tokensADD COLUMN IF NOT EXISTS refresh_token_locked_at timestamp with time zone;CREATE INDEX IF NOT EXISTS idx_oauth_tokens_server_refresh_status ON mcp_server_oauth_tokens(server_uuid, refresh_token_used_at, refresh_token_locked_at);
Impact: Zero downtime - existing tokens continue working
0075: PKCE State Audit and Constraints
Purpose: Prevents PKCE state replay attacksChanges:
Creates oauth_pkce_states_audit table with 30-day retention
Adds PostgreSQL trigger to auto-audit deleted states
Tracks used states to prevent replay
SQL:
Copy
CREATE TABLE IF NOT EXISTS oauth_pkce_states_audit ( state text PRIMARY KEY, server_uuid uuid NOT NULL, user_id text NOT NULL, used_at timestamp with time zone NOT NULL DEFAULT NOW(), expires_at timestamp with time zone NOT NULL, audit_reason text NOT NULL);CREATE INDEX IF NOT EXISTS idx_oauth_pkce_audit_expires ON oauth_pkce_states_audit(expires_at);CREATE OR REPLACE FUNCTION audit_pkce_state_deletion()RETURNS TRIGGER AS $$BEGIN INSERT INTO oauth_pkce_states_audit (state, server_uuid, user_id, used_at, expires_at, audit_reason) VALUES (OLD.state, OLD.server_uuid, OLD.user_id, NOW(), NOW() + INTERVAL '30 days', 'used') ON CONFLICT (state) DO NOTHING; RETURN OLD;END;$$ LANGUAGE plpgsql;CREATE TRIGGER pkce_state_audit_trigger BEFORE DELETE ON oauth_pkce_states FOR EACH ROW EXECUTE FUNCTION audit_pkce_state_deletion();
Impact: Zero downtime - new security layer, no breaking changes
# Trigger a test OAuth flow via UI# Navigate to: http://localhost:12005/mcp-servers# Add a new OAuth-enabled MCP server# Complete the OAuth flow# Check structured logscurl http://localhost:3100/loki/api/v1/query_range \ --data-urlencode 'query={service_name="pluggedin-app"} |= "oauth"' \ --data-urlencode "start=$(date -u -d '5 minutes ago' +%s)000000000" \ --data-urlencode "end=$(date -u +%s)000000000" | jq .
4
Verify Security Features
Copy
# Check PKCE cleanup is runningpsql -h $DB_HOST -U $DB_USER -d $DB_NAME -c \ "SELECT COUNT(*) FROM oauth_pkce_states WHERE expires_at < NOW() - INTERVAL '10 minutes';"# Should be 0 or very low (cleanup working)# Check audit table is populatingpsql -h $DB_HOST -U $DB_USER -d $DB_NAME -c \ "SELECT COUNT(*) FROM oauth_pkce_states_audit;"# Should increase over time as flows complete
If you need to manually remove migrations without full restore:
Copy
-- Remove audit table and triggerDROP TRIGGER IF EXISTS pkce_state_audit_trigger ON oauth_pkce_states;DROP FUNCTION IF EXISTS audit_pkce_state_deletion();DROP TABLE IF EXISTS oauth_pkce_states_audit;-- Remove lock columnALTER TABLE mcp_server_oauth_tokens DROP COLUMN IF EXISTS refresh_token_locked_at;-- Remove migration recordsDELETE FROM __drizzle_migrations WHERE name IN ( '0074_atomic_refresh_token_marking', '0075_pkce_state_audit_and_constraints');
Manual cleanup should only be done as a last resort. Prefer full database restore.
# Ensure NODE_ENV is NOT 'test'echo $NODE_ENV# Check logs for scheduler initializationdocker logs pluggedin-app | grep pkce_cleanup_scheduler_initialized# Verify VITEST is not definedecho $VITEST # Should be empty
High number of PKCE states not cleaned
Cause: Grace period protecting recent statesSolution:
Copy
# Check state agespsql -h $DB_HOST -U $DB_USER -d $DB_NAME -c \ "SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE expires_at > NOW()) as active, COUNT(*) FILTER (WHERE expires_at < NOW() - INTERVAL '10 minutes') as should_clean FROM oauth_pkce_states;"# States should only be cleaned if expired >10 min ago# This is expected behavior (grace period protection)