I have a simple use case, where my application is using vue-router and vuex. Then store contains a user object which is null in the beginning. After the user is validated from the server it sends back an user object which contains a JWT auth token which is assigned to the user object in the store. Now lets assume that the user came back after 3 hours and tried to visit a route or perform any other action, considering that the auth token has expired by then, what would be the best way to check that(need to call axios post to check it) and redirect user to the login page. My app will have loads of components so I know I can write logic to check the token valid in the mounted hook of each component but that would mean repeating it all of the components. Also I don't want to use the beforeEach navigation guard because I cannot show any visual feedback to the user like checking... or loading....
I do something similar in one of my projects, it's actually deceptively difficult to handle these types of situations, but you can add a beforeEnter guard to your protected routes, then redirect if the authentication failed.
const guard = function(to, from, next) {
// check for valid auth token
axios.get('/api/checkAuthToken').then(response => {
// Token is valid, so continue
next();
}).catch(error => {
// There was an error so redirect
window.location.href = "/login";
})
};
Then on your route you can do:
{
path: '/dashboard',
component: Dashboard,
beforeEnter: (to, from, next) => {
guard(to, from, next);
}
},
You may notice I've used location.href rather than router.push. I do that because my login form is csrf protected, so I need a new csrf_token.
Your other issue is going to be if the user tries to interact with your page without changing the route (i.e. they click a button and get a 401 response). For this I find it easiest to check authentication on each axios request and redirect to login when I receive a 401 response.
In terms of adding a loading spinner during the guard check you can simply add a loading flag to your vuex store then import your store into your router. Honestly though I wouldn't bother, on a decent production server the check will be done so quickly that the user is unlikely to ever see it.
Try Vue.JS Mixins
You can define a Global Mixin and use it via Vue.use(myMixin) - then all Components will inherit this mixin. If you define a mounted or probably better activated hook on the mixin, it will be called on every component.
There you can use everything a component can do - this will point to your component. And if the component also defines a hook itself, the mixin hook of the same type will run before the components own hook.
Or try a single top-level login component
We used a little different solution - we have a single component which handles everything login-related, which exists outside of the router-view in the parent index.html. This component is always active and can hide the div router-view and overlay a loading message or a login-screen. For an intranet-application this component will also use polling to keep the session alive as long as the browser stays open.
You can load of your router-navigation to this component. - So a child-component which wants to trigger a router-navigation just sets a global reactive property navigateTo which is watched by the top level authentication component. This will trigger an authentication check, possibly a login-workflow and after that the top-level component will call $router.push() With this approach you have complete control over any navigation.
You can use interceptors to silently get the auth token when some request happens.
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const rToken = window.localStorage.getItem('rToken');
return axios.post('url/to/get/refresh/token', { rToken })
.then(({data}) => {
window.localStorage.setItem('token', data.token);
window.localStorage.setItem('rToken', data.refreshToken);
axios.defaults.headers.common['Authorization'] = 'Bearer ' + data.token;
originalRequest.headers['Authorization'] = 'Bearer ' + data.token;
return axios(originalRequest);
});
}
return Promise.reject(error);
});
Because you use vuex, you can add some state like isLoading or isChecking.
And in your router.beforeEach, you can check and set isLoading or isChecking follow your current checking state. Then you can show loading message follow this state.
In our route.js we check in beforeEnter hooks the user has token or
not.
route.js
{
path: '/dashboard',
name: dashboard,
meta: {
layout: 'home-layout'
},
components: {
default: Dashboard,
header: UserHeader
},
beforeEnter: ifAuthenticated,
}
route.js
const ifAuthenticated = (to, from, next) => {
if (localStorage.getItem(token)) {
next();
return;
}
router.push({
name: 'login',
params: {
returnTo: to.path,
query: to.query,
},
});
};
Related
We're switching from SPA to statically generated, and are running into a problem with middleware.
Basically, when Nuxt is statically rendered, middleware is run on the build server first, and then is run after each page navigation client side. The important point is that middleware is not run client side on first page load. This is discussed here
We work around this for some use cases by creating a plugin that uses the same code, since plugins are run on the first client load.
However, this pattern doesn't work well for this use case. The following is an example of the middleware that we want to use:
// middleware/authenticated.js
export default function ({ store, redirect }) {
// If the user is not authenticated
if (!store.state.authenticated) {
return redirect('/login')
}
}
// Inside a component
<template>
<h1>Secret page</h1>
</template>
<script>
export default {
middleware: 'authenticated'
}
</script>
This example is taken directly from the Nuxt docs.
When rendered statically, this middleware is not called on first page load, so a user might end up hitting their dashboard before they've logged in, which causes problems.
To add this to a plugin, the only way I can think to do this is by adding a list of authenticated_routes, which the plugin could compare to and see if the user needs to be authed.
The problem with that solution though is that we'd then need to maintain a relatively complex list of authed pages, and it's made worse by having dynamic routes, which you'd need to match a regex to.
So my question is: How can we run our authenticated middleware, which is page specific, without needing to maintain some list of routes that need to be authenticated? Is there a way to actually get the middleware associated to a route inside a plugin?
To me it is not clear how to solve it the right way. We are just using the static site generation approach. We are not able to run a nuxt middleware for the moment. If we detect further issues with the following approach we have to switch.
One challenge is to login the user on hot reload for protected and unprotected routes. As well as checking the login state when the user switches the tabs. Maybe session has expired while he was on another tab.
We are using two plugins for that. Please, let me know what you think.
authRouteBeforeEnter.js
The plugin handles the initial page load for protected routes and checks if the user can access a specific route while navigating around.
import { PROTECTED_ROUTES } from "~/constants/protectedRoutes"
export default ({ app, store }) => {
app.router.beforeEach(async (to, from, next) => {
if(to.name === 'logout'){
await store.dispatch('app/shutdown', {userLogout:true})
return next('/')
}
if(PROTECTED_ROUTES.includes(to.name)){
if(document.cookie.indexOf('PHPSESSID') === -1){
await store.dispatch('app/shutdown')
}
if(!store.getters['user/isLoggedIn']){
await store.dispatch('user/isAuthenticated', {msg: 'from before enter plugin'})
console.log('user is logged 2nd try: ' + store.getters['user/isLoggedIn'])
return next()
}
else {
/**
* All fine, let him enter
*/
return next()
}
}
return next()
})
}
authRouterReady.js
This plugin ment for auto login the user on unprotected routes on initial page load dnd check if there is another authRequest required to the backend.
import { PROTECTED_ROUTES } from "~/constants/protectedRoutes";
export default function ({ app, store }) {
app.router.onReady(async (route) => {
if(PROTECTED_ROUTES.includes(route.name)){
// Let authRouterBeforeEnter.js do the job
// to avoid two isAuthorized requests to the backend
await store.dispatch('app/createVisibilityChangedEvent')
}
else {
// If this route is public do the full init process
await store.dispatch('app/init')
}
})
}
Additionally i have added an app module to the store. It does a full init process with auth request and adding a visibility changed event or just adds the event.
export default {
async init({ dispatch }) {
dispatch('user/isAuthenticated', {}, {root:true})
dispatch('createVisibilityChangedEvent')
},
async shutdown({ dispatch }, {userLogout}) {
dispatch('user/logout', {userLogout}, {root:true})
},
async createVisibilityChangedEvent({ dispatch }) {
window.addEventListener('visibilitychange', async () => {
if (document.visibilityState === 'visible') {
console.log('visible changed');
await dispatch('user/isAuthenticated', {}, {root:true})
}
})
},
}
I have a component which loads a single user, so I use a vue-router guard to load data and in case of error redirect back to the users list component.
Is there a way to show a vue-bootstrap $toast? Usually I access $toast from this component, but obviously this does not exists yet in beforeRouteEnter.
I know I could manage in other ways (show the error in the page and use created(), or use vuex to keep the error and show it in the next page), but since I am using $toast everywhere I would like to keep consistency.
I have no ideas... if only I could access the root component I would have access to $toast but I can't see a way.
// userComponent
// ...
beforeRouteEnter(to, from, next) {
Promise.all([store.dispatch("users/fetchOne", { id: to.params.id } )])
.then(next)
.catch(() => {
// this.$root.$bvToast.toast("ERROR!!!", { variant: "danger" }); // can't do this :(
next("/users");
});
},
//...
You don't have access to this inside beforeRouteEnter. So you can do below :
next(vm => {
vm.$root.$bvToast.toast(...)
})
I am building a SPA using Vue-CLI with a client-side OAuth 2.0 javascript library called JSO. It uses HTML 5.0 localStorage to cache Access Tokens.
In my full app, I have everything functioning properly with the exception of the following issue:
When the user arrives at my app for the first time, he catches a quick glimpse of the app and then automatically is redirected to a third party authentication login screen. I don't want that "quick glimpse" to happen -- I need to have the user immediately redirected to the third party login page BEFORE he sees any part of my app.
So, I thought I'd set up Global Before Guards using Vue-Router like so:
From: Main.js
const routes = [
{
path: '/',
name: 'home',
component: Home,
meta: {
requiresAuth: true
}
},
...more routes...and they all require auth...
]
router.beforeEach((to, from, next) => {
const token = window.localStorage.getItem('my-token-example')
if (to.matched.some(record => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
if (token == null) {
client.getToken()
next(false)
}
} else {
next()
}
})
new Vue({
created: function() {
//check for response parameters if user has an auth token (uses JSO plugin)
//if token received, then it is stashed into localStorage
client.callback()
},
render: h => h(App),
router: router
}).$mount('#app')
Example App.vue:
<template>
<div id="app">
<Header />
<routerView />
</div>
</template>
Info on client.callback():
I catch the headers response when user is returning to app
Info on client.getToken():
I get the token payload
Notes: The JSO auth docs state:
"You may also ensure that a token is available early in your application, to force all user interaction and redirection to happen before your application is fully loaded. To do that make a call to getToken, and wait for the callback before you continue.
REMEMBER to ALWAYS call the callback() function to process the response from the OAuth server, before you use getToken(), if not you will end up in an redirect_loop"
Does anyone have any suggestions on how I can prevent the user from seeing my app before he is redirected to the auth login site? Update: I think I see the problem...is the global guards only affecting the section of the app within the <RouterView /> component? Hence, we see the header and banner of my app before redirection?
I solved this. I had to simply do a v-if on my app.vue file like: <div v-if="token !== null>
That hides the app template until token is received.
I have an frontend Web app interfaces with API built in Laravel with Passport.
My problem is when I refresh my page (in SPA written with Vuejs/Vuex) I should refresh my token, for refresh session with my Api.
I tried in main.js but he problem is that the request is async and the response arrived after routing.
main.js
if (localStorage.getItem('refresh_token')) {
store.dispatch('refresh_token').then(function(response){
console.log(response);
});
}
new Vue({
router,
store,
env,
render: h => h(App)
}).$mount('#app')
The function refresh token, make a call to my Api, and with response set the new token and the new refresh token.
But my problem is that I make this call in this way I can make the first async call in my "dashboard" with old token and then with the new.
So I've tried in different ways but I don't know if there is a best practice.
So my question is:
Where I should refresh token in Vuejs App with vuex store?
I suggest putting this in the mounted property of you toplevel Vue component. If you have other components that depend on your token being refreshed, you can couple this with a state variable in your store that signals the refresh is completed. For example, with a top level component App.vue:
...
mounted () {
store.dispatch('refresh_token')
}
...
Adding the state variable to your vueex store:
const store = new Vuex.Store({
state: {
sessionRefreshed: false
},
..
mutations: {
[REFRESH_TOKEN] (state) {
// existing mutations, and..
state.sessionRefreshed = true
},
},
..
actions: {
refreshToken ({ commit }) {
myAsyncFetchToken().then(() => commit(REFRESH_TOKEN))
},
}
This ensures your entire application is aware of the state of your refresh without forcing it to be synchronous. Then if you have components which require the token to be refreshed, you can show loading widgets, placeholders, etc., and use a watcher to do things when the state changes.
How about using router#beforeEach guard? I use it to figure out if authentication token is stored in a cookie before accessing any "restricted" component. If token is not set I redirect to /login.
I realize that my scenario is exactly what you are are asking for but I hope you can use it to augment your implementation.
I m trying to redirect from one URL to another URL using Vue router eg code
{
path: '/verifyemail/:id/:token',
//can i somelogic here when user entered
},
{
path: '/login',
component:login
},
what I m trying to do? when the user registered himself. server send email verification link to his email when a user clicks on verify button from his email then it should first call verifyemail url. where it has an ajax call with a parameter which i m getting from verifyemail url now after the success it should move to login url note:- i don't have any component in my verfiyemail route
is it possible to do this or is there anyother way to achieve this
The route configuration is only data, so there's not really any way to do this exactly as you'd like.
If you create a component to handle the /verifyemail/ route you can just use this.$router.push(redirectToMe) in it. See the Vue Router docs for more information.
Alternatively, this sounds more like something a backend server would handle on it's own, so maybe you don't even need Vue to worry about it.
finally, I arrive with one solution may be this help other
let start, I send the email with verify button which has link something like "localhost:8080/#/verfiyemail/("ACCESSTOKEN")" NOW I my vue part i does something like
in vue-route
path: '/verifyemail/:uid',
beforeEnter:(to, from, next) => {
let uid=to.params.uid;
next({ path: '/', query: { id: uid}})
}
},
{
path: '/',
name: 'Login',
component: Login,
},
and i my login.vue
created(){
this.verfiyemai();
},
methods:{
verfiyemai(){
var _this=this
var submit=0
if(this.$route.query.id!=undefined ){
if(this.$route.query.id.length<=50){
this.$router.push('/');
submit=1;
}
if(submit==0){
this.$http.get('/api/users/confirm?uid='+this.$route.query.id+'')
.then(function (response) {
console.log(response)
})
.catch(function (error) {
console.log(error)
});
}
}
},
}
from email, I redirect the user to verfiyemail with token id as a parameter from verifyemail route, after that I redirect to the login URL passing the parameter as query and in my login vue I had created a method which checks query length if it greater than 50 then it will post axios response to the server