Nodejs-storage: Get long lived download URLs in addition to SignedURLs

Created on 6 May 2019  Â·  63Comments  Â·  Source: googleapis/nodejs-storage

Hi there,

I noticed that there is currently no way to get a long-lived download URL from an uploaded resource using the node admin package which causes problems when storing generated URLs in external DB as they expire after some time.

Would it be possible to generate long-lived URLs just like the getDownloadUrl() function from the client SDKs
(https://firebase.google.com/docs/reference/android/com/google/firebase/storage/StorageReference.html#getDownloadUrl() ).

Cheers!

storage question

Most helpful comment

Please, re-open this issue.
We need permanent download tokens or an explanation why we won't get them.
Thank you.

All 63 comments

From GCP Storage Docs,

Get the name of the bucket containing the public data.

Use the following URI to access an object in the bucket:

http://storage.googleapis.com/[BUCKET_NAME]/[OBJECT_NAME]

For example, the Google public bucket gcp-public-data-landsat contains the Landsat public dataset. You can link to the publicly shared object LC08/PRE/063/046/LC80630462016136LGN00/LC80630462016136LGN00_B11.TIF with the link:

http://storage.googleapis.com/gcp-public-data-landsat/LC08/PRE/063/046/LC80630462016136LGN00/LC80630462016136LGN00_B11.TIF

This link does not require authentication in order to use. It is suitable, for example, as a link in a web page, or for downloading with a command-line tool such as cURL.

@avin3sh thanks for the reply, this is the kind of long lived URLs i'd be looking for, however, this only applies to public data.

I write a book-length :-) summary of the problems associated with the three types of download URLs for Google Cloud Storage, on Stack Overflow. tl;dr the signed URLs expire in at most seven days (but the documentation doesn't say this, so your project crashes every week during the month or two you're trying to figure this out); making your file public isn't a good idea in every situation; and there's an undocumented property of the Storage object that enables setting your own token, which works but I hesitate to use undocumented features.

The maximum duration that the signed url should be valid for is mentioned here

Currently 7 day limit is only true for V4 signatures and API will throw a Max allowed expiration is seven days (604800 seconds). error.
V2 signatures still support longer duration.

Currently 7 day limit is only true for V4 signatures and API will throw a Max allowed expiration is seven days (604800 seconds). error.
V2 signatures still support longer duration.

Do you have a link of the docs for that? I don't understand why it's not possible to have an infinite duration, it would be much needed for things like user profile pictures

Currently 7 day limit is only true for V4 signatures and API will throw a Max allowed expiration is seven days (604800 seconds). error.
V2 signatures still support longer duration.

Version 4 of what software? Do you mean uuidv4?

And what about v5? Does it have the 7 day limit?

Currently 7 day limit is only true for V4 signatures and API will throw a Max allowed expiration is seven days (604800 seconds). error.
V2 signatures still support longer duration.

Version 4 of what software? Do you mean uuidv4?

I meant 'v2' vs 'v4' _signing with service account authentication_ (doc ref), can be specified inside config (API ref).

Currently 7 day limit is only true for V4 signatures and API will throw a Max allowed expiration is seven days (604800 seconds). error.
V2 signatures still support longer duration.

Do you have a link of the docs for that? I don't understand why it's not possible to have an infinite duration, it would be much needed for things like user profile pictures

It is documented here (_X-Goog-Expires_)

@AVaksman you closed the issue but did not give an answer to the issue:

how do I get the token download URL (not the public nor the signed with expiration) with the Admin SDK (from a cloud function for example)

Please urgent help for this issue.

My process steps:

  1. Creating a cloud function for creating thumbnails
    https://github.com/firebase/functions-samples/tree/master/generate-thumbnail
    (I have just changed "adding link to database" section from realtime db to firestore)

  2. Opening my Flutter app and uploading an image file from phone's gallery

  3. Then "creating thumbnails" function executes.
    When I check storage, I see generating thumb is okay and "Download URL"s are:
    image Download URL"
    thumbnail Download URL"

  4. That cloud function also adds urls to firestore
    image url
    thumbnail url

  5. For a while (7 days?) links on firestore work but at the end of this duration links are expiring...
    _The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method._

We are using createThumbnail function for our apps catalog images and that contains too much records.
So we can't delete images (when links expire) and reupload them again weekly.

Is there another solution for getting public links (never expires and can be accessible from my flutter app)?

If I want to get an unexpired link, can I use still signedURL?
If not, how can I add tokenURL system to generateThumnails function?
Otherwise appliying makingPublic() all images and their thumnails for my mobile app is easy to use?

It is demoralising that I would be test if links expire or not in every situation after 7 days.
So I am unable to add new catalog datas and images to my app in this period...

I think there must be few solutions, cause we shouln't be only users who are facing this problem.
So many apps are also using thumbnail creating like us.

Thanks for your cooperation.

I wrote a complete answer to this problem on Stack Overflow:

https://stackoverflow.com/questions/42956250/get-download-url-from-file-uploaded-with-cloud-functions-for-firebase/56010225#56010225

I’ve heard of one other way to fix the problem, involving reverting to an older version of the software. I haven’t had time to check that out.

--

Thomas David Kehoe

LanguageTwo.com

Rewire your brain to learn languages by ear

From: Kaiser LEKA notifications@github.com
Reply-To: googleapis/nodejs-storage reply@reply.github.com
Date: Sunday, September 8, 2019 at 5:53 PM
To: googleapis/nodejs-storage nodejs-storage@noreply.github.com
Cc: Thomas David Kehoe kehoe@casafuturatech.com, Comment comment@noreply.github.com
Subject: Re: [googleapis/nodejs-storage] Get long lived download URLs in addition to SignedURLs (#697)

Please urgent help for this issue.

My process steps:
Creating a cloud function for creating thumbnails
https://github.com/firebase/functions-samples/tree/master/generate-thumbnail
(I have just changed "adding link to database" section from realtime db to firestore)
Opening my Flutter app and uploading an image file from phone's gallery
Then "creating thumbnails" function executes.
When I check storage, I see generating thumb is okay and "Download URL"s are:
image Download URL"
thumbnail Download URL"
That cloud function also adds urls to firestore
image url
thumbnail url
For a while (7 days?) links on firestore work but at the end of this duration links are expiring...
The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.
We are using createThumbnail function for our apps catalog images and that contains too much records.
So we can't delete images (when links expire) and reupload them again weekly.

Is there another solution for getting public links (never expires and can be accessible from my flutter app)?

If I want to get an unexpired link, can I use still signedURL?
If not, how can I add tokenURL system to generateThumnails function?
Otherwise appliying makingPublic() all images and their thumnails for my mobile app is easy to use?

It is demoralising that I would be test if links expire or not in every situation after 7 days.
So I am unable to add new catalog datas and images to my app in this period...

I think there must be few solutions, cause we shouln't be only users who are facing this problem.
So many apps are also using thumbnail creating like us.

Thanks for your cooperation.

—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub, or mute the thread.

@kaiserleka @tdkehoe @theochampion the only workaround I found is to let the client generate the token download URL

the cloud function write the bucket path and the client the first time it consume that resource will write the download url

it is not an optimal solution but it is the only one available officially (I dont think that using the V2 auth will work forever...)

I am currently trying to get image link on my app like this:
(So I dont use anymore "adding to database section" on generateThumbnail cloud function )

I have added this dart code, (instead of getting data link from firestore)

      var _url=await FirebaseStorage.instance.ref()
      .child("catalogItems")
      .child("thumb_${curCatalogItem.id}_${imageData['suffix']}.jpg").getDownloadURL();
      _imageUrl=_url;

````

but if a user is not logged in, it gives no permission to get this url.

When thinking that a list having too many list items (i.e: 100), 
**I must execute this codes for every catalog item.**

Does this way affects performans or anything?

If my way is acceptable, I need to change this:

rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
```

I have changed storage rules to this (for not loggined users on my app):

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if request.auth != null;
      allow read: if request.auth == null;
    }
  }
}

It is working (I can display images and their thumbails) eventhough it gives this:
_error getting token java.util.concurrent.ExecutionException: com.google.firebase.internal.api.FirebaseNoSignedInUserException: Please sign in before trying to get a token._

@kaiserleka you should ask on stackoverflow or other q&a sites. this is not the right place.

that is not related to the issue

or if you think you are experiencing a bug than open a new issue

Why is this closed? There is no solution to the problem and no reason as to why this functionality doesn't exist.
This looks like a quite important feature to many people (including me) and using firebaseStorageDownloadTokens as @tdkehoe said doesn't seem like a very good idea since it's not an official feature and it could disappear or stop working anytime without notice. For now, I'll be using it, but I'd really appreciate it if the team took some time to either support firebaseStorageDownloadTokens officially (documentation, etc) so we can sleep at night knowing our apps won't break out of nowhere or implement a function for getting a permanent URL (which doesn't seem to be very hard to do since the functionality is already there for frontend libraries).

Please, re-open this issue.
We need permanent download tokens or an explanation why we won't get them.
Thank you.

This issue definitely needs to be reopened and suggestion to be implemented. I can't understand why such a simple activity requires so much hassle.

I finally achieved to implement this solution. Be careful to have __2 levels of "metadata"__. It's not perfect due to the hard coding of the firebase storage URL but it works. Moreover, I think the firebaseStorageDownloadTokens property is not documented.

let options = {
    metadata: {
        metadata: {
            firebaseStorageDownloadTokens: uuid
        }
    }
};
bucket.upload(localFile, options);

I'm in the same boat. Why is the issue closed?
Currently I'm using V2 signing with long lasting expiration dates, but for how long will V2 be supported?

Same boat here. It seems very strange that there is no equivalent of getDownloadUrl() from the admin side. Any explanation would be helpful.

Same here. My storage rules file has an explicit metadata check to decide whether the requesting user is amongst the users who can read the file. In other words, I really need the auth object populated in my storage rules file. The only way to achieve this is by using getDownloadUrl(), but I am not on the web.

Got to this same issue this morning - needs a way to get persistent urls for downloading files from the storage in the admin side, please reopen this issue.

@AVaksman @fhinkel @bcoe This issue is a real pain for developers. Please consider reopening it or point out to method of getting persistent download urls on server side. Thanks.

Hi all, sorry to leave an unresolved issue closed and ignored for so long. We shouldn't have let it sit for so long, but if we miss something like this in the future, feel free to email me so it stands out among the noise ([email protected]).

So, I'm trying to follow along, but it seems the solution to a non-expiring, download URL that offers security options requires Firebase Storage usage (?) Is there a Google Cloud Storage-only solution?

cc @frankyn

Hi @stephenplusplus, I hope things are going well! Any update on this issue?

Hi @agacia, sorry again for the delay. @frankyn could you take a glance at this issue and maybe steer us off in the right direction?

Thank you for the response, @stephenplusplus.
Hi @frankyn, thanks for looking into it. It'd be great to hear if there is a chance to prioritize this request, and if so, what is the estimated timeline? I know the times are pretty crazy now (hi from the Bay Area shelter in place!). Stay safe and healthy!

Not trying to project negativity, but I don't think the way this is being handled meets the standard of Google/Firebase. The thread was open last May, yet until today there is no solution only promises.

The only solution is to implement it yourself

I Always end up having headaches when dealing with google's sdk because of issues like this.

This should have been solved a long time ago. We need an official solution please!!!

Hi folks,
Apologies for the delay, I'm trying to figure out how to help by hopefully answering some questions.

After reading through these questions, I see that there's questions around handling v2 and v4 with long lived download URLs. Additionally, I think it's good to clear up the Firebase feature that is only for Storage for Firebase and not supported in the Storage API.

V2 Signed URLs do support no-limit on expiration signed URLs, but long lived Signed URLs are not recommended from a security perspective. Signed URLs are meant to be used for providing access over a certain period of time after which the URL expires and must be regenerated.

This leads me to "Why does my signed URL expire after ~2 weeks even after I've set the expiration years into the future in Cloud Functions?". When running Node.js Storage library in a Cloud Function it uses Application Default Credentials which are Google Managed Service accounts. Google Managed Service Accounts expire after ~2 weeks documented here.

You can get around this by creating a user managed service account and uploading with the Cloud Function, but strongly discourage it for long lived download URLs and not a path we want to support.

V4 Signed URLs follow a different signature process and is strict on expiration time of up to 7 days which is documented here. It imposes the 7 day expiration restriction as a specification in support.

Adding a bit more as to why we aren't going down this path. If an end-user gets access to a long lived Signed URL and exposes to non-authorized users, you can't disable that specific signed URL and would need to disable the service account causing a potentially large outage if Signed URLs aren't refreshed often.

I would go a step further and ask why not instead do short lived Signed URLs and refresh the Signed URL?

Alternatives

Public Download URLs using Object ACLs

The example provided by https://stackoverflow.com/a/49411199/1842892 is one way (giving credit).
The object is accessible publicly available and if necessary can be locked down without impacting other objects.

```node.js
const options = {
destination: 'object-name',
public: true
};

bucket.upload(attachment, options).then(result => {
const file = result[0];
return file.getMetadata();
}).then(results => {
const metadata = results[0];
// Long Lived Download URL
console.log('metadata=', metadata.mediaLink);
}).catch(error => {
console.error(error);
});

### Public Bucket using IAM

You can create a bucket for assets that should always be available that's always public.
The media URL is easy to generate as long as you have the bucket name and object name. Make a public bucket read [here](https://cloud.google.com/storage/docs/access-control/making-data-public)

https://storage.googleapis.com/bucket-name/object-name

### Refresh Signed URLs

If information should be more secure, you'll want to refresh your Signed URLs for each user to make sure it's locked down after certain period of time. This is more of a pattern and I don't have a specific application outside of provided example code for Node.js:

```node.js
/**
   * TODO(developer): Uncomment the following lines before running the sample.
   */
  // const bucketName = 'Name of a bucket, e.g. my-bucket';
  // const filename = 'File to access, e.g. file.txt';

  // Imports the Google Cloud client library
  const {Storage} = require('@google-cloud/storage');

  // Creates a client
  const storage = new Storage();

  async function generateV4ReadSignedUrl() {
    // These options will allow temporary read access to the file
    const options = {
      version: 'v4',
      action: 'read',
      expires: Date.now() + 15 * 60 * 1000, // 15 minutes
    };

    // Get a v4 signed URL for reading the file
    const [url] = await storage
      .bucket(bucketName)
      .file(filename)
      .getSignedUrl(options);

    console.log('Generated GET signed URL:');
    console.log(url);
    console.log('You can use this URL with any user agent, for example:');
    console.log(`curl '${url}'`);
  }

  generateV4ReadSignedUrl().catch(console.error);

Firebase Auth

If you want specific users to have access to certain resources, then Firebase Auth is the suggested direction because there's a credentials flow and you can check which user is logged in then determine which resources are available to them.

Unguessable Names

One reason I've heard that you want Signed URLs is that they're hard to guess by accident.
I would like to propose using something like a UUID to provide randomness to the object names. It might not be what you're looking for but it can help some.

```node.js
// const bucketName = 'Name of a bucket, e.g. my-bucket';
// const filename = 'Local file to upload, e.g. ./local/path/to/file.txt';

// Imports the Google Cloud client library
const {Storage} = require('@google-cloud/storage');
const UUID = require("uuid-v4");

// Creates a client
const storage = new Storage();

async function uploadFile() {
// Uploads a local file to the bucket
const uuid = UUID();
const unguessableFilename = ${filename}-${uuid};
await storage.bucket(bucketName).upload(pathToLocalFile, {
public: true,
destination: unguessableFilename,
});

console.log(${unguessableFilename} uploaded to ${bucketName}.);
}

uploadFile().catch(console.error);

// Object is located at:
// https://storage.googleapis.com/bucket-name/unguessableFilename

### Firebase DownloadTokens

I personally didn't know this existed but understand it would be something folks are interested in having in the library. For now this isn't something we are looking at supporting in the library because it's not supported by the GCS API only Storage for Firebase supports DownloadTokens. You can still do it using the library we have right now.

Source: https://stackoverflow.com/a/43764656/1842892

```node.js
const bucketName = 'anima-frank';
const filename = 'test.js';

// Imports the Google Cloud client library
const {Storage} = require('@google-cloud/storage');
const UUID = require("uuid-v4");

let uuid = UUID();

// Creates a client
const storage = new Storage();

async function uploadFile() {
  // Uploads a local file to the bucket
  await storage.bucket(bucketName).upload(filename, {
    metadata: {
          contentType: 'text/plain',
          metadata: {
            firebaseStorageDownloadTokens: uuid
          }
    }
  });
  console.log(`${filename} uploaded to ${bucketName}.`);
}

uploadFile().catch(console.error);


// Object is located at:
const fileUrl = `https://firebasestorage.googleapis.com/v0/b/${bucketName}/o/${filename}?alt=media&token=${uuid}`;

console.log(fileUrl);

I'm leaving this open if I missed a question.

Long story short,
You do not believe our requested feature should be implemented because you believe the solution already exists?

Am I correct in assuming that?

And then you said,
"I would go a step further and ask why not instead do short lived Signed URLs and refresh the Signed URL?"

Would you expect that to be a viable solution on upwards of hundreds of thousands of files?
How would you suggest we go about implementing resigning files on that scale?

Hi @Danebrouwer97,

You do not believe our requested feature should be implemented because you believe the solution already exists?

That wasn't my intention no. I may have the wrong interpretation as well.

I should take a step back, what originally lead you here with long lived Signed URLs?

My concern with this approach is if a Signed URL is shared with the user and only one is created then it's effectively "public" if an end-user shares it with others.

Thanks for chiming in, looking to dig into this properly.

I'll try maintain brevity, as my experience may not be similar to the next.

Frontend Package: AngularFire
A user uploads a file, Firebase responds with a reference. From that reference I can get a persistent URL via the getDownloadUrl() method. That URL I then put into user related objects, and that is then saved to Firestore. Then whenever I need that file, I just use the downloadUrl to retrieve the file. Job done.

Backend Package: Firebase Admin SDK
I upload a file on the User's behalf, I then want to use that reference to get a downloadUrl so that I can append it to the same related user document in Firestore. However, I don't have access to that method and as such cannot use the same method as on the Frontend. This means that the next best solution is long lived signed URLs.

Any feedback on the process I follow would be appreciated, maybe i'm not using the best methodology or have misunderstood the process.

Thanks @Danebrouwer97,

getDownloadUrl() is a special helper made for Storage for Firebase.

Based on my basic understanding, Storage for Firebase runs as a proxy between your end-users and Cloud Storage bucket data. This "proxy layer" add a feature to add support for DownloadTokens to Storage for Firebase without it being supported by the Cloud Storage API.

Given that the Admin side for Storage for Firebase uses this library, I now understand that you're trying to work around the limitation of getDownloadUrl() not being supported in this library.

The workaround for this is using the Storage library / Firebase Admin SDK is the following (I didn't come up with the solution and provided source for credit):
Source: https://stackoverflow.com/a/43764656/1842892

```node.js
const bucketName = 'anima-frank';
const filename = 'test.js';

// Imports the Google Cloud client library
const {Storage} = require('@google-cloud/storage');
const UUID = require("uuid-v4");

let uuid = UUID();

// Creates a client
const storage = new Storage();

async function uploadFile() {
// Uploads a local file to the bucket
await storage.bucket(bucketName).upload(filename, {
metadata: {
contentType: 'text/plain',
metadata: {
firebaseStorageDownloadTokens: uuid
}
}
});
console.log(${filename} uploaded to ${bucketName}.);
}

uploadFile().catch(console.error);

// Object is located at:
const fileUrl = https://firebasestorage.googleapis.com/v0/b/${bucketName}/o/${filename}?alt=media&token=${uuid};

console.log(fileUrl);
```

What would be the work around if I just want to download the files? not download after upload, I'm not uploading anything.
I need to dynamically fill an email with images.
I'm not asking for specific code, just want to know would be a path to reach that solution.

Hi @ihojmanb,

If the files are already stored in the Cloud Storage bucket, and the images are not private I'd recommend a public bucket.

  1. Enable public bucket
  2. In object metadata, you'll find the public media URL which you can use in the email.

You can also use object-level ACL if you don't want to make the bucket public.

  1. Make file public
  2. In object metadata, you'll find the public media URL which you can use in the email.

I'm staying away from Signed URL because the email images may expire before it's read. PLMK If I have answered your question.

If you want to download the content there's also: https://googleapis.dev/nodejs/storage/latest/File.html#download-examples

Hi @frankyn sorry if I intruded here... I have some doubt about security with firebase and it seem that you know much more than me and that this issues talk about it!
I am struggle with the auth roles of storage and the url that we can create with sdk...

For sake of semplicity, I report some step that there are not clear to me.

  1. I have write a simple rule in the "edit rules" of storage firebase console.
    rules_version = '2'; service firebase.storage { match /b/{bucket}/o { match /user/{userId}/{allImages=**} { allow read, write: if request.auth.uid == userId; } } }
  2. I upload a file with "client sdk" and the rule seem to works like I expect
    const ref = fb.storage?.ref('user/${uid}/photos/${file.name}'); const uploadTask = ref?.put(file);
    If I upload when I am authenticated it works, If I upload when I am not authenticate I receive:
    Firebase Storage: User does not have permission to access 'user/6sMoZyEixObAlCPBODdy6ovX3ap2/photos/IMG_20190518_204933.jpg'.
    so far, so good.
    But I have some problem when I try to get the file... Suppose that I want a "url" of this file (an image in my case) to put in to my tag (to make my app more fancy :) ) like you suggest: I have to adding to the code above
    ref?.getDownloadURL()
    And the the things goes well, my app show the image and I am happy. I am authenticated so I can view the image (the rules permitted it)... until I try to copy and past that url in a anonymous tab (or better in onother browser)... Oops I see the image?! Why? I trust in my rules! Why they don't work ? I wrong somethings? There is a good way to make this?
    Again, if this is correct (explain why)... somebody could create the url (in some way, tampering with our js friend, rolling the dice, or something other) and see my photo!

Thanks in advantage

Hi @seeiuu,

Thanks for raising your question, if I understand correctly you want to allow users to have access to only their files. The issue with ref?.getDownloadURL() is that the URL if copied, will continue to work outside of an unauthenticated session (different browser/incognito session).

I see this is FR in https://github.com/firebase/firebase-js-sdk/issues/76 which calls this issue out. The issue is that, getDownloadURL() works until the token in metadata is cleaned up by an Admin. This isn't secure because it doesn't expire on its own and can be shared with non-authorized users.

I'm not an expert in Firebase Auth and would recommend raising the question there, but I think there might be a solution using Create Custom Tokens and here's a video.

I could point to signedUrl() but it has the same issue. If an end-user copies it, they can share it with non-authorized users until it expires.

Ehi @frankyn thanks to you too!
I very appreciate your fast reply and your suggest will be securely helpful to me. You are right this is not the correct place to talk about that... but the flow was in this argument... so, only for complemetacy, I post this link, I found it very clear https://dev.to/daviddalbusco/firebase-storage-gotchas-1epa

Hi folks,

I'm closing this issue and will reopen if I missed a question. Thank you for your patience.

Appending previous comment that went into detail in case someone find this issue

After reading through these questions, I see that there's questions around handling v2 and v4 with long lived download URLs. Additionally, I think it's good to clear up the Firebase feature that is only for Storage for Firebase and not supported in the Storage API.

V2 Signed URLs do support no-limit on expiration signed URLs, but long lived Signed URLs are not recommended from a security perspective. Signed URLs are meant to be used for providing access over a certain period of time after which the URL expires and must be regenerated.

This leads me to "Why does my signed URL expire after ~2 weeks even after I've set the expiration years into the future in Cloud Functions?". When running Node.js Storage library in a Cloud Function it uses Application Default Credentials which are Google Managed Service accounts. Google Managed Service Accounts expire after ~2 weeks documented here.

You can get around this by creating a user managed service account and uploading with the Cloud Function, but strongly discourage it for long lived download URLs and not a path we want to support.

V4 Signed URLs follow a different signature process and is strict on expiration time of up to 7 days which is documented here. It imposes the 7 day expiration restriction as a specification in support.

Adding a bit more as to why we aren't going down this path. If an end-user gets access to a long lived Signed URL and exposes to non-authorized users, you can't disable that specific signed URL and would need to disable the service account causing a potentially large outage if Signed URLs aren't refreshed often.

I would go a step further and ask why not instead do short lived Signed URLs and refresh the Signed URL?

Alternatives

Public Download URLs using Object ACLs

The example provided by https://stackoverflow.com/a/49411199/1842892 is one way (giving credit).
The object is accessible publicly available and if necessary can be locked down without impacting other objects.

```node.js
const options = {
destination: 'object-name',
public: true
};

bucket.upload(attachment, options).then(result => {
const file = result[0];
return file.getMetadata();
}).then(results => {
const metadata = results[0];
// Long Lived Download URL
console.log('metadata=', metadata.mediaLink);
}).catch(error => {
console.error(error);
});

### Public Bucket using IAM

You can create a bucket for assets that should always be available that's always public.
The media URL is easy to generate as long as you have the bucket name and object name. Make a public bucket read [here](https://cloud.google.com/storage/docs/access-control/making-data-public)

https://storage.googleapis.com/bucket-name/object-name

### Refresh Signed URLs

If information should be more secure, you'll want to refresh your Signed URLs for each user to make sure it's locked down after certain period of time. This is more of a pattern and I don't have a specific application outside of provided example code for Node.js:

```node.js
/**
   * TODO(developer): Uncomment the following lines before running the sample.
   */
  // const bucketName = 'Name of a bucket, e.g. my-bucket';
  // const filename = 'File to access, e.g. file.txt';

  // Imports the Google Cloud client library
  const {Storage} = require('@google-cloud/storage');

  // Creates a client
  const storage = new Storage();

  async function generateV4ReadSignedUrl() {
    // These options will allow temporary read access to the file
    const options = {
      version: 'v4',
      action: 'read',
      expires: Date.now() + 15 * 60 * 1000, // 15 minutes
    };

    // Get a v4 signed URL for reading the file
    const [url] = await storage
      .bucket(bucketName)
      .file(filename)
      .getSignedUrl(options);

    console.log('Generated GET signed URL:');
    console.log(url);
    console.log('You can use this URL with any user agent, for example:');
    console.log(`curl '${url}'`);
  }

  generateV4ReadSignedUrl().catch(console.error);

Firebase Auth

If you want specific users to have access to certain resources, then Firebase Auth is the suggested direction because there's a credentials flow and you can check which user is logged in then determine which resources are available to them.

Unguessable Names

One reason I've heard that you want Signed URLs is that they're hard to guess by accident.
I would like to propose using something like a UUID to provide randomness to the object names. It might not be what you're looking for but it can help some.

```node.js
// const bucketName = 'Name of a bucket, e.g. my-bucket';
// const filename = 'Local file to upload, e.g. ./local/path/to/file.txt';

// Imports the Google Cloud client library
const {Storage} = require('@google-cloud/storage');
const UUID = require("uuid-v4");

// Creates a client
const storage = new Storage();

async function uploadFile() {
// Uploads a local file to the bucket
const uuid = UUID();
const unguessableFilename = ${filename}-${uuid};
await storage.bucket(bucketName).upload(pathToLocalFile, {
public: true,
destination: unguessableFilename,
});

console.log(${unguessableFilename} uploaded to ${bucketName}.);
}

uploadFile().catch(console.error);

// Object is located at:
// https://storage.googleapis.com/bucket-name/unguessableFilename

### Firebase DownloadTokens

I personally didn't know this existed but understand it would be something folks are interested in having in the library. For now this isn't something we are looking at supporting in the library because it's not supported by the GCS API only Storage for Firebase supports DownloadTokens. You can still do it using the library we have right now.

Source: https://stackoverflow.com/a/43764656/1842892

```node.js
const bucketName = 'anima-frank';
const filename = 'test.js';

// Imports the Google Cloud client library
const {Storage} = require('@google-cloud/storage');
const UUID = require("uuid-v4");

let uuid = UUID();

// Creates a client
const storage = new Storage();

async function uploadFile() {
  // Uploads a local file to the bucket
  await storage.bucket(bucketName).upload(filename, {
    metadata: {
          contentType: 'text/plain',
          metadata: {
            firebaseStorageDownloadTokens: uuid
          }
    }
  });
  console.log(`${filename} uploaded to ${bucketName}.`);
}

uploadFile().catch(console.error);


// Object is located at:
const fileUrl = `https://firebasestorage.googleapis.com/v0/b/${bucketName}/o/${filename}?alt=media&token=${uuid}`;

console.log(fileUrl);

end-user shares it with others.

Thanks for chiming in, looking to dig into this properly.

If the getDown

Hi @Danebrouwer97,

You do not believe our requested feature should be implemented because you believe the solution already exists?

That wasn't my intention no. I may have the wrong interpretation as well.

I should take a step back, what originally lead you here with long lived Signed URLs?

My concern with this approach is if a Signed URL is shared with the user and only one is created then it's effectively "public" if an end-user shares it with others.

Thanks for chiming in, looking to dig into this properly.

If the client Side getDownloadURL() is effectively providing a public link even if the file is locked behind storage rules, isn't this the same problem....A user could just generate a link with getDownloadUrl and share the link with other people.

Isn't up to the developer to determine whether this is the behaviour he wants. A function on the node storage package that gets a public url similar to the firebase client getDownloadUrl would be useful.

Hi @febg11, thanks for replying to this thread.

This library is scoped to support the Storage API and getDownloadUrl() is specific to Storage for Firebase API. Therefore we won't support getDownloadUrl() in this library.

If you're using Storage for Firebase and want to use this library to do the same thing as getDownloadUrl() follow this example (ONLY for Storage for Firebase API and does not work with Storage API):

const bucketName = 'anima-frank';
const filename = 'test.js';

// Imports the Google Cloud client library
const {Storage} = require('@google-cloud/storage');
const UUID = require("uuid-v4");

let uuid = UUID();

// Creates a client
const storage = new Storage();

async function uploadFile() {
  // Uploads a local file to the bucket
  await storage.bucket(bucketName).upload(filename, {
    metadata: {
          contentType: 'text/plain',
          metadata: {
            firebaseStorageDownloadTokens: uuid
          }
    }
  });
  console.log(`${filename} uploaded to ${bucketName}.`);
}

uploadFile().catch(console.error);


// Object is located at:
const fileUrl = `https://firebasestorage.googleapis.com/v0/b/${bucketName}/o/${filename}?alt=media&token=${uuid}`;

console.log(fileUrl);

using a guid for security... this is really wrong

Hi @giammin, thanks for chiming in.

The Storage for Firebase solution with a GUID is what's used by the service, there are other solutions documented https://github.com/googleapis/nodejs-storage/issues/697#issuecomment-610603232

sorry @frankyn but it seems that one cant have a proper way to protect a file without creating a custom service

you have public accessible file.
At max one can use "Unguessable" url which is security by obscurity done with guid which are not so "Unguessable"

and anyway the only solution provided to OP is using an undocumented feature

sorry @frankyn but it seems that one cant have a proper way to protect a file without creating a custom service

you have public accessible file.
At max one can use "Unguessable" url which is security by obscurity done with guid which are not so "Unguessable"

and anyway the only solution provided to OP is using an undocumented feature

I agree but Doug Stevenson said this here.

"It's not supported case to build a URL like this. It may work today, but could break in the future. You should only use the provided APIs to generate a proper download URL"

It seems that all the solutions are hacky/unsafe... is there not a better solution?

Hi @giammin and @febg11,

Thanks for the feedback. Could you both restate which the you're trying to solve? I want to make sure I'm not missing anything.

Hi @frankyn.

I am just looking for a solution that allows me to generate a url that will point to a file on firebase storage that will not expire and that can be generated in node.js. I will store this url in a cloud firestore document so users can read it instead than pointlessly calling getDowloadURL() over and over and wasting resources.

I would rather call a method that is defined in the cloud storage node library rather than implementing my own workaround they may not be secure/may break at any time.

Being able to revoke the link is not a problem for me, however I cannot speak for @giammin requirements.

Thanks for prompt reply.

Thanks @febg11, I chimed in on a related question here: https://github.com/firebase/firebase-js-sdk/issues/76 to address this gap. I can understand not wanting to rely on something that may change in the future for unforeseen circumstances.

Pending response from @giammin.

Hi @frankyn thanks for keeping the interest In this issue.

My need is to create image and file from a cloud function and from an external service (netcore)

These files are consumed by an android/iOS app (nativescript with firebase as backend)

Users upload files in the Firebase storage (profile pic or attachment) and those files need to be checked and elaborated and then are available to some other users.

But the service or cloud functions can’t create a protected link to those files. I need the client app to generate those public urls.

Another problem is that those files should not be public accessible but only to selected users. It is not acceptable that anyone with the right link can download them And the storage security rules does not apply to download link

To get around this problem I create a netcore service that works as a reverse proxy for the firebase storage.
It provide public link for the files and authorization is handled with tokens that are send with the request. Why can’t firebase storage work this way? It also have the user token and for authorization one can take advantage of the security rules...

I find those downloadurls cumbersome. If one is leaked or need to change you have to update every reference to it

Thanks @giammin, spoke with folks on the Storage for Firebase team last week and they're looking into addressing protected access in https://github.com/firebase/firebase-js-sdk/issues/76 (which I think addresses your issue) and considered a feature gap in their support.

Could you raise this feedback in that issue in case there's a path forward that I'm not aware of in that firebase issue? @avolkovi, can probably add more here as well.

I think I'm having a similar issue to what many people here are having.

I'm trying to implement a "search" function for people to search through proprietary PDFs stored in Firebase Storage. I don't want to allow anyone to be able to download the file unless they are authenticated. So far I haven't been able to find a way to do this without extensive workarounds - I don't fully trust getDownloadURL() or the signed URL because all someone has to do is share the URL and it's public (even if you set the life to the signed URL to be very short). This seems like a gaping hole in Firebase security to me. I'll explore some of the workarounds mentioned here but to be honest I need to get this project out and done and I might abandon Firebase altogether if I can't keep the files completely secure.

@iingles

I don't fully trust getDownloadURL() or the signed URL because all someone has to do is share the URL and it's public (even if you set the life to the signed URL to be very short).

Be careful, even a signed URL can expose the file forever due to how _Cloud Storage for Firebase_ uses download tokens.

More info: https://www.sentinelstand.com/article/guide-to-firebase-storage-download-urls-tokens

@nicoqh GCS Signed URLs are completely distinct from Cloud Storage for Firebase Download URLs. They go through different APIs (one is a Cloud API intended for backend based authentication or Google OAuth, the other is a Firebase API intended for client based authentication such as Firebase Authentication)

The idea behind Firebase Download URLs is that if someone has access to the link, it's true that they could share the link and thereby share access, but they could just as easily download the file and share it through some other means. Having access to the download URL is equivalent from a security standpoint as having permanent access to the file. If you upload a new version of the file, we regenerate the download URL, thereby preventing anyone with access to the previous version from also accessing the new version.

Hope that helps clarify the distinction a bit.

@avolkovi My point was that a signed URL will expose your Firebase download token because Cloud Storage for Firebase uses Cloud Storage's concept of metadata to store its download tokens, and Cloud Storage exposes metadata through its response headers. So a user will be able to construct a long-lived Firebase download URL (https://firebasestorage.googleapis.com/v0/b/...) from a short-lived signed Google Storage URL (https://storage.googleapis.com/...) even if they don't have read permissions (getDownloadUrl()) per the Firebase Storage security rules.

It's something to be aware of since signed URLs are suggested as an alternative to long-lived download URLs.

@nicoqh Good point! That is correct.

One thing to note, if the Firebase API is not being used, the download tokens will not be generated. They are only generated when uploading via the Firebase API or when calling getDownloadUrl() or getMetadata() via the Firebase SDK. If you're just using GCS signed URLs without Firebase in the mix then you won't have this issue. Yet another reason for us to rethink storing these in Metadata....

Closing out issue for now, will reopen if there's another option here.

Hello @frankyn,
Sorry if this is not the right place to ask, I've been following this thread for quite a long time and I've encountered the same issues than other developers when it comes to getSignedUrl for long-lived URLs.

Context

We are trying to build some UI on the web, around video files uploaded by our customers through our android and ios apps.
We upload the files using the Storage API from Firebase and once the upload is complete, a cloud function runs automatically to store our file in our database along with the metadata.
We originally tried to create signedUrl but we realize they stopped working after some time due to the expiration of the default service account, as you mentioned in the thread.
We then used your suggestion to use the mediaLink from the getMetadata function in our cloud function through the Storage API, and it's working almost fine. We have now a public file that is (up to now) correctly interpreted by the browser <video> tag when we fetch the data client-side.

Problem

However, when this same link is shared through a social network (for example) Twitter, as part of an og:video meta tag or twitter:video, we get a weird error:

Anonymous caller does not have storage.objects.get access to the Google Cloud Storage object.

I don't fully understand why Google Chrome, Safari, and other browsers are able to read this media link properly and anyone can download the full video when using the mediaLink, but the preview on social network is broken like that.
It was not the case with getSignedUrl but unfortunately, those stops working after ~2 weeks due to renewal of the service account used to generate them (default).

Do you have any insight or can you show me the right way to handle such use cases ?

I really appreciate any help you can provide.

Was this page helpful?
0 / 5 - 0 ratings