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 65dff17b95..895a62b2fa 100644 --- a/packages/web-runtime/src/container/bootstrap.ts +++ b/packages/web-runtime/src/container/bootstrap.ts @@ -848,6 +848,7 @@ export const registerSSEEventListeners = ({ previewService, configStore, userStore, + authStore, router }: { language: Language @@ -859,6 +860,7 @@ export const registerSSEEventListeners = ({ previewService: PreviewService configStore: ConfigStore userStore: UserStore + authStore: AuthStore router: Router }): void => { const resourceQueue = new PQueue({ @@ -883,7 +885,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..10d073f32a 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) { + // Log out all clients when no session id is provided according to 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/pages/oidcCallback.vue b/packages/web-runtime/src/pages/oidcCallback.vue index 497c9125b7..9eae7bd874 100644 --- a/packages/web-runtime/src/pages/oidcCallback.vue +++ b/packages/web-runtime/src/pages/oidcCallback.vue @@ -57,7 +57,7 @@ const handleRequestedTokenEvent = (event: MessageEvent): void => { } console.debug('[page:oidcCallback:handleRequestedTokenEvent] - received delegated access_token') - authService.signInCallback(event.data.data.access_token) + authService.signInCallback(event.data.data.access_token, event.data.data.session_id) } onMounted(() => { diff --git a/packages/web-runtime/src/services/auth/authService.ts b/packages/web-runtime/src/services/auth/authService.ts index 4f3e9e8623..0d89a14965 100644 --- a/packages/web-runtime/src/services/auth/authService.ts +++ b/packages/web-runtime/src/services/auth/authService.ts @@ -162,7 +162,7 @@ export class AuthService implements AuthServiceInterface { console.debug(`New User Loaded`) try { - await this.userManager.updateContext(user.access_token, fetchUserData) + await this.userManager.updateContext(user.access_token, user.profile.sid, fetchUserData) } catch (e) { console.error(e) await this.handleAuthError(unref(this.router.currentRoute)) @@ -209,12 +209,15 @@ export class AuthService implements AuthServiceInterface { // relevant for page reload: token is already in userStore // no userLoaded event and no signInCallback gets triggered - const accessToken = await this.userManager.getAccessToken() + const user = await this.userManager.getUser() + const accessToken = user?.access_token + const sessionId = user?.profile?.sid + if (accessToken) { console.debug('[authService:initializeContext] - updating context with saved access_token') try { - await this.userManager.updateContext(accessToken, fetchUserData) + await this.userManager.updateContext(accessToken, sessionId, fetchUserData) if (!this.tokenTimerInitialized) { const user = await this.userManager.getUser() @@ -245,7 +248,7 @@ export class AuthService implements AuthServiceInterface { /** * Sign in callback gets called from the IDP after initial login. */ - public async signInCallback(accessToken?: string) { + public async signInCallback(accessToken?: string, sessionId?: string) { try { if ( this.configStore.options.embed.enabled && @@ -253,7 +256,7 @@ export class AuthService implements AuthServiceInterface { accessToken ) { console.debug('[authService:signInCallback] - setting access_token and fetching user') - await this.userManager.updateContext(accessToken, true) + await this.userManager.updateContext(accessToken, sessionId, true) // Setup a listener to handle token refresh console.debug('[authService:signInCallback] - adding listener to update-token event') @@ -381,7 +384,7 @@ export class AuthService implements AuthServiceInterface { } console.debug('[authService:handleDelegatedTokenUpdate] - going to update the access_token') - return this.userManager.updateContext(event.data, false) + return this.userManager.updateContext(event.data.accesssToken, event.data.sessionId, false) } } diff --git a/packages/web-runtime/src/services/auth/userManager.ts b/packages/web-runtime/src/services/auth/userManager.ts index 78a2fe6fc2..d147252b81 100644 --- a/packages/web-runtime/src/services/auth/userManager.ts +++ b/packages/web-runtime/src/services/auth/userManager.ts @@ -132,6 +132,11 @@ export class UserManager extends OidcUserManager { return user?.access_token } + async getSessionId(): Promise { + const user = await this.getUser() + return user?.profile?.sid + } + async removeUser(unloadReason: UnloadReason = 'logout') { this._unloadReason = unloadReason await super.removeUser() @@ -155,7 +160,7 @@ export class UserManager extends OidcUserManager { } } - updateContext(accessToken: string, fetchUserData: boolean) { + updateContext(accessToken: string, sessionId: string, fetchUserData: boolean) { const userKnown = !!this.userStore.user const accessTokenChanged = this.authStore.accessToken !== accessToken if (!accessTokenChanged) { @@ -163,6 +168,7 @@ export class UserManager extends OidcUserManager { } this.authStore.setAccessToken(accessToken) + this.authStore.setSessionId(sessionId) this.updateAccessTokenPromise = (async () => { if (!fetchUserData) { 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, diff --git a/packages/web-runtime/tests/unit/pages/oidcCallback.spec.ts b/packages/web-runtime/tests/unit/pages/oidcCallback.spec.ts index 3a3062f3b7..0ea2abfdcd 100644 --- a/packages/web-runtime/tests/unit/pages/oidcCallback.spec.ts +++ b/packages/web-runtime/tests/unit/pages/oidcCallback.spec.ts @@ -83,14 +83,14 @@ describe('oidcCallback page', () => { window.postMessage( { name: 'opencloud-embed:update-token', - data: { access_token: 'access-token' } + data: { access_token: 'access-token', session_id: 'session-id' } }, '*' ) await new Promise((resolve) => setTimeout(() => resolve(), 10)) - expect(signInCallbackSpy).toHaveBeenCalledWith('access-token') + expect(signInCallbackSpy).toHaveBeenCalledWith('access-token', 'session-id') }) it('when token update event is received but name is incorrect does not call signInCallback', async () => { diff --git a/packages/web-runtime/tests/unit/services/auth/authService.spec.ts b/packages/web-runtime/tests/unit/services/auth/authService.spec.ts index 6d24ea5afe..aaf253c5d0 100644 --- a/packages/web-runtime/tests/unit/services/auth/authService.spec.ts +++ b/packages/web-runtime/tests/unit/services/auth/authService.spec.ts @@ -71,7 +71,9 @@ describe('AuthService', () => { Object.defineProperty(authService, 'userManager', { value: mock({ - getAccessToken: vi.fn().mockResolvedValue('access-token'), + getUser: vi + .fn() + .mockResolvedValue({ access_token: 'access-token', profile: { sid: 'session-id' } }), updateContext: mockUpdateContext }) }) @@ -80,7 +82,7 @@ describe('AuthService', () => { await authService.initializeContext(mock({})) - expect(mockUpdateContext).toHaveBeenCalledWith('access-token', true) + expect(mockUpdateContext).toHaveBeenCalledWith('access-token', 'session-id', true) }) it('when embed mode is disabled and access_token is not present, should not call updateContext', async () => { @@ -88,7 +90,7 @@ describe('AuthService', () => { Object.defineProperty(authService, 'userManager', { value: mock({ - getAccessToken: vi.fn().mockResolvedValue(null), + getUser: vi.fn().mockResolvedValue({ access_token: null, profile: { sid: null } }), updateContext: mockUpdateContext }) }) @@ -105,7 +107,9 @@ describe('AuthService', () => { Object.defineProperty(authService, 'userManager', { value: mock({ - getAccessToken: vi.fn().mockResolvedValue('access-token'), + getUser: vi + .fn() + .mockResolvedValue({ access_token: 'access-token', profile: { sid: 'session-id' } }), updateContext: mockUpdateContext }) }) @@ -114,7 +118,7 @@ describe('AuthService', () => { await authService.initializeContext(mock({})) - expect(mockUpdateContext).toHaveBeenCalledWith('access-token', true) + expect(mockUpdateContext).toHaveBeenCalledWith('access-token', 'session-id', true) }) it('when embed mode is enabled, access_token is present and auth is delegated, should not call updateContext', async () => { @@ -122,7 +126,9 @@ describe('AuthService', () => { Object.defineProperty(authService, 'userManager', { value: mock({ - getAccessToken: vi.fn().mockResolvedValue('access-token'), + getUser: vi + .fn() + .mockResolvedValue({ access_token: 'access-token', profile: { sid: 'session-id' } }), updateContext: mockUpdateContext }) }) @@ -141,7 +147,9 @@ describe('AuthService', () => { Object.defineProperty(authService, 'userManager', { value: mock({ - getAccessToken: vi.fn().mockResolvedValue('access-token'), + getUser: vi + .fn() + .mockResolvedValue({ access_token: 'access-token', profile: { sid: 'session-id' } }), updateContext: mockUpdateContext }) }) @@ -150,7 +158,7 @@ describe('AuthService', () => { await authService.initializeContext(mock({})) - expect(mockUpdateContext).toHaveBeenCalledWith('access-token', true) + expect(mockUpdateContext).toHaveBeenCalledWith('access-token', 'session-id', true) }) }) })