Firebase Cloud Storage rules based on Firestore Data work around - firebase

In my firebase app I have
a list of users(all users are registered via Firebase Authentication )
a list of followers for each user
Each user has added a profile photo, the filename of the photo is unique and is saved in a firestore doc called "photoName" document
Only Followers of a user are permitted to read the "photoName" document ( as specified in firestore rules )
I want only the followers to be able to read the profile photo of the user, I have added a rule in cloud storage as follows:
match /profilePhotos/{profilePhotoFile_id} {
allow read: if request.auth.uid != null
}
The cloud storage rules are set on a file-level so is it accurate for me to assume that only the users who are logged in and are also a follower will be able to read the profile photo of another user? Unless they somehow magically guess the unique names of a photo.

Update dated September 28, 2022
Security Rules in Cloud Storage for Firebase now supports cross-service Rules and let you query your projects’ Firestore data, similar to the get() and exists() functions in Firestore Rules.
See this Firebase blog article and this SO answer from Puf.
Previous answer
Is it accurate for me to assume that only the users who are logged in
and are also a follower will be able to read the profile photo of
another user?
No, because it is possible, with the client SDKs to list all the files in a Cloud Storage bucket, as explained in the doc and your Cloud Storage Security Rules allow any authenticated user to read the profile photos files.
Also note that you cannot read Firestore documents when writing Cloud Storage Security Rules.
One possible approach is to use a Cloud Function to generate a signed URL that you store in the Firestore document AND to forbid read access to the profile photos files. Since Cloud Functions use the Admin SDK they can bypass the security rules.
The following Cloud Function code will generate a signed URL each time a file is added to Cloud Storage and save it in a Firestore document. With this signed URL anyone can read the profile photos file.
It's up to you to adapt it to your case by:
If necessary, only treating the profile photos (check that the file name contains profilePhotos)
Saving the URL in the correct Firestore doc: I guess the file name allows linking back to the user document. Also, you will probably have to change from add() to update().
exports.generateFileURL = functions.storage.object().onFinalize(async object => {
try {
const bucket = admin.storage().bucket(object.bucket);
const file = bucket.file(object.name);
const signedURLconfig = { action: 'read', expires: '08-12-2025' };
const signedURLArray = await file.getSignedUrl(signedURLconfig);
const url = signedURLArray[0];
await admin.firestore().collection('...').add({ signedURL: url })
return null;
} catch (error) {
console.log(error);
return null;
}
});
Two additional considerations:
You can use Custom Claims in Cloud Storage Security Rules, but it is not really recommended to use them for your case, see here.
You can also use file metadata in Cloud Storage Security Rules, but again it is not adapted to your case (you are not going to add followers Ids in file metadata each time a new follower registers...)

Related

How to grant access to google cloud storage buckets based on users auth uid

I am running a cloud function that saves images like this:
//Pseudocode
const admin = await import("firebase-admin");
const bucket = admin.storage().bucket();
const file = bucket.file('myName');
const stream = file.createWriteStream({ resumable: false });
...
After the images are uploaded, I get the publicUrl like so
file.publicUrl()
and store it in an Object.
This object then gets stored to firestore.
When I now copy this url from the object(the url structure looks like this)
https://storage.googleapis.com/new-project.appspot.com/ZWHpYGSQWYXLlUcAwkRFQLC0u7s1/f2a48bdc-7eb3-4174-9f6e-3fd963003bd7/177373254.png
and paste it into a browser field am getting an error:
<Error>
<Code>AccessDenied</Code>
<Message>Access denied.</Message>
<Details>Anonymous caller does not have storage.objects.get access to the Google Cloud Storage
object.</Details>
</Error>
even with the test rules:
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read: if true;
allow write: if false;
}
}
}
I have read some issues here on stackoverflow and it seems like this is because I am using google cloud storage buckets and not firebase storage buckets (I thought they are the same)
but I am very confused about how to write rules in this case, so that only authenticated firebase users can read files.
Any help is highly appreciated.
The bucket is shared between Firebase and Cloud Storage, but the way you access the bucket is quite different.
When you access the bucket through a Firebase SDK, or through a download URL generated by a Firebase SDK, your access goes through a Firebase layer. This layer enforces the security rules (for the SDK), and grants temporary read-only access to the download URL.
When you access the bucket through a Cloud SDK, or a signed URL generated by a Cloud SDK, your access does not go through any Firebase layer, and thus Firebase security rules have no effect on this access.
The public URL you have is just a way to identify a file in your Cloud Storage bucket. It does not have any implied access permissions. You will need to make sure the IAM properties for your bucket allow the access you want the user to have to the file.

Reading firebase storage image security rules

I am using Firebase storage and firestore with flutter,
I came across two options to retrieve Firebase storage image
Setting Firebase storage image url in firestore database and then fetching it with network image
Getting image url from Firebase storage directly
I don't know much about tokens.
My security rules states that only auth users can read my Firebase storage but if I use first option my image url with token is stored in my firestore database using that url anyone can access my storage. I am not sure does Firebase refresh it's storage token automatically then if this is the case my app will experience crash.
Which is the most secure and long lasting way or please answer if any other secure way to fetch images
Firebase storage tokens won't expire unless you revoke them. The token may update if you overwrite the image i.e. update it. Now that's totally your call if you would like to make a separate request just to get the download URL or store the URL in realtime database when an image is uploaded and fetch it along with other data.
Security rules of Firebase Storage will prevent non-authenticated users from getting the download URL only. If an authenticated user shares the URL with anyone, they will be able to see the image as they have the URL with that random token now.
If the data you are fetching from realtime database requires the user to be logged in at first place, then I'd just store the URL in the database itself as I don't think it makes sense to make another request and have the same rules for Firebase storage. I don't know your exact use case so there may be exceptions for doing this.
If you don't need that image URL always then that might be waste of bandwidth, then you should consider making separate request to get the storage URLs.
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
These rules will allow any authenticated user to request the URL. But as I mentioned earlier, anyone with this link can access the file.

How to restrict firebase storage files only for the paid user?

I have the file stored in firebase cloud storage. This file will only available for the paid user download.
How to set up security rules to allow the paid user to have read access to that file?
[Updated]
I use the cloud firestore to store user collection
Each user doc contain
uid
email
name
photoUrl
provider
status
stripeCustomerId
purchasedProducts << this is the array of product name
I can verify paid user by looking if the product exist in purchasedProducts array.
However, inside the security rule from Firebase storage, it seem I can't access resource (user collection) in there. Or am I missing something?
Thanks
There is no way to access Cloud Firestore from within the security rules for Firebase Storage.
That means the only ways to currently implement your use-case is to:
include the necessary information in the ID token of the user, as a custom claim, which is then also available in security rules.
include the necessary information about the user (probably their UID) in your security rules
Since the second approach requires that you update your rules for every paying user, it's not very common.
Setting a custom claim can be done through the Firebase Admin SDK, for example from a Cloud Function that triggers when you write their payment information to Cloud Firestore.
Once you set the custom claim it may take up to an hour before it's available on the client, and from there in the security rules. The reason for that is that the claims are included in the ID token, which only auth-refreshes once an hour. If you want to get the updated claims sooner, you can force a refresh of the user's profile on the client.
Another approach you can try is to delete a file right after it was uploaded using the functions.storage.object().onFinalize webhook - this is wehere you can access the database and check if the user was allowed to upload the file.
Even though it may look a bit 'hacky' at the first glance, this is really a precaution measure in the first place - the UI itself would restrict the upload for the "good" users. And for those who messes up with the source code and tries to circumvent the system, onFinalize would do the job.
You can access cloud firestore through the firestorage service security rules:
https://firebase.google.com/docs/storage/security/rules-conditions#enhance_with_firestore

How to add metadata to Firebase authentication

I need to pass a custom value (device_id) during google signin with firebase authentication. This value is later obtained from cloud functions by listening for authentication event triggers and then the value is added to Firestore
I understand that you can pass values as query parameters for http triggers. However I only need to pass and get the value during and after authentication in my case. Hence is there some sort of auth.addMetaData(metadata) function in firebase authentication?
I need to be able to retrieve the custom data after an auth trigger just like we can do user.email. I need something like user.custom_data
Although Doug mentions Firebase Custom Claims, I think it’s worth extra documentation because it does allow you to add simple metadata to a Firebase User object.
Important notes
Big caveat: Custom claims are only refreshed when the user logs in. So an isAdministrator claim would require the user to logout/login before it is activated.
Firebase recommends “Use custom claims to store data for controlling user access only. All other data should be stored separately via the real-time database or other server side storage.”
Set metadata (server only)
Here’s an example on how to set device_id on a Firebase User object (on the server using firebase-admin):
await admin.auth().setCustomUserClaims(uid, { deviceId })
Note: You can not set custom claims on the client.
Get metadata (server and client)
Then to retrieve the the device_id from the User on the server:
const userRecord = await admin.auth().getUser(uid)
console.log(userRecord.customClaims.deviceId)
…and on the client:
const idTokenResult = await firebase.auth().currentUser.getIdTokenResult()
console.log(idTokenResult.claims.deviceId)
Use metadata in Firebase Security Rules
The neat thing is that custom claims are also available in Firebase Security Rules. This (slightly unrealistic) example only allows users with deviceId === 123 to see the data:
{
"rules": {
"secureContent": {
".read": "auth.token.deviceId === 123"
}
}
}
More information
Official docs: https://firebase.google.com/docs/auth/admin/custom-claims
Deep dive: https://medium.com/google-developers/controlling-data-access-using-firebase-auth-custom-claims-88b3c2c9352a
A clever pattern of synching custom claims with a Firebase database collection: https://medium.com/firebase-developers/patterns-for-security-with-firebase-supercharged-custom-claims-with-firestore-and-cloud-functions-bb8f46b24e11
Firebase Authentication doesn't support any sort of extra data provided by the client. The closest thing to metadata that gets stored per user by Firebase would be custom claims, however, the JSON blob stored there can only be set by privileged server-side applications.
If you need to store data per user, written by client apps, you should probably be using a database for that (Cloud Firestore or Realtime Database), protected by Firebase security rules, so that only the end user can read and write their own data. You could also use an HTTP type Cloud Function to pass data into your function to be recorded in a database.

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