From 0efe8912bf0bf835cd44394de7c090bfa7189eab Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 17 Oct 2026 11:54:16 -0700 Subject: [PATCH 1/2] fix(auth): end OAuth grants on SCIM address change and add sign out other devices --- .../settings/components/general/general.tsx | 32 +++++++++ .../lib/application/users/provision-user.ts | 4 +- .../scim/lib/application/users/update-user.ts | 18 ++--- apps/sim/hooks/queries/user-profile.ts | 14 ++++ apps/sim/scripts/test-scim-e2e.ts | 66 +++++++++++++++++++ 5 files changed, 124 insertions(+), 10 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx index dd916ade3c1..22511883b7a 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx @@ -6,6 +6,7 @@ import { Button, Chip, ChipCombobox, + ChipConfirmModal, ChipModal, ChipModalBody, ChipModalError, @@ -17,9 +18,11 @@ import { Label, Switch, Tooltip, + toast, } from '@sim/emcn' import { Camera, Check, CircleInfo, Pencil } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' import dynamic from 'next/dynamic' import Image from 'next/image' import { useRouter } from 'next/navigation' @@ -47,6 +50,7 @@ import { useBrandConfig } from '@/ee/whitelabeling' import { useGeneralSettings, useUpdateGeneralSetting } from '@/hooks/queries/general-settings' import { useResetPassword, + useSignOutOtherDevices, useUpdateUserProfile, useUserProfile, } from '@/hooks/queries/user-profile' @@ -118,6 +122,8 @@ export function General() { }) const [showResetPasswordModal, setShowResetPasswordModal] = useState(false) const resetPassword = useResetPassword() + const [showSignOutOtherDevicesModal, setShowSignOutOtherDevicesModal] = useState(false) + const signOutOtherDevices = useSignOutOtherDevices() const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false) @@ -221,6 +227,14 @@ export function General() { ) } + const confirmSignOutOtherDevices = () => { + signOutOtherDevices.mutate(undefined, { + onSuccess: () => toast.success('Signed out of other devices'), + onError: (error) => toast.error(getErrorMessage(error)), + onSettled: () => setShowSignOutOtherDevicesModal(false), + }) + } + const handleThemeChange = async (value: string) => { await updateSetting.mutateAsync({ key: 'theme', value: value as 'system' | 'light' | 'dark' }) } @@ -288,6 +302,11 @@ export function General() { ...(session?.user?.id && !isAuthDisabled ? [ { id: 'sign-out', text: 'Sign out', onSelect: () => signOutAndRedirect(router.push) }, + { + id: 'sign-out-other-devices', + text: 'Sign out other devices', + onSelect: () => setShowSignOutOtherDevicesModal(true), + }, { id: 'reset-password', text: 'Reset password', @@ -640,6 +659,19 @@ export function General() { /> + + { + const { error } = await client.revokeOtherSessions() + if (error) throw new Error(error.message ?? 'Failed to sign out other devices') + }, + }) +} diff --git a/apps/sim/scripts/test-scim-e2e.ts b/apps/sim/scripts/test-scim-e2e.ts index d81e5955608..463a4fc1315 100644 --- a/apps/sim/scripts/test-scim-e2e.ts +++ b/apps/sim/scripts/test-scim-e2e.ts @@ -543,6 +543,72 @@ async function run() { } ) + await check( + 'An address change ends sessions and OAuth grants issued under the old one', + async () => { + const carolEmail = `carol@${primary.domain}` + const created = await scim(token, '/Users', { + method: 'POST', + expected: 201, + body: { + schemas: [USER_SCHEMA], + userName: carolEmail, + active: true, + name: { givenName: 'Carol', familyName: 'Example' }, + emails: [{ value: carolEmail, type: 'work', primary: true }], + }, + }) + const carolId = string(created.body.id) + const userId = string( + (await sql`select user_id from scim_user where id = ${carolId}`)[0]?.user_id + ) + const sessionId = generateId() + const clientId = generateId() + await sql`insert into session (id, token, user_id, expires_at, created_at, updated_at) + values (${sessionId}, ${generateShortId()}, ${userId}, now() + interval '1 day', now(), now())` + await sql`insert into oauth_client (id, client_id, name, public, token_endpoint_auth_method, grant_types, redirect_uris) + values (${clientId}, ${clientId}, 'SCIM E2E OAuth client', true, 'none', + ${['authorization_code', 'refresh_token']}, ${['http://127.0.0.1/callback']})` + try { + await sql`insert into oauth_consent (id, client_id, user_id, scopes, created_at, updated_at) + values (${generateId()}, ${clientId}, ${userId}, ${['api:read']}, now(), now())` + await sql`insert into oauth_token_family (id, client_id, user_id, session_id, created_at, expires_at) + values (${generateId()}, ${clientId}, ${userId}, ${sessionId}, now(), now() + interval '1 day')` + await sql`insert into oauth_access_token (id, token, client_id, user_id, scopes, created_at, expires_at) + values (${generateId()}, ${generateShortId()}, ${clientId}, ${userId}, ${['api:read']}, + now(), now() + interval '1 hour')` + + await patch(token, `/Users/${carolId}`, [ + { op: 'replace', path: 'name.givenName', value: 'Caroline' }, + ]) + assert.equal( + (await sql`select id from oauth_access_token where user_id = ${userId}`).length, + 1 + ) + + await patch(token, `/Users/${carolId}`, [ + { + op: 'replace', + path: 'emails[type eq "work"].value', + value: `carol-renamed@${primary.domain}`, + }, + ]) + + assert.equal((await sql`select id from session where user_id = ${userId}`).length, 0) + assert.equal( + (await sql`select id from oauth_token_family where user_id = ${userId}`).length, + 0 + ) + assert.equal( + (await sql`select id from oauth_access_token where user_id = ${userId}`).length, + 0 + ) + } finally { + await sql`delete from oauth_client where client_id = ${clientId}` + } + } + ) + await check( 'Group CRUD supports Entra member PATCH, duplicates, foreign IDs, and membership-only PUT versions', async () => { From b36b17b8fd8f31e33d7f1583b56c7ab5004cc38b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 17 Oct 2026 12:05:46 -0700 Subject: [PATCH 2/2] test(scim): remove the address-change fixture user after its check --- apps/sim/scripts/test-scim-e2e.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/sim/scripts/test-scim-e2e.ts b/apps/sim/scripts/test-scim-e2e.ts index 463a4fc1315..bdbb789d422 100644 --- a/apps/sim/scripts/test-scim-e2e.ts +++ b/apps/sim/scripts/test-scim-e2e.ts @@ -605,6 +605,7 @@ async function run() { ) } finally { await sql`delete from oauth_client where client_id = ${clientId}` + await sql`delete from "user" where id = ${userId}` } } )