Files
myeasycms-v2/apps/web/supabase/migrations/20251024141215_optimize-verify-nonce-performance.sql
Giancarlo Buomprisco 8bfcfe4a22 chore: bump version to 2.19.3 in package.json and optimize nonce veri… (#401)
* chore: bump version to 2.19.3 in package.json and optimize nonce verification logic in SQL schema

- Incremented application version from 2.19.2 to 2.19.3 in package.json.
- Enhanced nonce verification function in 12-one-time-tokens.sql for improved performance and concurrency handling.
2025-11-01 08:59:55 +07:00

125 lines
4.8 KiB
PL/PgSQL

-- Add optimized index for verify_nonce function query pattern
-- Matches filter order: purpose → expires_at → user_id
CREATE INDEX IF NOT EXISTS idx_nonces_verify_lookup ON public.nonces (purpose, expires_at DESC, user_id)
WHERE used_at IS NULL AND revoked = FALSE;
-- Optimize verify_nonce function for performance
-- New implementation:
-- 1. Adds optimized index for the verify query pattern
-- 2. Uses CTE to filter candidates using index first
-- 3. Only does bcrypt comparison on filtered candidates
-- 4. Updates only the matched token in a single operation
create or replace function public.verify_nonce (
p_token TEXT,
p_purpose TEXT,
p_user_id UUID default null,
p_required_scopes text[] default null,
p_max_verification_attempts INTEGER default 5,
p_ip INET default null,
p_user_agent TEXT default null
) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER
set
SEARCH_PATH to '' as $$
DECLARE
v_nonce RECORD;
BEGIN
-- Find and update the nonce in a single operation
-- First filter by indexed columns to reduce candidate rows, then do bcrypt comparison
WITH candidate_nonces AS (
-- Use index to filter candidates by purpose, user_id, expiry, status
SELECT id, client_token, user_id, purpose, metadata, scopes,
verification_attempts, expires_at, used_at, revoked
FROM public.nonces
WHERE purpose = p_purpose
AND used_at IS NULL
AND NOT revoked
AND expires_at > NOW()
-- Only apply user_id filter if the token was created for a specific user
AND (
-- Case 1: Anonymous token (user_id is NULL in DB)
(user_id IS NULL)
OR
-- Case 2: User-specific token (check if user_id matches)
(user_id = p_user_id)
)
ORDER BY created_at DESC
-- Safety net: Limit to 100 most recent candidates to cap worst-case performance
-- In production, auto-revocation keeps this low, but this protects against edge cases
LIMIT 100
-- CRITICAL: Lock rows to prevent race conditions in concurrent verifications
-- SKIP LOCKED ensures other requests fail fast instead of waiting
FOR UPDATE SKIP LOCKED
),
matched_nonce AS (
-- Now do the expensive bcrypt comparison only on filtered candidates
SELECT *
FROM candidate_nonces
WHERE client_token = extensions.crypt(p_token, client_token)
LIMIT 1
),
updated_nonce AS (
-- Update only the matched nonce
UPDATE public.nonces
SET verification_attempts = verification_attempts + 1,
last_verification_at = NOW(),
last_verification_ip = COALESCE(p_ip, last_verification_ip),
last_verification_user_agent = COALESCE(p_user_agent, last_verification_user_agent)
WHERE id = (SELECT id FROM matched_nonce)
RETURNING *
)
SELECT * INTO v_nonce FROM updated_nonce;
-- Check if nonce exists
IF v_nonce.id IS NULL THEN
RETURN jsonb_build_object(
'valid', false,
'message', 'Invalid or expired token'
);
END IF;
-- Check if max verification attempts exceeded (using the incremented value)
IF p_max_verification_attempts > 0 AND v_nonce.verification_attempts > p_max_verification_attempts THEN
-- Automatically revoke the token
UPDATE public.nonces
SET revoked = TRUE,
revoked_reason = 'Maximum verification attempts exceeded'
WHERE id = v_nonce.id;
RETURN jsonb_build_object(
'valid', false,
'message', 'Token revoked due to too many verification attempts',
'max_attempts_exceeded', true
);
END IF;
-- Check scopes if required
IF p_required_scopes IS NOT NULL AND array_length(p_required_scopes, 1) > 0 THEN
-- Fix scope validation to properly check if token scopes contain all required scopes
-- Using array containment check: array1 @> array2 (array1 contains array2)
IF NOT (v_nonce.scopes @> p_required_scopes) THEN
RETURN jsonb_build_object(
'valid', false,
'message', 'Token does not have required permissions',
'token_scopes', v_nonce.scopes,
'required_scopes', p_required_scopes
);
END IF;
END IF;
-- Mark nonce as used
UPDATE public.nonces
SET used_at = NOW()
WHERE id = v_nonce.id;
-- Return success with metadata
RETURN jsonb_build_object(
'valid', true,
'user_id', v_nonce.user_id,
'metadata', v_nonce.metadata,
'scopes', v_nonce.scopes,
'purpose', v_nonce.purpose
);
END;
$$;