JoseGoncalves / vue-keycloak

Keycloak plugin for Vue 3 with Composition API
Apache License 2.0
20 stars 6 forks source link

Url parameters are not removed after first login #7

Closed TravisRick closed 7 months ago

TravisRick commented 7 months ago

I currently having the problem that after a login parameters state and session_state are not removed from the url.

How to reproduce?

So i made sure there is no active session in Keyloak. I load my application and as expected I am redirected to the Keycloak login page. I fill in my credentials, a session is created in Keycloak and i am redirected back to my application. I now have the problem that parameters state and session_state are not being processed and removed from the url. The url looks like this: http://localhost/customer/dashboard?state=0b8d9bc1-966c-4e7a-8ba0-0ee677a74420&session_state=a548c01a-a50c-4653-8688-51e94a48ffad&code=d4069561-ac41-416b-bf14-290db8a56e36.a548c01a-a50c-4653-8632-51e94a48ffad.8bf9e1a8-c409-4103-8053-b1f7426a3fdd. It causes the following error:

Uncaught (in promise) Error: Failed to refresh the token, or the session has expired
    at updateToken (index.mjs:63:1)
    at Axios.request (Axios.js:45:1)
    at async Promise.all (/customer/index 0)

I have checked in Keycloak and a session is created. When i move to a different page after the app is created everything works fine. The getToken method in the interceptor returns the right value and an API call is made. This problem only occurs on the first load or when refreshing the page manually.

I came along this topic https://github.com/keycloak/keycloak/issues/14742 which is probably related, but so far i did not manage to find a solution. Can i somehow await the initialisation of keycloak before continuing?

Here is my current config. I removed all irrelevant stuff.

// main.js
import { createApp } from 'vue'
import { vueKeycloak } from '@josempgon/vue-keycloak'
import App from './App.vue'
import router from './router'

const app = createApp(App)

// Keycloak
app.use(vueKeycloak, {
  config: {
    url: 'http://keycloak:8080',
    realm: 'travis-dev',
    clientId: 'travis-cuda',
  },
  initOptions: {
    onLoad: 'login-required',
    enableLogging: true,
    responseMode: "query"
  }
})

app.use(router)

app.mount('#app')

Following the documentation i have added a request interceptor in my api client.

//apiClient.js
...
import { getToken } from '@josempgon/vue-keycloak'

apiClient.interceptors.request.use(
  async config => {
    const token = await getToken()
    console.log('token from interceptor', token)
    config.headers['Authorization'] = `Bearer ${token}`
    return config
  },
  error => {
    Promise.reject(error)
  },
)

Vue-keycloak: 2.6.0 Vue: 3.4.21 Vue-router: 4.3.0

JoseGoncalves commented 7 months ago

Hi @TravisRick. Never have set responseMode different from the default in initOptions, but I think that your issue should not be related with that. From the error that you report, it seems to me that you are calling getToken() before you have been authenticated. You should not make any API call before the authentication process is complete. You can avoid this by setting your src/App.vue with something like this:

<script setup>
import { useKeycloak } from '@josempgon/vue-keycloak';
const { isAuthenticated } = useKeycloak();
</script>

<template>
    <div v-if="isAuthenticated">
        <router-view />
    </div>
    <div v-else>
        <!-- Template for a progress indicator component -->
    </div>
</template>
TravisRick commented 7 months ago

Hi @JoseGoncalves, thanks for your reply! :)

I assumed the url parameters and error i got were related, but that was not the case. Your solution resolved the error, but the parameter still persist in the URL. I have checked for both the default and hash responseMode.

In the discussion thread at https://github.com/keycloak/keycloak/issues/14742#issuecomment-1663069438, it was concluded that there is a conflict between the router and Keycloak. How do you ensure they don't interfere with each other in your application?

JoseGoncalves commented 7 months ago

@TravisRick Thank you for bringing this issue to my attention. I really never bothered before on having the URL cluttered with stateand session_state, but I agree that it's not an optimal solution.

After checking the thread that you pointed out, I've found a solution that works for me:

router/index.js

import { createRouter, createWebHistory } from 'vue-router';

const routes = [ /* Your routes */ ];

const initRouter = () => {
    const history = createWebHistory(import.meta.env.BASE_URL);
    return createRouter({ history, routes });
};

export { initRouter };

main.js

import { createApp } from 'vue';
import { vueKeycloak } from '@josempgon/vue-keycloak';

import App from './App.vue';
import { initRouter } from './router';

const app = createApp(App);

await vueKeycloak.install(app, {
    config: {
        url: 'http://keycloak-server/auth',
        realm: 'myrealm',
        clientId: 'myapp',
    },
});

app.use(initRouter());

app.mount('#app');

Can you please check it?

JoseGoncalves commented 7 months ago

P.S. If you are building for a browser that does not support Top-level await, you should wrap the vue plugins initialization in an async IIFE:

(async () => {
    await vueKeycloak.install(app, {
        config: {
            url: 'http://keycloak-server/auth',
            realm: 'myrealm',
            clientId: 'myapp',
        },
    });

    app.use(initRouter());

    app.mount('#app');
})();
TravisRick commented 7 months ago

I tried your setup, but i get a white page after reloading without any error in the console.

When further investigating i came along the option silentCheckSsoRedirectUri. I quickly checked but it looks like it solves the problems for me. When mounting the app the parameters no longer remain in the url. I had to change my authentication from login-required to check-sso anyway, since i have some public pages in my app.

JoseGoncalves commented 7 months ago

I tried your setup, but i get a white page after reloading without any error in the console.

With the previous setup your app only renders when the authentication is complete, so, if you have a watch on isAuthenticated you need to be sure that is setup has a Eager Watcher, or else, your callback code would not be executed.

JoseGoncalves commented 7 months ago

Another option (without waiting for the authentication completion in app setup) would be to use a router navigation guard to strip out the keycloak parameters:

router/index.js

import { createRouter, createWebHistory } from 'vue-router';

const routes = [ /* Your routes */ ];

const history = createWebHistory(import.meta.env.BASE_URL);

const router = createRouter({ history, routes });

router.beforeEach((to, from) => {
    if (to.hash?.length > 0) {
        return { path: to.path, hash: '' };
    }
});

export default router;

This solution assumes you are leaving initOptions.responseMode in it's default value (fragment) and that you are not using hash parameters in your routes.

TravisRick commented 7 months ago

Could be an option. I came along a similar setup where state and session_state are removed from the url, but it feels a bit hacky. https://github.com/dsb-norge/vue-keycloak-js/issues/94#issuecomment-1120791906

I had to switch to authentication type check-sso since i have some public pages, but now i run again into problems with keycloak and vue-router. Possible solution would be https://github.com/dsb-norge/vue-keycloak-js/issues/94#issuecomment-1794403391, but then i need to check if i can do it with your package.

Thanks for the help!

JoseGoncalves commented 7 months ago

Could be an option. I came along a similar setup where state and session_state are removed from the url, but it feels a bit hacky. https://github.com/dsb-norge/vue-keycloak-js/issues/94#issuecomment-1120791906

I agree... it's hacky, because it depends on prop names that keycloak-js appends to the URL (that can change). I prefer my take that strips out all hash parameters (if you can do that, i.e., if you don't use hash parameters in your routes).

Possible solution would be https://github.com/dsb-norge/vue-keycloak-js/issues/94#issuecomment-1794403391, but then i need to check if i can do it with your package.

This is essentialy the same option that I sugested here.

TravisRick commented 7 months ago

Yes, correct. The onReady hook would be quite handy. I will check again if i can fix it with that top level await. If not, i probably need to switch package.

JoseGoncalves commented 7 months ago

I don't see the need for an onReady hook on my package, because you can wait for the plugin installation, which gives you the same functionality. If top-level await can not be used, you can use an async IIFE instead.

TravisRick commented 7 months ago

Top level await is not supported in my project, so i tried it with the async function as you suggested. For some reason it keeps returning the error Uncaught TypeError: app.use(...) is not a function on line app.use(pinia). You have any idea what causes this?

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { vueKeycloak } from '@josempgon/vue-keycloak'
import App from './App.vue'
import initializeRouter from '@/router'

const app = createApp(App)

//... some other app.use statements

// Pinia
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)

(async () => {
  await vueKeycloak.install(app, {
    config: {
      url: 'http://keycloak:8080',
      realm: 'travis-dev',
      clientId: 'travis-cuda',
    },
    initOptions: {
      checkLoginIframe: false,
      onLoad: 'login-required'
    }
  })

  app.use(initializeRouter())
  app.mount('#app')
})

export { app }
JoseGoncalves commented 7 months ago

Don't see anything on your sample code that can trigger that error... You are exporting app... maybe the app object is corrupted in another module?

JoseGoncalves commented 7 months ago

Looking with a bit more attention to your code, it's missing the async function evocation, i.e. the () after the async function definition.

TravisRick commented 7 months ago

Sorry for the confusion, but i removed the () while debugging. In both scenarios i get the error Uncaught TypeError: app.use(...) is not a function. I will give it a try with a fresh project, to make sure it is not related to my other code.

TravisRick commented 7 months ago

Looked at it again and made a stupid mistake. The other use statements should be within the function. All fine now! Thanks for all your help @JoseGoncalves!

(async () => {
  //... some other app.use statements

  // Pinia
  const pinia = createPinia()
  pinia.use(piniaPluginPersistedstate)
  app.use(pinia)

  await vueKeycloak.install(app, {
    config: {
      url: 'http://keycloak:8080',
      realm: 'travis-dev',
      clientId: 'travis-cuda',
    },
    initOptions: {
      checkLoginIframe: false,
      onLoad: 'login-required'
    }
  })

  app.use(initializeRouter())
  app.mount('#app')
})()
benny-noumena commented 5 months ago

There is a TS error with this as the type definitions do not require an install method to be present on vueKeycloak

image

image
JoseGoncalves commented 5 months ago

Hi @benny-noumena, thanks for reporting this. I did not notice this issue previously because I've tested only calling the install method in JavaScript. I will check how to fix this.

JoseGoncalves commented 5 months ago

Hi again @benny-noumena. The issue you reported should be fixed in version 2.7.1.