Vue.js: How to protect login component from authenticated user - firebase

I'm designing an application where auth is implemented using firebase-auth. I have already protected the user component from unauthenticated users. But I now want to protect the login component from authenticated users. I've tried this so far. Here is my router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import {
fb
} from "#/firebase/init";
import Home from '#/views/Home'
Vue.use(VueRouter)
const routes = [{
path: '/',
name: "home",
component: Home
},
{
path: '/login',
name: 'login',
component: () => import('#/components/auth/Login'),
meta: {
alreadyAuth: true
}
},
{
path: '/signup',
name: 'signup',
component: () => import('#/components/auth/Signup'),
},
{
path: '/user/:id',
name: 'user',
component: () => import('#/components/user/User'),
meta: {
requiresAuth: true
}
},
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
router.beforeEach((to, from, next) => {
let user = fb.auth().currentUser
if (to.matched.some(rec => rec.meta.requiresAuth)) {
if (user) {
next()
} else {
next({
name: 'login'
})
}
} else if (to.matched.some(rec => rec.meta.alreadyAuth)) {
if (user) {
next({
name: 'user'
})
} else {
next()
}
} else {
next()
}
})
export default router
And one more problem is that when I'm authenticated and refresh the user component it redirects me back to the login component. Why so?
Thanks in advance

Since I don't know your main.js this is just an assumption, but I think currentUser is undefined. You should log user and check if it is defined on page load. Firebase acts async, so in order to have everything prepared you need to wrap your whole application in a firebase callback.
firebase.auth().onAuthStateChanged(() => {
new Vue({
router,
render: h => h(App)
}).$mount('#app');
});
Otherwise currentUser probably is not yet initialized.

So you want to block logged-in users from hitting your login route, but you also want to stop non-logged in users from hitting certain pages in your Vue app. Pretty standard use case. Here's how I implemented it:
First, my routes have meta tags requiresAuth and forVisitors. If the user is logged in and tries to hit a forVisitors route, I return the user to the homepage (routeObject.path = '/' in my example below). If the user is not logged in and tries to hit a requiresAuth path, I kick them to /login. I found this approach is pretty flexible for route construction.
My router needs to know what constitutes a logged-in user. Using Firebase's auth module, I can subscribe to onAuthStateChanged and figure out whether the user is logged in from the first emission.
Note, I store the current user in the VueX store (store.state.userModule.user), you can ignore that bit if you do not. Also I store the Firebase Auth object in my store as store.state.userModule.auth, so when you see that, it's the same as Firebase auth.
Additionally, you can find more details on why I wrap my onAuthStateChanged subscription in a Promise here: Getting 'Uncaught' after catching error in firebase.auth() with async
const router = new Router({
/* example of forVisitors path */
{
path: '/Login',
name: 'Login',
component: Login,
meta: {
requiresAuth: false,
forVisitors: true
}
},
/* example of requiresAuth path */
{
path: '/Submit/:mode?/:primary?/:secondary?',
name: 'Submit',
component: Submit,
props: true,
meta: {
requiresAuth: true
}
}
});
router.beforeEach(async (to, from, next) => {
/* Check for a user in my store, or fallback to Firebase auth() user */
let currentUser =
store.state.userModule.user ||
(await new Promise((resolve) => {
store.state.userModule.auth().onAuthStateChanged(user => {
resolve(user);
});
}));
const requiresAuth = to.meta.requiresAuth;
const forVisitors = to.meta.forVisitors;
const routeObject = {};
if (forVisitors && currentUser) {
routeObject.path = '/';
} else if (requiresAuth && !currentUser) {
routeObject.path = '/login';
}
next(routeObject);
});

Related

NextAuth signIn pass parameter to callback

I am using NextAuth to enable users to sign up/in with their Google account and also to link their Google account to their current account on my site.
In order to differentiate between signing up and linking an account when already signed in, I want to pass an extra parameter to signIn that I can access in the signIn callback that will allow me to take the correct action. I have tried:
signIn("google", null, { linkAccount: "true" });
However, this is only passed into the signIn request as a query parameter and is not passed through to the callback. Is there any way I can make a custom argument accessible in the callback?
Edit: Including more code below.
Call to next-auth's signIn client API:
signIn("google", null { linkAccount: "true" });
[...nextauth.js]
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import axios from 'axios';
const authOptions = (req) => ({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
secret: "secret",
callbacks: {
async signIn({
user, account, profile, email, credentials
}) {
// GOAL: How can I specify to this endpoint that I am just linking an account?
let res = await axios.post('http://localhost:8000/users/third_party_sign_in', {
third_party_id: user.id,
email: user.email,
type: account.provider
justLink: true|false
}, { withCredentials: true })
let path;
if (res.data.action === "login") {
path = `/?action=${res.data.action}&id=${res.data.user_id}&email=${user.email}&third_party=${account.provider}`
} else if (res.data.action === "create") {
path = `/?action=${res.data.action}&name=${user.name}&email=${user.email}&third_party=${account.provider}&third_party_id=${user.id}`
}
return path;
},
async redirect({ url }) {
return Promise.resolve(url)
}
},
});
function testNextApiRequest(req) {
if (req.query.nextauth
&& req.query.nextauth.length === 2
&& req.query.linkAccount) {
/// logs for signIn API call but not for callback
console.log("QUERY PARAMS: ", req.query);
}
}
export default (req, res) => {
testNextApiRequest(req);
return NextAuth(req, res, authOptions(req));
}
I also spent a lot of time on this trying to figure out how to get a param in a callback when using the signIn function.
Here's the solution
call signIn like you were doing
signIn("google", null, { linkAccount: "true" });
Now in [...nextauth].ts you want to parse req.query BEFORE passing it to next-auth like so
authOptions is just a function that returns next-auth callbacks and config.
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
console.log(req.query); // This will have your linkAccount param
return await NextAuth(req, res, authOptions(req, res));
}
Now that you have access to the params you can do whatever logic you want. This will only work for some callbacks like redirect. Trying to get the params in the session callback is still proving to be impossible for me.
It isn't great, honestly it's pretty bad if you do db queries you'll be slowing down all the requests but I think this is currently the best way to do it. Hope it helps!
More discussion here https://github.com/nextauthjs/next-auth/discussions/901

Next-Auth Signin without user consent when app is already authorized

I have a Next application that let's the user signup using their discord account. The signup part is working. Now I am wondering how can I let the user sign-in using the discord account without going through the authorize part again, if they had already done the signup.
In the /pages/api/auth/[...nextauth].ts file
export default (req: NextApiRequest, res: NextApiResponse<any>) => {
if (req.query.firstName && req.query.lastName) {
firstName = req.query.firstName;
lastName = req.query.lastName;
}
return NextAuth(req, res, {
providers: [
DiscordProvider({
clientId: process.env.DISCORD_CLIENT_ID,
clientSecret: process.env.DISCORD_CLIENT_SECRET,
authorization: { params: { scope: DISCORD_SCOPES } },
}),
],
session: { strategy: "jwt" },
callbacks: {
async session({ session, token, user }) {
session.jwt = token.jwt;
session.id = token.id;
return session;
},
async jwt({ token, user, account }) {
}
}
});
}
Using above logic I am saving the user data to strapi after signup.
How to let the user sign-in with discord without getting a new access token and going through authorization
If the user already authorized your app to log in. You can use the following in your front-end client.
import { signIn } from "next-auth/react";
signIn("discord", { callbackUrl: '', redirect: true }, { prompt: "none" });
References:
https://next-auth.js.org/getting-started/client#additional-parameters

Vue + Firebase route to prevent user back to login page after login

I develop SPA using VueJS 3 and firebase. I want prevent user from accessing login page after they login. But it seems the routes are keep looped or error after trying different code.
the routes:
{
path: '/', name: 'login', component: () => import('../views/LoginView.vue')
},
{
path: '/dashboard', name: 'dashboard', component: () => import('../views/DashboardView.vue'),
meta: {
requiresAuth: true,
},
},
the logic(didnt show anything):
const getCurrentUser = () => {
return new Promise((resolve, reject) => {
const removeListener = onAuthStateChanged(
getAuth(),
(user) => {
removeListener()
resolve(user)
},
reject
)
})
}
router.beforeEach(async (to, from, next) =>{
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (await getCurrentUser()) {
next()
} else {
next("/")
}
} else {
next("/dashboard")
}
})
this one keep looping:
router.beforeEach(async (to, from, next) =>{
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (await getCurrentUser()) {
next()
} else {
next("/")
}
}
next("/dashboard")
})
As mentionned in the Vue Router Doc :
You must call "next" exactly once in any given pass through a navigation guard. It can appear more than once, but only if the logical paths have no overlap, otherwise the hook will never be resolved or produce errors.:
They provide those examples :
// BAD
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
// if the user is not authenticated, `next` is called twice
next()
})
// GOOD
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
else next()
})
Also, make sure you do not have a infinite loop :
next("/dashboad") forces a redirection to the Dashboard page, no matter what page we come from. You may end in a loop where every page redirects to Dashboard, including the Dashboard page itself

How do I redirect the vue router after a firebase sign out?

I'm using Firebase, FirebaseUI, Vue, vue router, and vuex.
I have a home view ('/') that shows a "Log In" button. At this point, the vuex state has a user variable set to null. When I click on it, it takes me to my login view ('/login'). Suspicious thing: Looking at Vue DevTools, home remains the active view.
I use FirebaseUI to handle my logging in stuff. Once successfully logged in, my site redirects to a console view ('/console') which has a "Log Out" button, and the user variable in vuex is set to an object. Suspicious thing: Looking at Vue DevTools, login is now the active view.
Clicking the "Log Out" button still keeps me in the '/console/ vue. Vuex has a user variable set to null.
Refreshing the page automatically redirects me to the '/login' view.
I'm wondering if the fact that the active view isn't correct is the root of this issue. I found other people with the same problem saying they weren't using the webpack correctly by importing their App.vue file as a js, but I'm importing it as a .vue file in my main.js file.
I'm also suspicious of the FirebaseUI uiConfig in my Login.vue file. I have to set signInSuccessUrl: '#/console', (note the #). If I don't have the #, it redirects me to home (url = http://localhost:8081/console#/).
App.Vue
<template>
<v-app>
<v-content>
<router-view></router-view>
</v-content>
</v-app>
</template>
<script>
export default {
name: 'App',
methods: {
setUser: function() {
this.$store.dispatch('setUser');
}
},
data: () => ({
//
}),
created () {
// when the app is created run the set user method
// this uses Vuex to check if a user is signed in
// check out mutations in the store/index.js file
this.setUser();
},
};
</script>
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import vuetify from './plugins/vuetify'
import { store } from './store/index';
// Firebase App (the core Firebase SDK) is always required and
// must be listed before other Firebase SDKs
import firebase from "firebase/app";
// Add the Firebase services that you want to use
import "firebase/auth";
import "firebase/firestore";
var firebaseConfig = {
// I'm editing this part out
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
// Check before each page load whether the page requires authentication/
// if it does check whether the user is signed into the web app or
// redirect to the sign-in page to enable them to sign-in
router.beforeEach((to, from, next) => {
const currentUser = firebase.auth().currentUser;
const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
if (requiresAuth && !currentUser) {
next('/login');
} else if (requiresAuth && currentUser) {
next();
} else {
next();
}
});
// Wrap the vue instance in a Firebase onAuthStateChanged method
// This stops the execution of the navigation guard 'beforeEach'
// method until the Firebase initialization ends
firebase.auth().onAuthStateChanged(function (user) {
user;
new Vue({
el: '#app',
store,
router,
vuetify,
render: h => h(App)
});
});
router\index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
component: () => import('../views/About.vue')
},
{
path: '/tryit',
name: 'tryit',
component: () => import('../views/TryIt.vue')
},
{
path: '/pricing',
name: 'pricing',
component: () => import('../views/Pricing.vue')
},
{
path: '/login',
name: 'login',
component: () => import('../views/Login.vue')
},
{
path: '/console',
name: 'console',
component: () => import('../views/Console.vue'),
meta: { requiresAuth: true }
}
]
const router = new VueRouter({
routes
})
export default router
store\index.js
import Vue from 'vue';
import Vuex from 'vuex';
import Firebase from 'firebase';
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
user: null
},
getters: {
getUser: state => {
return state.user;
}
},
mutations: {
setUser: state => {
state.user = Firebase.auth().currentUser;
},
logoutUser: state => {
state.user = null;
}
},
actions: {
setUser: context => {
context.commit('setUser');
},
logoutUser: context => {
context.commit('logoutUser');
}
}
});
Login.vue
<template>
<div class="login">
<NavbarAnon />
<!-- The surrounding HTML is left untouched by FirebaseUI.
Your app may use that space for branding, controls and other customizations.-->
<h1>Welcome to My Awesome App</h1>
<div id="firebaseui-auth-container"></div>
<div id="loader">Loading...</div>
</div>
</template>
<script>
// # is an alias to /src
import NavbarAnon from '#/components/layout/NavbarAnon';
import 'firebaseui/dist/firebaseui.css';
// Firebase App (the core Firebase SDK) is always required and
// must be listed before other Firebase SDKs
var firebase = require("firebase/app");
// Add the Firebase products that you want to use
require("firebase/auth");
require("firebase/firestore");
var firebaseui = require('firebaseui');
export default {
name: 'login',
components: {
NavbarAnon
},
mounted() {
// Initialize the FirebaseUI Widget using Firebase.
let ui = firebaseui.auth.AuthUI.getInstance();
if (!ui) {
ui = new firebaseui.auth.AuthUI(firebase.auth());
}
var uiConfig = {
callbacks: {
signInSuccessWithAuthResult: function(authResult, redirectUrl) {
// User successfully signed in.
// Return type determines whether we continue the redirect automatically
// or whether we leave that to developer to handle.
authResult + redirectUrl;
return true;
},
uiShown: function() {
// The widget is rendered.
// Hide the loader.
document.getElementById('loader').style.display = 'none';
}
},
// Will use popup for IDP Providers sign-in flow instead of the default, redirect.
signInFlow: 'popup',
signInSuccessUrl: '#/console',
signInOptions: [
// Leave the lines as is for the providers you want to offer your users.
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
firebase.auth.EmailAuthProvider.PROVIDER_ID
],
// Terms of service url.
tosUrl: '<your-tos-url>',
// Privacy policy url.
privacyPolicyUrl: '<your-privacy-policy-url>'
};
// The start method will wait until the DOM is loaded.
ui.start('#firebaseui-auth-container', uiConfig);
}
};
</script>
In the HeaderBar.vue (which has the logout button):
methods: {
signOutUser() {
//this.$emit("trying to sign out user");
firebase.auth().signOut().then(function() {
// Sign-out successful.
this.$store.dispatch('logoutUser');
this.$route.push('/');
}).catch(function(error) {
//this.$emit("signout error", error);
error;
// An error happened.
});
}
}

vue js router + firebase authentication

I use vue js 2.4.2 , vue router 2.7.0 , and firebase 4.3.0. I can't get the route authentication stuff to work. This is my current route.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
import Firebase from './firebase'
import Dashboard from './components/Dashboard.vue'
import Auth from './components/Auth.vue'
let router = new Router({
mode: 'history',
routes: [
{
path: '/',
component: Dashboard,
meta: {
auth: true
}
},
{
path: '/login',
component: Auth
}
]
})
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.auth)) {
if (!Firebase.auth().currentUser) {
next({
path: '/login'
})
} else {
next()
}
} else {
next()
}
})
export default router
But now everytime I go to / it redirects me back to /login, probably because the Firebase.auth().currentUser is null, even though I am in fact logged in. How do I fix this?
try using Firebase.auth().onAuthStateChanged instead of Firebase.auth().currentUser; it is the recommended way to get the current user.
Getting the user by currentUser may cause a problem similar to what you are seeing. This is from the official doc of firebase.
Note: currentUser might also be null because the auth object has not finished initializing. If you use an observer to keep track of the user's sign-in status, you don't need to handle this case.
Try to set you authentication logic like this:
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.auth)) {
Firebase.auth().onAuthStateChanged(function(user) {
if (!user) {
next({
path: '/login'
})
} else {
next()
}
}
} else {
next()
}
})

Resources