A file stored in Cloud Storage for Firebase can be accessed in (at least) three ways:
- Persistent download URLs (
getDownloadUrl()
): Public and long-lived, but hard to guess - Signed, short-lived URLs (
getSignedUrl()
): Public, short-lived, and hard to guess - Public download URLs: Public, persistent, without security
Firebase Download URLs
getDownloadUrl()
A Firebase download URL is a long-lived, publicly accessible URL that is used to access a file from Cloud Storage. The download URL contains a download token which acts as a security measure to restrict access only to those who possess the token.
The download token is created automatically whenever a file is uploaded to Cloud Storage for Firebase. It's a random UUIDv4, which makes the URL hard to guess.
To retrieve a download URL, your client app needs to call the getDownloadUrl()
method on the file reference. (The examples in this article are using the
JavaScript SDK, but the concepts apply to all platforms.)
const storage = firebase.storage();
storage.ref('image.jpg').getDownloadURL()
.then((url) => {
// Do something with the URL ...
})
The resulting download URL will look like this:
https://firebasestorage.googleapis.com/v0/b/<projectId>.appspot.com/o/image.jpg?alt=media&token=<token>
Only authorized users can call getDownloadURL()
(more on this below). But the
method will return the same download URL for every invocation because there's
only one (long-lived) token stored per file. This means that anyone who gets
their hands on the download URL will be able to access the file, whether they're
authorized or not! In order words, retrieving the download URL is a restricted
operation, but the download URL itself is public.
To revoke a download URL, you need to manually revoke the download token in the Firebase Console. You can't do this on your client app.
Restricting access to files
In order to keep files private, developers are encouraged to restrict read
access by defining Firebase Storage security rules. Only authorized users
(users with read access) are able to call getDownloadUrl()
. Here's an example
security rule:
match /path/to/{file} {
// Deny reads
allow read: if false;
// Or, allow reads by authenticated users
allow read: if request.auth != null;
}
More on Security rules: Firestore Security Rules examples
As mentioned earlier, even if calls to getDownloadUrl()
are restricted, the
actual download URLs are public (although hard to guess because of the random
UUIDv4). So if anyone shares the result of getDownloadUrl()
, the file won't be
very secret anymore!
Creating download URLs
The Firebase client SDK allows you to retrieve, but not create, download URLs.
Technically, download tokens are stored as custom metadata on the storage
object. If you browse to a file in the Google Cloud
Console (not the Firebase Console) you'll
be able to see the download token stored as firebaseStorageDownloadTokens
.
This means that you can manipulate download tokens by updating the value of
firebaseStorageDownloadTokens
. You can do this with the Cloud Storage SDK,
which is bundled with the Firebase Admin SDK and accessible by calling
admin.storage()
.
For example, you can upload a file from a Firebase Cloud Function and set a custom download token:
const admin = require("firebase-admin");
const bucket = admin.storage().bucket();
const uuid = require("uuid-v4"); // npm install uuid-v4
// ... inside your function:
await bucket.upload("file.txt", {
destination: "uploads/file.txt",
metadata: {
cacheControl: "max-age=31536000",
// "custom" metadata:
metadata: {
firebaseStorageDownloadTokens: uuid(), // Can technically be anything you want
},
},
})
You can also update an existing file's download token:
const admin = require("firebase-admin");
const bucket = admin.storage().bucket();
const uuid = require("uuid-v4");
// ... inside your function:
const file = bucket.file("uploads/file.txt");
await file.setMetadata({
metadata: {
// Update the download token:
firebaseStorageDownloadTokens: uuid(),
// Or delete it:
firebaseStorageDownloadTokens: null,
},
})
Now, calling the client-side method getDownloadUrl()
on the file reference
will give you a download URL that contains your newly created token. Note that
if you delete the token (set it to null
), calling getDownloadUrl()
will
create and store a new one; Firebase seems to always want at least one token for
every file.
Note: This is not a documented feature! Firebase doesn't expose the concept of download tokens in its public SDKs or APIs, so manipulating tokens this way feels a bit "hacky". How Firebase deals with download tokens may be subject to change. On a positive note, official Firebase products like Firebase Extensions also manipulate download tokens, and the approach has been suggested by Google employees.
There's no getDownloadUrl() in the Node SDK
The concept of download tokens is specific to Cloud Storage for Firebase.
Google Cloud Storage itself, which Firebase relies on, doesn't know the meaning
of firebaseStorageDownloadTokens
. But the field can be edited like any other
custom metadata.
The Firebase Node SDKs bundles the Cloud Storage SDK which isn't aware of
any Firebase-specific functionality. That's why you can't call
getDownloadUrl()
on the Node SDK. But you can construct a download URL
manually:
const createPersistentDownloadUrl = (bucket, pathToFile, downloadToken) => {
return `https://firebasestorage.googleapis.com/v0/b/${bucket}/o/${encodeURIComponent(
pathToFile
)}?alt=media&token=${downloadToken}`;
};
Again, note that this is not a documented feature.
getDownloadUrl() incurs network traffic
A call to getDownloadUrl()
utilizes some Google Cloud resources. Specifically,
it's a "Class B" operation (check the Google Cloud pricing page)
that triggers a bit of data transfer. This is why the method is asynchronous.
If you don't want your client app to call getDownloadUrl()
and wait for the
result, you can store download URLs in a database like Firestore. This works
because the URL doesn't change unless you revoke the token.
Summary
- Every file gets a download token during upload.
- If you overwrite a file, a new download token is generated.
- The download token, and thus the download URL, is long-lived and public.
- Download tokens can be revoked in the Firebase Console, which replaces it with a new one.
- The token is stored as custom metadata,
firebaseStorageDownloadTokens
, and you can write to this field from the Google Cloud Console or using the Storage SDKs. - This concept of download URLs is specific to Firebase. Google Cloud Storage
itself doesn't know the meaning of
firebaseStorageDownloadTokens
, but it let's you edit it like any other custom metadata field.
Signed URLs
Instead of using Firebase download URLs (getDownloadUrl()
), which are
long-lived and public, signed URLs
allow you to create short-lived URLs that give access to files for a limited
amount of time. For example, to grant a user permission to download a file, you
can create a signed URL that expires in 2 minutes. Even if the user shares or
saves the URL, it won't be accessible after it expires.
Here's an example of how you can create a signed URL in a Firebase Cloud Function:
const admin = require("firebase-admin");
const bucket = admin.storage().bucket();
// ... inside your function:
const urlOptions = {
version: "v4",
action: "read",
expires: Date.now() + 1000 * 60 * 2, // 2 minutes
}
const [url] = await bucket
.file("uploads/file.txt")
.getSignedUrl(urlOptions);
// Return `url` to the user.
// It will look something like this:
// https://storage.googleapis.com/project-id.appspot.com/uploads/file.txt?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=loooooong string ...
If your strategy is to keep your files private and only give access using
signed URLs, you don't want anyone to retrieve the long-lived Firebase download
URL by calling getDownloadUrl()
. This operation can be restricted by denying
reads in the Storage security rules:
match /path/to/{file} {
// Deny reads
allow read: if false;
}
Be careful when signing URLs with Firebase Storage!
There's a big caveat when signing URLs to Firebase Storage objects.
As mentioned, the file's download token is created automatically during upload.
This applies both to the client-side Firebase SDK and the Firebase Console.
And you can restrict calls to getDownloadUrl()
by defining security rules,
which means the token will stay secret and the file is kept private.
However, metadata (even custom metadata) is exposed through HTTP headers when a file is downloaded. As the Cloud Storage documentation states:
"x-goog-meta- headers are stored with an object and are always returned in a response header when you do a GET or HEAD request on an object."
We can test this on a signed URL:
GET
https://storage.googleapis.com/project-id.appspot.com/file.txt?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=loooooong string ...
Response headers:
x-goog-meta-firebasestoragedownloadtokens: 95ded11d-952f-4d26-b730-b920f0881542
This means that someone can take the download token, which was exposed in the response, and construct a Firebase download URL and obtain unrestricted access to the file:
https://firebasestorage.googleapis.com/v0/b/project-id.appspot.com/o/uploads/file.txt?alt=media&token=95ded11d-952f-4d26-b730-b920f0881542
The only way to remove the download token from the header is to remove the
firebaseStorageDownloadTokens
field from the custom metadata. But you can't
set the field to null
when uploading files through the client SDK. It would
result in the error "Not allowed to set custom metadata for
firebaseStorageDownloadTokens".
You also can't remove (only renew) the token in the Firebase Console. Only the Google Cloud Console allows you to edit or remove the field, but this would be a manual operation.
The only option left is to remove the token server-side, e.g. in a Firebase Function triggered by file uploads:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const bucket = admin.storage().bucket();
exports.removeDownloadToken = functions.storage.object().onFinalize((object) => {
const file = bucket.file(object.name);
return remoteFile.setMetadata({
// Metadata is merged, so this won't delete other existing metadata
metadata: {
// Delete the download token
firebaseStorageDownloadTokens: null,
},
})
});
Another option is to upload files using other Google Cloud tools (not Firebase).
As long as calls to getDownloadUrl()
are disallowed in the security rules, the
download token won't be regenerated.
Public URLs
If you don't require any security checks, you can make your files entirely
public. Using the Google Cloud Storage SDK, you can call makePublic()
on a
storage object.
Make an existing file public in a Firebase Cloud Function:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const bucket = admin.storage().bucket();
// ... inside your function:
const [file] = await bucket.file("uploads/file.txt").makePublic();
const [metadata] = file.getMetadata();
const url = metadata.mediaLink;
Or, uploading a public file:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const bucket = admin.storage().bucket();
// ... inside your function:
const [file] = await bucket.upload("file.txt", {
destination: "uploads/file.txt",
public: true, // Alias for predefinedAcl = 'publicRead'
});
const [metadata] = file.getMetadata();
const url = metadata.mediaLink;
This will let anyone access the files with the following URL:
https://storage.googleapis.com/project-id.appspot.com/file.txt
However, Firebase doesn't let you access files through their API without a
download token, even if the download token has been removed from the file; a
Firebase download URL without a token will respond with an error. Notice how
this URL lacks a token
parameter in the query string:
GET https://firebasestorage.googleapis.com/v0/b/project-id.appspot.com/o/file.txt?alt=media
Response:
{
"error": {
"code": 403,
"message": "Permission denied. Could not perform this operation"
}
}
This is good. If the operation succeeded, it would be impossible to store completely private files in Firebase Storage.