Search code examples
javascriptfirebasegoogle-cloud-firestorenext.jsfile-upload

Google Firebase Storage Rules prevent sending file upload from Next.JS endpoint


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:

enter image description here

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' });
  }
}

Solution

  • 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}`);