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.
This commit is contained in:
committed by
GitHub
parent
c74beb27ac
commit
8bfcfe4a22
@@ -0,0 +1,124 @@
|
|||||||
|
-- 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;
|
||||||
|
$$;
|
||||||
@@ -37,6 +37,11 @@ CREATE TABLE IF NOT EXISTS public.nonces (
|
|||||||
CREATE INDEX IF NOT EXISTS idx_nonces_status ON public.nonces (client_token, user_id, purpose, expires_at)
|
CREATE INDEX IF NOT EXISTS idx_nonces_status ON public.nonces (client_token, user_id, purpose, expires_at)
|
||||||
WHERE used_at IS NULL AND revoked = FALSE;
|
WHERE used_at IS NULL AND revoked = FALSE;
|
||||||
|
|
||||||
|
-- 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;
|
||||||
|
|
||||||
-- Enable Row Level Security (RLS)
|
-- Enable Row Level Security (RLS)
|
||||||
ALTER TABLE public.nonces ENABLE ROW LEVEL SECURITY;
|
ALTER TABLE public.nonces ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
@@ -147,41 +152,52 @@ create or replace function public.verify_nonce (
|
|||||||
SEARCH_PATH to '' as $$
|
SEARCH_PATH to '' as $$
|
||||||
DECLARE
|
DECLARE
|
||||||
v_nonce RECORD;
|
v_nonce RECORD;
|
||||||
v_matching_count INTEGER;
|
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Count how many matching tokens exist before verification attempt
|
-- Find and update the nonce in a single operation
|
||||||
SELECT COUNT(*)
|
-- First filter by indexed columns to reduce candidate rows, then do bcrypt comparison
|
||||||
INTO v_matching_count
|
WITH candidate_nonces AS (
|
||||||
FROM public.nonces
|
-- Use index to filter candidates by purpose, user_id, expiry, status
|
||||||
WHERE purpose = p_purpose;
|
SELECT id, client_token, user_id, purpose, metadata, scopes,
|
||||||
|
verification_attempts, expires_at, used_at, revoked
|
||||||
-- Update verification attempt counter and tracking info for all matching tokens
|
FROM public.nonces
|
||||||
UPDATE public.nonces
|
WHERE purpose = p_purpose
|
||||||
SET verification_attempts = verification_attempts + 1,
|
AND used_at IS NULL
|
||||||
last_verification_at = NOW(),
|
AND NOT revoked
|
||||||
last_verification_ip = COALESCE(p_ip, last_verification_ip),
|
AND expires_at > NOW()
|
||||||
last_verification_user_agent = COALESCE(p_user_agent, last_verification_user_agent)
|
-- Only apply user_id filter if the token was created for a specific user
|
||||||
WHERE client_token = extensions.crypt(p_token, client_token)
|
AND (
|
||||||
AND purpose = p_purpose;
|
-- Case 1: Anonymous token (user_id is NULL in DB)
|
||||||
|
(user_id IS NULL)
|
||||||
-- Find the nonce by token and purpose
|
OR
|
||||||
-- Modified to handle user-specific tokens better
|
-- Case 2: User-specific token (check if user_id matches)
|
||||||
SELECT *
|
(user_id = p_user_id)
|
||||||
INTO v_nonce
|
)
|
||||||
FROM public.nonces
|
ORDER BY created_at DESC
|
||||||
WHERE client_token = extensions.crypt(p_token, client_token)
|
-- Safety net: Limit to 100 most recent candidates to cap worst-case performance
|
||||||
AND purpose = p_purpose
|
-- In production, auto-revocation keeps this low, but this protects against edge cases
|
||||||
-- Only apply user_id filter if the token was created for a specific user
|
LIMIT 100
|
||||||
AND (
|
-- CRITICAL: Lock rows to prevent race conditions in concurrent verifications
|
||||||
-- Case 1: Anonymous token (user_id is NULL in DB)
|
-- SKIP LOCKED ensures other requests fail fast instead of waiting
|
||||||
(user_id IS NULL)
|
FOR UPDATE SKIP LOCKED
|
||||||
OR
|
),
|
||||||
-- Case 2: User-specific token (check if user_id matches)
|
matched_nonce AS (
|
||||||
(user_id = p_user_id)
|
-- Now do the expensive bcrypt comparison only on filtered candidates
|
||||||
)
|
SELECT *
|
||||||
AND used_at IS NULL
|
FROM candidate_nonces
|
||||||
AND NOT revoked
|
WHERE client_token = extensions.crypt(p_token, client_token)
|
||||||
AND expires_at > NOW();
|
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
|
-- Check if nonce exists
|
||||||
IF v_nonce.id IS NULL THEN
|
IF v_nonce.id IS NULL THEN
|
||||||
@@ -191,7 +207,7 @@ BEGIN
|
|||||||
);
|
);
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Check if max verification attempts exceeded
|
-- 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
|
IF p_max_verification_attempts > 0 AND v_nonce.verification_attempts > p_max_verification_attempts THEN
|
||||||
-- Automatically revoke the token
|
-- Automatically revoke the token
|
||||||
UPDATE public.nonces
|
UPDATE public.nonces
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "next-supabase-saas-kit-turbo",
|
"name": "next-supabase-saas-kit-turbo",
|
||||||
"version": "2.19.2",
|
"version": "2.19.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
Reference in New Issue
Block a user