Take a look at the following HTTPS function. It performs the following tasks:
- 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).
- Pulls the user's UID out of the decoded token.
- 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.
- 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.
- The Admin SDK is used along with
userApp
to make a database query to some protect path.
- If the query was successful, remember the response to send to the cleint.
- If the query failed due to security rues, remember an error response to send to the client.
- 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.
- 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 => {
const uid = decoded.uid
const options = Object.assign({}, functions.config().firebase)
options.databaseAuthVariableOverride = { uid }
userApp = admin.initializeApp(options, 'user')
return admin.database(userApp).ref("/some/protected/path").once('value')
})
.then(snapshot => {
response = snapshot.val()
return null
})
.catch(error => {
console.error('error', error)
isError = true
response = error
return null
})
.then(() => {
if (userApp) {
return userApp.delete()
}
else {
return null
}
})
.then(() => {
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.