Nuxt.js with Firebase Authentication [JavaScript/TypeScript]


This is a tutorial for introducing Nuxt.js and Bulma with TypeScript.

  • Nuxt.js : Vue.js Meta Framework to create complex, fast & universal web applications quickly.
  • Bulma : CSS framework based on Flexbox

🚜 TLDR

If you want to use this project, please copy as follows:

https://github.com/morizyun/nuxt-edge-serverless-firebase-auth-template

🏀 Installation

git clone https://github.com/jeehyukwon/nuxt-edge-serverless-template.git myApp

🍄 Start Server

cd myApp

# Install npm dependencies
yarn

# install additional dependencies for development
yarn add @nuxtjs/bulma @nuxtjs/dotenv

# install firebase dependencies
yarn add firebase firebase-admin

# start server
yarn run dev

After executing above commands, please execute open http:localhost:3000 or see the URL in your browser.

nuxt-js-first-myApp

😸 Create Firebase Authentication Project

It will be needs for using Firebase authentication.

After then, get the secret information by Firebase Admin SDK to Your Server.

Set the above credential information to .env.

🎂 configuration

.env

FIREBASE_SERVER_CLIENT_EMAIL=xxx
FIREBASE_SERVER_DATABASE_URL=xxx
FIREBASE_SERVER_PRIVATE_KEY=xxx
FIREBASE_SERVER_PROJECT_ID=xxx

FIREBASE_CLIENT_API_KEY=xxx
FIREBASE_CLIENT_AUTH_DOMAIN=xxx
FIREBASE_CLIENT_DATABASE_URL=xxx
FIREBASE_CLIENT_MESSAGING_SENDER_ID=xxx
FIREBASE_CLIENT_PROJECT_ID=xxx
FIREBASE_CLIENT_STORAGE_BUCKET=xxx

nuxt.config.js

const cookieParser = require("cookie-parser");
const yaml = require("require-yml");
require("dotenv").config();

const serverlessConfig = yaml("./serverless.yml");
const awsRegion = serverlessConfig.provider.region;
const awsS3AssetsBucketName =
serverlessConfig.resources.Resources.AssetsBucket.Properties.BucketName;

module.exports = {
apollo: { clientConfigs: { default: "~/apollo/clientConfigs/default.ts" } },
build: {
extractCSS: true,
publicPath: `https://s3.${awsRegion}.amazonaws.com/${awsS3AssetsBucketName}/`,
extend(config, { isServer }) {
const tsLoader = {
exclude: [/vendor/, /\.nuxt/],
loader: "ts-loader",
options: { appendTsSuffixTo: [/\.vue$/], transpileOnly: true }
};
config.module.rules.push({
test: /((client|server)\.js)|(\.tsx?)$/,
...tsLoader
});
config.resolve.extensions.push(".ts");
config.module.rules.map(rule => {
if (rule.loader === "vue-loader") {
rule.options.loaders = { ts: tsLoader };
}
return rule;
});
if (isServer) {
config.externals = [];
}
}
},
env: {
FIREBASE_CLIENT_API_KEY: process.env.FIREBASE_CLIENT_API_KEY,
FIREBASE_CLIENT_AUTH_DOMAIN: process.env.FIREBASE_CLIENT_AUTH_DOMAIN,
FIREBASE_CLIENT_DATABASE_URL: process.env.FIREBASE_CLIENT_DATABASE_URL,
FIREBASE_CLIENT_MESSAGING_SENDER_ID:
process.env.FIREBASE_CLIENT_MESSAGING_SENDER_ID,
FIREBASE_CLIENT_PROJECT_ID: process.env.FIREBASE_CLIENT_PROJECT_ID,
FIREBASE_CLIENT_STORAGE_BUCKET: process.env.FIREBASE_CLIENT_STORAGE_BUCKET
},
extensions: ["js", "ts"],
head: {
meta: [
{ charset: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{
content: "Nuxt-edge Serverless Template",
hid: "description",
name: "description"
}
],
title: "Nuxt Edge Serverless Template"
},
loading: { color: "#51cf66" },
modules: ["@nuxtjs/apollo", "@nuxtjs/bulma", "@nuxtjs/dotenv"],
plugins: [
{ src: "~/plugins/firebase-client-init.ts", ssr: false },
{ src: "~/plugins/auth-cookie.ts", ssr: false }
],
render: {
etag: false,
// Disabled gzip compression
gzip: { threshold: 1073741824 }
},
router: {
middleware: "router-auth"
},
serverMiddleware: [
cookieParser(),
"~/serverMiddleware/validateFirebaseIdToken"
],
srcDir: "src/",
vendor: ["firebase"]
};

🤔 Create Application

src/apollo/clientConfigs/default.ts

export default context => {
return {
httpEndpoint: "https://localhost:8080/graphql"
};
};

src/components/LoginForm.vue

<template>
<div>
<h2 class="title">Sign In</h2>

<div v-if="errorMessage" class="notification is-danger">errorMessage: {{ errorMessage }}</div>
<form @submit.prevent="emailLogin">
<div class="field">
<label class="label">Email: </label>
<input type="text" class="input" v-model="email">
</div>
<div class="field">
<label class="label">Password: </label>
<input type="password" class="input" v-model="password">
</div>
<input type="submit" class="button" value="button">
</form>
</div>
</template>

<script lang="ts">
import { Component, Vue } from "nuxt-property-decorator";

@Component()
export default class LoginForm extends Vue {
email = "";
password = "";
errorMessage = "";

emailLogin() {
if (!this.email || !this.password) {
this.errorMessage = "Invalid email or password!";
return;
}

this.$store
.dispatch("user/signInWithEmail", {
email: this.email,
password: this.password
})
.then(() => {
if (process.client) {
window.location.reload();
}
})
.catch(e => {
this.errorMessage = e.message;
});
}
}
</script>

src/components/Navbar.vue

<template>
<nav class="navbar is-white" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<div class="navbar-item">Site Name</div>
<div class="navbar-burger" data-target="navMenu" @click="this.toggleMenu" :class="{'is-active': isMenuActive}">
<span></span>
<span></span>
<span></span>
</div>
</div>
<div class="navbar-menu" id="navMenu" :class="{'is-active': isMenuActive}">
<div class="navbar-end">
<nuxt-link to="/" class="navbar-item">Top</nuxt-link>
<a v-if="user" class="navbar-item" @click="this.signOut">Logout</a>
<nuxt-link v-if="!user" to="/login" class="navbar-item">Login</nuxt-link>
</div>
</div>
</nav>
</template>

<script lang="ts">
import { Component, Vue } from "nuxt-property-decorator";
import { State } from "vuex-class";

@Component
export default class Index extends Vue {
@State(state => state.navbar.isMenuActive)
private isMenuActive: boolean;

@State(state => state.user.user)
private user: object;

toggleMenu() {
this.$store.dispatch("navbar/toggleMenu");
}

signOut() {
this.$store.dispatch("user/signOut");
}
}
</script>

src/layouts/default.vue

<template>
<div>
<navbar/>
<nuxt/>
</div>
</template>

<script>
import Navbar from "~/components/Navbar.vue";

export default {
components: {
Navbar
}
};
</script>

src/middleware/router-check.ts

export default function({ store, redirect, route }) {
if (!store.state.user.user) {
return redirect("/login");
} else if (route.path === "/login") {
return redirect("/");
}
}

src/pages/index.vue

<template>
<div>
<section class="hero is-primary is-bold">
<div class="hero-body">
<h1 class="title is-size-2">
Top Page
</h1>
</div>
</section>
</div>
</template>

<script lang="ts">
import { Component, Vue } from "nuxt-property-decorator";
import { State } from "vuex-class";
import LoginForm from "~/components/LoginForm";

@Component({
components: {
LoginForm
}
})
export default class Index extends Vue {
@State(state => state.user.user)
private user: object;

@State(state => state.user.loadingUser)
private loadingUser: boolean;
}
</script>

src/pages/login.vue

<template>
<div>
<section class="hero is-primary is-bold">
<div class="hero-body">
<h1 class="title is-size-2">
Login Page
</h1>
</div>
</section>
<loginForm />
</div>
</template>

<script lang="ts">
import { Component, Vue } from "nuxt-property-decorator";
import LoginForm from "~/components/LoginForm";

@Component({
components: {
LoginForm
}
})
export default class Login extends Vue {}
</script>

src/plugins/auth-cookie.ts

import { Auth } from "./firebase-client-init";

export default context => {
(Auth as any).addAuthTokenListener(idToken => {
document.cookie =
"__session=" + idToken + ";max-age=" + (idToken ? 3600 : 0);
});
};

src/plugins/firebase-client-init.ts

import * as firebase from "firebase/app";
import "firebase/auth";

const config = {
apiKey: process.env.FIREBASE_CLIENT_API_KEY,
authDomain: process.env.FIREBASE_CLIENT_AUTH_DOMAIN,
databaseURL: process.env.FIREBASE_CLIENT_DATABASE_URL,
messagingSenderId: process.env.FIREBASE_CLIENT_MESSAGING_SENDER_ID,
projectId: process.env.FIREBASE_CLIENT_PROJECT_ID,
storageBucket: process.env.FIREBASE_CLIENT_STORAGE_BUCKET
};

export default (!firebase.apps.length
? firebase.initializeApp(config)
: firebase.app());
export const Auth: firebase.auth.Auth = firebase.auth();

src/serverMiddleware/validateFirebaseIdToken.ts

import cookieParser from "cookie-parser";
import admin from "firebase-admin";
import * as xml from "xmlhttprequest";
// @ts-ignore
global.XMLHttpRequest = xml.XMLHttpRequest;

const AdminApp = admin.initializeApp({
credential: admin.credential.cert({
clientEmail: process.env.FIREBASE_SERVER_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_SERVER_PRIVATE_KEY,
projectId: process.env.FIREBASE_SERVER_PROJECT_ID
}),
databaseURL: process.env.FIREBASE_SERVER_DATABASE_URL
});

module.exports = (req, res, next) => {
getIdTokenFromRequest(req, res).then(idToken => {
if (idToken) {
addDecodedIdTokenToRequest(idToken, req).then(() => {
next();
});
} else {
next();
}
});
};

function getIdTokenFromRequest(req, res) {
if (
req.headers.authorization &&
req.headers.authorization.startsWith("Bearer ")
) {
return Promise.resolve(req.headers.authorization.split("Bearer ")[1]);
}
return new Promise(resolve => {
cookieParser()(req, res, () => {
if (req.cookies && req.cookies.__session) {
resolve(req.cookies.__session);
} else {
resolve();
}
});
});
}

/**
* Returns a Promise with the Decoded ID Token and adds it to req.user.
*/
function addDecodedIdTokenToRequest(idToken, req) {
return AdminApp.auth()
.verifyIdToken(idToken)
.then(decodedIdToken => {
req.user = decodedIdToken;
})
.catch(error => {
console.error("Error while verifying Firebase ID token:", error);
});
}

src/store/index.ts

export const actions = {
async nuxtServerInit({ commit }, { req }) {
if (req.user) {
commit("user/setUser", req.user);
}
}
};

src/store/navbar.ts

export const state = () => ({
isMenuActive: false
});

export const mutations = {
toggleMenu(state) {
state.isMenuActive = !state.isMenuActive;
}
};

export const actions = {
async toggleMenu(ctx) {
ctx.commit("toggleMenu");
},
};

src/store/user.ts

import { Auth } from "~/plugins/firebase-client-init";

export const state = () => ({
loadingUser: true,
user: null
});

export const mutations = {
setUser(state, payload) {
state.user = payload;
state.loadingUser = false;
}
};

export const actions = {
async signInWithEmail({ commit }, { email, password }) {
return new Promise((resolve, reject) => {
Auth.signInWithEmailAndPassword(email, password)
.then(res => {
resolve();
})
.catch(err => reject(err));
});
},

setUser({ commit }, user) {
commit("setUser", user);
},

signOut({ commit }) {
Auth.signOut()
.then(() => {
commit("setUser", null);
// @ts-ignore
if (process.client) {
window.location.reload();
}
})
.catch(err => console.error(err));
}
};

🏈 Result

After applying the above code, you can see the following sample app:

nuxt-with-firebase-authentication-bulma-dotenv

🐞 Reference

🖥 Recommended VPS Service

VULTR provides high performance cloud compute environment for you. Vultr has 15 data-centers strategically placed around the globe, you can use a VPS with 512 MB memory for just $ 2.5 / month ($ 0.004 / hour). In addition, Vultr is up to 4 times faster than the competition, so please check it => Check Benchmark Results!!