18
votes

I am writing a Firebase application in vanilla JavaScript. I am using Firebase Authentication and FirebaseUI for Web. I am using Firebase Cloud Functions to implement a server that receives requests for my page routes and returns rendered HTML. I am struggling to find the best practice for utilizing my authenticated ID tokens on the client side to access protected routes served by my Firebase Cloud Function.

I believe I understand the basic flow: the user logs in, which means an ID token is sent to the client, where it is received in the onAuthStateChanged callback and then inserted into the Authorization field of any new HTTP request with the proper prefix, and then checked by the server when the user attempts to access a protected route.

I do not understand what I should do with the ID token inside the onAuthStateChanged callback, or how I should modify my client side JavaScript to modify the request headers when necessary.

I am using Firebase Cloud Functions to handle routing requests. Here is my functions/index.js, which exports the app method that all requests are redirected to and where ID tokens are checked:

const functions = require('firebase-functions')
const admin = require('firebase-admin')
const express = require('express')
const cookieParser = require('cookie-parser')
const cors = require('cors')

const app = express()
app.use(cors({ origin: true }))
app.use(cookieParser())

admin.initializeApp(functions.config().firebase)

const firebaseAuthenticate = (req, res, next) => {
  console.log('Check if request is authorized with Firebase ID token')

  if ((!req.headers.authorization || !req.headers.authorization.startsWith('Bearer ')) &&
    !req.cookies.__session) {
    console.error('No Firebase ID token was passed as a Bearer token in the Authorization header.',
      'Make sure you authorize your request by providing the following HTTP header:',
      'Authorization: Bearer <Firebase ID Token>',
      'or by passing a "__session" cookie.')
    res.status(403).send('Unauthorized')
    return
  }

  let idToken
  if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
    console.log('Found "Authorization" header')
    // Read the ID Token from the Authorization header.
    idToken = req.headers.authorization.split('Bearer ')[1]
  } else {
    console.log('Found "__session" cookie')
    // Read the ID Token from cookie.
    idToken = req.cookies.__session
  }

  admin.auth().verifyIdToken(idToken).then(decodedIdToken => {
    console.log('ID Token correctly decoded', decodedIdToken)
    console.log('token details:', JSON.stringify(decodedIdToken))

    console.log('User email:', decodedIdToken.firebase.identities['google.com'][0])

    req.user = decodedIdToken
    return next()
  }).catch(error => {
    console.error('Error while verifying Firebase ID token:', error)
    res.status(403).send('Unauthorized')
  })
}

const meta = `<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link type="text/css" rel="stylesheet" href="https://cdn.firebase.com/libs/firebaseui/2.6.0/firebaseui.css" />

const logic = `<!-- Intialization -->
<script src="https://www.gstatic.com/firebasejs/4.10.0/firebase.js"></script>
<script src="/init.js"></script>

<!-- Authentication -->
<script src="https://cdn.firebase.com/libs/firebaseui/2.6.0/firebaseui.js"></script>
<script src="/auth.js"></script>`

app.get('/', (request, response) => {
  response.send(`<html>
  <head>
    <title>Index</title>

    ${meta}
  </head>
  <body>
    <h1>Index</h1>

    <a href="/user/fake">Fake User</a>

    <div id="firebaseui-auth-container"></div>

    ${logic}
  </body>
</html>`)
})

app.get('/user/:name', firebaseAuthenticate, (request, response) => {
  response.send(`<html>
  <head>
    <title>User - ${request.params.name}</title>

    ${meta}
  </head>
  <body>
    <h1>User ${request.params.name}</h1>

    ${logic}
  </body>
</html>`)
})

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

Her is my functions/package.json, which describes the configuration of the server handling HTTP requests implemented as a Firebase Cloud Function:

{
  "name": "functions",
  "description": "Cloud Functions for Firebase",
  "scripts": {
    "lint": "./node_modules/.bin/eslint .",
    "serve": "firebase serve --only functions",
    "shell": "firebase experimental:functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "dependencies": {
    "cookie-parser": "^1.4.3",
    "cors": "^2.8.4",
    "eslint-config-standard": "^11.0.0-beta.0",
    "eslint-plugin-import": "^2.8.0",
    "eslint-plugin-node": "^6.0.0",
    "eslint-plugin-standard": "^3.0.1",
    "firebase-admin": "~5.8.1",
    "firebase-functions": "^0.8.1"
  },
  "devDependencies": {
    "eslint": "^4.12.0",
    "eslint-plugin-promise": "^3.6.0"
  },
  "private": true
}

Here is my firebase.json, which redirects all page requests to my exported app function:

{
  "functions": {
    "predeploy": [
      "npm --prefix $RESOURCE_DIR run lint"
    ]
  },
  "hosting": {
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "function": "app"
      }
    ]
  }
}

Here is my public/auth.js, where the token is requested and received on the client. This is where I get stuck:

/* global firebase, firebaseui */

const uiConfig = {
  // signInSuccessUrl: '<url-to-redirect-to-on-success>',
  signInOptions: [
    // Leave the lines as is for the providers you want to offer your users.
    firebase.auth.GoogleAuthProvider.PROVIDER_ID,
    // firebase.auth.FacebookAuthProvider.PROVIDER_ID,
    // firebase.auth.TwitterAuthProvider.PROVIDER_ID,
    // firebase.auth.GithubAuthProvider.PROVIDER_ID,
    firebase.auth.EmailAuthProvider.PROVIDER_ID
    // firebase.auth.PhoneAuthProvider.PROVIDER_ID
  ],
  callbacks: {
    signInSuccess () { return false }
  }
  // Terms of service url.
  // tosUrl: '<your-tos-url>'
}
const ui = new firebaseui.auth.AuthUI(firebase.auth())
ui.start('#firebaseui-auth-container', uiConfig)

firebase.auth().onAuthStateChanged(function (user) {
  if (user) {
    firebase.auth().currentUser.getIdToken().then(token => {
      console.log('You are an authorized user.')

      // This is insecure. What should I do instead?
      // document.cookie = '__session=' + token
    })
  } else {
    console.warn('You are an unauthorized user.')
  }
})

What should I do with authenticated ID tokens on the client side?

Cookies/localStorage/webStorage do not seem to be fully securable, at least not in any relatively simple and scalable way that I can find. There may be a simple cookie-based process which is as secure as directly including the token in a request header, but I have not been able to find code I could easily apply to Firebase for doing so.

I know how to include tokens in AJAX requests, like:

var xhr = new XMLHttpRequest()
xhr.open('GET', URL)
xmlhttp.setRequestHeader("Authorization", 'Bearer ' + token)
xhr.onload = function () {
    if (xhr.status === 200) {
        alert('Success: ' + xhr.responseText)
    }
    else {
        alert('Request failed.  Returned status of ' + xhr.status)
    }
}
xhr.send()

However, I don't want to make a single page application, so I cannot use AJAX. I cannot figure out how to insert the token into the header of normal routing requests, like the ones triggered by clicking on an anchor tag with a valid href. Should I intercept these requests and modify them somehow?

What is the best practice for scalable client side security in a Firebase for Web application that is not a single page application? I do not need a complex authentication flow. I am willing to sacrifice flexibility for a security system I can trust and implement simply.

3
This depends on your app architecture (are you using other firebase services, single page app or traditional web app, hosting your own server, etc). One option would be after sign-in, send the ID token to your backend and verify at. You can check the auth_time for recent sign in and then issue a session cookie using expressjs session.bojeil
My only backend is an express server hosted in a firebase cloud function. I don't want to do a single page app. The express server builds an html string and sends it directly to the user as shown in my code. Is there a way to use a a cookie's auth_time to fully secure the routing? Can you provide an answer showing how to do so?David Y. Stephenson
provide more info about your setup, post package.json if possibleMunim Munna
I have included my package.json and firebase.json. My post now includes all of my application other than my public/init.js which just runs the initialization code copied from the Firebase Console. On the server side, a Firebase Cloud Function receives all requests and responds with rendered HTML. I use the code from the FirebaseUI documentation to authenticate requests for protected routes. On the client side, users login with FirebaseUI and receive an ID token. They need to send that token in their HTTP requests to the server to access protected routes. How can I in a fully secure way?David Y. Stephenson
Why do you think cookies are not secured? what are you concerned about?Munim Munna

3 Answers

5
votes

Why cookies are not secured?

  1. Cookie data can be easily tempered with, if a developer is stupid enough to store logged in user's role in cookie, user can easily alter his cookie data, document.cookie = "role=admin". (voila!)
  2. ‎Cookie data can be easily picked up by a hacker by XSS attack and he can login to your account.
  3. ‎Cookie data can be easily collected from your browser, and your roommate can steal your cookie and login as you from his computer.
  4. ‎Anyone who is monitoring your network traffic can collect your cookie if you are not using SSL.

Do you need to be concerned?

  1. We are not storing anything stupid in the cookie the user can modify to gain any unauthorized access.
  2. ‎If a hacker can pick-up cookie data by XSS attack, he can also pickup the Auth token if we don't use single page application (because we will be storing the token somewhere eg localstorage).
  3. ‎Your roommate can also pickup your localstorage data.
  4. ‎Anyone monitoring your network can also pickup your Authorization header unless you use SSL. Cookie and Authorization are both sent as plain text in http header.

What should we do?

  1. If we are storing the token somewhere, there is no security advantage over cookies, Auth token are best suited for single page applications adding additional security or where cookies are not an available option.
  2. ‎If we are concerned of someone monitoring network traffic, we should host our site with SSL. Cookies and http-headers cannot be intercepted if SSL is used.
  3. ‎If we are using single page application, we should not store the token anywhere, just keep it in a JS variable and create ajax request with Authorization header. If you are using jQuery you can add a beforeSend handler to the global ajaxSetup that sends the Auth token header whenever you make any ajax request.

    var token = false; /* you will set it when authorized */
    $.ajaxSetup({
        beforeSend: function(xhr) {
            /* check if token is set or retrieve it */
            if(token){
                xhr.setRequestHeader('Authorization', 'Bearer ' + token);
            }
        }
    });
    

If we want to use Cookies

If we don't want to implement a single page application and stick to cookies, then there are two options to choose from.

  1. Non-Persistent (or session) cookies: Non-persistent cookies has no max-life/expiration date and gets deleted when the user closes browser window, thus making it so much preferable in situations where security is concerned.
  2. Persistent cookies: Persistent cookies are those with a max-life/expiration date. These cookies persist until the time period is over. Persistent cookies are preferred when you want the cookie to exist even if the user closes the browser and comes back next day, thus preventing authentication every time and improving user's experience.
document.cookie = '__session=' + token  /* Non-Persistent */
document.cookie = '__session=' + token + ';max-age=' + (3600*24*7) /* Persistent 1 week */

Persistent or Non-Persistent which one to use, the choice is completely the project dependent. And in case of Persistent cookies the max-age should be balanced, it should not be a month, or an hour. 1 or 2 weeks look better option to me.

4
votes

You're overly skeptical of storing the Firebase ID token in a cookie. By storing it in a cookie, it would be sent with every request to your Firebase Cloud function.

Firebase ID token:

Created by Firebase when a user signs in to a Firebase app. These tokens are signed JWTs that securely identify a user in a Firebase project. These tokens contain basic profile information for a user, including the user's ID string, which is unique to the Firebase project. Because the integrity of ID tokens can be verified, you can send them to a backend server to identify the currently signed-in user.

As its stated in the definition of a Firebase ID token, the integrity of the token can be verified, so it should be safe to store and send to your server. The problem arises in that you don't want to be needing to provide this token in the Authentication header for every request to your Firebase Cloud Function, since you want to avoid using AJAX requests for routing.

This brings it back to utilizing cookies, since cookies are automatically sent with server requests. They're not as dangerous as you're thinking they are. Firebase even has an example application called "Server-side generated pages w/ Handlebars templating and user sessions" that utilizes session cookies for sending the Firebase ID Token.

You can see their example of this here:

// Express middleware that checks if a Firebase ID Tokens is passed in the `Authorization` HTTP
// header or the `__session` cookie and decodes it.
// The Firebase ID token needs to be passed as a Bearer token in the Authorization HTTP header like this:
// `Authorization: Bearer <Firebase ID Token>`.
// When decoded successfully, the ID Token content will be added as `req.user`.
const validateFirebaseIdToken = (req, res, next) => {
    console.log('Check if request is authorized with Firebase ID token');

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

/**
 * Returns a Promise with the Firebase ID Token if found in the Authorization or the __session cookie.
 */
function getIdTokenFromRequest(req, res) {
    if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
        console.log('Found "Authorization" header');
        // Read the ID Token from the Authorization header.
        return Promise.resolve(req.headers.authorization.split('Bearer ')[1]);
    }
    return new Promise((resolve, reject) => {
        cookieParser(req, res, () => {
            if (req.cookies && req.cookies.__session) {
                console.log('Found "__session" cookie');
                // Read the ID Token from cookie.
                resolve(req.cookies.__session);
            } else {
                resolve();
            }
        });
    });
}

This would allow you to not need AJAX and allow routes to be handled by your Firebase Cloud Function. Just be sure to take a look at Firebase's template where they're checking the header on every page.

<script>
    function checkCookie() {
    // Checks if it's likely that there is a signed-in Firebase user and the session cookie expired.
    // In that case we'll hide the body of the page until it will be reloaded after the cookie has been set.
    var hasSessionCookie = document.cookie.indexOf('__session=') !== -1;
    var isProbablySignedInFirebase = typeof Object.keys(localStorage).find(function (key) {
            return key.startsWith('firebase:authUser')
}) !== 'undefined';
    if (!hasSessionCookie && isProbablySignedInFirebase) {
        var style = document.createElement('style');
    style.id = '__bodyHider';
        style.appendChild(document.createTextNode('body{display: none}'));
    document.head.appendChild(style);
}
}
checkCookie();
    document.addEventListener('DOMContentLoaded', function() {
        // Make sure the Firebase ID Token is always passed as a cookie.
        firebase.auth().addAuthTokenListener(function (idToken) {
            var hadSessionCookie = document.cookie.indexOf('__session=') !== -1;
            document.cookie = '__session=' + idToken + ';max-age=' + (idToken ? 3600 : 0);
            // If there is a change in the auth state compared to what's in the session cookie we'll reload after setting the cookie.
            if ((!hadSessionCookie && idToken) || (hadSessionCookie && !idToken)) {
                window.location.reload(true);
            } else {
                // In the rare case where there was a user but it could not be signed in (for instance the account has been deleted).
                // We un-hide the page body.
                var style = document.getElementById('__bodyHider');
                if (style) {
                    document.head.removeChild(style);
                }
            }
        });
    });
</script>
0
votes

Use Generating a Secure Token libraries and add token directly (Custom auth payload):

var token = tokenGenerator.createToken({ "uid": "1234", "isModerator": true });

Your token data is uid (or app_user_id) and isModerator inside expression of rule, for example:

{
  "rules": {
    ".read": true,
    "$comment": {
      ".write": "(!data.exists() && newData.child('user_id').val() == auth.uid) || auth.isModerator == true"
    }
  }
}