\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;