Context: I have a web app built in Next.JS using Firebase Storage for file storage. I am successfully able to upload files when I have removed Firebase rules/set Firebase rules to public. I need to implement stronger rules to protect the DB.
I am not using Firebase authentication, I am using NextAuth. The use case requires the metadata to be checked against users and organisation in Firestore (firestore.get() / firestore.exists()).
I have tried some baseline rules and the below rules work, I am able to successfully upload files:
// Allow anyone to read/write - PUBLIC
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write;
}
}
}
// Allow anyone to read/write if there is a request
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request != null;
}
}
}
Problem: When trying to implement any other rules the upload fails with:
status 403 - Firebase Storage: User does not have permission to access
The problem seems to stem from the request.resource is always undefined (I can see this when I console log in my code). This is very confusing as when the rules are removed/made public, the file uploads successfully which tells me that the request.resource is not undefined. Further adding to my confusion, when I have uploaded files with laxed rules I can see in the files metadata that the fields are definitely there:
When testing the below rule in Firebase rules playground it works as expected, but when publishing the rule and trigger the endpoint, it fails with the above error:
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.resource.metadata != null;
}
}
}
Backend code:
imports...
export const config = {
api: {
bodyParser: false, // Disable Next.js's default body parser
},
};
export interface ParsedForm {
fields: formidable.Fields;
files: formidable.Files;
}
// Helper function to initialize formidable and parse form data
const parseForm = (req: IncomingMessage): Promise<ParsedForm> =>
new Promise((resolve, reject) => {
const form = formidable({ multiples: true, keepExtensions: true });
form.parse(req, (err, fields, files) => {
if (err) reject(err);
resolve({ fields, files });
});
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions(req, res));
if (!session || !session.user) {
res.status(401).json({ success: false, message: 'Unauthorized' });
return;
}
const user = session.user as User;
const { method } = req;
if (method !== 'POST') {
res.setHeader('Allow', ['POST']);
res.status(405).end(`Method ${method} Not Allowed`);
return;
}
try {
const { fields, files } = await parseForm(req);
// Ensure `file` is an array and access the first element
const fileArray = files.file;
if (!fileArray || !Array.isArray(fileArray) || fileArray.length === 0) {
res.status(400).json({ success: false, message: 'No file provided' });
return;
}
const metaData = {
contentType: fileArray[0].mimetype || 'application/octet-stream',
customMetadata: {
userId: user.id,
organisationId: user.organisationID,
},
};
const file = fileArray[0];
const currentDateTimeSinceEpoch = secondsSinceEpoch();
const storageRef = ref(storage, `uploads/${user.organisationID}/${currentDateTimeSinceEpoch}_${file.originalFilename}`);
// Read the file as a buffer
const fileBuffer = await fs.readFile(file.filepath);
// Upload the file to Firebase Storage
const uploadResult = await uploadBytesResumable(storageRef, fileBuffer, metaData);
console.log("🚀 ~ handler ~ uploadResult:", uploadResult)
// Get the download URL
const downloadURL = await getDownloadURL(storageRef);
// Optionally, remove the file from the local filesystem after uploading
await fs.unlink(file.filepath);
res.status(200).json({ success: true, url: downloadURL });
} catch (error) {
console.error('Failed to upload file:', error);
res.status(500).json({ success: false, message: 'Failed to upload file' });
}
}
Solved
The issue/error I was facing came down to multiple things but in summary I was using a mix of v9 and v10 Firebase, as well as using Firebase (client side) on the server instead of using Firebase-admin (SDK), causing some strange behaviour.
When using v10 Firebase, ensure you are using the correct imports and functions if you initialized your app with Firebase-admin. Also, make sure that you are using the correct libraries/SDK imports.
The below fixed my issues and I did not need the rules at all having set up the Firebase-admin SDK properly:
import { getStorage } from "firebase-admin/storage";
const storage = getStorage();
const bucket = storage.bucket(process.env.FIREBASE_STORAGE_BUCKET);
const fileRef = bucket.file(`uploads/${user.organisationID}/${file.originalFilename}`);
await fileRef.save(fileData, metaData);
The below was how I was previously doing it and causing errors:
import { storage } from '@/config/firebase';
import { ref, uploadBytesResumable } from "firebase/storage"; //This is actually the client side SDK
const storageRef = ref(storage, `uploads/${user.organisationID}/${currentDateTimeSinceEpoch}_${file.originalFilename}`);