firebase / firebase-js-sdk

Firebase Javascript SDK
https://firebase.google.com/docs/web/setup
Other
4.81k stars 884 forks source link

Firebase Storage READ security rules: almost pointless, as currently implemented. #5342

Open LeadDreamer opened 3 years ago

LeadDreamer commented 3 years ago

Environment

The problem

So, Google Folk:

Would someone please tell me under what conditions would any authenticated rule ever be passed for reads in the Firebase client SDKs?

For Example:

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

It seems obvious enough - until you finally realize that the client SDK's failed to implement the (extremely obvious) .get() method on a storage reference.

.getDownloadURL() creates a TOKENIZED URL, which BYPASSES the above security rules.

Use of an UNtokenized URL in a browser context (such as an image URL) DOES NOT INCLUDE authentication headers, so the above rule will ALWAYS fail.

IF there is no .get() method, and... IFgetDownloadURL() always generates a URL that bypasses the rule...

What the ever-loving-heck is the POINT of such a rule?

And PLEASE don't repeat the usual pushback of "once a URL is in the world what's the point of security" - I have very good reasons (which I can explain in confidence, out-of-band) why I want this level of control.

It's ALSO clear someone INTENDED there to be this level of control.

I can, of course, write a wrapper that some-way-or-another gets the authentication headers and add them to an HTTPS request (SPECIFICALLY WHAT YOUR CODE IS DOING ALREADY) to download an item (whether image or not) to the client JS...

But WHY would I do that when I can see that virtually ALL of the needed code ALREADY EXISTS in the SDK, you just didn't expose a .get() method...

so: either some way to cause browser image fetches to include/use the cookie for authentication (is it a CORS policy?) OR just simply add the .get() method to StorageReference in the client SDK

(hey, put me on contract, I can probably get it done - I've already started poking in the repository, which is why I know most of the code structure is there)

Steps to reproduce:

(FYI, I am creating a @leaddreamer/firebase-wrapper library for my own use, which INCLUDES wrapper to make the client API almost identical between client and Admin SDK. Obviously the .get() is trivial in Cloud Storage. It doesn't help the browser, though)

akauppi commented 3 years ago

@LeadDreamer You state you’ve started a wrapper project. Did you consider just making a patch that gets automatically installed at npm install and exposes the said feature?

I’ve resorted to this approach once with Firebase clients, and now considering it with Raygun client. If npm dependencies are almost there - ”needed code already exists” - I find this a reasonable way to not be kept back by the often sluggish development of libraries that I cannot directly influence.

Feiyang1 commented 3 years ago

It is a long standing feature request and we are experimenting with it recently in https://github.com/firebase/firebase-js-sdk/pull/4973. We still need to go through internal API reviews to be able to release it. I can't guarantee when everything will be ready, but stay tuned!

LeadDreamer commented 3 years ago

Beyond the .get(), .getBytes() or whatever - have you found ANY scenario where the Storage Security rules are relevant? I've set CORS policies - they only apply, it seems, to GCP authentication not Firebase Authentication.

For example:

https://storage.cloud.google.com/theBucket/Artists/recF5G3FqK958V00P/Tours/recUlZr2Wbtu0mfoN/TourStops/reckAynVOU7YmI2fg/image/image.jpg

...can be accessed by me because I have Access to my google account, but cannot be accessed by others. Note this access is INDEPENDENT of security rules. Note it actually gets a 307 status and redirected to:

https://00f74ba44b36a7a4021e61479a11af1c59e94e4c9f-apidata.googleusercontent.com/download/storage/v1/b/myBucket/o/Artists%2FrecF5G3FqK958V00P%2FTours%2FrecUlZr2Wbtu0mfoN%2FTourStops%2FreckAynVOU7YmI2fg%2Fimage%2Fimage.jpg?jk=AFshE3Xo89eif3oJeHMy3o5j6RgsruTw_XlM9_4YAGayXpLkN8fGcpawlpZc0L_nvHjEQKjhbYNP6RShRv2Tgt6u3i0uUsbfFJTP9e7fwfXNgFQ7_iRjn7DSdqZXR7wFxCHBFZlQV_v94ZZaQeIbjfor4CXi9c1SBsjZZE-4G79nLU_LTUn5S1LEzQ4JF0tSbPuFjQ69Gtz8lvVRcjhBKRSs45uvOP7rVFg-k17gr4R_RxyOJdrvfH4UJc-oC3IKpz8DwlNVVSirgdb6Nk25sAnzsOkQnlq9Y76CPL9IRLWbWm7SPBg1r0WntAHvi0CYBUAwcMF_VAVNocxc5Clc4ItzF7SgMvnmKFF5aRO7X-n24cmJ0yjVMHSm31RunDCd9SwWYpqptfe8pAMEH0AAsrm3pWPTL_ZQ4-f8Jlh0FlrYmjoQU-bD1LF6xlUbFJ0slX8cOpt38Y-fiqDQOXQLQ3-IArW_7BfEmhkz17tjLrDt2dlTmVuqy7JFLIDprO_CgaIaQ_fEO6PuJYHYlS-0K5MgRT1SkYCf3I_bxhyQBjg6u3TIZhEHh6VnrDwPPh3B9cO5HSSR0uOYXi4K0Qulm4SvKO1wG_HOUzTvvmCZBdx1Tqd6HUKaugtL2DSLaeEmcOGJFkX_GuhrS3PVEL6LY54-bG7vP35wZcm37HTj0Ui5-J2T5MWELb993ltWKhbSwCWfK-696cwAPNI_TTOFa6hQF8aJYyZJP1fGxJkNM4sjPgPIBogZfPginpgNPGMp0Q0uwHINoQMGXAPGegup9OI_213F0C_dLzK_YZxVpP10_TQDy3m7f619AwEnjT5_rK59GWviJrgb-cONBnCwh0VRf89B9QCIAYux_Jby2Wk4YlCl_R_RUr6AQUjBw5dGr80SPgb1u47nbOhLBEKh4cISnUrBSf1EBvjcQCxmLt9IILHoArNPSf5Y2MapNEtd8Xpsxl4ETi8UcLlF0t7hAH-OrhxhXFCWZJzbB8FUzWxBqQv_g37k8pg9Fp3-9F3b0e2qW20v530zfBTHt7D2sWr14YGamIc8EYiTgK96yA0p38nKHf0b6-Wla6eeowdqE-_xgLqF4Urx8QMdXC6iEOGDpKwLffYJ&isca=1

On the other hand:

https://firebasestorage.googleapis.com/v0/b/myBucket/o/Artists%2Frecq5cURMyJsRKFbO%2FTours%2FrecoVCnOyTvNom0fZ%2FTourStops%2FrecH2Ec5SIatBdGw0%2Fimage%2Fimage.jpg?alt=media&token=whatever-the-token-is

has access by ANYBODY (status 200) whether authenticated or not, while

https://firebasestorage.googleapis.com/v0/b/myBucket/o/Artists%2Frecq5cURMyJsRKFbO%2FTours%2FrecoVCnOyTvNom0fZ%2FTourStops%2FrecH2Ec5SIatBdGw0%2Fimage%2Fimage.jpg?alt=media

cannot be accessed (403 error) by anybody, authenticated or not.

So there is currently literally only the two scenarios:

I intended on hacking some code to axios or fetch a file from storage into my browser javascript, just to see if it can be done (I doubt it)

LeadDreamer commented 3 years ago

With much experimentation, here is what I've learned so far for images stored in Firebase Storage (private information obscured):

=> With a token on the URL, security is bypassed altogether.

=> WITHOUT a token, it is possible to set rules to allow access at various levels of a hierarchy - but ONLY simple on/off, not conditional on request.auth for example:

WORKS

service firebase.storage {
    match /b/{bucket}/o {
      match /Artists/{artistID}/{files=**} {
      allow read: if true;
    }
}

fetched from URL

https://firebasestorage.googleapis.com/v0/b/mybucket.appspot.com/o/Artists%2Fall%2fthe%2Frest%2fimage.jpg

the above ALLOWS for images to be fetched IF they are stored under /Artists, BUT all / after /o/ MUST be %2F encoded.

Also, rules can be written to ONLY permit access to LOWER levels in the hierarchy, for example:

service firebase.storage {
    match /b/{bucket}/o {
      match /Artists/{artistID}/Tours/{tourID}/{files=**} {
      allow read: if true;
    }
}

fetched from URL

https://firebasestorage.googleapis.com/v0/b/mybucket.appspot.com/o/Artists%2FTours%2FtourID%2Fall%2fthe%2Frest%2Fimage.jpg

will succeed, but

https://firebasestorage.googleapis.com/v0/b/mybucket.appspot.com/o/Artists%2Fimage.jpg

will properly fail

Note that these work regardless of whether accessed in an authenticated page or not.

WHAT DOES NOT WORK

service firebase.storage {
    match /b/{bucket}/o {
      match /Artists/{artistID}/{files=**} {
      allow read: if request.auth != null;
    }
}

fetched from URL

https://firebasestorage.googleapis.com/v0/b/mybucket.appspot.com/o/Artists%2Fall%2fthe%2Frest%2fimage.jpg

WILL NOT SUCCEED, whether fetched in an authenticated session (using Auth, Firestore and Cloud Functions) or not.

REFERENCE RESULTS: WORKING EXAMPLE

service firebase.storage {
    match /b/{bucket}/o {
      match /Artists/{artistID}/{files=**} {
      allow read: if true;
    }
}

fetched from URL

https://firebasestorage.googleapis.com/v0/b/mybucket.appspot.com/o/Artists%2Fall%2fthe%2Frest%2fimage.jpg

HEADERS:

GENERAL:
Request URL: https://firebasestorage.googleapis.com/v0/b/mybucket.appspot.com/o/Artists%2Fall%2fthe%2Frest%2fimage.jpg?alt=media
Request Method: GET
Status Code: 200 
Remote Address: 74.125.136.95:443
Referrer Policy: strict-origin-when-cross-origin

RESPONSE:
accept-ranges: bytes
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
cache-control: private, max-age=0
content-disposition: inline; filename*=utf-8''image.jpg
content-length: 51471
content-type: image/jpeg
date: Sun, 22 Aug 2021 23:40:23 GMT
etag: "b5a087cf582c6493db5b9ff12f061dfd"
expires: Sun, 22 Aug 2021 23:40:23 GMT
last-modified: Mon, 19 Jul 2021 23:10:01 GMT
server: UploadServer
vary: Origin
x-goog-generation: 1626736201167790
x-goog-hash: crc32c=myFUFA==
x-goog-hash: md5=taCHz1gsZJPbW5/xLwYd/Q==
x-goog-meta-firebasestoragedownloadtokens: 4e17fd71-0847-4f5c-bdd3-e9edc5bc63c1
x-goog-metageneration: 1
x-goog-storage-class: STANDARD
x-goog-stored-content-encoding: identity
x-goog-stored-content-length: 51471
x-guploader-uploadid: ADPycduDIHZeant9PO_UVNzlkfjyIC4qwkmYw7QllKwThxfgpnRhtQazTe_E-BHO47SbauOnlPMREnCe3tup9CzIQUGosUDh-w

REQUEST:
:authority: firebasestorage.googleapis.com
:method: GET
:path: /v0/b/mybucket.appspot.com/o/Artists%2Fall%2fthe%2Frest%2fimage.jpg?alt=media
:scheme: https
accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://obscured.netlify.app/
sec-ch-ua: "Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"
sec-ch-ua-mobile: ?0
sec-fetch-dest: image
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
x-client-data: CLO1yQEIlbbJAQiitskBCMG2yQEIqZ3KAQiMnssBCKCgywEI7/LLAQjN9ssBCLP4ywEInvnLAQj1+csB
Decoded:
message ClientVariations {
  // Active client experiment variation IDs.
  repeated int32 variation_id = [3300019, 3300117, 3300130, 3300161, 3313321, 3329804, 3330080, 3340655, 3341133, 3341363, 3341470, 3341557];
}

QUERY STRING PARAMETERS:
alt: media

FAILING EXAMPLE

service firebase.storage {
    match /b/{bucket}/o {
      match /Artists/{artistID}/{files=**} {
      allow read: if request.auth != null;
    }
}

fetched from URL

https://firebasestorage.googleapis.com/v0/b/mybucket.appspot.com/o/Artists%2Fall%2fthe%2Frest%2fimage.jpg

HEADERS

GENERAL:
Request URL: https://firebasestorage.googleapis.com/v0/b/mybucket.appspot.com/o/Artists%2Fall%2fthe%2Frest%2fimage.jpg?alt=media?alt=media
Request Method: GET
Status Code: 403 
Remote Address: 64.233.177.95:443
Referrer Policy: strict-origin-when-cross-origin

RESPONSE:
access-control-allow-origin: *
access-control-expose-headers: Content-Range, X-Firebase-Storage-XSRF
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
cache-control: private, max-age=0
content-length: 106
content-type: application/json; charset=UTF-8
date: Sun, 22 Aug 2021 23:48:30 GMT
expires: Sun, 22 Aug 2021 23:48:30 GMT
server: UploadServer
x-content-type-options: nosniff
x-guploader-uploadid: ADPycdt8g0ZwZSk-Mjhi80cqJuG4Mn5HHqVT7YC_q6VNjik77yZFRbOWKRFpWB15hBj2Cp4WdOEHYdoHeNvE8UtTQL4

REQUEST:
:authority: firebasestorage.googleapis.com
:method: GET
:path: /v0/b/myBucket.appspot.com/o/Artists%2Fall%2fthe%2Frest%2fimage.jpg?alt=media?alt=media
:scheme: https
accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://obscured.netlify.app/
sec-ch-ua: "Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"
sec-ch-ua-mobile: ?0
sec-fetch-dest: image
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
x-client-data: CLO1yQEIlbbJAQiitskBCMG2yQEIqZ3KAQiMnssBCKCgywEI7/LLAQjN9ssBCLP4ywEInvnLAQj1+csB
Decoded:
message ClientVariations {
  // Active client experiment variation IDs.
  repeated int32 variation_id = [3300019, 3300117, 3300130, 3300161, 3313321, 3329804, 3330080, 3340655, 3341133, 3341363, 3341470, 3341557];
}

QUERY STRING PARAMETERS:
alt: media

FURTHER REFERENCE, SUCCESSFUL FIRESTORE CALL:

HEADERS

GENERAL:
Request URL: https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?database=projects%2FmyProject%2Fdatabases%2F(default)&gsessionid=ggRbj_GXcUqsSgaiOVDyViVkuQIrjNweShFZV0Djnsc&VER=8&RID=rpc&SID=ECzssQEhAI8zxYsHShx8ug&CI=0&AID=237&TYPE=xmlhttp&zx=sibt8dlrl1de&t=1
Request Method: GET
Status Code: 200 
Remote Address: 142.251.32.202:443
Referrer Policy: strict-origin-when-cross-origin

RESPONSE:
access-control-allow-credentials: true
access-control-allow-origin: https:/obscured.netlify.app
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
cache-control: private, max-age=0
content-encoding: gzip
content-type: text/plain; charset=utf-8
date: Sun, 22 Aug 2021 23:49:20 GMT
server: ESF
vary: origin
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-xss-protection: 0
:authority: firestore.googleapis.com
:method: GET
:path: /google.firestore.v1.Firestore/Listen/channel?database=projects%2FmyProject%2Fdatabases%2F(default)&gsessionid=ggRbj_GXcUqsSgaiOVDyViVkuQIrjNweShFZV0Djnsc&VER=8&RID=rpc&SID=ECzssQEhAI8zxYsHShx8ug&CI=0&AID=237&TYPE=xmlhttp&zx=sibt8dlrl1de&t=1
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
cache-control: no-cache
origin: https://obscured.netlify.app
pragma: no-cache
referer: https://obscured.netlify.app/
sec-ch-ua: "Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"
sec-ch-ua-mobile: ?0
sec-fetch-dest: empty
sec-fetch-mode: cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
x-client-data: CLO1yQEIlbbJAQiitskBCMG2yQEIqZ3KAQiMnssBCKCgywEI7/LLAQjN9ssBCLP4ywEInvnLAQj1+csB
Decoded:
message ClientVariations {
  // Active client experiment variation IDs.
  repeated int32 variation_id = [3300019, 3300117, 3300130, 3300161, 3313321, 3329804, 3330080, 3340655, 3341133, 3341363, 3341470, 3341557];
}
QUERY STRING PARAMETERS:
database: projects/myProject/databases/(default)
gsessionid: ggRbj_GXcUqsSgaiOVDyViVkuQIrjNweShFZV0Djnsc
VER: 8
RID: rpc
SID: ECzssQEhAI8zxYsHShx8ug
CI: 0
AID: 237
TYPE: xmlhttp
zx: sibt8dlrl1de
t: 1
LeadDreamer commented 3 years ago

More interesting data points: In my code I "dynamically" generate a filename:

export const setDefaultImage = () => {
  (async () => {
    const imageURL = await getDefaultImageURL(
      `${COMPANY_NAME.toLowerCase()}_image`
    );
    setImage(imageURL);
  })();
  return null;
};

...which calls

/**
 * ----------------------------------------------------------------------
 * @function
 * @static
 * @param {!string} key name/key of default image file
 * @returns {string}
 */
export const getDefaultImageURL = (key) => {
  let filename = key + ".jpg";
  return Promise.resolve(
    FirebaseStorage.ref("/defaultImages").child(filename).getDownloadURL()
  );
};

which sends TWO requests to the backend. FIRST:

https://firebasestorage.googleapis.com/v0/b/tickets-unlimited-fast.appspot.com/o/defaultImages%2Fgigstartr_image.jpg

which returns

{name: "defaultImages/gigstartr_image.jpg", bucket: "tickets-unlimited-fast.appspot.com",…}
bucket: "tickets-unlimited-fast.appspot.com"
contentDisposition: "inline; filename*=utf-8''gigstartr_image.jpg"
contentEncoding: "identity"
contentType: "image/jpeg"
crc32c: "Q1tIxQ=="
downloadTokens: "12af9fae-afe8-4a12-a124-008014737e7b"
etag: "CMfT3rPdjuwCEAE="
generation: "1601395039381959"
md5Hash: "ktoTPHXMgfe6m1rIkRTB1Q=="
metageneration: "1"
name: "defaultImages/gigstartr_image.jpg"
size: "63487"
storageClass: "STANDARD"
timeCreated: "2020-09-29T15:57:19.381Z"

OBVIOUSLY, .getDownloadURL() uses this to generate the tokenized URL, also adding ?alt=media&token=12af9fae-afe8-4a12-a124-008014737e7b to get the media instead of the meta-data. a SUBSEQUENT call that includes ?alt=media returns the media content.

WHY IS THIS INTERESTING?

BECAUSE the above call FOLLOWS THE SECURITY RULES. If the rule is set to:

service firebase.storage {
  match /b/{bucket}/o {
    match /defaultImages/{image} {
      allow read: if true;
    }
}

a fetch OUTSIDE OF AUTHENTICATED CONTEXT of:

https://firebasestorage.googleapis.com/v0/b/myBucket.appspot.com/o/defaultImages%2Fthat_image.jpg

returns THE METADATA If the rule is set to:

service firebase.storage {
  match /b/{bucket}/o {
    match /defaultImages/{image} {
      allow read: if request.auth.uid != null;
    }
}

a fetch OUTSIDE OF AUTHENTICATED CONTEXT of:

https://firebasestorage.googleapis.com/v0/b/myBucket.appspot.com/o/defaultImages%2Fthat_image.jpg

FAILS WITH 403 status - AS IT SHOULD

The INTERESTING part is that WITHIN the authenticated context, the above DOES return the metadata (200 status) - as it should!!!

BUT BUT BUT, we then see

https://firebasestorage.googleapis.com/v0/b/myBucket.appspot.com/o/defaultImages%2Fthat_image.jpg?alt=media

FAILS with a 403 status - THAT fetch requires a token. Security rules SHOULD HAVE ALLOWED THAT FETCH, The only difference is the parameter ?alt=media

LeadDreamer commented 3 years ago

So continuing successful vs unsuccessful fetches, both in authentication context:

SUCCESSFUL (SDK call FirebaseStorage.ref("/defaultImages").child(filename).getDownloadURL() ) [note authorization header]

GENERAL:
Request URL: https://firebasestorage.googleapis.com/v0/b/tickets-unlimited-fast.appspot.com/o/defaultImages%2Fgigstartr_image.jpg
Request Method: GET
Status Code: 200 
Remote Address: 142.250.105.95:443
Referrer Policy: strict-origin-when-cross-origin

RESPONSE HEADERS:
access-control-allow-origin: *
access-control-expose-headers: Cache-Control, Content-Length, Content-Range, Date, Expires, Server, Transfer-Encoding, X-Firebase-Storage-XSRF, X-GUploader-UploadID, X-Google-Trace
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
cache-control: private, max-age=0
content-length: 586
content-type: application/json; charset=UTF-8
date: Mon, 23 Aug 2021 17:20:29 GMT
expires: Mon, 23 Aug 2021 17:20:29 GMT
server: UploadServer
x-content-type-options: nosniff
x-guploader-uploadid: ADPycduQtbHJBpytEr5OWYc5NriqklzRZvISMyBf96O6Ww3EBpoQ9Fl3ocRItsNeGbHVY_wKh_lKIbQN5s4Ydb7CNnE

REQUEST HEADERS:
:authority: firebasestorage.googleapis.com
:method: GET
:path: /v0/b/tickets-unlimited-fast.appspot.com/o/defaultImages%2Fgigstartr_image.jpg
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
authorization: Firebase eyJhbGciOiJSUzI1NiIsImtpZCI6IjkwMDk1YmM2ZGM2ZDY3NzkxZDdkYTFlZWIxYTU1OWEzZDViMmM0ODYiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS90aWNrZXRzLXVubGltaXRlZC1mYXN0IiwiYXVkIjoidGlja2V0cy11bmxpbWl0ZWQtZmFzdCIsImF1dGhfdGltZSI6MTYyOTczOTIxMCwidXNlcl9pZCI6Ijd2TE9mdmViM2tnSU5ZWXFjcktxaVFWZVhrNDIiLCJzdWIiOiI3dkxPZnZlYjNrZ0lOWVlxY3JLcWlRVmVYazQyIiwiaWF0IjoxNjI5NzM5MjEyLCJleHAiOjE2Mjk3NDI4MTIsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnt9LCJzaWduX2luX3Byb3ZpZGVyIjoiYW5vbnltb3VzIn19.vm8HGD3KdBSx6Fn4_tNSNby0YVf59_t_Ynzc9BCx2zc86-pcnAWCUZrfKyL3jb-csya0TlYFxJdDHTtAzi6V4W4li9H7-kOR3YDRmMdoPSTix9BTeQZo2IPa0M0v2kMYQDGNOL3WrMaeVmP7PhS4Bu9hwRzG2tL3CJ7vSBUhNdCQN5r0NYFnGRBzpXxwxyIPRH1inq4LTm9iiVjCv5itusdxVj3pV9MWNG0rhzNYFVPpLFWUvRGtAmMdRwIi-a6cthiwhMtqoyVWr34TJWILvnL0Pasca_aOIVcTfqU_NPTDpEfjT5pqT9XtRmpBvnVrycPNl6XIgx8HqFH1RIIi7w
cache-control: no-cache
origin: https://friendly-rosalind-62a81a.netlify.app
pragma: no-cache
referer: https://friendly-rosalind-62a81a.netlify.app/
sec-ch-ua: "Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"
sec-ch-ua-mobile: ?0
sec-fetch-dest: empty
sec-fetch-mode: cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
x-client-data: CLO1yQEIlbbJAQiitskBCMG2yQEIqZ3KAQiMnssBCKCgywEI7/LLAQjN9ssBCLP4ywEInvnLAQj1+csB
Decoded:
message ClientVariations {
  // Active client experiment variation IDs.
  repeated int32 variation_id = [3300019, 3300117, 3300130, 3300161, 3313321, 3329804, 3330080, 3340655, 3341133, 3341363, 3341470, 3341557];
}
x-firebase-storage-version: webjs/8.9.1

UNSUCCESSFUL (HTML embedded image) [note NO authorization header]

<div style="position: absolute; top: 2%; left: 2%; width: 96%; height: 96%; border-radius: 50%; background-image: url(&quot;https://firebasestorage.googleapis.com/v0/b/tickets-unlimited-fast.appspot.com/o/defaultImages%2Fgigstartr_image.jpg&quot;); background-position: center center; background-size: 100% 100%;"></div>

with headers:

GENERAL:
Request URL: https://firebasestorage.googleapis.com/v0/b/tickets-unlimited-fast.appspot.com/o/defaultImages%2Fgigstartr_image.jpg
Request Method: GET
Status Code: 403 
Remote Address: 74.125.136.95:443
Referrer Policy: strict-origin-when-cross-origin

RESPONSE HEADERS:
access-control-allow-origin: *
access-control-expose-headers: Content-Range, X-Firebase-Storage-XSRF
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
cache-control: private, max-age=0
content-length: 106
content-type: application/json; charset=UTF-8
date: Mon, 23 Aug 2021 17:27:09 GMT
expires: Mon, 23 Aug 2021 17:27:09 GMT
server: UploadServer
x-content-type-options: nosniff
x-guploader-uploadid: ADPycdsNP-4WGZQ7PAJ_tGb_qwzMQMblH01dF7_UK6GK_Spd-ZHxjOdU-76BOw5KD4MVfaa8wowzy2NDw0MkQ7aIFu4

REQUEST HEADERS:
:authority: firebasestorage.googleapis.com
:method: GET
:path: /v0/b/tickets-unlimited-fast.appspot.com/o/defaultImages%2Fgigstartr_image.jpg
:scheme: https
accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://friendly-rosalind-62a81a.netlify.app/
sec-ch-ua: "Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"
sec-ch-ua-mobile: ?0
sec-fetch-dest: image
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
x-client-data: CLO1yQEIlbbJAQiitskBCMG2yQEIqZ3KAQiMnssBCKCgywEI7/LLAQjN9ssBCLP4ywEInvnLAQj1+csB
Decoded:
message ClientVariations {
  // Active client experiment variation IDs.
  repeated int32 variation_id = [3300019, 3300117, 3300130, 3300161, 3313321, 3329804, 3330080, 3340655, 3341133, 3341363, 3341470, 3341557];
}
LeadDreamer commented 3 years ago

Researching a canvas approach that might help, based on: https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image

LeadDreamer commented 3 years ago

Using crossorigin="user-credentials"

with attr crossorigin="user-credentials" in an authenticated context. Not only Failed; never got a proper status response

HTML

<img src="https://firebasestorage.googleapis.com/v0/b/tickets-unlimited-fast.appspot.com/o/Artists%2Frecq5cURMyJsRKFbO%2Fimage%2Fimage.jpg?alt=media" crossorigin="use-credentials" alt="good try" style="position: absolute; top: 2%; left: 2%; width: 96%; height: 96%; border-radius: 50%;">

HEADERS

GENERAL:
Request URL: https://firebasestorage.googleapis.com/v0/b/tickets-unlimited-fast.appspot.com/o/Artists%2Frecq5cURMyJsRKFbO%2Fimage%2Fimage.jpg?alt=media
Referrer Policy: strict-origin-when-cross-origin

RESPONSE HEADERS:
access-control-allow-origin: *
access-control-expose-headers: Cache-Control, Content-Length, Content-Range, Date, Expires, Server, Transfer-Encoding, X-Firebase-Storage-XSRF, X-GUploader-UploadID, X-Google-Trace
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
cache-control: private, max-age=0
content-length: 106
content-type: application/json; charset=UTF-8
date: Mon, 23 Aug 2021 18:56:04 GMT
expires: Mon, 23 Aug 2021 18:56:04 GMT
server: UploadServer
x-content-type-options: nosniff
x-guploader-uploadid: ADPycdv85GVcVMiK3QQxm1Y8l-NrPv-8QFQhLYoGc2S04Ze_L1HOl-FW6XYAqj3zjzXsLXFXKTS0RH-PW3SfKX7i5dK2vbAcXA

REQUEST HEADERS:
:authority: firebasestorage.googleapis.com
:method: GET
:path: /v0/b/tickets-unlimited-fast.appspot.com/o/Artists%2Frecq5cURMyJsRKFbO%2Fimage%2Fimage.jpg?alt=media
:scheme: https
accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
cache-control: no-cache
origin: https://xmy18.csb.app
pragma: no-cache
referer: https://xmy18.csb.app/
sec-ch-ua: "Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"
sec-ch-ua-mobile: ?0
sec-fetch-dest: image
sec-fetch-mode: cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
x-client-data: CLO1yQEIlbbJAQiitskBCMG2yQEIqZ3KAQiMnssBCKCgywEI7/LLAQjN9ssBCLP4ywEInvnLAQj1+csB
Decoded:
message ClientVariations {
  // Active client experiment variation IDs.
  repeated int32 variation_id = [3300019, 3300117, 3300130, 3300161, 3313321, 3329804, 3330080, 3340655, 3341133, 3341363, 3341470, 3341557];
}

QUERY STRING PARAMETERS
alt: media
LeadDreamer commented 3 years ago

Using crossorigin="anonymous"

When using the following HTML

<img src="https://firebasestorage.googleapis.com/v0/b/tickets-unlimited-fast.appspot.com/o/Artists%2Frecq5cURMyJsRKFbO%2Fimage%2Fimage.jpg?alt=media" crossorigin="anonymous" alt="good try" style="position: absolute; top: 2%; left: 2%; width: 96%; height: 96%; border-radius: 50%;">

...we receive a 403 status.

LeadDreamer commented 3 years ago

Using background-image-crossorigin: "use-credentials"

with the following HTML

<div style="position: absolute; top: 2%; left: 2%; width: 96%; height: 96%; border-radius: 50%; background-image: url(&quot;https://firebasestorage.googleapis.com/v0/b/tickets-unlimited-fast.appspot.com/o/Artists%2Frecq5cURMyJsRKFbO%2Fimage%2Fimage.jpg?alt=media&quot;); background-position: center center; background-size: 100% 100%;">

we get a 403 status, with headers:

GENERAL:
Request URL: https://firebasestorage.googleapis.com/v0/b/tickets-unlimited-fast.appspot.com/o/Artists%2Frecq5cURMyJsRKFbO%2Fimage%2Fimage.jpg?alt=media
Request Method: GET
Status Code: 403 
Remote Address: 64.233.177.95:443
Referrer Policy: strict-origin-when-cross-origin

RESPONSE HEADERS:
access-control-allow-origin: *
access-control-expose-headers: Content-Range, X-Firebase-Storage-XSRF
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
cache-control: private, max-age=0
content-length: 106
content-type: application/json; charset=UTF-8
date: Mon, 23 Aug 2021 19:24:04 GMT
expires: Mon, 23 Aug 2021 19:24:04 GMT
server: UploadServer
x-content-type-options: nosniff
x-guploader-uploadid: ADPycdvqVEiUCshuIZf0PxORQ3DPdpIFyikikocK-LO-KN1idEqayzB6gZiS6F-G-LHpFKv0WAj9p9veEfO9GgXlWG0

REQUEST HEADERS:
:authority: firebasestorage.googleapis.com
:method: GET
:path: /v0/b/tickets-unlimited-fast.appspot.com/o/Artists%2Frecq5cURMyJsRKFbO%2Fimage%2Fimage.jpg?alt=media
:scheme: https
accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://xmy18.csb.app/
sec-ch-ua: "Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"
sec-ch-ua-mobile: ?0
sec-fetch-dest: image
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
x-client-data: CLO1yQEIlbbJAQiitskBCMG2yQEIqZ3KAQiMnssBCKCgywEI7/LLAQjN9ssBCLP4ywEInvnLAQj1+csB
Decoded:
message ClientVariations {
  // Active client experiment variation IDs.
  repeated int32 variation_id = [3300019, 3300117, 3300130, 3300161, 3313321, 3329804, 3330080, 3340655, 3341133, 3341363, 3341470, 3341557];
}

QUERY STRING PARAMETERS
alt: media
LeadDreamer commented 3 years ago

What Should Work But Doesn't

Adding crossOrigin:"use-credentials" to an <img> should work. Also background-image-crossOrigin: "use-credentials". They don't. They get:

Access to image at 'https://firebasestorage.googleapis.com/v0/b/tickets-unlimited-fast.appspot.com/o/Artists%2Frecq5cURMyJsRKFbO%2Fimage%2Fimage.jpg?alt=media' from origin 'https://xmy18.csb.app' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.

The reason given isn't really correct, though...

WHY?

Look at these response headers:

access-control-allow-origin: *
access-control-expose-headers: Cache-Control, Content-Length, Content-Range, Date, Expires, Server, Transfer-Encoding, X-Firebase-Storage-XSRF, X-GUploader-UploadID, X-Google-Trace
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
cache-control: private, max-age=0
content-length: 106
content-type: application/json; charset=UTF-8
date: Tue, 24 Aug 2021 16:02:44 GMT
expires: Tue, 24 Aug 2021 16:02:44 GMT
server: UploadServer
x-content-type-options: nosniff
x-guploader-uploadid: ADPycdsf1M1uKmEWTQbwpTAzPwKIFDn8nO_j72BsmUNhqPmLYtnKNjRGw9fX5a3HaD2_9ZvJcmkMB8xPOfESJPtOBGaqwoGJmw

What's missing?

access-control-allow-credentials is NOT returned by Google's servers. Why isn't it returned by Google's Servers? I don't know.

LeadDreamer commented 3 years ago

What Have I Observed "working"?

One of my routines programatically generates a URL, and successfully fetches an image, using the below:

export const setDefaultImage = () => {
  (async () => {
    const imageURL = await getDefaultImageURL(
      `${COMPANY_NAME.toLowerCase()}_image`
    );
    setImage(imageURL);
  })();
  return null;
};

export const getDefaultImageURL = (key) => {
  let filename = key + ".jpg";
  return Promise.resolve(
    FirebaseStorage.ref("/defaultImages").child(filename).getDownloadURL()
  );
};

What I have observed in the console "network" tab is a GET request for the URL is made, which does NOT return the jpg - it returns a JSON object with the file metadata. It turns out one of the "secrets" of Firebase Storage is that a request for an object (when allowed) WITH NO PARAMETERS does NOT return the file - it returns the METADATA. Happy undocumented feature. Also noted on that same transaction is a header added to the fetch:

authorization: Firebase eyJhbGciOiJSUzI1NiIsImtpZCI6IjkwMDk1YmM2ZGM2ZDY3NzkxZDdkYTFlZWIxYTU1OWEzZDViMmM0ODYiLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoiVHJhY3kgSGFsbCIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS0vQUF1RTdtQkFtZ2ZwMnBvMERlbUltREtYZ3JoNmJoZlBHOFp3ei0tOEI5ek5uNlUiLCJhcnRpc3QiOnsiSWQiOiJyZWNxNWNVUk15SnNSS0ZiTyIsInJvbGUiOiJtYW5hZ2VyIiwidHlwZSI6IkFydGlzdHMifSwidmVudWUiOm51bGwsImFkbWluIjpmYWxzZSwiYWRtaW5TdGF0dXMiOnRydWUsImJvcmsiOmZhbHNlLCJzdXBlclVzZXIiOnRydWUsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS90aWNrZXRzLXVubGltaXRlZC1mYXN0IiwiYXVkIjoidGlja2V0cy11bmxpbWl0ZWQtZmFzdCIsImF1dGhfdGltZSI6MTYyOTc0MzM0NSwidXNlcl9pZCI6Ill2UEJuVHlsQ3lNT2wwT2FNbWxaZnFkZmlDQzIiLCJzdWIiOiJZdlBCblR5bEN5TU9sME9hTW1sWmZxZGZpQ0MyIiwiaWF0IjoxNjI5ODIwMTY5LCJleHAiOjE2Mjk4MjM3NjksImVtYWlsIjoibGVhZGRyZWFtZXJAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZ29vZ2xlLmNvbSI6WyIxMDQ3MTc2MzMyODcyMDU4Njg4NDYiXSwiZW1haWwiOlsibGVhZGRyZWFtZXJAZ21haWwuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoiZ29vZ2xlLmNvbSJ9fQ.lTuvvILwf8jNDU1MZ5pkF0YbZUP7OVaXUaOKHHWHGa7RDgrlWUxP0a211AF8fsOKWUhK_Dt5hk3fnS8cmJx3nQgx8tAazgnCCiP_0FeBJg1_qqE521yYqfFHCos2nasMg8cvyjCicJ1YokAq69WuTIZsP_uTo4ARUjebCF5cgAhFGRpb367_RFhWd-YM8-zCzaO7tCHzSIGEa655kcF1afLu-FY_xhmhPMhEIqnsWxszRpZhwbANGH1sVKOwR60f43BG1k30iAIQi6ONK72XdSABUCrJJqGBlZxM9aX0vCbJezBIV_MvkZTVYjtz6ENUD52RP8MzKeMujVHPQKE81Q

SOMETHING STARTS TO WORK

As a "pure hack", I added this header to an image fetch, and SUCCESSFULLY GOT THE METADATA. As suspected, this code is VERY time limited (I suspect an hour), but can be used on ANY fetch.

I ALSO tested security rules (changing which level of the hierarchy was allowed, whether an entire tree was allowed or not, whether authentication was required - AND THE RULES APPLIED TO THESE FETCHES.

SOMETHING STARTS TO WORK BETTER

When we observed the URLs returned by .getDownloadURL() we see that TWO parameters are added - alt=media and token={uuid} . I've been doing this a LONG time, so I tried to add JUST the alt=media parameter. And...

HOLY CRAP I GOT THE IMAGE WITH SECURITY RULES APPLIED

Short-lived joy, however, because I just manually copied the authorization token. What I have proven is that there is a way to fetch images that follows the security rules, but I don't believe it is usable (yet) in-the-wild - I'd need to find a way to access that authorization token, or have a function from Firebase Storage that gives me such a token.

WHAT THIS INDICATES AS A POSSIBILITY

Security-rule guarded, SHORT-TERM access to images from the tag in the browser.

LeadDreamer commented 3 years ago

Hacky Code that fetched Image within Security Rules

const authorization = "{Big long string I copied from a different image fetched via above-mentioned code}"

  const aspectRatioBorderedImage = {
    position: "absolute",
    top: borderSize + units,
    left: borderSize + units,
    width: imgCalcWidth + units,
    height: imgCalcWidth + units,
    borderRadius: "50%"
  };

  const errorHandler = async (e) => {
    e.stopPropagation();
    const target = e.target;
    const src = image; //$(target).attr("src");
    const options = {
      headers: {
        authorization: authorization
      }
    };
    console.log("errorHandler", src);
    await fetch(src, options)
      .then((res) => {
        console.log("res", res);
        return res.status === 200 && res.blob();
      })
      .then((blob) => {
        if (!blob) {
          console.log("res error");
          return;
        }
        console.log("URL", URL.createObjectURL(blob));
        $(target).prop("src", URL.createObjectURL(blob));
      });
  };

...using JSX...

              <img
                src={image}
                crossOrigin="use-credentials"
                style={aspectRatioBorderedImage}
                alt="good try"
                onError={errorHandler}
              />
LeadDreamer commented 3 years ago

Probably related to this:

Google Cloud Storage https://cloud.google.com/storage/docs/authentication/signatures

katowulf commented 3 years ago

Security rules are primarily aimed at use with SDK calls (as you noted, get(), getBytes(), and the corresponding write calls). Download URLs are intended to be static and work without rules, because they are based on the underlying GCP service without Firebase wrappings. I'd take this a step further and say that the primary reason to have request.auth at all is to get to the custom claims, which allow you to enforce some primitive role-based access scenarios, admin privileges, and similar based on user accounts.

You can of course drop down to the Google Cloud Storage REST API and use that with Authorization headers and that's a documented workflow (in GCP). You can also work with IAM permissions here rather than security rules if you wish.

There's certainly more that we can do here in the future to fill out the security rules story (such as some day allowing you to do get() requests against your Firestore data to help with roles and groups), but functionality is limited in scope for now as described here.

LeadDreamer commented 3 years ago

Yes, that is the reason I want to have the Rules apply - but FIRST the rules have to be enforced at all. I have gone into the code to see exactly how your code creates the headers, and it now seems apparent that you are using the JWT returned from auth.getIdToken() as the signature (I can point you to the line in the code, if you want). Given that, I am now writing my own code to do the same thing as an experiment, then build it into my own wrapper package (open-source and public).

The Google Cloud Storage REST API doesn't seem to have the same hierarchical Rules structure that Firebase Storage does - but since your code can follow the rules, mine can as well.

LeadDreamer commented 3 years ago

A proposed Browser/Node-side solution

Not the most straight forward, due to browser limitations. It is NOT possible to add custom headers directly to <img> tags, but it IS possible to set an onError handler to do an independent fetch.

JSX

              <img
                src={image}
                //crossOrigin="use-credentials"
                style={whatever}
                alt={whatever}
                onError={errorHandler}
              />

where: {image} is the Firebase Storage URL, which can be entirely programatically generated. NOTE: the URL MUST include the alt=media tag (or whatever is appropriate to the file type). If this parameter is missing, Firebase Storage will return the metadata JSON object. {errorHandler} see below

Javascript

//built in React
 const [authorization, setAuthorization] = useState(null);

//Asynchronously fetches users JWT.  Note closure, as useEffect itself must be synchronous
  useEffect(() => {
    if (user) {
      (async () => {
        const authToken = await fetchJWT();
        setAuthorization(authToken);
      })();
    }
  }, [user]);

//Technically, this is in my wrapper library - uses the Firebase Auth service to fetch a user's JWT
 export const fetchJWT = async (user) => {
  const thisUser = user || FirebaseAuth.currentUser;
  //the "true" below forces a reset
  const JWT = await thisUser.getIdToken(true);
  return JWT;
 }

//More-or-less copied from firebase-js-sdk/storage/src/implementation/requests.ts
  const addAuthHeader_ = async (headers) => {
    if (authorization !== null && authorization.length > 0) {
      headers["Authorization"] = "Firebase " + authorization;
    }
  };

// handles the actual error event 
  const errorHandler = async (e) => {
    e.stopPropagation();
    const target = e.target;
    const src = $(target).attr("src");
    let headers = {};
    addAuthHeader_(headers);
    const options = {
      headers: headers
    };
    await fetch(src, options)
      .then((res) => {
        return res.status === 200 && res.blob();
      })
      .then((blob) => {
        if (!blob) {
          return;
        }
        $(target).prop("src", URL.createObjectURL(blob));
      });
  };

Explanation

when the <img> tag encounters an error, the errorHandler

Result

A file (in this case, an image) is fetched from Firebase Storage, following all of the Security Rules set therein. No permanent token needed. Only authenticated users have access. (I will note that I use Anonymous User accounts for new users, with appropriate restrictions set on their access)

Going Forward

I am likely to added a class to <img> elements that are intended to show images from Firebase Storage, or possibly examine the URLs. That way a single errorHandler can be designated to handle all of them.

LeadDreamer commented 3 years ago

What Would be Better!!

GOOGLE should set Firebase Storage to allow the use of Credentials, which CAN be added to both <img> tags and background-image-credentials properties.

LeadDreamer commented 3 years ago

What Might Be Acceptable

an extremely simple .getPrivateURL() that at least created the URL without the token, which could then be used with the JWT token (mentioned above) without having to re-create the full path construction (including the bucket domain, the bucket name a top-level "path", and URL-escaped (%2F-instead-of-/) filePath.

Although I suppose I could just strip the silly token. The problem is that would be on the client side - I would rather the token never be on the client side. Same issue for using .getIdToken() - it brings the token to the user side - but as you may have noticed up there in various places, said (very short lived) token is already present on the client side.

Still comes down to...

MUCH MUCH MUCH BETTER to allow the use of the credentials, verified on the SERVER side.

rgbskills commented 1 year ago

I'm facing the same issue, I want urls to my files saved in firestore for quick access but I also want them to respect the rules I wrote for the files, right now if I generate a download URL it bypasses the rules, this means that the entire system is useless, might as well remove rules.