At JoyLabs we wanted to deploy a Jekyll site, but only allow authenticated users access to it. GitHub Pages is a very convenient way to deploy Jekyll-based static sites, but, unfortunately, it’s impossible to restrict access to only authenticated users.

After some research, it seemed that Firebase would provide us everything we need:

  • Static file hosting
  • Authentication
  • Cloud functions

Unfortunately, out of the box, Firebase doesn’t support content restriction to authenticated users. However, with a simple cloud function, we can do just that.

Let’s start with creation of the Firebase project. To create a new Firebase project, visit the Firebase Console and add a new project.

Add Firebase project

Then, inside the project, visit the Hosting section and follow instructions to install firebase-tools

$ npm install -g firebase-tools
$ firebase login
$ firebase init

Select Functions and Hosting:

Firebase init

Now, let’s try to make a test deploy:

$ firebase deploy

Next, visit our test site. Nice! Our site is deployed, but it’s wide open to the public. Let’s add authentication, now. Luckily, Firebase supports authentication out of the box, so we can let users authenticate on our site easily. To enable authentication on a Firebase-hosted site, visit the Authentication section in the firebase console and set up a sign-in method. For now, just enable the Google authentication provider, then click Web Setup in top right corner and get firebase JavaScript config:

<script src="https://www.gstatic.com/firebasejs/5.8.2/firebase.js"></script>
<script>
  // Initialize Firebase
  var config = {
    apiKey: "AIzaSyCQdOWtObJQ0oWuvOmb-b-BRdd5mQgYKUg",
    authDomain: "fir-auth-example-a4f18.firebaseapp.com",
    databaseURL: "https://fir-auth-example-a4f18.firebaseio.com",
    projectId: "fir-auth-example-a4f18",
    storageBucket: "fir-auth-example-a4f18.appspot.com",
    messagingSenderId: "983265458691"
  };
  firebase.initializeApp(config);
</script>

Create a login.html page under the public directory and put firebase init scripts there. Now we can trigger google authentication:

var provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider).then(function (result) {
    // This gives you a Google Access Token. You can use it to access the Google API.
    var token = result.credential.accessToken;
    // The signed-in user info.
    var user = result.user;
    firebase.auth().currentUser.getIdToken(/* forceRefresh */ true).then(function (idToken) {
        window.location.href = "/?idToken=" + idToken;
    }).catch(function (error) {
        // Handle error
    });
    // ...
}).catch(function (error) {
    // Handle Errors here.
    var errorCode = error.code;
    var errorMessage = error.message;
    // The email of the user's account used.
    var email = error.email;
    // The firebase.auth.AuthCredential type that was used.
    var credential = error.credential;
    alert("Authentication error");
});

This code will let us retrieve an ID Token, which we can decode and verify in our cloud function (that we have yet to write). Before moving on, let’s check that our login pop-up works, visiting https://fir-auth-example-a4f18.firebaseapp.com/login.html (don’t forget to deploy the site first).

Now we are able to authenticate a user, but everything is still publicly accessible. Our workaround is to have only login.html deployed to be accessible to everyone and serve everything else from GCS, using cloud function as a proxy that will also check if the user is authenticated. Let’s move everything except login.html to new folder content. We will upload this folder to private GCS bucket, not accessible to the outside world. We have to redirect all requests to our cloud function in firebase.json:

"rewrites": [
    {
    "source": "**",
    "function": "authorizeAccess"
    },

Now, copy our hidden content to GCS bucket:

gsutil -m cp -r content/* gs://fir-auth-example-a4f18.appspot.com/

And create our proxy cloud function functions/index.js:

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

const express = require('express');
const cookieParser = require('cookie-parser')();
const cors = require('cors')({origin: true});
const {Storage} = require('@google-cloud/storage');

const app = express();
app.use(cors);
app.use(cookieParser);

const expiresIn = 60 * 60 * 24 * 1000 * 1; // cookie expires in 1 day

const gcs = new Storage();
const bucket = gcs.bucket("fir-auth-example-a4f18.appspot.com");

function serveContent(request, response) {
    var path = request.path;
    if (path == "/favicon.ico") {
        return response.status(404).send('Not Found');
    }
    if (path.endsWith("/")) {
        path += "index.html";
    }
    var file = bucket.file(path);
    file.createReadStream().pipe(response);
}

app.get('/*', (request, response) => {
    const sessionCookie = request.cookies ? request.cookies.__session || '' : '';
    const idToken = request.query.idToken;
    if (sessionCookie) {
        admin.auth().verifySessionCookie(sessionCookie, false /** checkRevoked */).then((decodedClaims) => {
            return serveContent(request, response);
        }).catch(error => {
            // Session cookie is unavailable or invalid. Force user to login.
            return response.redirect('/login.html');
        });
    } else if (idToken) {
        admin.auth().verifyIdToken(idToken)
            .then(function (decodedToken) {
                if (decodedToken.email.endsWith("@gmail.com") && new Date().getTime() / 1000 - decodedToken.auth_time < 5 * 60) {
                    // Create session cookie and set it.
                    admin.auth().createSessionCookie(idToken, {expiresIn}).then((sessionCookie) => {
                        // Set cookie policy for session cookie.
                        const options = {maxAge: expiresIn, httpOnly: true, secure: false};
                        response.cookie('__session', sessionCookie, options);
                        response.redirect('/');
                        response.end();
                    }, error => {
                        alert(error);
                        return response.redirect('/login.html');
                    });
                } else {
                    alert("Not authorized!");
                    return response.redirect('/login.html');
                }
            }).catch(function (error) {
                alert(error);
                return response.redirect('/login.html');
            });
    } else {
        alert("Not authorized!");
        return response.redirect('/login.html');
    }
});

exports.authorizeAccess = functions.https.onRequest(app);

Now we can firebase deploy our site and, voilà!; our static website is protected with google authentication (pop-ups have to be enabled for authentication to work; however, firebase also supports inline authentication widget).

All code on this page is provided for illustrative purposes only. These examples have not been thoroughly tested under all conditions. JoyLabs cannot guarantee or imply reliability, serviceability, or function of these programs.