javascriptsecurityfirebasefirebase-authenticationfirebaseui

Handling Firebase ID tokens on the client side with vanilla JavaScript


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.


Solution

  • 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.