Url parameters are not removed after first login
TravisRick opened this issue · 19 comments
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 keycloak/keycloak#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
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>
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 keycloak/keycloak#14742 (comment), 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?
@TravisRick Thank you for bringing this issue to my attention. I really never bothered before on having the URL cluttered with state
and 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?
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');
})();
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.
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.
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.
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. dsb-norge/vue-keycloak-js#94 (comment)
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 dsb-norge/vue-keycloak-js#94 (comment), but then i need to check if i can do it with your package.
Thanks for the help!
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. dsb-norge/vue-keycloak-js#94 (comment)
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 dsb-norge/vue-keycloak-js#94 (comment), but then i need to check if i can do it with your package.
This is essentialy the same option that I sugested here.
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.
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.
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 }
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?
Looking with a bit more attention to your code, it's missing the async function evocation, i.e. the ()
after the async function definition.
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.
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')
})()
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.
Hi again @benny-noumena. The issue you reported should be fixed in version 2.7.1.