Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/web-pkg/src/composables/piniaStores/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { PublicLinkType } from '@opencloud-eu/web-client'

export const useAuthStore = defineStore('auth', () => {
const accessToken = ref<string>()
const sessionId = ref<string>()
const idpContextReady = ref(false)
const userContextReady = ref(false)
const publicLinkToken = ref<string>()
Expand All @@ -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
}
Expand All @@ -34,6 +38,7 @@ export const useAuthStore = defineStore('auth', () => {

const clearUserContext = () => {
setAccessToken(null)
setSessionId(null)
setIdpContextReady(null)
setUserContextReady(null)
}
Expand All @@ -49,6 +54,7 @@ export const useAuthStore = defineStore('auth', () => {

return {
accessToken,
sessionId,
idpContextReady,
userContextReady,
publicLinkToken,
Expand All @@ -57,6 +63,7 @@ export const useAuthStore = defineStore('auth', () => {
publicLinkContextReady,

setAccessToken,
setSessionId,
setIdpContextReady,
setUserContextReady,
setPublicLinkContext,
Expand Down
5 changes: 4 additions & 1 deletion packages/web-runtime/src/container/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,7 @@ export const registerSSEEventListeners = ({
previewService,
configStore,
userStore,
authStore,
router
}: {
language: Language
Expand All @@ -859,6 +860,7 @@ export const registerSSEEventListeners = ({
previewService: PreviewService
configStore: ConfigStore
userStore: UserStore
authStore: AuthStore
router: Router
}): void => {
const resourceQueue = new PQueue({
Expand All @@ -883,7 +885,8 @@ export const registerSSEEventListeners = ({
previewService,
language,
router,
resourceQueue
resourceQueue,
authStore
} satisfies Partial<SseEventWrapperOptions>

clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.ITEM_RENAMED, (msg) =>
Expand Down
11 changes: 9 additions & 2 deletions packages/web-runtime/src/container/sse/common.ts
Original file line number Diff line number Diff line change
@@ -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' })
}
}
5 changes: 4 additions & 1 deletion packages/web-runtime/src/container/sse/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from 'zod'
import {
AuthStore,
ClientService,
ConfigStore,
MessageStore,
Expand All @@ -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<typeof eventSchema>
Expand All @@ -31,6 +33,7 @@ export interface SSEEventOptions {
messageStore: MessageStore
sharesStore: SharesStore
configStore: ConfigStore
authStore: AuthStore
clientService: ClientService
previewService: PreviewService
router: Router
Expand Down
1 change: 1 addition & 0 deletions packages/web-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ export const bootstrapApp = async (configurationPath: string, appsReadyCallback:
userStore,
previewService,
configStore,
authStore,
router
})
}
Expand Down
2 changes: 1 addition & 1 deletion packages/web-runtime/src/pages/oidcCallback.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
15 changes: 9 additions & 6 deletions packages/web-runtime/src/services/auth/authService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -245,15 +248,15 @@ 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 &&
this.configStore.options.embed.delegateAuthentication &&
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')
Expand Down Expand Up @@ -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)
}
}

Expand Down
8 changes: 7 additions & 1 deletion packages/web-runtime/src/services/auth/userManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ export class UserManager extends OidcUserManager {
return user?.access_token
}

async getSessionId(): Promise<string | null> {
const user = await this.getUser()
return user?.profile?.sid
}

async removeUser(unloadReason: UnloadReason = 'logout') {
this._unloadReason = unloadReason
await super.removeUser()
Expand All @@ -155,14 +160,15 @@ 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) {
return this.updateAccessTokenPromise
}

this.authStore.setAccessToken(accessToken)
this.authStore.setSessionId(sessionId)

this.updateAccessTokenPromise = (async () => {
if (!fetchUserData) {
Expand Down
3 changes: 3 additions & 0 deletions packages/web-runtime/tests/unit/container/sse/files.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
ClientService,
PreviewService,
useAuthStore,
useConfigStore,
useMessages,
useResourcesStore,
Expand Down Expand Up @@ -442,6 +443,7 @@ const getMocks = ({
const userStore = useUserStore()
const sharesStore = useSharesStore()
const configStore = useConfigStore()
const authStore = useAuthStore()
const clientService = mockDeep<ClientService>({ initiatorId: 'local1' })
const previewService = mockDeep<PreviewService>()
const router = mockDeep<Router>()
Expand All @@ -458,6 +460,7 @@ const getMocks = ({
userStore,
sharesStore,
configStore,
authStore,
clientService,
previewService,
resourceQueue,
Expand Down
3 changes: 3 additions & 0 deletions packages/web-runtime/tests/unit/container/sse/helpers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createTestingPinia } from '@opencloud-eu/web-test-helpers'
import {
ClientService,
PreviewService,
useAuthStore,
useConfigStore,
useMessages,
useResourcesStore,
Expand Down Expand Up @@ -63,6 +64,7 @@ const getMocks = ({
const userStore = useUserStore()
const configStore = useConfigStore()
const sharesStore = useSharesStore()
const authStore = useAuthStore()
const clientService = mockDeep<ClientService>()
const previewService = mockDeep<PreviewService>()
const router = mockDeep<Router>()
Expand All @@ -77,6 +79,7 @@ const getMocks = ({
userStore,
sharesStore,
configStore,
authStore,
clientService,
previewService,
resourceQueue,
Expand Down
3 changes: 3 additions & 0 deletions packages/web-runtime/tests/unit/container/sse/shares.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
ClientService,
eventBus,
PreviewService,
useAuthStore,
useConfigStore,
useMessages,
useResourcesStore,
Expand Down Expand Up @@ -700,6 +701,7 @@ const getMocks = ({
const userStore = useUserStore()
const configStore = useConfigStore()
userStore.user = mockDeep<User>({ id: '1' })
const authStore = useAuthStore()
const sharesStore = useSharesStore()
const clientService = mockDeep<ClientService>({ initiatorId: 'local1' })
const previewService = mockDeep<PreviewService>()
Expand Down Expand Up @@ -737,6 +739,7 @@ const getMocks = ({
userStore,
sharesStore,
configStore,
authStore,
clientService,
previewService,
resourceQueue,
Expand Down
4 changes: 2 additions & 2 deletions packages/web-runtime/tests/unit/pages/oidcCallback.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>((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 () => {
Expand Down
24 changes: 16 additions & 8 deletions packages/web-runtime/tests/unit/services/auth/authService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ describe('AuthService', () => {

Object.defineProperty(authService, 'userManager', {
value: mock<UserManager>({
getAccessToken: vi.fn().mockResolvedValue('access-token'),
getUser: vi
.fn()
.mockResolvedValue({ access_token: 'access-token', profile: { sid: 'session-id' } }),
updateContext: mockUpdateContext
})
})
Expand All @@ -80,15 +82,15 @@ describe('AuthService', () => {

await authService.initializeContext(mock<RouteLocation>({}))

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 () => {
const authService = new AuthService()

Object.defineProperty(authService, 'userManager', {
value: mock<UserManager>({
getAccessToken: vi.fn().mockResolvedValue(null),
getUser: vi.fn().mockResolvedValue({ access_token: null, profile: { sid: null } }),
updateContext: mockUpdateContext
})
})
Expand All @@ -105,7 +107,9 @@ describe('AuthService', () => {

Object.defineProperty(authService, 'userManager', {
value: mock<UserManager>({
getAccessToken: vi.fn().mockResolvedValue('access-token'),
getUser: vi
.fn()
.mockResolvedValue({ access_token: 'access-token', profile: { sid: 'session-id' } }),
updateContext: mockUpdateContext
})
})
Expand All @@ -114,15 +118,17 @@ describe('AuthService', () => {

await authService.initializeContext(mock<RouteLocation>({}))

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 () => {
const authService = new AuthService()

Object.defineProperty(authService, 'userManager', {
value: mock<UserManager>({
getAccessToken: vi.fn().mockResolvedValue('access-token'),
getUser: vi
.fn()
.mockResolvedValue({ access_token: 'access-token', profile: { sid: 'session-id' } }),
updateContext: mockUpdateContext
})
})
Expand All @@ -141,7 +147,9 @@ describe('AuthService', () => {

Object.defineProperty(authService, 'userManager', {
value: mock<UserManager>({
getAccessToken: vi.fn().mockResolvedValue('access-token'),
getUser: vi
.fn()
.mockResolvedValue({ access_token: 'access-token', profile: { sid: 'session-id' } }),
updateContext: mockUpdateContext
})
})
Expand All @@ -150,7 +158,7 @@ describe('AuthService', () => {

await authService.initializeContext(mock<RouteLocation>({}))

expect(mockUpdateContext).toHaveBeenCalledWith('access-token', true)
expect(mockUpdateContext).toHaveBeenCalledWith('access-token', 'session-id', true)
})
})
})