I've already implemented authentication in my app, and am having no problem with creating a user and authenticating. However, now I am trying to upload a file to Firebase Storage, but it will only work when I remove the auth rule and make access public. If I leave the default rules to only allow authenticated users to access (which is what I want), I get the error: Firebase Storage: User does not have permission to access 'profile-image/test.PNG'.
I called a method to verify my auth state right before the put request, and I can read/write to the firestore database with no issues, so I know for sure I'm authenticated.
I'm a complete FNG so there's a good chance the problem is something silly that I've done/not done. I'm using Angular if that is relevant. I also activated a billing account with Google Cloud Platform, but that didn't make a difference.
Here's my console log showing the reference I used, the file I'm attempting to add (which again, both of those work just fine when I make access public), my uid from the auth state call, and then the error:
STARTING UPLOAD SERVICE upload.service.ts:26
FIREBASE STORAGE REFERENCE: upload.service.ts:27
Reference {authWrapper: AuthWrapper, location: Location}
authWrapper: AuthWrapper {bucket_: "my-app.appspot.com", deleted_: false, app_: FirebaseAppImpl, storageRefMaker_: ƒ, requestMaker_: ƒ, …}
bucket: (...)
fullPath: (...)
location: Location {bucket: "my-app.appspot.com", path_: "profile-image/test.PNG"}
name: (...)
parent: (...)
root: (...)
storage: (...)
__proto__: Object
upload.service.ts:28
FILE CONTENTS:
upload.service.ts:29
File(286831) {name: "test.PNG", lastModified: 1542480795011, lastModifiedDate: Sat Nov 17 2018 13:53:15 GMT-0500 (Eastern Standard Time), webkitRelativePath: "", size: 286831, …}
upload.service.ts:24
USER AUTHENTICATED: Er6sWsDvEjM69WBAKxQffcbdPZG2
POST https://firebasestorage.googleapis.com/v0/b/{my-app-name}/o?name=profile-image%2Ftest.PNG 403
upload.service.ts:33
FirebaseStorageError {code_: "storage/unauthorized", message_: "Firebase Storage: User does not have permission to access 'profile-image/test.PNG'.",
serverResponse_: "{↵ "error": {↵ "code": 403,↵ "message": "Pe…n denied. Could not perform this operation"↵ }↵}", name_: "FirebaseError"}
code: (...)
code_: "storage/unauthorized"
message: (...)
message_: "Firebase Storage: User does not have permission to access 'profile-image/test.PNG'."
name: (...)
name_: "FirebaseError"
serverResponse: (...)
serverResponse_: "{↵ "error": {↵ "code": 403,↵ "message": "Permission denied. Could not perform this operation"↵ }↵}"
__proto__: Object
Firebase Storage Rules
service firebase.storage {
match /b/my-app.appspot.com/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
(I also tried request.auth.uid != null but that didn't make a difference.)
My upload service:
import { Injectable } from '@angular/core';
import * as firebase from 'firebase/app';
import 'firebase/storage';
import { AuthService } from '../services/auth.service';
@Injectable({
providedIn: 'root'
})
export class UploadService {
constructor(
private authService: AuthService
) { }
pushUpload(uploadFile: File) {
console.log("STARTING UPLOAD SERVICE")
var storage = firebase.storage();
var storageRef = storage.ref();
var profileRef = storageRef.child('profile-image');
var docRef = profileRef.child(uploadFile.name);
this.authService.getAuthState().subscribe(auth => {
console.log("USER AUTHENTICATED: " + auth.uid);
})
console.log("FIREBASE STORAGE REFERENCE:")
console.log(docRef);
console.log("FILE CONTENTS:");
console.log(uploadFile);
docRef.put(uploadFile).then(res => {
console.log(res);
}).catch(err => {
console.log(err);
});
}
}
Firebase config in environment.ts:
import * as fb from 'firebase/app';
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false,
firebase: {
apiKey: "{my-api-key}",
authDomain: "my-app.firebaseapp.com",
databaseURL: "my-app.firebaseio.com",
projectId: "my-app",
storageBucket: "gs://my-app.appspot.com",
messagingSenderId: "####"
}
};
fb.initializeApp(environment.firebase);
I replaced some of the identifying information with generic in the console log & environment.ts file.
I should also mention that authentication was working just fine for me before I added fb.initializeApp(environment.firebase);
but I was getting an error without this line once I tried to make an upload request.
Thanks so much in advance for any advice you may have & if I should provide any more info, please let me know!
As it turns out, it did matter that I was using Angular. I needed to add AngularFireStorage
to my app.module.ts
like so:
import { AngularFireStorage } from '@angular/fire/storage';
@NgModule({
....
providers: [..., AngularFireStorage]
and then also had to import to my uploads.component.ts:
import { AngularFireStorage, AngularFireUploadTask } from '@angular/fire/storage';
and then I completely scrapped my UploadService and snagged parts of this guy's tutorial: https://angularfirebase.com/lessons/firebase-storage-with-angularfire-dropzone-file-uploader/ which uses an AngularFireUploadTask and some Observables to accomplish the entire upload process really easily. So here's the resulting method I ended up with in my uploads.component.ts:
import { Component, OnInit, Input } from '@angular/core';
import { AngularFireStorage, AngularFireUploadTask } from '@angular/fire/storage';
import { Observable } from 'rxjs';
@Component({
selector: 'app-uploads',
templateUrl: './uploads.component.html',
styleUrls: ['./uploads.component.css']
})
export class UploadsComponent implements OnInit {
@Input() uploadFolder: string; //the folder to save this particular upload to in the Storage Bucket
selectedFile: File;
task: AngularFireUploadTask; // Main task
percentage: Observable<number>; // Progress monitoring
snapshot: Observable<any>;// Progress monitoring
constructor(
private storage: AngularFireStorage
) { }
ngOnInit() {}
.
.
.
startUpload() {
if (this.selectedFile.type.split('/')[0] !== 'image') {
console.error('unsupported file type :( ')
return;
} else {
const path = this.uploadFolder + "/" + this.userID;
// The main task
this.task = this.storage.upload(path, this.selectedFile)
// Progress monitoring
this.percentage = this.task.percentageChanges();
this.percentage.subscribe(data => {
// Do something with my progress
})
this.snapshot = this.task.snapshotChanges();
this.snapshot.subscribe(data => {
// Do something with my progress
})
// The file's download URL
this.task.then(snapshot => {
console.log("UPLOAD SUCCESS!");
snapshot.ref.getDownloadURL().then(url => {
console.log(url);
//Do something with my new file's url
})
},
(err) => {
//Do something about errors...
});
}
}
}
And I removed the firebase initialization from my environment.ts
file, so clearly Angular is initializing firebase for me somewhere since this is unnecessary. I believe this is why there was a discrepancy between why Firestore showed I was authenticated (because I initialized firebase for logging in and firestore through AngularFire), but Firebase Storage showed I was NOT authenticated (because I initialized firebase for THAT separately in my environment.ts
file, which was my work-around for not utilizing AngularFireStorage like I should have). So basically this all boils down to the fact that I didn't(don't) fully understand what exactly Angular was doing for me. ugh...I really hope this helps someone at some point...