diff --git a/dev/docker/ocis.idp.config.yaml b/dev/docker/ocis.idp.config.yaml index ae4faf852f9..506383a7cf5 100644 --- a/dev/docker/ocis.idp.config.yaml +++ b/dev/docker/ocis.idp.config.yaml @@ -8,15 +8,19 @@ clients: - https://host.docker.internal:9200/ - https://host.docker.internal:9200/oidc-callback.html - https://host.docker.internal:9200/oidc-silent-redirect.html + - https://host.docker.internal:9200/web-oidc-popup-callback - https://host.docker.internal:9201/ - https://host.docker.internal:9201/oidc-callback.html - https://host.docker.internal:9201/oidc-silent-redirect.html + - https://host.docker.internal:9201/web-oidc-popup-callback - https://ocis.owncloud.test:10200/ - https://ocis.owncloud.test:10200/oidc-callback.html - https://ocis.owncloud.test:10200/oidc-silent-redirect.html + - https://ocis.owncloud.test:10200/web-oidc-popup-callback - https://ocis.owncloud.test:10201/ - https://ocis.owncloud.test:10201/oidc-callback.html - https://ocis.owncloud.test:10201/oidc-silent-redirect.html + - https://ocis.owncloud.test:10201/web-oidc-popup-callback origins: - https://host.docker.internal:9200 - https://host.docker.internal:9201 diff --git a/packages/web-app-webfinger/src/views/Resolve.vue b/packages/web-app-webfinger/src/views/Resolve.vue index 18c1ef6cf28..226fac6d328 100644 --- a/packages/web-app-webfinger/src/views/Resolve.vue +++ b/packages/web-app-webfinger/src/views/Resolve.vue @@ -62,7 +62,7 @@ export default defineComponent({ } catch (e) { console.error(e) if (e.response?.status === 401) { - return authService.handleAuthError(unref(route), { forceLogout: true }) + return authService.handleAuthError(unref(route)) } hasError.value = true } diff --git a/packages/web-pkg/src/composables/authContext/useAuthService.ts b/packages/web-pkg/src/composables/authContext/useAuthService.ts index 224533ce49d..e24e5357567 100644 --- a/packages/web-pkg/src/composables/authContext/useAuthService.ts +++ b/packages/web-pkg/src/composables/authContext/useAuthService.ts @@ -2,10 +2,13 @@ import { useService } from '../service' import { NavigationFailure } from 'vue-router' export interface AuthServiceInterface { - handleAuthError(route: any, options?: { forceLogout?: boolean }): any + handleAuthError(route: any): any signinSilent(): Promise logoutUser(): Promise getRefreshToken(): Promise + showSessionExpiredModal(): void + loginUserPopup(): Promise + reloadUserFromStorage(): Promise } export const useAuthService = (): AuthServiceInterface => { diff --git a/packages/web-pkg/src/composables/piniaStores/auth.ts b/packages/web-pkg/src/composables/piniaStores/auth.ts index 7ab0efe0144..9bf683fc426 100644 --- a/packages/web-pkg/src/composables/piniaStores/auth.ts +++ b/packages/web-pkg/src/composables/piniaStores/auth.ts @@ -5,6 +5,7 @@ export const useAuthStore = defineStore('auth', () => { const accessToken = ref() const idpContextReady = ref(false) const userContextReady = ref(false) + const sessionExpired = ref(false) const publicLinkToken = ref() const publicLinkPassword = ref() const publicLinkType = ref() @@ -19,6 +20,9 @@ export const useAuthStore = defineStore('auth', () => { const setUserContextReady = (value: boolean) => { userContextReady.value = value } + const setSessionExpired = (value: boolean) => { + sessionExpired.value = value + } const setPublicLinkContext = (context: { publicLinkToken: string publicLinkPassword: string @@ -50,6 +54,7 @@ export const useAuthStore = defineStore('auth', () => { accessToken, idpContextReady, userContextReady, + sessionExpired, publicLinkToken, publicLinkPassword, publicLinkType, @@ -58,6 +63,7 @@ export const useAuthStore = defineStore('auth', () => { setAccessToken, setIdpContextReady, setUserContextReady, + setSessionExpired, setPublicLinkContext, clearUserContext, clearPublicLinkContext diff --git a/packages/web-pkg/src/composables/webWorkers/tokenTimerWorker/useTokenTimerWorker.ts b/packages/web-pkg/src/composables/webWorkers/tokenTimerWorker/useTokenTimerWorker.ts index 9788ad20d61..c326e0f1f09 100644 --- a/packages/web-pkg/src/composables/webWorkers/tokenTimerWorker/useTokenTimerWorker.ts +++ b/packages/web-pkg/src/composables/webWorkers/tokenTimerWorker/useTokenTimerWorker.ts @@ -24,10 +24,10 @@ export const useTokenTimerWorker = ({ authService }: { authService: AuthServiceI console.error('token renewal error:', error) - // log out user if they don't have a refresh token + // show session expired modal if there's no refresh token to renew silently const refreshToken = await authService.getRefreshToken() if (!refreshToken) { - return authService.logoutUser() + return authService.showSessionExpiredModal() } }) } diff --git a/packages/web-runtime/src/App.vue b/packages/web-runtime/src/App.vue index d6be50d9934..2a89ea1d517 100644 --- a/packages/web-runtime/src/App.vue +++ b/packages/web-runtime/src/App.vue @@ -7,11 +7,13 @@ + + + diff --git a/packages/web-runtime/src/composables/layout/useLayout.ts b/packages/web-runtime/src/composables/layout/useLayout.ts index 55e0ded3547..1777281a13b 100644 --- a/packages/web-runtime/src/composables/layout/useLayout.ts +++ b/packages/web-runtime/src/composables/layout/useLayout.ts @@ -21,6 +21,7 @@ export const useLayout = (options?: LayoutOptions) => { 'logout', 'oidcCallback', 'oidcSilentRedirect', + 'oidcPopupCallback', 'resolvePublicLink', 'accessDenied' ] diff --git a/packages/web-runtime/src/helpers/loginWithPopupCoopFallback.ts b/packages/web-runtime/src/helpers/loginWithPopupCoopFallback.ts new file mode 100644 index 00000000000..30dcd96da11 --- /dev/null +++ b/packages/web-runtime/src/helpers/loginWithPopupCoopFallback.ts @@ -0,0 +1,60 @@ +import { authService } from '../services/auth' + +const POPUP_COMPLETE_CHANNEL = 'oc_oidc_popup_complete' +const FALLBACK_TIMEOUT_MS = 120_000 +// Grace period after the popup flow rejects, to let an already-dispatched COOP +// "complete" message (posted by the popup just before it closed) be delivered. +// If none arrives within this window, the popup was genuinely blocked or dismissed. +const POPUP_REJECTION_GRACE_MS = 2_000 + +/** + * Runs popup OIDC login and waits for the COOP BroadcastChannel fallback when the popup + * cannot use window.opener. + * + * A popup promise rejection does not fail the flow immediately: under COOP the opener + * handshake can reject even though the popup authenticated successfully and a "complete" + * message is already in flight. We wait a short grace period for that message before + * surfacing the failure, so a genuinely blocked popup still fails fast. + */ +export const loginWithPopupCoopFallback = (popupLogin: () => Promise): Promise => { + return new Promise((resolve, reject) => { + const bc = new BroadcastChannel(POPUP_COMPLETE_CHANNEL) + let settled = false + let graceTimeoutId: ReturnType + + const finish = (callback: () => void) => { + if (settled) { + return + } + settled = true + clearTimeout(timeoutId) + clearTimeout(graceTimeoutId) + bc.close() + callback() + } + + const timeoutId = setTimeout(() => { + finish(() => reject(new Error('popup_auth_timeout'))) + }, FALLBACK_TIMEOUT_MS) + + bc.addEventListener('message', async (e) => { + if (e.data?.type !== 'complete') { + return + } + try { + await authService.reloadUserFromStorage() + finish(resolve) + } catch (error) { + finish(() => reject(error)) + } + }) + + popupLogin() + .then(() => finish(resolve)) + .catch((error) => { + graceTimeoutId = setTimeout(() => { + finish(() => reject(error)) + }, POPUP_REJECTION_GRACE_MS) + }) + }) +} diff --git a/packages/web-runtime/src/helpers/silentRedirect.ts b/packages/web-runtime/src/helpers/silentRedirect.ts index a923cf97814..9e8627827b8 100644 --- a/packages/web-runtime/src/helpers/silentRedirect.ts +++ b/packages/web-runtime/src/helpers/silentRedirect.ts @@ -1 +1,2 @@ export const isSilentRedirectRoute = () => window.location.pathname === '/web-oidc-silent-redirect' +export const isPopupCallbackRoute = () => window.location.pathname === '/web-oidc-popup-callback' diff --git a/packages/web-runtime/src/pages/accessDenied.vue b/packages/web-runtime/src/pages/accessDenied.vue index e70dea8de72..9382a1a021b 100644 --- a/packages/web-runtime/src/pages/accessDenied.vue +++ b/packages/web-runtime/src/pages/accessDenied.vue @@ -1,7 +1,9 @@