1
votes

I want to have my server-side as clean of credentials as possible. For that reason, I authenticate myself with Firebase Auth using Firebase Auth Rest Api. From the request, I obtain the firebase ID Token and with this token, I make requests to Firebase Realtime Database as Authenticate Rest Request (Authenticate with an ID token) explains.

The problem is that when I try to do the same with Firebase Storage (Google Cloud Storage Request Endpoints) I don´t find any solution avoiding credentials storing in server-side (for example, with Admin SDK I can write or read any file, but this means a possible future issue with security in my server because my credentials are exposed) despite in Authenticate Rest Request (Authenticate with an ID token) clearly says: "When a user or device signs in using Firebase Authentication, Firebase creates a corresponding ID token that uniquely identifies them and grants them access to several resources, such as Realtime Database and Cloud Storage."

The question is: how can I use the Firebase ID Token to authorize Firebase Storage Api Rest calls as I did with Firebase Runtime Database?

Thanks.

4

4 Answers

2
votes

Finally I found a solution to my answer. The solution is to use Cloud Functions.

Cloud Functions allows us to create endPoints and to use AdminSdk in a nodejs enviroment which is part of our firebase project. With this approach we can send an http request to that endpoint, this checks if the received Token with the request is valid and if it is, it saves the file.

This is the functions code:

const functions = require("firebase-functions");
const admin = require("firebase-admin");
const path = require("path");
const os = require("os");
const fs = require("fs");
const Busboy = require("busboy");

// Follow instructions to set up admin credentials:
// https://firebase.google.com/docs/functions/local-emulator#set_up_admin_credentials_optional
admin.initializeApp({
  credential: admin.credential.cert(
    __dirname + "/path/to/cert.json"
  ),
  storageBucket: "bucket-name",
});

const express = require("express");
const app = express();

// Express middleware that validates Firebase ID Tokens passed in the Authorization HTTP header.
// The Firebase ID token needs to be passed as a Bearer token in the Authorization HTTP header like this:
// `Authorization: Bearer <Firebase ID Token>`.
// when decoded successfully, the ID Token content will be added as `req.user`.
const authenticate = async (req, res, next) => {
  if (
    !req.headers.authorization ||
    !req.headers.authorization.startsWith("Bearer ")
  ) {
    res.status(403).send("Unauthorized");
    return;
  }
  const idToken = req.headers.authorization.split("Bearer ")[1];
  try {
    const decodedIdToken = await admin.auth().verifyIdToken(idToken);
    req.user = decodedIdToken;
    next();
    return;
  } catch (e) {
    res.status(403).send("Unauthorized");
    return;
  }
};

app.use(authenticate);

// POST /api/messages
// Create a new message, get its sentiment using Google Cloud NLP,
// and categorize the sentiment before saving.
app.post("/test", async (req, res) => {
  const busboy = new Busboy({ headers: req.headers });
  const tmpdir = os.tmpdir();

  // This object will accumulate all the fields, keyed by their name
  const fields = {};

  // This object will accumulate all the uploaded files, keyed by their name.
  const uploads = {};

  // This code will process each non-file field in the form.
  busboy.on("field", (fieldname, val) => {
    // TODO(developer): Process submitted field values here
    console.log(`Processed field ${fieldname}: ${val}.`);
    fields[fieldname] = val;
  });

  const fileWrites = [];

  // This code will process each file uploaded.
  busboy.on("file", (fieldname, file, filename) => {
    // Note: os.tmpdir() points to an in-memory file system on GCF
    // Thus, any files in it must fit in the instance's memory.
    console.log(`Processed file ${filename}`);
    const filepath = path.join(tmpdir, filename);
    uploads[fieldname] = filepath;

    const writeStream = fs.createWriteStream(filepath);
    file.pipe(writeStream);

    // File was processed by Busboy; wait for it to be written.
    // Note: GCF may not persist saved files across invocations.
    // Persistent files must be kept in other locations
    // (such as Cloud Storage buckets).
    const promise = new Promise((resolve, reject) => {
      file.on("end", () => {
        writeStream.end();
      });
      writeStream.on("finish", resolve);
      writeStream.on("error", reject);
    });
    fileWrites.push(promise);
  });

  // Triggered once all uploaded files are processed by Busboy.
  // We still need to wait for the disk writes (saves) to complete.
  busboy.on("finish", async () => {
    await Promise.all(fileWrites);
   
    // Process saved files here
    for (const file in uploads) {
      admin.storage().bucket().upload(uploads[file], function(err, file) {
        if (err) {
          res.status(403).send("Error saving the file.");
        }
        res.status(201).send("Saved");
      });
    }
  });

  busboy.end(req.rawBody);
});

exports.api = functions.https.onRequest(app);
0
votes

If you need to access Firebase Storage on the server side, then you won't be able to avoid storing credentials somewhere. The only thing you can do is to pass user credentials that were obtained on the client to your server with an API request. Though this won't give you any security benefits because if the attacker has got access to your server, then he would be able to access your storage anyway.

In general, it is safe to store storage credentials on the server. You need to make your server as secure as possible anyway.

0
votes

Since Firebase Storage is actually just a repacking of Cloud Storage, you can use the Cloud Storage JSON API to work with content in a storage bucket. Start with the section on user account credentials. You will need to provide an OAuth token for the Firebase Auth user to send with the request.

0
votes

Since for the moment there are no solutions for the question, I decide to store files in binary format in the Realtime Database. In this way I don´t need to expose my credentials in the server because the Firebase ID Token does the authentication.