diff --git a/changelog/unreleased/bugfix-clear-state-when-unauthorized b/changelog/unreleased/bugfix-clear-state-when-unauthorized new file mode 100644 index 0000000..5e9da23 --- /dev/null +++ b/changelog/unreleased/bugfix-clear-state-when-unauthorized @@ -0,0 +1,8 @@ +Bugfix: Clear state when unauthorized + +The filepicker now reacts to `401` responses by resetting the internal authentication state to `unauthorized`, +forcing the user to log in again. +This situation can happen when an access token that's not expired, yet, was invalidated server side (e.g. through +a backchannel logout or session inactivity) and would previously lead to a broken application state. + +https://github.com/owncloud/file-picker/pull/211 diff --git a/changelog/unreleased/bugfix-token-renewal-hook b/changelog/unreleased/bugfix-token-renewal-hook new file mode 100644 index 0000000..83eb073 --- /dev/null +++ b/changelog/unreleased/bugfix-token-renewal-hook @@ -0,0 +1,7 @@ +Bugfix: Reduce requests on token renewal + +We've fixed a bug that caused always re-fetching the logged in user and the server capabilities on token renewal +under certain circumstances. Now the logged in user and the server capabilities are only fetched once after +successful authentication. + +https://github.com/owncloud/file-picker/pull/211 diff --git a/src/App.vue b/src/App.vue index 5b8f273..9da9469 100644 --- a/src/App.vue +++ b/src/App.vue @@ -17,10 +17,11 @@ :is-select-btn-displayed="isSelectBtnDisplayed" :cancel-btn-label="cancelBtnLabel" :is-initial-focus-enabled="isInitialFocusEnabled" - @update="selectResources" - @select="emitSelectBtnClick" + @auth-error="handleAuthError" @cancel="cancel" - @folderLoaded="onFolderLoaded" + @folder-loaded="onFolderLoaded" + @select="emitSelectBtnClick" + @update="selectResources" /> @@ -170,11 +171,25 @@ export default { authInstance.mgr.signinPopupCallback() } - const get_axios_instance = (token) => { + const handleAuthError = async () => { + if (authInstance) { + await authInstance.mgr.clearStaleState() + state.value = 'unauthorized' + } + } + + const createAxiosInstance = (token) => { const instance = axios.create({ headers: { Authorization: token.startsWith('Bearer') ? token : `Bearer ${token}` } }) - + instance.interceptors.response.use( + (response) => response, + (error) => { + if (error?.response?.status === 401) { + return handleAuthError() + } + } + ) return instance } @@ -199,7 +214,7 @@ export default { const initApp = async () => { try { const token = await authInstance.getToken() - axiosInstance = get_axios_instance(token) + axiosInstance = createAxiosInstance(token) const _client = webClient(config.value.server, axiosInstance) // const { data: userData } = await _client.graph.users.getMe() @@ -215,8 +230,11 @@ export default { state.value = 'authorized' emit('token', token) + + return true } catch (error) { console.error(error) + return false } } @@ -231,24 +249,6 @@ export default { emit('token', token) } - const checkUserAuthentication = async () => { - if (await authInstance.isAuthenticated()) { - // If the user is authenticated, we add event listener when he's updated to automatically update the token - authInstance.mgr.events.addUserLoaded(() => { - updateBearerToken() - }) - - return initApp() - } - - state.value = 'unauthorized' - - // If the user is not authenticated, we add event listener when he logs in to automatically init the application afterwards - authInstance.mgr.events.addUserLoaded(() => { - initApp() - }) - } - const initAuthentication = async () => { config.value = await loadConfig(props.configObject, props.configLocation) @@ -268,7 +268,25 @@ export default { return } - checkUserAuthentication() + authInstance.mgr.events.addUserLoaded(() => { + if (state.value === 'authorized') { + updateBearerToken() + } else { + initApp() + } + }) + + authInstance.mgr.events.addSilentRenewError(() => { + handleAuthError() + }) + + if (await authInstance.isAuthenticated()) { + if (await initApp()) { + return + } + await authInstance.mgr.clearStaleState() + } + state.value = 'unauthorized' } const authenticate = () => { @@ -321,7 +339,8 @@ export default { authenticate, emitSelectBtnClick, onFolderLoaded, - selectResources + selectResources, + handleAuthError } } } diff --git a/src/components/FilePicker.vue b/src/components/FilePicker.vue index b668c11..46cc495 100644 --- a/src/components/FilePicker.vue +++ b/src/components/FilePicker.vue @@ -101,7 +101,7 @@ export default defineComponent({ } }, - emits: ['select', 'cancel', 'update', 'folderLoaded'], + emits: ['authError', 'cancel', 'folderLoaded', 'select', 'update'], setup(props, { emit }) { let currentSpace = null @@ -181,6 +181,9 @@ export default defineComponent({ isInitial = false } catch (error) { console.error(error) + if (error?.response?.status === 401) { + emit('authError') + } state.value = 'failed' } diff --git a/src/services/auth.ts b/src/services/auth.ts index af0609c..f219c3e 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -89,12 +89,12 @@ export function initVueAuthenticate(config) { }) mgr.events.addSilentRenewError(() => { - mgr.clearStaleState() + return mgr.clearStaleState() }) return { - authenticate() { - mgr.clearStaleState() + async authenticate() { + await mgr.clearStaleState() return mgr.signinPopup() },