diff --git a/packages/web-pkg/src/composables/piniaStores/auth.ts b/packages/web-pkg/src/composables/piniaStores/auth.ts index ac4b24e743..091379390e 100644 --- a/packages/web-pkg/src/composables/piniaStores/auth.ts +++ b/packages/web-pkg/src/composables/piniaStores/auth.ts @@ -4,6 +4,7 @@ import { PublicLinkType } from '@opencloud-eu/web-client' export const useAuthStore = defineStore('auth', () => { const accessToken = ref() + const sessionId = ref() const idpContextReady = ref(false) const userContextReady = ref(false) const publicLinkToken = ref() @@ -14,6 +15,9 @@ export const useAuthStore = defineStore('auth', () => { const setAccessToken = (value: string) => { accessToken.value = value } + const setSessionId = (value: string) => { + sessionId.value = value + } const setIdpContextReady = (value: boolean) => { idpContextReady.value = value } @@ -34,6 +38,7 @@ export const useAuthStore = defineStore('auth', () => { const clearUserContext = () => { setAccessToken(null) + setSessionId(null) setIdpContextReady(null) setUserContextReady(null) } @@ -49,6 +54,7 @@ export const useAuthStore = defineStore('auth', () => { return { accessToken, + sessionId, idpContextReady, userContextReady, publicLinkToken, @@ -57,6 +63,7 @@ export const useAuthStore = defineStore('auth', () => { publicLinkContextReady, setAccessToken, + setSessionId, setIdpContextReady, setUserContextReady, setPublicLinkContext, diff --git a/packages/web-runtime/src/container/bootstrap.ts b/packages/web-runtime/src/container/bootstrap.ts index b4cd3ec6e2..651b5b0356 100644 --- a/packages/web-runtime/src/container/bootstrap.ts +++ b/packages/web-runtime/src/container/bootstrap.ts @@ -862,6 +862,7 @@ export const registerSSEEventListeners = ({ previewService, configStore, userStore, + authStore, router }: { language: Language @@ -873,6 +874,7 @@ export const registerSSEEventListeners = ({ previewService: PreviewService configStore: ConfigStore userStore: UserStore + authStore: AuthStore router: Router }): void => { const resourceQueue = new PQueue({ @@ -897,7 +899,8 @@ export const registerSSEEventListeners = ({ previewService, language, router, - resourceQueue + resourceQueue, + authStore } satisfies Partial clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.ITEM_RENAMED, (msg) => diff --git a/packages/web-runtime/src/container/sse/common.ts b/packages/web-runtime/src/container/sse/common.ts index f2a5275f9d..acef2960a0 100644 --- a/packages/web-runtime/src/container/sse/common.ts +++ b/packages/web-runtime/src/container/sse/common.ts @@ -1,5 +1,12 @@ import { SSEEventOptions } from './types' -export const onSSEBackchannelLogoutEvent = ({ router }: SSEEventOptions) => { - return router.push({ name: 'logout' }) +export const onSSEBackchannelLogoutEvent = ({ router, authStore, sseData }: SSEEventOptions) => { + if (!sseData.sessionid) { + // No session ID in event: log out all clients per OIDC spec + return router.push({ name: 'logout' }) + } + + if (authStore.sessionId === sseData.sessionid) { + return router.push({ name: 'logout' }) + } } diff --git a/packages/web-runtime/src/container/sse/types.ts b/packages/web-runtime/src/container/sse/types.ts index b5ba5fc705..eb7e65a833 100644 --- a/packages/web-runtime/src/container/sse/types.ts +++ b/packages/web-runtime/src/container/sse/types.ts @@ -1,5 +1,6 @@ import { z } from 'zod' import { + AuthStore, ClientService, ConfigStore, MessageStore, @@ -19,7 +20,8 @@ export const eventSchema = z.object({ spaceid: z.string().optional(), initiatorid: z.string().optional(), etag: z.string().optional(), - affecteduserids: z.array(z.string()).optional().nullable() + affecteduserids: z.array(z.string()).optional().nullable(), + sessionid: z.string().optional() }) export type EventSchemaType = z.infer @@ -31,6 +33,7 @@ export interface SSEEventOptions { messageStore: MessageStore sharesStore: SharesStore configStore: ConfigStore + authStore: AuthStore clientService: ClientService previewService: PreviewService router: Router diff --git a/packages/web-runtime/src/index.ts b/packages/web-runtime/src/index.ts index 5202fe44d1..d6f2e1d753 100644 --- a/packages/web-runtime/src/index.ts +++ b/packages/web-runtime/src/index.ts @@ -239,6 +239,7 @@ export const bootstrapApp = async (configurationPath: string, appsReadyCallback: userStore, previewService, configStore, + authStore, router }) } diff --git a/packages/web-runtime/src/services/auth/userManager.ts b/packages/web-runtime/src/services/auth/userManager.ts index 78a2fe6fc2..a8e6f57fb7 100644 --- a/packages/web-runtime/src/services/auth/userManager.ts +++ b/packages/web-runtime/src/services/auth/userManager.ts @@ -164,6 +164,11 @@ export class UserManager extends OidcUserManager { this.authStore.setAccessToken(accessToken) + // Sync session ID from OIDC user profile for backchannel logout matching + void this.getUser().then((user) => { + this.authStore.setSessionId(user?.profile?.sid ?? null) + }) + this.updateAccessTokenPromise = (async () => { if (!fetchUserData) { this.authStore.setIdpContextReady(true) diff --git a/packages/web-runtime/tests/unit/container/sse/files.spec.ts b/packages/web-runtime/tests/unit/container/sse/files.spec.ts index d247ea987e..225964a7ec 100644 --- a/packages/web-runtime/tests/unit/container/sse/files.spec.ts +++ b/packages/web-runtime/tests/unit/container/sse/files.spec.ts @@ -1,6 +1,7 @@ import { ClientService, PreviewService, + useAuthStore, useConfigStore, useMessages, useResourcesStore, @@ -442,6 +443,7 @@ const getMocks = ({ const userStore = useUserStore() const sharesStore = useSharesStore() const configStore = useConfigStore() + const authStore = useAuthStore() const clientService = mockDeep({ initiatorId: 'local1' }) const previewService = mockDeep() const router = mockDeep() @@ -458,6 +460,7 @@ const getMocks = ({ userStore, sharesStore, configStore, + authStore, clientService, previewService, resourceQueue, diff --git a/packages/web-runtime/tests/unit/container/sse/helpers.spec.ts b/packages/web-runtime/tests/unit/container/sse/helpers.spec.ts index dcc33dd194..c66e9ccd8a 100644 --- a/packages/web-runtime/tests/unit/container/sse/helpers.spec.ts +++ b/packages/web-runtime/tests/unit/container/sse/helpers.spec.ts @@ -3,6 +3,7 @@ import { createTestingPinia } from '@opencloud-eu/web-test-helpers' import { ClientService, PreviewService, + useAuthStore, useConfigStore, useMessages, useResourcesStore, @@ -63,6 +64,7 @@ const getMocks = ({ const userStore = useUserStore() const configStore = useConfigStore() const sharesStore = useSharesStore() + const authStore = useAuthStore() const clientService = mockDeep() const previewService = mockDeep() const router = mockDeep() @@ -77,6 +79,7 @@ const getMocks = ({ userStore, sharesStore, configStore, + authStore, clientService, previewService, resourceQueue, diff --git a/packages/web-runtime/tests/unit/container/sse/shares.spec.ts b/packages/web-runtime/tests/unit/container/sse/shares.spec.ts index 6c4aad6fce..50a27aaa4c 100644 --- a/packages/web-runtime/tests/unit/container/sse/shares.spec.ts +++ b/packages/web-runtime/tests/unit/container/sse/shares.spec.ts @@ -2,6 +2,7 @@ import { ClientService, eventBus, PreviewService, + useAuthStore, useConfigStore, useMessages, useResourcesStore, @@ -700,6 +701,7 @@ const getMocks = ({ const userStore = useUserStore() const configStore = useConfigStore() userStore.user = mockDeep({ id: '1' }) + const authStore = useAuthStore() const sharesStore = useSharesStore() const clientService = mockDeep({ initiatorId: 'local1' }) const previewService = mockDeep() @@ -737,6 +739,7 @@ const getMocks = ({ userStore, sharesStore, configStore, + authStore, clientService, previewService, resourceQueue,