Well, I'm starting with nuxt and I have following routes:
/home
/dashboard
/login
I want to protect the /dashboard, but only for users logged in with a token in Cookie.
Then i created a middleware
/middleware/auth.js
import Cookie from 'js-cookie'
export default function({ req, redirect }) {
if (process.server) {
if (!req.headers.cookie) return redirect('/login')
const jwtCookie = req.headers.cookie.split(';').find(c => c.trim().startsWith('jwt='))
if (!jwtCookie) return redirect('/login')
} else {
const jwt = Cookie.get('jwt')
if (!jwt) { window.location = '/login' }
}
}
and register the middleware in my layout or dashboard page
<script>
export default {
middleware: 'auth',
}
</script>
when I access /dashboard apparently works perfectly
but the problem is that the middleware is being registered globally, it is running on all pages, all routes
So when you access /home that is a published page, if you do not have the cookie, you end up being redirected to login page
anyone help?
How about creating a condition based on the route.path param ?
export default function({ req, redirect, route }) {
if (!route.path.includes('dashboard')) { // if path doesn't include "dashboard", stop there
return;
}
if (process.server) {
if (!req.headers.cookie) return redirect('/login')
const jwtCookie = req.headers.cookie.split(';').find(c => c.trim().startsWith('jwt='))
if (!jwtCookie) return redirect('/login')
} else {
const jwt = Cookie.get('jwt')
if (!jwt) { window.location = '/login' }
}
}
Therefore you still benefit from the pre-render middleware system.
You probably have registered your middleware/auth.js in your nuxt.config.js.
When you register a middleware in nuxt.config.js, you're registering it globally, meaning it will be called for every route change.
Docs:
https://nuxtjs.org/guide/routing#middleware
In my opinion, you should call them plugin, because of
middleware called by each route changed also you can't use middleware in layout and subComponent, you can use it as plugin and call it manually everywhere also it's reactive and runtime.
path: /plugind/auth.js
import Cookie from 'js-cookie';
export default function({ req, redirect }) {
if (process.server) {
if (!req.headers.cookie) return redirect('/login')
const jwtCookie = req.headers.cookie.split(';').find(c =>
c.trim().startsWith('jwt='))
if (!jwtCookie) return redirect('/login')
} else {
const jwt = Cookie.get('jwt')
if (!jwt) { window.location = '/login'
}
}
}
Related
I am attempting to build a Vue application that uses Supabase authentication. Inside one of the route guards in the router file, I implemented supabase.auth.getUser() in order to retrieve the user login status for a conditional that prevents next() from executing before the user is authenticated:
// Route guard for auth routes
router.beforeEach((to, from, next) => {
// const user = supabase.auth.user();
const { data: { user } } = await supabase.auth.getUser();
if (to.matched.some((res) => res.meta.auth)) {
if (user) {
next();
return;
}
next({ name: "Login" });
return;
}
next();
});
However, when I implement supabase.auth.getUser() inside the router guard, I get the following error in the console before logging in: "invalid claim: missing sub claim". After logging in, the error goes away. If I remove the supabase.auth.getUser conditional from the route guard, the error also goes away. After some additional digging online and running my Supabase anon key in jwt.io, it appears that my key is missing sub claim in the payload. If this is preventing the key from being authenticated properly, how can I resolve it?
You should be checking against the session rather than the user. The user will try to check against a JWT first which wouldn't exist at the time of checking since you're not logged in. Use getSession instead:
// Route guard for auth routes
router.beforeEach((to, from, next) => {
// const user = supabase.auth.user();
const { data: { session } } = await supabase.auth.getSession();
if (to.matched.some((res) => res.meta.auth)) {
if (session?.user) {
next();
return;
}
next({ name: "Login" });
return;
}
next();
});
I'm trying to run an action in my store that initializes an authenticated user. It checks to see if there is a JWT stored in local storage, then sets that JWT to the store. Since there is no local storage on the server side I use the Nuxt helper "process.client" in my middleware. When the named middleware runs when I visit an authorized route it's only running once on the server, and not running again on the client. I could add it as SSR false in the Nuxt config, but I don't want it to run on every page load... is there something I'm missing?
// middlware/check-auth.ts
import { Middleware } from '#nuxt/types'
import { useCustomerStore } from '#/store/customer'
const checkAuth: Middleware = ({ $pinia }) => {
const { initAuth } = useCustomerStore($pinia)
if (process.client) {
initAuth()
}
}
export default checkAuth
// pages/account/details
// page calling middleware
<script lang="ts">
import { defineComponent } from '#nuxtjs/composition-api'
export default defineComponent({
middleware: ['check-auth', 'auth']
})
</script>
<template>
<coming-soon></coming-soon>
</template>
<style module lang="postcss"></style>
// store/customer.ts
// action being called in middleware
actions: {
initAuth(): void {
const accessToken = localStorage.getItem('accessToken')
if (!accessToken) {
return
}
this.accessToken = accessToken
},
}
I have a VueJS single-page application and I use JWT authentication.
I'm trying to figure out how to make sure that User is authenticated after page reload and if not, redirect them to login page.
accessToken and refreshToken are stored in the cookies and also in Vuex
Vuex.state:
auth: {
user: {},
isAuthenticated: false,
accessToken: null,
refreshToken: null
},
Vuex.actions.refreshToken
refreshToken: async ({state, commit, dispatch}) => {
try {
await api.jwtRefresh(state.auth.refreshToken).then(response => {
if (response.status === 200) {
dispatch("setAuthData",{
accessToken:response.data.access,
isAuthenticated:true
})
}
}).catch(err => {
dispatch('logout')
});
} catch (e) {
dispatch('logout')
}
},
App.vue
export default {
data: () => ({}),
mounted() {
this.$store.dispatch('setAuthDataFromCookies')
this.$store.dispatch('refreshToken') // checks if user is authenticated, redirect to login page if not
this.$router.push('/dashboard')
}
}
My idea is to try to refresh the JWT token. If it was successfully refreshed User can proceed to /dashboard. If not, User is redirected to the /login
The problem is that mounted doesn't wait until refreshToken is done and it redirects User immediately to the /dashboard even before token is refreshed.
How can I make it wait? (The idea is that refreshToken will redirect user to /login in case of error.
You can setup a meta auth field in your router, and a global beforeEnter or beforeEach guard that checks Vuex (or your cookies, or both) for a token.
In your router.js file you'd have something like
routes: [
{
name: 'Login'
},
{
name: 'Dashboard', // + path, component, etc
meta: {
auth: true
}
}
]
Then you setup a global guard, something like this:
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.auth)) {
if (!store.getters.authToken) {
next({ name: 'Login' });
} else {
next();
}
} else {
next(); // Very important to call next() in this case!
}
})
This will check before each and every route transition whether the next route has the auth meta field. If it does, it checks your Vuex state for the token, and otherwise navigates as normally.
Vue Router Docs on Navigation Guards
In your case, you're trying to authenticate the user, so you can just call your endpoint inside of the beforeEach guard and redirect like that based on the response. Just make sure to make the callback asynchronous, like router.beforeEach(async (to, from, next) => {})
I am trying to redirect non-logged in user from all pages to /login. I tried beforeEach() but it doesn't fire when user enter site with direct url like /home, /event.
Per-Route Guard beforeEnter() works perfectly since it fires once the user lands on that particular page. However, it requires me to add beforeEnter() on every routes.
I am looking for a way to duplicate that beforeEnter() on almost every page on the router (even on dynamic pages) which non-logged in user will be redirected to /login.
This one works when user enter with direct url /home.
routes: [
{
path: '/home',
name: 'home',
beforeEnter(to, from, next){
if ( to.name !== 'login' && !this.isloggedin ){
next({
path: 'login',
replace: true
})
} else {
next()
}
}
},
...
]
This one only works after user entered the site and route changed
vm.$router.beforeEach((to, from, next)=>{
if ( to.name !== 'login' && !this.isloggedin ){
next({
path: 'login',
replace: true
})
} else {
next();
}
})
Thanks in advance.
It looks like this beforeEach is being defined inside an initialized component, which means the first routing has already occured. Define it in the router module with your routes instead:
const router = new VueRouter({
...
})
router.beforeEach((to, from, next)=>{
if ( to.name !== 'login' && !this.isloggedin ){
next({
path: 'login',
replace: true
})
} else {
next();
}
})
Hopefully you are using Vuex and can import the store for store.state.isloggedin. If not using Vuex yet, this illustrates why it is useful for global state.
For a global and neat solution, you can control the router behavior in the App.vue using the router.beforeResolve(async (to, from, next) => {});.
beforeResolve is better than beforeEach, as beforeResolve will not load the component of the accessed path URL unless you fire manually the next function.
This is very helpful as you'll not render any interafce unless you check the authentication status of the user and then call next().
Example:
router.beforeResolve(async (to, from, next) => {
// Check if the user is authenticated.
let isUserAuthenticated = await apiRequestCustomFunction();
// Redirect user to the login page if not authenticated.
if (!isUserAuthenticated) {
location.replace("https://example.com/signin");
return false;
}
// When next() is called, the router will load the component corresponding
// to the URL path.
next();
});
TIP: You can display a loader while you check if the user is authenticated or not and then take an action (redirect to sign in page or load the app normally).
I want to dynamically import data from component file to router file and allow next() depending upon value of imported data.
in App.vue I'm triggering this.$router.push({name: "Dashboard"}) when data authenticated: false changes to true. I'm triggering it with watch.
Problem is, router file will always receive original value false even with dynamic importing.
App.vue
watch: {
authenticated(){
console.log(this.authenticated) //Outputs 'true'
this.$router.push({name: 'Dashboard'}); //Triggers routing
}
}
router file (index.js)
{
path: '/dashboard',
name: 'Dashboard',
component: Dashboard,
beforeEnter(to, from, next){
(async ()=>{
const mod = await import('../view/App.vue'); //Dynamic import
let result = mod.default.data().auth; //Access 'authenticated' value from App.vue
console.log(result); //Output is 'false', but it should be 'true'
result ? next() : next({name: 'Login'});
})()
}
}
I tried many different async methods as well but nothing worked.
Use In-Component Guard in your App.vue as specified here and remove the authentication login from router file:
beforeRouteLeave(to, from, next) {
if (to.name === 'Dashboard' && this.authenticated) {
next();
}
else {
next({ name: 'Login' });
}
}