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.
Why cookies are not secured?
document.cookie = "role=admin"
. (voila!) Do you need to be concerned?
What should we do?
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.
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.