diff --git a/packages/web-runtime/src/pages/accessDenied.vue b/packages/web-runtime/src/pages/accessDenied.vue index e70dea8de72..bc4222abf58 100644 --- a/packages/web-runtime/src/pages/accessDenied.vue +++ b/packages/web-runtime/src/pages/accessDenied.vue @@ -27,6 +27,7 @@ appearance="filled" variation="primary" v-bind="logoutButtonsAttrs" + @click="lowAssuranceLevelError && handleLogout()" > {{ navigateToLoginText }} @@ -41,7 +42,9 @@ import { queryItemAsString, useConfigStore, useRouteQuery, - useThemeStore + useRouter, + useThemeStore, + useAuthService } from '@ownclouders/web-pkg' export default defineComponent({ @@ -50,7 +53,13 @@ export default defineComponent({ const themeStore = useThemeStore() const { currentTheme } = storeToRefs(themeStore) const configStore = useConfigStore() + const authService = useAuthService() + const router = useRouter() const redirectUrlQuery = useRouteQuery('redirectUrl') + const reasonQuery = useRouteQuery('reason') + const lowAssuranceLevelError = computed( + () => queryItemAsString(unref(reasonQuery)) === 'lowAssuranceLevel' + ) const { $gettext } = useGettext() @@ -59,9 +68,17 @@ export default defineComponent({ const logoImg = computed(() => currentTheme.value.logo.login) const cardTitle = computed(() => { + if (unref(lowAssuranceLevelError)) { + return $gettext('Cannot log in') + } return $gettext('Not logged in') }) const cardHint = computed(() => { + if (unref(lowAssuranceLevelError)) { + return $gettext( + 'Please login to your CERN account using the CERN credentials instead of a guest account.' + ) + } return $gettext( 'This could be because of a routine safety log out, or because your account is either inactive or not yet authorized for use. Please try logging in after a while or seek help from your Administrator.' ) @@ -69,7 +86,15 @@ export default defineComponent({ const navigateToLoginText = computed(() => { return $gettext('Log in again') }) + const handleLogout = async () => { + await authService.logoutUser() + await router.push({ name: 'login' }) + } + const logoutButtonsAttrs = computed(() => { + if (unref(lowAssuranceLevelError)) { + return { type: 'button' } + } const redirectUrl = queryItemAsString(unref(redirectUrlQuery)) if (configStore.options.loginUrl) { const configLoginURL = new URL(encodeURI(configStore.options.loginUrl)) @@ -99,7 +124,9 @@ export default defineComponent({ footerSlogan, navigateToLoginText, accessDeniedHelpUrl, - logoutButtonsAttrs + logoutButtonsAttrs, + handleLogout, + lowAssuranceLevelError } } }) diff --git a/packages/web-runtime/src/router/setupAuthGuard.ts b/packages/web-runtime/src/router/setupAuthGuard.ts index f876542cebb..297c8e8ae52 100644 --- a/packages/web-runtime/src/router/setupAuthGuard.ts +++ b/packages/web-runtime/src/router/setupAuthGuard.ts @@ -47,6 +47,9 @@ export const setupAuthGuard = (router: Router) => { if (isUserContextRequired(router, to)) { if (!authStore.userContextReady) { + if (authService.lowAssuranceError) { + return { name: 'accessDenied', query: { reason: 'lowAssuranceLevel' } } + } if (unref(isDelegatingAuthentication)) { return { path: '/web-oidc-callback' } } diff --git a/packages/web-runtime/src/services/auth/authService.ts b/packages/web-runtime/src/services/auth/authService.ts index 7d9ea9342a8..1dc8308c34d 100644 --- a/packages/web-runtime/src/services/auth/authService.ts +++ b/packages/web-runtime/src/services/auth/authService.ts @@ -1,3 +1,4 @@ +import { ErrorResponse } from 'oidc-client-ts' import { UserManager } from './userManager' import { PublicLinkManager } from './publicLinkManager' import { @@ -44,6 +45,7 @@ export class AuthService implements AuthServiceInterface { private accessTokenExpiryThreshold = 10 public hasAuthErrorOccurred: boolean + public lowAssuranceError: boolean public initialize( configStore: ConfigStore, @@ -60,6 +62,7 @@ export class AuthService implements AuthServiceInterface { this.clientService = clientService this.router = router this.hasAuthErrorOccurred = false + this.lowAssuranceError = false this.ability = ability this.language = language this.userStore = userStore @@ -165,6 +168,10 @@ export class AuthService implements AuthServiceInterface { try { await this.userManager.updateContext(user.access_token, fetchUserData) } catch (e) { + if (this.isLowAssuranceLevelError(e)) { + this.lowAssuranceError = true + return + } console.error(e) await this.handleAuthError(unref(this.router.currentRoute)) } @@ -227,6 +234,10 @@ export class AuthService implements AuthServiceInterface { this.tokenTimerInitialized = true } } catch (e) { + if (this.isLowAssuranceLevelError(e)) { + this.lowAssuranceError = true + return + } console.error(e) await this.handleAuthError(unref(this.router.currentRoute)) } @@ -269,6 +280,10 @@ export class AuthService implements AuthServiceInterface { ...(redirectRoute.query && { query: redirectRoute.query }) }) } catch (e) { + if (this.isLowAssuranceLevelError(e)) { + this.lowAssuranceError = true + return this.router.push({ name: 'accessDenied', query: { reason: 'lowAssuranceLevel' } }) + } console.warn('error during authentication:', e) return this.handleAuthError(unref(this.router.currentRoute)) } @@ -329,6 +344,10 @@ export class AuthService implements AuthServiceInterface { this.hasAuthErrorOccurred = true } + private isLowAssuranceLevelError(e: unknown): boolean { + return e instanceof ErrorResponse && e.error === 'low_assurance_level' + } + public async resolvePublicLink( token: string, passwordRequired: boolean, diff --git a/packages/web-runtime/src/services/auth/userManager.ts b/packages/web-runtime/src/services/auth/userManager.ts index 6430d3e2640..5cbdcaae3f6 100644 --- a/packages/web-runtime/src/services/auth/userManager.ts +++ b/packages/web-runtime/src/services/auth/userManager.ts @@ -187,7 +187,15 @@ export class UserManager extends OidcUserManager { private async fetchUserInfo() { const graphClient = this.clientService.graphAuthenticated - const [graphUser, roles] = await Promise.all([graphClient.users.getMe(), this.fetchRoles()]) + let graphUser: Awaited>, roles: SettingsBundle[] + try { + ;[graphUser, roles] = await Promise.all([graphClient.users.getMe(), this.fetchRoles()]) + } catch (e) { + if (e?.response?.status === 409) { + throw new ErrorResponse({ error: 'low_assurance_level' } as any) + } + throw e + } const role = await this.fetchRole({ graphUser, roles }) this.userStore.setUser({ @@ -307,6 +315,10 @@ export class UserManager extends OidcUserManager { user.access_token = revaToken user.expires_at = claims.exp } catch (e) { + // We do not want to fail/raise exception here, even on 409. + // If we get a 409 we still want the user to be persisted, so that + // we can terminate the session in the SSO as well (on logout). + // The 409 will be catched later. console.error('Failed to get reva token, continue with sso one', e) } // end