Firebase Auth check with Vue-router - firebase

The problem is that the vue-router's beforeEnter is triggered earlier than the beforeCreate hook in the main.js and have second delay, while after reload the vuex action set the user in state. This results in the user being bounced to the login page.
How can I delay vue-router's beforeEnter check until vuex set the authorized user in state.
router.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '../views/Home.vue'
import auth from './auth'
Vue.use(Router)
export default new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/reservations',
name: 'Reservations',
component: () => import('#/views/Reservation/Reservations.vue'),
beforeEnter: auth
}
]
})
auth.js
import { store } from "../store";
export default (to, from, next) => {
if(store.getters.user) {
next()
} else {
console.log('auth')
next('/signin')
}
}
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router/router'
import {store} from './store'
import firebase from 'firebase/app'
import 'firebase/app'
import config from './config'
new Vue({
router,
store,
render: h => h(App),
beforeCreate() {
firebase.initializeApp(config).firestore().settings({timestampsInSnapshots: true})
console.log('main')
this.$store.dispatch('authCheck')
}
}).$mount('#app')
store/user.js
import firebase from 'firebase/app'
import 'firebase/auth'
export default{
state: {
user: null
},
mutations: {
setUser (state, payload) {
state.user = payload
}
},
actions: {
authCheck({ commit }) {
commit('setLoading', true)
firebase.auth().onAuthStateChanged((user) => {
if(user){
commit('setUser', {
id: user.uid,
name: user.displayName,
email: user.email
})
commit('setLoading', false)
}
})
},
logout({commit}) {
firebase.auth().signOut()
commit('setUser', null)
}
},
getters: {
user (state) {
return state.user
}
}
}
In the console I see first 'auth' from beforeEnter check, and then 'main' from beforeCreate. How to trigger an 'authCheck' before router's check

In your auth.js file instead of monitoring the store, you should wait for the authStateChanged event, then when you receive the user from firebase do your processing then.
auth.js
export default (to, from, next) => {
firebase.auth().onAuthStateChanged(function (user) {
if (user)
next();
else
next('/signin');
});
}

Related

How to get data from protected API route

I need to get data from protected API route. I am using Next Auth with google provider.
I have seen some solutions to similar problem but all of them were using JWT token. I don't use JWT. I don't have much experience with auth so I don't know what to pass to axios request.
This is /api/auth/[...nextauth].ts
import NextAuth, { NextAuthOptions } from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'
import { PrismaAdapter } from '#next-auth/prisma-adapter'
import { prisma } from '../../../server/client'
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
}),
],
session: {
strategy: 'database',
},
}
export default NextAuth(authOptions)
This is how my route is protected:
const session = await getServerAuthSession({ req, res })
if (!session) {
return res.status(403).send({
error: 'You must be signed in to view the protected content on this page.',
})
}
This is get-server-auth-session.ts file
// Wrapper for unstable_getServerSession https://next-auth.js.org/configuration/nextjs
import type { GetServerSidePropsContext } from 'next'
import { unstable_getServerSession } from 'next-auth'
import { authOptions as nextAuthOptions } from '../../pages/api/auth/[...nextauth]'
// Next API route example - /pages/api/restricted.ts
export const getServerAuthSession = async (ctx: {
req: GetServerSidePropsContext['req']
res: GetServerSidePropsContext['res']
}) => {
return await unstable_getServerSession(
ctx.req,
ctx.res,
nextAuthOptions
)
}

AngularFireModule has not been provided using v7.0.1 and new method of initializing the firebase app

I'm attempting to connect to the firebase emulator within an integration test, using the new AngularFire API (>v7)
import {
TestBed
} from '#angular/core/testing';
import {
initializeApp,
provideFirebaseApp
} from '#angular/fire/app';
import {
doc,
enableIndexedDbPersistence,
Firestore,
getFirestore,
provideFirestore,
setDoc
} from '#angular/fire/firestore';
import {
connectFirestoreEmulator
} from "firebase/firestore";
describe('FirestoreEmulatorSmoketest', () => {
let projectId: string;
let firestore: Firestore;
beforeAll(() => {
const testConfig = {
projectId,
auth: ...
};
TestBed.configureTestingModule({
imports: [
provideFirebaseApp(() => initializeApp(testConfig)),
provideFirestore(() => {
const firestore = getFirestore();
connectFirestoreEmulator(firestore, 'localhost', 8080);
enableIndexedDbPersistence(firestore);
return firestore;
}),
],
})
});
beforeEach(() => {})
afterAll(() => {})
it('should connect', () => {
const fooDoc = doc(firestore, "foo/12345");
return setDoc(fooDoc, {
updated: new Date()
})
})
});
This code produces the following error "AngularFireModule has not been provided"
I can only assume I'm not initialising angular fire somehow?
First of all, my native language is not English, so if I write like a fool you know why.
Try this.
environment.ts
export const environment = {
production: false,
useEmulators: true,
firebaseConfig: {
apiKey: 'YOUR-API-KEY',
authDomain: 'YOUR-AUTH-DOMAIN',
projectId: 'YOUR-PROJECT-ID',
storageBucket: 'YPUR-STORAGE-BUCKET',
messagingSenderId: 'YOUR-MESSAGING-SENDER-ID',
appId: 'YOUR-APP-ID',
measurementId: 'YOUR-MEASUREMENT-ID',
},
};
Important: As you can see I have the variable useEmulators, which in the following lines I will explain what it is going to be used for.
app.module.ts
import { provideFirebaseApp, initializeApp } from '#angular/fire/app';
import { getAuth, provideAuth, connectAuthEmulator } from '#angular/fire/auth';
import { getFirestore, provideFirestore, connectFirestoreEmulator, enableIndexedDbPersistence } from '#angular/fire/firestore';
import { getStorage, provideStorage, connectStorageEmulator } from '#angular/fire/storage';
import { getAnalytics, provideAnalytics } from '#angular/fire/analytics';
import { getFunctions, provideFunctions, connectFunctionsEmulator} from '#angular/fire/functions';
import { environment } from 'environments/environment'; // <--- Environment variables.
imports: [
// Firebase
provideFirebaseApp(() => initializeApp(environment.firebaseConfig)),
provideFirestore(() => {
if (environment.useEmulators) {
const firestore = getFirestore();
connectFirestoreEmulator(firestore, 'localhost', 8080);
enableIndexedDbPersistence(firestore);
return firestore;
} else {
getFirestore();
}
}),
provideAuth(() => {
if (environment.useEmulators) {
const fireauth = getAuth();
connectAuthEmulator(fireauth, 'http://localhost:9099'); // <---FireAuth Port
return fireauth;
} else {
getAuth();
}
}),
provideStorage(() => {
if (environment.useEmulators) {
const firestorage = getStorage();
connectStorageEmulator(firestorage, 'localhost', 9199); // <---- Firestorage Port
return firestorage;
} else {
getStorage();
}
}),
provideFunctions(() => {
if (environment.useEmulators) {
const firefunctions = getFunctions();
connectFunctionsEmulator(firefunctions, 'localhost', 5001); // <--- FireFunctions Port
return firefunctions;
} else {
getFunctions();
}
}),
provideAnalytics(() => getAnalytics()),
],
Important (Environment Path): Change the environment path of the variables in case you don't have them in the default location.
Important (Local Ports): In case you use different local ports than the default ones, change them.
As you can see I have added code to the initializations to be able to switch between the emulated project and the online project, in a simple way:
useEmulators: true // We load the emulator environment
useEmulators: false // We load the production environment
_fireAuth.service.ts
import { Auth } from '#angular/fire/auth';
constructor(private _fireAuth: Auth) {}
_fireStorage.service.ts
import { Storage } from '#angular/fire/storage';
constructor(private _fireStorage: Storage) {}
_fireStore.service.ts
import { Firestore } from '#angular/fire/firestore';
constructor(private _fireStore: Firestore) {}
It only remains to import the functions you are going to use, e.g. { doc, Collection, etc... }.
Use the documentation provided by Google to see how they changed the functions: https://firebase.google.com/docs/build
and use the code found in the "Web version 9 (modular)" tab.

Vue + Pinia + Firebase Authentication: Fetch currentUser before Route Guard

Recently I started to use Pinia as a global store for my Vue 3 Project. I use Firebase for the user authentication and am trying to load the current user before Vue is initialized. Ideally everything auth related should be in a single file with a Pinia Store. Unfortunately (unlike Vuex) the Pinia instance needs to be passed to the Vue instance before I can use any action and I believe that is the problem. On first load the user object in the store is empty for a short moment.
This is the store action that is binding the user (using the new Firebase Web v9 Beta) in auth.js
import { defineStore } from "pinia";
import { firebaseApp } from "#/services/firebase";
import {
getAuth,
onAuthStateChanged,
getIdTokenResult,
} from "firebase/auth";
const auth = getAuth(firebaseApp);
export const useAuth = defineStore({
id: "auth",
state() {
return {
user: {},
token: {},
};
},
actions: {
bindUser() {
return new Promise((resolve, reject) => {
onAuthStateChanged(
auth,
async (user) => {
this.user = user;
if (user) this.token = await getIdTokenResult(user);
resolve();
},
reject()
);
});
},
// ...
}})
and this is my main.js file
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { createPinia } from "pinia";
import { useAuth } from "#/store/auth";
(async () => {
const app = createApp(App).use(router).use(createPinia());
const auth = useAuth();
auth.bindUser();
app.mount("#app");
})();
How can I set the user before anything else happens?
I figured it out. Had to register the router after the async stuff
//main.js
(async () => {
const app = createApp(App);
app.use(createPinia());
const { bindUser } = useAuth();
await bindUser();
app.use(router);
app.mount("#app");
})();

How to send httponly cookies client side when using next-auth credentials provider?

I'm creating a next js application, using next-auth to handle authentication.
I have an external backend api, so I'm using Credentials Provider.
The problem is that the backend sends httponly cookies, but those are not being attached to the browser when i make a request client side.
In /pages/api/[...auth].js
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import clientAxios from '../../../config/configAxios'
export default NextAuth({
providers: [
Providers.Credentials({
async authorize(credentials) {
try {
const login = await clientAxios.post('/api/login', {
username: credentials.username,
password: credentials.password,
is_master: credentials.is_master
})
const info = login.data.data.user
const token = {
accessToken: login.data.data.access_token,
expiresIn: login.data.data.expires_in,
refreshToken: login.data.data.refresh_token
}
// I can see cookies here
const cookies = login.headers['set-cookie']
return { info, token, cookies }
} catch (error) {
console.log(error)
throw (Error(error.response.data.M))
}
}
})
],
callbacks: {
async jwt(token, user, account, profile, isNewUser) {
if (token) {
// Here cookies are set but only in server side
clientAxios.defaults.headers.common['Cookie'] = token.cookies
}
if (user) {
token = {
user: user.info,
...user.token,
}
}
return token
},
async session(session, token) {
// Add property to session, like an access_token from a provider.
session.user = token.user
session.accessToken = token.accessToken
session.refreshToken = token.refreshToken
return session
}
},
session: {
jwt: true
}
})
my axios config file
import axios from 'axios';
const clientAxios = axios.create({
baseURL: process.env.backendURL,
withCredentials: true,
headers:{
'Accept' : 'application/json',
'Content-Type' : 'application/json'
}
});
export default clientAxios;
a page component
import { getSession } from "next-auth/client";
import clientAxios from "../../../config/configAxios";
import { useEffect } from "react"
export default function PageOne (props) {
useEffect(async () => {
// This request fails, cookies are not sent
const response = await clientAxios.get('/api/info');
}, [])
return (
<div>
<h1>Hello World!</h1>
</div>
)
}
export async function getServerSideProps (context) {
const session = await getSession(context)
if (!session) {
return {
redirect: {
destination: '/login',
permanent: false
}
}
}
// This request works
const response = await clientAxios.get('/api/info');
return {
props: {
session,
info: response.data
}
}
}
After time of researching I have figured it out.
I had to make a change in /pages/api/auth in the way I'm exporting NextAuth.
Instead of
export default NextAuth({
providers: [
...
]
})
Export it like this, so we can have access to request and response object
export default (req, res) => {
return NextAuth(req, res, options)
}
But to access them in the options object, we can make it a callback
const nextAuthOptions = (req, res) => {
return {
providers: [
...
]
}
}
export default (req, res) => {
return NextAuth(req, res, nextAuthOptions(req, res))
}
To send a cookie back to the frontend from the backed we must add a 'Set-Cookie' header in the respond
res.setHeader('Set-Cookie', ['cookie_name=cookie_value'])
The complete code would be
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
const nextAuthOptions = (req, res) => {
return {
providers: [
CredentialsProvider({
async authorize(credentials) {
try {
const response = await axios.post('/api/login', {
username: credentials.username,
password: credentials.password
})
const cookies = response.headers['set-cookie']
res.setHeader('Set-Cookie', cookies)
return response.data
} catch (error) {
console.log(error)
throw (Error(error.response))
}
}
})
]
}
}
export default (req, res) => {
return NextAuth(req, res, nextAuthOptions(req, res))
}
Update - Typescript example
Create a type for the callback nextAuthOptions
import { NextApiRequest, NextApiResponse } from 'next';
import { NextAuthOptions } from 'next-auth';
type NextAuthOptionsCallback = (req: NextApiRequest, res: NextApiResponse) => NextAuthOptions
Combining everything
import { NextApiRequest, NextApiResponse } from 'next';
import NextAuth, { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import axios from 'axios'
type NextAuthOptionsCallback = (req: NextApiRequest, res: NextApiResponse) => NextAuthOptions
const nextAuthOptions: NextAuthOptionsCallback = (req, res) => {
return {
providers: [
CredentialsProvider({
credentials: {
},
async authorize(credentials) {
try {
const response = await axios.post('/api/login', {
username: credentials.username,
password: credentials.password
})
const cookies = response.headers['set-cookie']
res.setHeader('Set-Cookie', cookies)
return response.data
} catch (error) {
console.log(error)
throw (Error(error.response))
}
}
})
],
callbacks: {
...
},
session: {
...
}
}
}
export default (req: NextApiRequest, res: NextApiResponse) => {
return NextAuth(req, res, nextAuthOptions(req, res))
}
To remove cookie in nextAuth after signing out, I used the following block of code - set the cookie parameters to match what you have for the cookie to be expired - Use the SignOut event in [...nextauth].js file
export default async function auth(req, res) {
return await NextAuth(req, res, {
...
events: {
async signOut({ token }) {
res.setHeader("Set-Cookie", "cookieName=deleted;Max-Age=0;path=/;Domain=.techtenum.com;");
},
},
...
}
}
You need to configure clientAxios to include cookies that the server sends as part of its response in all requests back to the server. Setting api.defaults.withCredentials = true; should get you what you want. See the axios configuration for my vue application below:
import axios from "axios";
export default ({ Vue, store, router }) => {
const api = axios.create({
baseURL: process.env.VUE_APP_API_URL
});
api.defaults.withCredentials = true; ------> this line includes the cookies
Vue.prototype.$axios = api;
store.$axios = api;
};

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.
});
}
}

Resources