I'm trying to authenticate a Vue.js app using firebase.
I have an issue where if trying to access a login-protected URL directly while logged in, the router will load and check for auth state before firebase.js has time to return the auth response. This results in the user being bounced to the login page (while they are already logged in).
How do I delay vue-router navigation until auth state has been retrieved from firebase? I can see that firebase stores the auth data in localStorage, would it be safe to check if that exists as a preliminary authentication check? Ideally the end result would be to show a loading spinner or something while the user is authenticated, then they should be able to access the page they navigated to.
router/index.js
let router = new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/example',
name: 'Example',
component: Example,
beforeEnter: loginRequired
}
})
function loginRequired (to, from, next) {
if (authService.authenticated()) {
next()
} else {
next('/login')
}
}
auth.js
import * as firebase from 'firebase'
var config = {
// firebase config
}
firebase.initializeApp(config)
var authService = {
firebase: firebase,
user: null,
authenticated () {
if (this.user == null) {
return false
} else {
return !this.user.isAnonymous
}
},
setUser (user) {
this.user = user
},
login (email, password) {
return this.firebase.auth().signInWithEmailAndPassword(email, password)
.then(user => {
this.setUser(user)
})
},
logout () {
this.firebase.auth().signOut().then(() => {
console.log('logout done')
})
}
}
firebase.auth().onAuthStateChanged(user => {
authService.setUser(user)
})
export default authService
app.vue
<template>
<div id="app">
<p v-if="auth.user !== null">Logged in with {{ auth.user.email }}</p>
<p v-else>not logged in</p>
<router-view v-if="auth.user !== null"></router-view>
</div>
</template>
<script>
import authService from './auth'
export default {
name: 'app',
data () {
return {
auth: authService
}
}
}
</script>
To build on Richard's answer, for those who are using regular Vue (not Vuex)
//initialize firebase
firebase.initializeApp(config);
let app: any;
firebase.auth().onAuthStateChanged(async user => {
if (!app) {
//wait to get user
var user = await firebase.auth().currentUser;
//start app
app = new Vue({
router,
created() {
//redirect if user not logged in
if (!user) {
this.$router.push("/login");
}
},
render: h => h(App)
}).$mount("#app");
}
});
//route definitions
//...
router.beforeEach((to, from, next) => {
const currentUser = firebase.auth().currentUser;
const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
if (requiresAuth && !currentUser) {
const loginpath = window.location.pathname;
next({ name: 'login', query: { from: loginpath } });
} else if (!requiresAuth && currentUser) {
next("defaultView");
} else {
next();
}
});
Firebase always triggers an auth state change event on startup but it's not immediate.
You'll need to make authService.authenticated
return a promise in order to wait for Firebase to complete its user/auth initialisation.
const initializeAuth = new Promise(resolve => {
// this adds a hook for the initial auth-change event
firebase.auth().onAuthStateChanged(user => {
authService.setUser(user)
resolve(user)
})
})
const authService = {
user: null,
authenticated () {
return initializeAuth.then(user => {
return user && !user.isAnonymous
})
},
setUser (user) {
this.user = user
},
login (email, password) {
return firebase.auth().signInWithEmailAndPassword(email, password)
},
logout () {
firebase.auth().signOut().then(() => {
console.log('logout done')
})
}
}
You don't need to call setUser
from the signInWith...
promise as this will already be handled by the initializeAuth
promise.
I just had this same problem and ended up delaying the creation of the Vue object until the first onAuthStatedChanged.
# main.js
// wait for first firebase auth change before setting up vue
import { AUTH_SUCCESS, AUTH_LOGOUT } from "@/store/actions/auth";
import { utils } from "@/store/modules/auth";
let app;
firebase.auth().onAuthStateChanged(async user => {
if (!app) {
if (user) {
await store.dispatch(AUTH_SUCCESS, utils.mapUser(user));
} else {
await store.dispatch(AUTH_LOGOUT);
}
app = new Vue({
router,
store,
i18n,
render: h => h(App)
}).$mount("#app");
}
});
And then in my route I check as normal and if they end up on login route I just push them to my overview page which is kind of my dashboard page.
#router.js
router.beforeEach((to, from, next) => {
let authenticated = store.getters.isAuthenticated;
if (to.matched.some(record => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
// if not, redirect to login page.
if (!authenticated) {
next({
name: "Login",
query: { redirect: to.fullPath }
});
} else {
next();
}
} else {
// doesn't require auth, but if authenticated already and hitting login then go to overview
if (authenticated && to.name === "Login") {
next({
name: "Overview"
});
}
next(); // make sure to always call next()!
}
});
You have two options:
1) Use beforeRouteEnter from the component:
export default {
name: "example",
....
beforeRouteEnter(to, from, next){
if (authService.authenticated()) {
next()
} else {
next('/login')
}
},
}
2) use beforeResolve from the router.
router.beforeResolve((to, from, next) => {
if(to.fullPath === '/example' && !authService.authenticated()){
next('/login')
}else{
next()
}
})
Life-circle of Vue route guards
To delay the auth state, all you need to do is
firebase.auth().onAuthStateChanged(function(user) {
console.log(user)
new Vue({
router,
render: h => h(App),
}).$mount('#app')
}
});
Stage b)
....
....
....
{
path: "/dashboard",
name: "dashboard",
component: Dashboard,
meta: { requiresAuth: true },//Add this
children: [
{
path: "products",
name: "products",
component: Products,
},
],
....
....
....
Stage c)
router.beforeEach((to, from, next) => {
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
const currentUser = firebase.auth().currentUser
if(requiresAuth && !currentUser) {
next("/")
} else if(requiresAuth && currentUser) {
next()
}else{
next()
}
})
I believe you're good to go this way.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With