Vue-Router: Protecting private routes with authentication the right way - vue.js

I have a /profile route that should only be accessible by an authenticated user. From research, the recommended approach is to use vue-router's navigation guard:
Here is the route object:
{
path: '/profile',
name: 'MyProfile',
component: () => import('#/views/Profile.vue'),
meta: { requiresAuth: true }
},
And here is the router's navigation guard:
router.beforeEach((to, from, next) => {
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (isAuthenticated()) {
next()
}
else {
alert('Auth required!')
next('/login')
}
}
})
The isAuthenticated() function above sends a jwt token (stored in cookie) to the /jwt/validate endpoint which validates the token and returns a 200 OK response:
export function isAuthenticated() {
axios.post(`${baseUrl}/token/validate`, {}, { withCredentials: true })
.then(resp => resp.statusText == 'OK')
.catch(err => false)
}
With this approach, every time we visit the /profile route, we are making a POST request to the /token/validate endpoint. And this works quite well. However, is it too excessive to make this request every time?
My Solutions
I wonder if there is some way to store the data locally in memory. I thought of two possible solutions:
Option 1: Storing the data on vuex store, however I have learned that the vuex store object is accessible via the browser's console. This means that anyone can modify the access logic to gain access to protected routes.
Option 2: Store it inside a custom Vue.prototype.$user object, however this is similarly accessible via console and therefore has the same security risk as option 1.
Essentially, my question is: is there an option to access a protected route without having to validate the jwt token on the server-side every time?

You should query the server for if the token is valid once when the user initially loads the application. After that, there is usually no reason to check again. If the session expires on the server, then any other API calls you do should return a 401 response (or any other way that you choose to return an error) and your application can act on that.and your application can act on that.
If your profile route is getting data from the server to display and the server is properly validating the user for that request, then it doesn't matter if the user tries to manipulate the Vuex store or Vue state because they won't be able to load the data.
Doing authentication in the vue router is really more for convenience than for actual security.
Don't waste time trying to prevent a malicious user from exploring the Vue application - that is guaranteed to be a losing battle since all of the code is loaded into the browser.
If you really insist on some kind of protection, you can split the application using webpack chunks that are loaded dynamically and configure your web server to only serve those chunks to properly authenticated and authorize users. That said, I would expect such configuration to be difficult and error prone, and I don't recommend it.

Related

Reevaluate Nuxt.js middleware without a route change

I'm wondering if it's possible to essentially "reevaluate" the middleware conditions without actually changing the current route.
The middleware's purpose is to prevent non-logged-in users from accessing the "dashboard".
My issue is, a user could become logged in or logged out without necessarily changing route but they wouldn't be redirected until they try and change pages.
I have a VueX action that triggers when the user's auth state changes but this (from what I can see), can't access the redirect or route variables.
// /mixins/auth.js
const reevaluateAuthStatus = (store, redirect, route) => {
console.log(route)
const redirectPolicy = route.meta.map((meta) => {
if (meta.auth && typeof meta.auth.redirectPolicy !== 'undefined') { return meta.auth.redirectPolicy[0] }
return []
})
const user = store.getters['auth/getUser']
if (redirectPolicy.includes('LOGGEDOUT')) {
if (user) {
return redirect('/dashboard')
}
} else if (redirectPolicy.includes('LOGGEDIN')) {
if (!user) {
return redirect('/login')
}
}
}
module.exports = {
reevaluateAuthStatus
}
// /middleware/auth.js
import { reevaluateAuthStatus } from '../mixins/auth'
export default function ({ store, redirect, route }) {
reevaluateAuthStatus(store, redirect, route)
}
Appreciate any help on this :)
You cannot re-evaluate a middleware AFAIK because it's mainly this (as stated in the documentation)
middlewares will be called [...] on the client-side when navigating to further routes
2 clean ways you can still achieve this IMO:
use some websockets, either with socket.io or something similar like Apollo Subscriptions, to have your UI taking into account the new changes
export your middleware logic to some kind of call, that you could trigger again by calling the $fetch hook again or any other data-related fetching hook in Nuxt
Some more ugly solutions would probably be:
making an internal setInterval and check if the actual state is still valid every 5s or so
move to the same page you are actually on with something like this.$router.go(0) as somehow explained in the Vue router documentation
Still, most of the cases I don't think that this one may be a big issue if the user is logged out, because he will just be redirected once he tries something.
As if the user becomes logged-in, I'm not even sure on which case this one can happen if he is not doing something pro-active on your SPA.
I don't know if it's relevant or not, but I solved a similar problem this way:
I have a global middleware to check the auth status. It's a function that receives Context as a parameter.
I have a plugin that injects itself into context (e.g. $middleware).
The middleware function is imported here.
In this plugin I define a method that calls this middleware passing the context (since the Plugin has Context as parameter as well): ctx.$middleware.triggerMiddleware = () => middleware(ctx);
Now the middleware triggers on every route change as intended, but I can also call this.$middleware.triggerMiddleware() everywhere I want.

Save user authentication status in vuex state is safe Solution?

I've watched various videos on spa vue authentication. All those videos provided a vuex based solution
Like this ;
They created function in getters such as loggedIn that it returns user authentication status .
store.js
[...]
state:{
token: null,
user: null
},
getters: {
loggedIn: (state) => {
return state.token && state.user ? true : false
},
},
[...]
Dashboard.vue (This is a protected route)
[...]
beforeRouteEnter (to, from, next) {
if(store.getters.loggedIn)
next()
else
next({ name:'login' })
}
[...]
Everything is working correctly but I can change state manually by vue dev tools thus I can enter the protected route so authentication is not working properly.
My questions :
Can the user modify the vuex state in production mode?
Save user authentication status in vuex state is safe Solution?
Can the user modify the vuex state in production mode?
Yes, the user can do anything to the state of the app. You can obfuscate some things, but you cannot prevent the user from inspecting or modifying the data of your app.
Save user authentication status in vuex state is safe Solution?
It is one approach, and I do recommend it especially as your app size grows it will become easier to manage shared state in your app. But I wouldn't say it is "safe" or "unsafe".
I can change state manually by vue dev tools thus I can enter the protected route so authentication is not working properly.
The app's behavior can be easily circumvented as you have described. You only have complete control over the server code. If the protected route becomes accessible on the client because the user tampered with the data using dev tools, then the server should reject any requests for privileged data that the protected route requires.

Nuxt Vuex Store Cookies Issue

Good time of the day!
After a few weeks of development of my project, i've decided to migrate from plain Vue to Nuxt.
Mainly because i need SSR, although i know that Google can execute JS presented on the page and therefore generate appropriate content for their search bot.
Another reason is a general workflow of the project development. I like idea with automatic instantiation of routes, store, etc.
I've faced, however, a pretty strange behavior of the application when it is running in the mode: universal instead of mode: spa. And i don't want to switch to mode: spa since then i lose the SSR i was migrating for in the first place.
I' have an account module in the store - account.js which is responsible for handling any operations related to the account management, such as login/logout, get profile of authenticated user, store the token obtained from the login request as well as the logic for handling the 2FA TOTP procedure.
In my legacy application, i was able to get the token from the cookies by just using the following bit of the code
import Cookies from 'js-cookie';
export const state = {
user: null,
token: Cookies.get('token')
}
And save token after the successful authentication by executing the following mutation:
[types.ACCOUNT_SAVE_TOKEN] (state, { token, remember }) {
state.token = token;
Cookies.set('token', token, {
expires: 365,
httpOnly: true
});
}
But after migration to Nuxt.js, every time im loggin in, the token value in the state is getting populated, but no cookie is set, and after navigating to the other page inside the project (it is pwa, so no reloading, etc) token is reset back to null.
This issue however is gone if application is switched to the mode: spa from mode: universal and everything is working just fine.
I've read many issues on the github as well as done pretty big portion of crawling throught the websites which are trying to solve the same issue, though none of the suggestions are working for me.
I've even installed the cookie-universal-nuxt package from NPM which claims to be working with the SSR, but yet every time I'm trying to access this.$cookies.get('token') in the state, or anywhere else (mutations for example), I'm just getting error about using the method (get/set/remove) on null.
Does anyone know or have an idea on how to overcome this issue, at least if it is possible without going back to the mode: spa?
P.S. Running npm run build|generate yields same files as for the normal vue (without the content, just the structure until the target element is readched) while in mode: spa.
Okay, after around 12 hours trying to wrap my head around this issue, i've decided to go the 'dirty' way and create middleware which is doing, in my opinion, way to much processing on each request.
import CookieParser from 'cookieparser';
export default async function ({ store, req }) {
if (!store.getters['account/check']) {
if (!store.state.account.token) {
if (process.server) {
let requestCookies = CookieParser.parse(req.headers.cookie);
if (requestCookies.hasOwnProperty('token')) {
store.dispatch('account/saveToken', {
token: requestCookies.token,
remember: true
});
}
}
}
if (store.state.account.token) {
if (!store.state.account.user) {
try {
await store.dispatch('account/fetchUser');
} catch (error) { }
}
}
}
return Promise.resolve();
}
Seems like useCookie has been created for this use case

Is there a more comprehensive way to handle authentication in Angular2 rc3 than guards?

I have an existing Angular2 app where login/authentication was handled by creating an AuthRouterOutletDirective that extended the RouterOutlet. This made it easy to use componentInstruction in the activate function to check if a user was logged in, if not redirect them to our login portal (for various reasons I have no control over this is through a separate app, which has it's own login screen and then sends back a token that we would save in the Angular2 app) before navigating to the next component.
I just upgraded to the router 3.0.0-alpha.8 and I see that this is no longer an option and it has been replaced by creating an authGuard and and using the canActivate method to handle authentication (I'm referring to this section of the documentation). The problem I'm struggling with is it seems like this is designed for apps where only a small number of routes are protected, and you can just add canActivate: [AuthGuard] to each route that requires authentication in the RouterConfig.
The problem I'm having is that every single route needs to be protected by authentication. There also needs to be continuous checking (unless there is a better fix) because we also (again due in part to the external login service we have to use) will log out the users and clear their token and the next time you navigate to a new route (regardless of what the route is) it should redirect you to login again. I understand that I could just add canActivate: [AuthGuard] to every single route but that seems like an insane and tedious fix, especially for apps with a large amount of routes, all of which require a user to be authenticated to view.
I've been searching for a fix but it seems like, perhaps in part because the upgrade is fairly new, that all the resources are for how to implement these AuthGuards on just one or two routes. I've been digging through the source code (specifically here) to see if there is another method I can extend that's more comprehensive than canActivate or a more universal way to have every route include a particular guard but I can't seem to find anything. Any advice on how best to implement this would be very much appreciated! Thanks.
Referring to my comment, you can add extra guard like that:
import {provideRouter, RouterConfig, Route, CanActivate, ActivatedRouteSnapshot} from '#angular/router';
import {Injectable} from "#angular/core";
#Injectable()
class UniversalGuard implements CanActivate {
canActivate(route: ActivatedRouteSnapshot) {
console.log('applying can activate');
return true;
}
}
let routes: RouterConfig = [
{path: 'path1', component: Component1} as Route,
{path: 'path2', component: Component2} as Route,
{path: 'path3', component: Component3} as Route
];
const routeAugumenter = route => {
let guards = route.canActivate || [];
guards.push(UniversalGuard);
route.canActivate = guards;
if (route.children) {
route.children = route.children.map(routeAugumenter);
}
return route;
};
let augumentedRoutes = routes.map(routeAugumenter);
console.log(augumentedRoutes);
export const APP_ROUTER_PROVIDERS = [
provideRouter(augumentedRoutes), UniversalGuard
];
I have not tested it with child routes, but should work as well.
Edit: updated information how to inject services into UniversalGuard.
If your guard needs some services injected, you inject them in the constructor, as usually. You have to provide these services (and the services they depend on, etc.) in Angular bootstrap call rather than in the components.

Disable login route if a user is logged in with Hapi.js

Basically I have a login/register landing page of my app. After a user logs in or registers I don't want them to be able to access this route anymore. I'm not sure how to achieve this with Hapi.js. The login page doesn't use any auth strategies so it has no idea if a user is logged in or not.
The approach I normally use for this is not disabling the route per se, but redirecting logged-in users away from the login route.
As you point out correctly your login route currently doesn't know whether a user is logged in if it has no auth strategy configured. The solution is to add an auth strategy to the login route, but using the try mode. This means the handler will be executed regardless of whether auth was successful. The trick is that you can then check if the user is authenticated (by inspecting the value of request.auth.isAuthenticated) or not and react accordingly.
So your route might look like this:
server.route({
config: {
auth: {
strategy: 'session',
mode: 'try'
}
},
method: 'GET',
path: '/login',
handler: function (request, reply) {
if (request.auth.isAuthenticated) {
return reply.redirect('/'); // user is logged-in send 'em away
}
return reply.view('login'); // do the login thing
}
});
Another approach with the same result is to set your auth strategy on try mode as the default for all routes:
server.auth.strategy('session', 'cookie', 'try', {
password: 'password-that-is-longer-than-32-chars',
isSecure: true
});
Notice that third argument try. With this approach you don't need to add any auth config to the route itself because it will try this strategy by default. According to the server.auth.strategy docs:
mode - if true, the scheme is automatically assigned as a required strategy to any route without an auth config. Can only be assigned to a single server strategy. Value must be true (which is the same as 'required') or a valid authentication mode ('required', 'optional', 'try'). Defaults to false.
There's some more info about modes in the Authentication tutorial on the hapi site.