d3ro-voice/server/supabase/tests/device-session-revocation.integration.sql

167 lines
7.8 KiB
PL/PgSQL

\set ON_ERROR_STOP on
-- Regression for 20260929000005_device_session_revocation.sql.
-- Revoking a device must end that device's auth session (and, by cascade, its
-- refresh tokens) so a stolen device cannot keep refreshing its token and
-- reading account data through PostgREST. The session binding is
-- server-managed: clients cannot forge it, and it can never point at another
-- user's session.
BEGIN;
CREATE OR REPLACE FUNCTION pg_temp.assert_true(condition boolean, message text)
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
IF condition IS NOT TRUE THEN
RAISE EXCEPTION 'assertion_failed: %', message;
END IF;
END;
$$;
CREATE OR REPLACE FUNCTION pg_temp.act_as(uid uuid, sid uuid)
RETURNS void
LANGUAGE sql
AS $$
SELECT set_config(
'request.jwt.claims',
json_build_object('sub', uid, 'role', 'authenticated', 'session_id', sid)::text,
true
);
$$;
INSERT INTO auth.users (
id, aud, role, email, encrypted_password, email_confirmed_at,
raw_app_meta_data, raw_user_meta_data, created_at, updated_at
) VALUES
('33000000-0000-4000-8000-000000000001', 'authenticated', 'authenticated',
'device-session-owner@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
'{"provider":"email","providers":["email"]}'::jsonb, '{}'::jsonb, now(), now()),
('33000000-0000-4000-8000-000000000002', 'authenticated', 'authenticated',
'device-session-other@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
'{"provider":"email","providers":["email"]}'::jsonb, '{}'::jsonb, now(), now());
-- Sessions: laptop (L), phone (P), a second laptop (L2) of the owner, and one
-- session (O) that belongs to a different user.
INSERT INTO auth.sessions (id, user_id, created_at, updated_at) VALUES
('33000000-0000-4000-8000-00000000a001', '33000000-0000-4000-8000-000000000001', now(), now()),
('33000000-0000-4000-8000-00000000a002', '33000000-0000-4000-8000-000000000001', now(), now()),
('33000000-0000-4000-8000-00000000a003', '33000000-0000-4000-8000-000000000001', now(), now()),
('33000000-0000-4000-8000-00000000a0ff', '33000000-0000-4000-8000-000000000002', now(), now());
INSERT INTO auth.refresh_tokens (token, user_id, revoked, created_at, updated_at, session_id) VALUES
('device-session-laptop-token', '33000000-0000-4000-8000-000000000001', false, now(), now(),
'33000000-0000-4000-8000-00000000a001'),
('device-session-phone-token', '33000000-0000-4000-8000-000000000001', false, now(), now(),
'33000000-0000-4000-8000-00000000a002');
-- 1. Registration stamps the caller's own session; a forged value is ignored.
SET LOCAL ROLE authenticated;
SELECT pg_temp.act_as('33000000-0000-4000-8000-000000000001', '33000000-0000-4000-8000-00000000a001');
INSERT INTO public.devices (id, user_id, installation_id, platform, device_name, app_version, auth_session_id)
VALUES ('33000000-0000-4000-8000-00000000d001', '33000000-0000-4000-8000-000000000001',
'33000000-0000-4000-8000-00000000e001', 'windows', 'Laptop', '1.0.0',
'33000000-0000-4000-8000-00000000a002');
SELECT pg_temp.act_as('33000000-0000-4000-8000-000000000001', '33000000-0000-4000-8000-00000000a002');
INSERT INTO public.devices (id, user_id, installation_id, platform, device_name, app_version)
VALUES ('33000000-0000-4000-8000-00000000d002', '33000000-0000-4000-8000-000000000001',
'33000000-0000-4000-8000-00000000e002', 'android', 'Phone', '1.0.0');
RESET ROLE;
SELECT pg_temp.assert_true(
(SELECT auth_session_id FROM public.devices WHERE id = '33000000-0000-4000-8000-00000000d001')
= '33000000-0000-4000-8000-00000000a001',
'laptop registration binds the laptop session, not the forged phone session'
);
SELECT pg_temp.assert_true(
(SELECT auth_session_id FROM public.devices WHERE id = '33000000-0000-4000-8000-00000000d002')
= '33000000-0000-4000-8000-00000000a002',
'phone registration binds the phone session'
);
-- 2. A client cannot re-point its row at another session: a plain update keeps
-- the binding, and a check-in only ever stamps the caller's own session.
SET LOCAL ROLE authenticated;
SELECT pg_temp.act_as('33000000-0000-4000-8000-000000000001', '33000000-0000-4000-8000-00000000a001');
UPDATE public.devices
SET auth_session_id = '33000000-0000-4000-8000-00000000a002', device_name = 'Renamed laptop'
WHERE id = '33000000-0000-4000-8000-00000000d001';
UPDATE public.devices
SET auth_session_id = '33000000-0000-4000-8000-00000000a0ff', last_seen_at = now() + interval '1 minute'
WHERE id = '33000000-0000-4000-8000-00000000d001';
RESET ROLE;
SELECT pg_temp.assert_true(
(SELECT auth_session_id FROM public.devices WHERE id = '33000000-0000-4000-8000-00000000d001')
= '33000000-0000-4000-8000-00000000a001',
'client writes to auth_session_id are ignored'
);
-- 3. Revoking the laptop from the phone ends the laptop session and its
-- refresh tokens; the phone session is untouched.
SET LOCAL ROLE authenticated;
SELECT pg_temp.act_as('33000000-0000-4000-8000-000000000001', '33000000-0000-4000-8000-00000000a002');
CREATE TEMP TABLE revoke_result ON COMMIT DROP AS
SELECT public.revoke_device('33000000-0000-4000-8000-00000000d001') AS payload;
RESET ROLE;
SELECT pg_temp.assert_true(
(SELECT (payload->>'revoked_at') IS NOT NULL AND NOT (payload ? 'auth_session_id') FROM revoke_result),
'revoke_device returns the revoked row without the session binding'
);
SELECT pg_temp.assert_true(
NOT EXISTS (SELECT 1 FROM auth.sessions WHERE id = '33000000-0000-4000-8000-00000000a001'),
'revoked device session is deleted'
);
SELECT pg_temp.assert_true(
NOT EXISTS (SELECT 1 FROM auth.refresh_tokens WHERE token = 'device-session-laptop-token'),
'revoked device refresh token is gone with its session'
);
SELECT pg_temp.assert_true(
EXISTS (SELECT 1 FROM auth.sessions WHERE id = '33000000-0000-4000-8000-00000000a002')
AND EXISTS (SELECT 1 FROM auth.refresh_tokens WHERE token = 'device-session-phone-token'),
'revoking another device keeps the caller session'
);
-- 4. A revoke never deletes a session of a different user, even if the binding
-- was tampered with by a privileged path.
INSERT INTO public.devices (id, user_id, installation_id, platform, device_name, app_version, auth_session_id)
VALUES ('33000000-0000-4000-8000-00000000d003', '33000000-0000-4000-8000-000000000001',
'33000000-0000-4000-8000-00000000e003', 'macos', 'Laptop 2', '1.0.0',
'33000000-0000-4000-8000-00000000a0ff');
SET LOCAL ROLE authenticated;
SELECT pg_temp.act_as('33000000-0000-4000-8000-000000000001', '33000000-0000-4000-8000-00000000a002');
SELECT public.revoke_device('33000000-0000-4000-8000-00000000d003');
RESET ROLE;
SELECT pg_temp.assert_true(
EXISTS (SELECT 1 FROM auth.sessions WHERE id = '33000000-0000-4000-8000-00000000a0ff'),
'revoke_device only deletes sessions of the caller'
);
-- 5. A device that removes itself from the list ends its own session, so a
-- stolen session cannot hide its device row and survive.
SET LOCAL ROLE authenticated;
SELECT pg_temp.act_as('33000000-0000-4000-8000-000000000001', '33000000-0000-4000-8000-00000000a003');
INSERT INTO public.devices (id, user_id, installation_id, platform, device_name, app_version)
VALUES ('33000000-0000-4000-8000-00000000d004', '33000000-0000-4000-8000-000000000001',
'33000000-0000-4000-8000-00000000e004', 'windows', 'Laptop 3', '1.0.0');
SELECT public.unregister_current_device('33000000-0000-4000-8000-00000000e004');
RESET ROLE;
SELECT pg_temp.assert_true(
NOT EXISTS (SELECT 1 FROM public.devices WHERE id = '33000000-0000-4000-8000-00000000d004'),
'unregistered device row is removed'
);
SELECT pg_temp.assert_true(
NOT EXISTS (SELECT 1 FROM auth.sessions WHERE id = '33000000-0000-4000-8000-00000000a003'),
'unregistered device session is deleted'
);
SELECT pg_temp.assert_true(
EXISTS (SELECT 1 FROM auth.sessions WHERE id = '33000000-0000-4000-8000-00000000a002'),
'unregister leaves other sessions of the user alone'
);
ROLLBACK;