5
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?

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
JLIN
  • 53
  • 1
  • 3
  • 1
    Could you edit your question to explain what it means to "protect" a function? That is kind of vague. – Doug Stevenson Feb 02 '18 at 04:43
  • We want to make sure our http trigger api endpoint's database read/write access follows the database-rules.json in firebase. – JLIN Feb 02 '18 at 17:38
  • Our first attempt is using firebase web (signed in with token) instead of firebase admin in some of our get/update/set/remove accesses in our http trigger apis. It works but it is not scalable. – JLIN Feb 02 '18 at 17:41
  • I'm curious to know what was not scalable about that? (It's definitely not the best solution, but what are you measuring?) – Doug Stevenson Feb 02 '18 at 17:47
  • ` const firebase = require('firebase'); const userGet = (validFirebasePath) => firebaseDatabase .database() .ref(validFirebasePath) .once('value') .then(snapshot => snapshot.val()); exports.apiHandler = (request, response) => { // we get accessToken from the request getCustomToken(accessToken) .then((customToken) => firebase.auth().signInWithCustomToken(customToken) .then(() => userGet(‘users/userA’)) .then(data => response.json(data)) }; ` Here is our first attempt. – JLIN Feb 02 '18 at 18:04
  • 1. signInWithCustomToken will slow down every api calls 2. firebase is now signed in as userA, so any other api calls' firebase.auth().currentUser will be userA. – JLIN Feb 02 '18 at 18:08
  • What is the best solution for cloud function http endpoint protected by accessToken and database-rules? Thanks in advance. – JLIN Feb 02 '18 at 19:15

1 Answers1

13

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.

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
  • Can u help, what if someone can intercepts the calls to firebase and take the ID token request done by Identity service API and then use that ID token to acess all https calls and modify things. what can be done to prevent man in the middle attack or secure the cloud functions while am using IOS/Android app with firebase. – Dickson Xavier Oct 17 '20 at 19:47
  • Does this require the invoking role for allUsers on the cloud function? – Chris Lang Oct 20 '21 at 23:34