0

I am trying to upload a file to Firebase Cloud Storage using a signed URL, as described in the docs: https://cloud.google.com/storage/docs/access-control/signed-urls#signing-resumable

I'm using the node.js Admin SDK.

I'm getting a signed URL which looks valid, but when sending the POST request for getting the Session URI, I'm getting the following error:

<?xml version='1.0' encoding='UTF-8'?><Error><Code>AccessDenied</Code><Message>Access denied.</Message><Details>Anonymous caller does not have storage.objects.create access to projname.appspot.com/filename.ext.</Details></Error>

Here's the code I'm using to make the request:

const admin = require('firebase-admin');
const axios = require('axios').default;
const queryString = require('query-string');
const serverKey = require(<server key path>)

admin.initializeApp({
    credential: admin.credential.cert(serverKey),
    storageBucket: "projname.appspot.com",
});


async function run() {
    const bucket = admin.storage().bucket()
    const file = bucket.file(`filename.ext`)
    var expires = new Date()
    expires.setTime(expires.getTime() + (12 * 60 * 60 * 1000))
    const signedUrlArr = await file.getSignedUrl({
        action: 'resumable',
        expires: expires,
    })
    const signedUrl = signedUrlArr[0]
    const qsArr = signedUrl.split('?')
    const params = queryString.parse(qsArr[1]);

    try {
        const options = {
            headers: { "x-goog-resumable": "start" }
        };
        const response = await axios.post(
            qsArr[0],
            { params },
            options,
        )
        console.log(response)

    } catch (e) {
        console.error(e.response.data)
    }
}


run();

The service account used to run the cloud function has the Editor, Service Account Token Creator and Storage Object Creator permissions (had to add the last two so I could generate a signed URL. enter image description here

I don't think this is readlly a lack of permissions because the error message says the request is made from an anonymous user, which isn't the case.

syonip
  • 2,762
  • 25
  • 27
  • From the error message it seems to be a problem with the service account permissions, could you edit the question to add which permissions the service account has?. Separately, I'm not sure you should be parsing the response uri since in the [documentation](https://cloud.google.com/storage/docs/performing-resumable-uploads#single-request) it seems that it should be used as it is. Moreover the method should be `PUT` instead of `POST` as shown in the previous link. – Happy-Monad Aug 18 '20 at 10:20
  • I've added the permission roles the service account has. I'm parsing the URL because axios doesn't know how to handle a URL containing query string params, they need to be sent in a JS object. And according to the documentation, there should be a perliminary POST request for getting the session URI, only then can you call the PUT with the actual data. – syonip Aug 19 '20 at 09:25
  • I believe that the error message that you are receiving might be a bit misleading, given that you've added the correct set of permissions for uploading files. Now, what I'm not sure about is the way you are making the `POST` request and passing the query paramaters. Can you please use print statements to debug that part? Also, this [Stackoverflow](https://stackoverflow.com/a/55291055/7725879) may be helpful to you (they're using cURL and Java though). – sllopis Sep 01 '20 at 07:30

1 Answers1

0

With a signed URL, the token is sent in a url query. So the following lines of code are essentially stripping the url of the token and so it's anonymous.

    const qsArr = signedUrl.split('?')
...
        const response = await axios.post(
            qsArr[0],
            { params },
            options,
        )

changing those lines to this (or something functionally identical) might fix the issue

    const qsArr = signedUrl
...
        const response = await axios.post(
            qsArr,
            { params },
            options,
        )

At least that's what the issue looks like to me. Is there any reason you chose to cut out the url query for the signed url?

Brett S
  • 579
  • 4
  • 8
  • Yes, I'm splitting the query params and sending them in the {params} array, because that's how axios handles query string params. I tried just sending the URL as is, but then I get an error saying there's no signature. – syonip Aug 26 '20 at 06:02
  • maybe `queryString.parse` is causing an issue? – Brett S Aug 31 '20 at 19:44
  • or perhaps `signedUrl` has multiple `?`s and so calling `signedUrl.split('?')` is losing information – Brett S Aug 31 '20 at 19:45