How to use Firebase Security Rules to secure Cloud Functions calls with Firebase Authentication? - firebase

I'm starting deep on Firebase Security Rules and I'm following the Firebase Security Rules docs, but this document just says about Realtime Database, Cloud Firestore, and Cloud Storage options. There is a way to use Firebase Authentication to protect an invoke of a Google Cloud Function from Client-Side?
I'm trying to use a GC Function as a backend to access the Cloud SQL from a Web Application.

Cloud Functions generally use Admin SDK (or service accounts, application default credentials to access any other services like Cloud SQL) which has complete access to your Firebase project's resources and also bypasses all security rules. That being said you would have to authorize requests yourself. For example, if you are using onCall function:
export const fnName = functions.https.onCall((data, context) => {
const { auth } = context
if (!auth) console.log('User not logged in')
const { uid } = auth;
// UID of user who called the function
// Check if user has access to requested resource
// process request
})
If the caller of function is not authenticated, then context.auth will be undefined.
If your question is if you can prevent the invocation of function at first place, then there's currently no way to do so. You can use Firebase App Check to ensure the function is called from your registered application only.

Related

Accessing firestore from self-hosted server

I need to be able to access (read, write) firestore from my self-hosted server (not on gcloud).
I also need to reserve write permissions to the app hosted on my self-hosted server, and allow noone else to be able to write to the server, so the security rules for write applications are denied by default.
According to this document:
https://firebase.google.com/docs/firestore/client/libraries#server_client_libraries
using a service account should allow me to be able to access all firebase products, including firestore, without any restrictions.
But, as soon as I restrict the security rules, I am unable to write to firestore from my service, instantiated with service account credentials following the steps in this documentation:
https://firebase.google.com/docs/admin/setup#initialize-sdk
I get the following error:
PERMISSION_DENIED: Missing or insufficient permissions.
What do I have to do to allow my application to write to firestore from my own servers?
Update:
I am using java for my backend applications.
Servers (or cloud functions) are secure environment which can be accessed only by you and people you have authorized. You don't actually use the Firebase Client SDKs in a server. Instead you use the Firebase Admin SDK. It uses a service account and has privileged access to your Firebase Project.
What does privileged access mean?
The documentation has explained it all but long story short it has complete access like you to the project. It does not follow any security rules.
If you are implementing the Admin SDK which will deal with API requests coming from your clients then make sure you authenticate them. To do so you would ideally pass the Firebase ID Token from your frontend to the API and verify
it using the Admin SDK which returns a decoded token object containing UID, custom claims and some metadata about that user.
I'm not sure which language you use but here's a simple getting started for NodeJS:
// Install the Admin SDK
npm install firebase-admin
// index.js
const admin = require("firebase-admin")
const path = require("path");
admin.initializeApp({
credential: admin.credential.cert("path/to/key")),
databaseURL: "https://<project-id>.firebaseio.com/"
})
const firestore = admin.firestore()
const auth = admin.auth()
// firebase.firestore() and firebase.auth() in client SDKs
Rest of the syntax is mostly the same as Client SDK.
You can create a service account from the project settings.
The issue that I was facing has already been reported here:
https://github.com/googleapis/java-firestore/issues/431
The fix was an odd one, and cannot be explained without going deep into the SDK and how it works, which I did not have time to check:

Firebase functions authorize only requests from Firebase hosting app

I have a simple Firebase Hosting web application (based on a Vue app) which invokes Firebase Function (Google cloud function):
import firebase from "firebase/app";
import "firebase/functions";
firebase.initializeApp(firebaseConfig);
let functions = firebase.app().functions("us-west4");
let testFunction = functions.httpsCallable("testFunction");
and corresponding functions index.js file:
const functions = require("firebase-functions");
exports.testFunction = functions.region("us-west4").https.onCall(async (data, context) => {
console.log("Very important things here");
return {"response": "data"};
});
From security perspective is it possible to
Allow this invocation only from my domain name (Firebase hosting) myhostedapp.web.app
Check for any kind of authentication (e.g. token) that my JS app provides during the request?
I've tried accessing context.auth property (see docs) buth seems like some kind of service account is required and this cannot be used when called from Firebase hosting web application.
Basically I don't want my function to be publicly accessible (simple invocation via trigger url), so any advice or best practice for securing Firebase Hosting + Functions would be appreciated.
Firebase just released a new feature called App Check that does precisely this: it allows the Cloud Functions in your project to only be invoked from apps that are registered in that project.
For web apps this happens through reCAPTCHA v3, which . Then once you enable enforcement of the check on Cloud Functions, it will reject any requests coming from other sources.
You'll typically want to combine App Check with your current user-based approach, so that you can easily block calls from outside your web app, but also still ensure authenticated users only can make calls that they're authorized for.

How do you read the userInfo of another user with Firebase's cloud functions?

I had in mind to create a cloud function that let a user read some of the user infos of another user under certaine conditions.
For example:
const user1 = ??? // user1 is the current user
const user1Data = await firestore().collection('Users').doc('user1.uid').get()
const user2 = ??? // user2 is the user whith user2.uid == user1Data.partnerUid
const user2Data = await firestore().collection('Users').doc('user2.uid').get()
if (user1Data.partnerEmail == user2.email && user1Data.partnerEmail == user2.email) {
// ...
// the endpoint deliver some of the user2 data to user1.
// ...
}
I have seen the documentation of Cloud functions:
https://firebase.google.com/docs/reference/functions/providers_auth_
I have seen that with the admin API we can call getUser:
admin.auth().getUser(uid)
The difference between functions.auth() and admin.auth() is not clear for me. Can we call admin within cloud functions ?
The difference between functions.auth() and admin.auth() is not clear for me.
When you import functions from firebase-functions, all that gets you is an SDK used for building the definition of functions for deployment. It doesn't do anything else. You can't access user data using functions.
When you import admin from firebase-admin, that gives you access to the Firebase Admin SDK that can actually manage user data in Firebase Authentication. You will want to use this to look up and modify users as needed, and it works just fine when running code in Cloud Functions.
The difference between functions.auth() and admin.auth() is not clear for me. Can we call admin within cloud functions ?
Basically functions.auth(), will let you trigger Cloud Functions in response to the creation and deletion of Firebase user accounts. For example, you could send a welcome email to a user who has just created an account in your app:
exports.sendWelcomeEmail = functions.auth.user().onCreate((user) => {
// ...
});
functions.auth() is from the cloud function package:
// The Cloud Functions for Firebase SDK to create Cloud Functions and setup triggers.
const functions = require('firebase-functions');
Using the above package you can preform firestore, database or auth triggers that will run in response to creating data in the database or creating a new user...
// The Firebase Admin SDK to access Cloud Firestore.
const admin = require('firebase-admin');
admin.initializeApp();
The firebase admin sdk is used to access the database from privileged environments example inside cloud functions.
Check the following links:
https://firebase.google.com/docs/functions/use-cases
https://firebase.google.com/docs/functions/auth-events
https://firebase.google.com/docs/admin/setup
https://firebase.google.com/docs/functions/auth-events

Is there a way to restrict public access from firebase callable could functions

Firebase callable cloud functions can be accessed via client sdks, which requires a valid auth context for authentication and authorization. But and at the same time it is exposed as an HTTP endpoint, thus can be called but will receive an unauthorized response.
My questions is, is there a way to completely restrict public access for a callable cloud functions? since firebase will charge cloud functions based on function executions. Even to return an unauthorized response, the request has already gone through to the function, thus during a DDoS attack this could be problematic.
There is no built-in support for rejecting a request to a Cloud Function before it reaches your code. If you want such functionality consider setting up Cloud Endpoints in front of your Cloud Functions.
The best you can with just Cloud Functions do is check whether the caller is authorized as the first thing in your function code, so that you reduce the amount of time the function is active. You'll still be charged for the invocation in that case, but you'll minimize the GB-seconds and CPU-seconds.
I tried out as #Frank suggested using google cloud run to deploy and ESP container which can by used to invoke private cloud functions. A detailed overview is described in the documentations itself.
https://cloud.google.com/endpoints/docs/openapi/get-started-cloud-functions#deploy_endpoints_proxy
Above given answer by #Frank van Puffelen is perfect but you can utilize a trik to restrict the access by securing that route. Here is the example,
const functions = require('firebase-functions');
exports.scheduleSampleJob = functions.https.onRequest((req , res) => {
let auth = req.header('Authorization');
if(auth == 'YOUR_API_AUTHORIZATION_KEY'){
// valid Authorization key, process the call
}else{
//send forbidden if Authorization key not valid
return res.status(403).send('Access is Forbidden');
}
});
Now, if you want to call the endpoint, It will require a Authorization header in request having value your secret key.
As firebase cloud function can also be used with firebase-auth, you can create custom logic to allow access to users having auth only and restrict the access for public excluding your app's authentic users.

Can Firebase Cloud Storage rules validate against Firestore data?

Can we use Firestore data to grant or restrict access to files hosted on Firebase Cloud Storage?
Exemple of what I would like to use as Firebase Security Rule
allow write: if get(/databases/mydbname/documents/guilds/$(guildID)).data.users[(request.auth.uid)] in ["Admin", "Member"];
Update (Oct 2022): it is now possible to access Cloud Firestore from within your Cloud Storage security rules with the new firestore.get() and firestore.exists() functions. See the blog post Announcing cross-service Security Rules and the documentation on enhancing Cloud Storage security rules with Cloud Firestore.
Previous answer below for reference:
There is currently no way to access different Firebase products from within the security rules of another product. See: is there a way to authenticate user role in firebase storage rules?
But it seems like you are trying to check group-membership of the user. Instead of looking that group-membership up in the database, I recommend that you model it as a so-called custom claim. Instead of (or in addition to) writing the membership to the database, you'll set the claim "user {uid} is a member of group {guild1}" into the user profile using the Firebase Admin SDK:
admin.auth().setCustomUserClaims(uid, {guild1: true}).then(() => {
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
});
With that done, you can check the guild membership in the security rules:
allow read: if request.auth.token.guild1 == true;
(I'm not sure if/how you can model the guild membership as a map, I'll update this answer if that turns out to be the case.)
Firebase recently announced cross-service security rules that let's you access Firestore data in Firebase storage's security rules. You just need to use firestore. prefix before get() and exist() functions as shown below:
allow write: if firestore.get(/databases/(default)/documents/col/docId/).data.field == "value";
Firebase current supports only 1 database instance per project so the name must be (default) in path. It's not a wildcard as in Firestore rules so not $(database)
Update: As of 2022-09-28, Firebase introduced cross-service Security Rules, so the answer below is outdated. See #Dharmaraj's answer below for an example.
You can retroactively validate and delete the file after it's been uploaded using a cloud function trigger.
Warning: this technique is not bullet proof, as the invalid file will be stored in Cloud Storage temporarily or potentially forever if the Cloud Function trigger fails. My preference is to prevent the upload in the first place, but if the logic to determine permission resides in Firestore and can't be stuffed in custom claims, then this is currently the only way if you're uploading files using Firebase's Client SDKs. If you're building a mission critical system, you should upload the file to a Cloud Function and let the Cloud Function store the file in Cloud Storage instead.
When uploading a file, add some metadata indicating who's doing the upload:
const storageRef = ref(
storage,
`files/${fileName}`,
);
const uploadTask = uploadBytesResumable(storageRef, file, {
customMetadata: {
uploaderId: userId,
},
});
Set storage rule to ensure that the user identity metadata can be trusted:
match /files/{fileName} {
allow create: if request.auth != null &&
request.resource.metadata.uploaderId == request.auth.uid
}
Create a cloud function trigger that retroactively validates and deletes:
export const onFinalize = functions
.storage.object()
.onFinalize(async object => {
// We can trust object.metadata.uploaderId, so check Firestore if user is able to upload file
if (!(await canUploadFile(object.metadata.uploaderId, object.name))) {
await storage.bucket(object.bucket).file(object.name).delete();
throw new Error(
`Permission error: ${object.metadata.uploaderId} not allowed to upload ${object.name}`,
);
}
// Continue
});

Resources