Firebase Storage security rules and download tokens for uploaded files - firebase

TLDR: This is a question about the URL returned from the task.snapshot.ref.getDownloadURL() and its respective downloadToken. It asks about what is the main role and functionality of the token and if it's necessary for files that will be publicly available by security rules.
I've just finished the tutorial guides about uploading and downloading files to Firebase Storage found on https://firebase.google.com/docs/storage/web/upload-files and also on this Youtube tutorial from the Firebase official channel.
I'm building a Content Management System for a blog section in one of my Firebase web apps (React + Firebase).
I have a component that lets the admin pick an image and upload it to Firebase Storage Bucket to be displayed in a specific blog post. All images to a specific blogPost should be inside a folder for the specific blog-post-slug.
Example:
//bucket/some-blog-post-slug/image1.jpg
Code that runs whenever the admin selected a new file on the <input type='file'/>:
function onFileSelect(e) {
const file = e.target.files[0];
const storageRef = firebase.storage().ref('some-slug/' + file.name);
const task = storageRef.put(file);
task.on('state_changed',
function progress(snapshot) {
setPercent((snapshot.bytesTransferred / snapshot.totalBytes) * 100);
},
function error(err) {
console.log(err);
},
function complete() {
console.log('Upload complete!');
task.snapshot.ref.getDownloadURL().then(function(downloadURL) {
console.log('File available at', downloadURL);
props.changeImageSrc(downloadURL);
});
}
);
}
The code above returns the downloadURL that will be saved to the Firestore inside the blogPost document.
The downloadURL has the following format:
https://firebasestorage.googleapis.com/v0/b/MYFIREBASEAPP.appspot.com/o/some-slug%2FILE_NAME.jpg?alt=media&token=TOKEN_VALUE
You can see that it comes with a "basic URL": https://firebasestorage.googleapis.com/v0/b/MYFIREBASEAPP.appspot.com/o/some-slug%2FILE_NAME.jpg
And the basicURL is appended with the following GET parameters:
alt=media and token=TOKEN_VALUE
I was not aware that I would be getting a token, so I'm now testing its behavior to know more about it.
BEHAVIOR WITH STORAGE READS ALLOWED:
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write;
}}}
When I access the basicURL:
I get an object in response with the uploaded file details:
{
"name": "some-slug/FILE_NAME.jpg",
"bucket": "MYBUCKET",
"generation": "GENERATION_NUMBER",
"metageneration": "1",
"contentType": "image/jpeg",
"timeCreated": "2019-06-05T13:53:57.070Z",
"updated": "2019-06-05T13:53:57.070Z",
"storageClass": "STANDARD",
"size": "815155",
"md5Hash": "Mj4aCPs21NUNxXpKg1bHirFIO0A==",
"contentEncoding": "identity",
"contentDisposition": "inline; filename*=utf-8''FILE_NAME.jpg",
"crc32c": "zhkQMQ==",
"etag": "CKu4a1+u2+0ucI412CEAE=",
"downloadTokens": "TOKEN_VALUE"
}
When I access the basicURL?alt=media
The image is displayed.
When I access the basicURL?alt=media&token=TOKEN_VALUE
The image is displayed.
BEHAVIOR WITH STORAGE READS RESTRICTED:
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}}}
When I access the basicURL:
I get the following error object:
{
"error": {
"code": 403,
"message": "Permission denied. Could not perform this operation"
}
}
When I access the basicURL?alt=media
I get the same error object:
{
"error": {
"code": 403,
"message": "Permission denied. Could not perform this operation"
}
}
When I access the basicURL?alt=media&token=TOKEN_VALUE
The image is displayed.
CONCLUSION AND QUESTIONS
It seems to me that the security rule allow read: if request.auth != null; should have blocked any reads from unauthorized users, but with the TOKEN parameter, the file is accessible even for requests without an auth object (Note: I wasn't logged in when I ran the tests above).
I know it's not the best practice to ask more than 1 question, but in this case I think it's necessary:
QUESTION 1:
What is this TOKEN mainly used for and why does it overried the auth rule?
QUESTION 2:
I want these images to be publicly availabe, as the blog section will be for all users. What URL should I save to Firestore?
Option 1: allow the reads for everyone and just save the basicURL.
Option 2: keep the reads restricted and save the basicURL + token.
QUESTION 3:
To display the images in <image src="imgSrc"> tags do I need the alt=media parameter? Or the basicURL ending in the FILE_NAME is good enough?
EDIT: QUESTION 3 ANSWER: Just tested it and found out that the alt=media GET parameter IS necessary to display the image inside an <img> tag.
NOTE: if you upload the same file and replace the old one, you'll get a different token each time and the older token becomes invalid.

service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write;
}
}
}
This is security permissions for Firebase Storage. All Types of Data(Images, Video etc)

Unfortunately, described method from https://firebase.google.com/docs/storage/web/create-reference does not add authorization data to request and storage rule like
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
returns permission denied,
using AngularFireStorage module resolves this issue.
import { AngularFireStorage } from '#angular/fire/storage';
constructor(
private storage: AngularFireStorage
) {
}
getFileUrl(path: string): Observable<string> {
const storageRef = this.storage.ref(path);
return from(storageRef.getDownloadURL());
}

Related

Error: com.google.firebase.FirebaseException: No AppCheckProvider installed

i need to delete a file from firebase storage, i am following this code:
static deleteFile(String urlFile) async {
try {
final ref = FirebaseStorage.instance.refFromURL(urlFile);
await ref.delete();
} catch (e) {
if (kDebugMode) {
print(e.toString());
}
}
}
the file is deleted, but the console displays this error:
W/StorageUtil(13434): Error getting App Check token; using placeholder token instead. Error: com.google.firebase.FirebaseException: No AppCheckProvider installed.
the user is successfully authenticated.
App Check Attestation Providers:
For IOS: DeviceCheck or App Attest &
For Android: Play Integrity or SafetyNet
Use Anyone of these you like, I personally use SafetyNet for my android apps.
Add this inside apps/build.gradle:
dependencies {
implementation 'com.google.firebase:firebase-appcheck-safetynet:16.0.0'
}
Then Initialize App Check Add this:
FirebaseApp.initializeApp(/context=/ this);
FirebaseAppCheck firebaseAppCheck = FirebaseAppCheck.getInstance();
firebaseAppCheck.installAppCheckProviderFactory(
SafetyNetAppCheckProviderFactory.getInstance());
I hope it fix your error. for more you can follow official doc: [https://firebase.google.com/docs/app-check/android/safetynet-provider].
Another Temporary fix... for TP & Practice Apps
Simply Remove If Condition from your firebase storage/db/auth rules
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if false;
}
}
}

How to getDownloadURL in a Firebase Function

When using Firebase Storage to store images, there is a URL to the image that looks like this :
https://firebasestorage.googleapis.com/v0/b/[MY-APP].appspot.com/o/[FILE-NAME]?alt=media&token=[TOKEN]
I want to get this URL.
According to this, this, and this and this, I need to use the .getDownloadURL() method, which is on the "storage ref" .. but the documentation of the available objects does not fit with the actual object.
And when I attempt to access the .getDownloadURL() method on the objects suggested by the documentation in the code below I get various property not found errors.
const admin = require('firebase-admin');
admin.initializeApp();
admin
.storage()
.bucket()
.upload(imageToBeUploaded.filepath, {
destination: storageFolder,
resumable: false,
metadata: {
metadata: {
contentType: imageToBeUploaded.mimetype
}
}
})
.then((taskSnapshot) => {
// HOW DO I GET THE DOWNLOADABLE URL
})
I've tried the following :
taskSnapshot[1].getDownloadURL(),
admin.storage().ref(storageFolder+'/'+imageFileName).getDownloadURL(),
admin.storageRef(storageFolder+'/'+imageFileName).getDownloadURL(),
admin.storage().ref().child(storageFolder+'/'+imageFileName).getDownloadURL(),
.. and a bunch of others.
Trial and error is not finding the "storageRef" that has that method,
How can I get the downloadable url for my image ?
The solution I've used is to first change the access rules for my storage bucket, like so :
https://console.firebase.google.com/u/0/project/[MY_APP]/storage[MY_APP].appspot.com/rules
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read;
allow write: if request.auth != null;
}
}
}
which means the token is not required on the URL in order to be able to view the image.
And then I have just hardcoded the URL :
.then((taskSnapshot) => {
const imageUrl = `https://firebasestorage.googleapis.com/v0/b/` +
`${MY_APP_ID}.appspot.com/o/${imageFileName}?alt=media`;
return imageUrl;
})
The Firebase Admin SDK wraps the Google Cloud Storage SDK, so their APIs are the same. The Cloud Storage SDK doesn't offer download URLs that are exactly like the ones provided by the mobile and web SDKs.
What you can do instead is generate a signed URL, which is functionally similar.
See also: Get Download URL from file uploaded with Cloud Functions for Firebase
For anybody else that stumbled onto this, the following code works for me.
The other answers didn't work for my usage, as I didn't want a link that expired.
I'm still learning JS and programming in general so I'm sure this code can be optimized further, for example:
UploadResponse contains both a File AND a Metadata object but I couldn't figure out how to filter the metadata directly and had to use the File in order to get its metadata.
const uploadResponse: storage.UploadResponse = await bucket.upload(tempFilePath);
for (const value of uploadResponse) {
if (value instanceof storage.File) {
const metadata = await value.getMetadata();
const downloadUrl: string = metadata.shift().mediaLink;
}
}
fs.unlinkSync(tempFilePath);

Error on uploading file to firebase cloud storage: "Listing objects in a bucket is disallowed for rules_version = 1"

I've setup firebase auth and cloud storage on my application. Whenever I try to upload a file, however, I'm getting an error I can't find any information about. The error is a 400: Bad Request with the following message
Listing objects in a bucket is disallowed for rules_version = "1". Please update storage security rules to rules_verison = "2" to use list.
I can't seem to find anything about updating security rules_version. Note, when I look at the firebase console, the upload actually successfully goes through, but the HTTP return is still the error above. What does it mean by listing objects, and how can I update my security rules?
For more information, my upload code (in Kotlin) is
fun uploadImage(uri: Uri, path: String): Task<Uri> {
val storageRef = FirebaseStorage.getInstance().reference
val storagePath = storageRef.child(path)
return storagePath.putFile(uri).continueWithTask{ storageRef.downloadUrl }
}
I call it with
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode in 0..2 && resultCode == Activity.RESULT_OK) {
val cropImageUri = CropImage.getActivityResult(data).uri
val systemTime = System.currentTimeMillis()
val path = "$userId/$systemTime"
//IMAGE UPLOAD HERE:
FirebaseImageResource.uploadImage(cropImageUri, path)
.addOnCompleteListener {
if (it.isSuccessful) {
GlideApp.with(this)
.load(cropImageUri)
.into(imageViewForPosition(requestCode)!!)
imageUris[requestCode] = it.result.toString()
}
}
}
}
My firebase rules are the default:
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
I'm also successfully authing with Facebook Login
override fun onSuccess(loginResult: LoginResult) {
val credential = FacebookAuthProvider.getCredential(loginResult.accessToken.token)
auth.signInWithCredential(credential)
}
(It lacks a success listener right now, but I know it's working because when I don't use it, I get an unauthorized error instead, and the file doesn't actually upload)
You need to prepend this to your Firebase Storage rules:
rules_version = '2';
An example is this:
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
I suspect that this is a case of a not-so-helpful error message.
Just replace your upload code from
fun uploadImage(uri: Uri, path: String): Task<Uri> {
val storageRef = FirebaseStorage.getInstance().reference
val storagePath = storageRef.child(path)
return storagePath.putFile(uri).continueWithTask{ storageRef.downloadUrl }}
to
fun uploadImage(uri: Uri, path: String): Task<Uri> {
val storageRef = FirebaseStorage.getInstance().reference
val storagePath = storageRef.child(path)
return storagePath.putFile(uri).continueWithTask{ storagePath.downloadUrl }}
I solved it with the following setps
Sign in Firebase console
Select project
Click Storage button
Select Rules tab
Insert top "rules_version = '2';"

Firebase JS Error: getToken aborted due to token change

I'm getting a Firebase error "Error: getToken aborted due to token change" while running Firestore transaction using the JavaScript library. The error doesn't get thrown every time and I couldn't find the pattern. I suppose I've implemented some race conditions somewhere.
The user flow in my app goes like this:
Register a new account & submit an additional string in the same form
Log user in after registration using the same credentials
After log in, take that additional string and save it to Firestore (in a transaction).
Transaction fails due to Error: getToken aborted due to token change.
The flow of promises:
firebase.auth().createUserWithEmailAndPassword(email, password)
.catch(signupError => {
// no problems here
})
.then(() => {
return firebase.auth().signInWithEmailAndPassword(email, password)
})
.catch(loginError => {
// no problem here
})
.then((user) => {
// Database write call which fails (see lower code block)
return this.claimInput.writeClaimedPlace(user.user.uid, claimedPlace);
})
.catch(error => {
// "getToken aborted" ERROR GETS CAUGHT HERE, transaction fails
})
}
The database transaction call
firestore.runTransaction(function(transaction) {
return transaction.get(usersBusinessRef).then(function(usersBusinesDoc) {
let claimedPlaces = [];
if (usersBusinesDoc.exists && usersBusinesDoc.data().claimedPlaces) {
claimedPlaces = usersBusinesDoc.data().claimedPlaces;
}
claimedPlaces.push(claimedPlace);
return transaction.set(usersBusinessRef, { claimedPlaces }, { merge: true });
});
});
I couldn't find the error anywhere on google.
I'm thinking the error is caused by the token change that happens at log in. On the other hand I'm reading that Firebase accepts old tokens for a few more moments. Any thoughts?
I got a similar error[error code] while debugging my client which was connecting to firebase via a React App.
The solution was
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read;
allow write: if false;
}
}
}
Putting the above inside the rules part of the firestore settings which apparently means you need to allow reads for external apis but writes are blocked and it was previously blocking both reads and writes.
This could be one of the issues if you are trying to read from your client/server
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read;
allow write;
}
}
}
DISCLAIMER: * I am not an Expert at firebase. I am not sure if this would compromise your DB to be written by external apis since you are opening your firestore firewall by doing this
P.S. there is a firebase-admin module which I think helps in doing writes by handling authentication in a separate fashion. I think that module is more suited for writes and the normal firebase.firestore(app) is for reads.

How can you use Firebase rules to allow user and admin write privileges?

I am using Firebase rules to set permissions and I am having trouble setting up rule to allow write permissions to include the user and any admin.
Below is the database I am trying to set permissions for:
'rules': {
'rankings': {
'user': {
'userShortenedAuthId': { //user's rankings },
'anotherShortenedAuthId': { //another user's rankings }
}
},
'admin': {
'adminAuthId': true
},
'users': {
'userAuthId': {
'rank': 'userShortenedAuthId'
},
'anotherAuthId': {
'rank': 'anotherShortenedAuthId'
}
}
}
These are the rules in place for this data:
"rankings":{
"user":{
"$rankId": {
".read": true,
".write": "auth != null && ((root.child('users/' + auth.uid + '/rank').val() == $rankId) || root.child('admin/' + auth.uid).exists())"
}
}
}
I am trying to write to a 'rankings/user/userShortenedId' while logged in under the admin, but I get a permission denied error from Firebase. (Logged in as the user works just fine). The error is somewhere in the 'rankings/user/$rankId/.write' rule. What is confusing for me, is I have been using the 'root.child('admin/' + auth.uid).exists())' rule elsewhere for admin privileges and that seems to work fine too.
Here is the code that produces the permission error.
firebase.database().ref('rankings/' + userShortenedAuthId).update({newPlayerKey:totalPlayers}))
.then(function(){
console.log('successfully updated user ranking');
})
.catch(function(err){
console.log('error updated user ranking', err);
});
Found the answer to my own question. The firebase ref URL that was referenced when trying to update
firebase.database().ref('rankings/' + userShortenedAuthId).update({newPlayerKey:totalPlayers}))
should actually be
firebase.database().ref('rankings/user/' + userShortenedAuthId).update({newPlayerKey:totalPlayers}))
So the rules were actually working correctly. I was simply trying to write to the wrong place.

Resources