Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to protect firebase Cloud Function HTTP endpoint using authenticated id token and database rules?

admin.auth().verifyIdToken(tokenId)
      .then((decoded) => res.status(200).send(decoded))

I understand verifyIdToken() can verify a user id token from Firebase Authentication clients. However we need to protect our Cloud Function by making sure database queries are limited by the security rules defined for the database for the user identified in the token. Given that the admin SDK has unlimited access by default, how to I limit its access to just that of the authenticated user?

like image 389
JLIN Avatar asked Feb 02 '18 04:02

JLIN


1 Answers

Take a look at the following HTTPS function. It performs the following tasks:

  1. Verifies a Firebase Authentication ID token using the Admin SDK. The token comes from the query string (but you should use a better solution to transmit the token).
  2. Pulls the user's UID out of the decoded token.
  3. Makes a copy of the default Firebase init configuration object, then adds a property called databaseAuthVariableOverride to it, using the UID, to limit the privileges of the caller.
  4. Initializes a new non-default instance of an App object (named "user") with the new options. This App object can now be used to access the database while observing security rules in place for that user.
  5. The Admin SDK is used along with userApp to make a database query to some protect path.
  6. If the query was successful, remember the response to send to the cleint.
  7. If the query failed due to security rues, remember an error response to send to the client.
  8. Clean up this instance of the Admin SDK. This code takes all precautions to make sure userApp.delete() is called in all circumstances. Don't forget to do this, or you will leak memory as more users access this function.
  9. Actually send the response. This terminates the function.

Here's a working function:

const admin = require("firebase-admin")
admin.initializeApp()

exports.authorizedFetch = functions.https.onRequest((req, res) => {
    let userApp
    let response
    let isError = false

    const token = req.query['token']

    admin.auth().verifyIdToken(token)
    .then(decoded => {
        // Initialize a new instance of App using the Admin SDK, with limited access by the UID
        const uid = decoded.uid
        const options = Object.assign({}, functions.config().firebase)
        options.databaseAuthVariableOverride = { uid }
        userApp = admin.initializeApp(options, 'user')
        // Query the database with the new userApp configuration
        return admin.database(userApp).ref("/some/protected/path").once('value')
    })
    .then(snapshot => {
        // Database fetch was successful, return the user data
        response = snapshot.val()
        return null
    })
    .catch(error => {
        // Database fetch failed due to security rules, return an error
        console.error('error', error)
        isError = true
        response = error
        return null
    })
    .then(() => {
        // This is important to clean up, returns a promise
        if (userApp) {
            return userApp.delete()
        }
        else {
            return null
        }
    })
    .then(() => {
        // send the final response
        if (isError) {
            res.status(500)
        }
        res.send(response)
    })
    .catch(error => {
        console.error('final error', error)
    })
})

Again, note that userApp.delete() should be called in all circumstances to avoid leaking instances of App. If you had the idea to instead give each new App a unique name based on the user, that's not a good idea, because you can still run out of memory as new users keep accessing this function. Clean it up with each call to be safe.

Also note that userApp.delete() should be called before the response is sent, because sending a response terminates the function, and you don't want to have the cleanup interrupted for any reason.

like image 127
Doug Stevenson Avatar answered Sep 22 '22 14:09

Doug Stevenson