Why Firestore Security Rules auth token name is always null? - firebase

I try to compare the request.auth.token.name with the document name in the Firestore Security Rules, to give only the user access to his specific document. Unfortunately, it still doesn't work out. I tried a bit and it seems that the name is null. But why? Can anybody relate or have an answer for this?
The problem:
W/Firestore(30226): (21.3.0) [Firestore]: Write failed at users/johndoe: Status{code=PERMISSION_DENIED, description=Missing or insufficient permissions., cause=null}
The security rule:
match /users/{username} {
allow create: if true;
allow update: if request.auth.token.name == username;
allow read: if true;
The client code (written in Dart for Flutter):
#override
Future<void> uploadProfileData(ProfileSetupDataModel data) async {
// Checks if username is already in use and creates a document (works perfectly)
final userDocReference = await _createDocIfUsernameAvailable(data.username);
// Here the username is saved in the display name of the FirebaseUser
await _saveUsernameInAuthUser(data.username);
// Write all the other information into the user document
await _fillProfileInformation(userDocReference, data); // -> here the problem occurs
}
Future<void> _saveUsernameInAuthUser(String username) async {
try {
final user = await auth.currentUser();
final profileUpdate = UserUpdateInfo();
profileUpdate.displayName = username;
await user.updateProfile(profileUpdate);
} catch (e) {
throw GeneralAuthException();
}
}
Future<void> _fillProfileInformation(
DocumentReference userDocReference, ProfileSetupDataModel data) async {
try {
await userDocReference.setData(
{
FirestoreUserKeys.birthday: data.birthday,
FirestoreUserKeys.sex: data.sex,
FirestoreUserKeys.country: data.country,
FirestoreUserKeys.storiesCount: 0,
FirestoreUserKeys.subscribersCount: 0,
FirestoreUserKeys.subscriptionsCount: 0,
FirestoreUserKeys.totalLikes: 0,
},
merge: true,
);
} on PlatformException {
throw GeneralCloudFirestoreException();
}
}

Related

How to modify Flutter Firebase Stream listener based on Firebase Auth stream when using Provider for DI?

In my Flutter Firebase App with Provider for state management, I have a stream for reacting to FirebaseAuth.instance.authStateChanges() and a separate stream for listening to the my app related metadata for the logged in uid provided by FirebaseAuth.
return MultiProvider(
providers: [
// This returns a stream of firebase user auth events so my app can react to
// login, force logout, etc.
StreamProvider<fireauth.User>.value(
value: FirebaseAuth.instance.authStateChanges(),
),
// conditional on non-null FirebaseAuth User, I would like to register a Firestore listener
// for the provided userId.
// For security purposes, if the authenticated uid changes, the listener should be dereigstered.
// After logout, if a different user is logged in, the this stream should listen to that uid's doc.
StreamProvider<MyUser>.value(
value: FirebaseFirestore.instance.collection('users')
.doc(/* use the userId from firebaseAuth here! */)
.snapshots()
.map((ds) => MyUser.fromJson(ds.data()))
),
],
);
I think I can use ProxyProvider to allow the MyUser stream to take a dependency on the FirebaseAuth.User stream, but once the MyUser stream is registered for this uid, it seems to be immutable. How can I "reload" the Firestore stream based on the result from the FirebaseAuth.User?
Ran into the same problem but with Firebase Realtime Database. Using the async package I created a StreamGroup. A StreamGroup.
Create StreamGroup:
StreamGroup group = StreamGroup();
Create a stream Variable:
Stream streamvar;
created function with this scheme:
Stream<PlaceHolderClass> functionName(User user) async*{
if (user != null) {
await for (var x in streamOfFirestoreDocuments) {
yield PlaceHolderClass();
}
} else {
yield PlaceHolderClass();
}
}
Created an initializer:
where I listened to:
FirebaseAuth.instance.authStateChanges()
and added it to group by group.add(),
also set the stream variable and added it to the group:
streamvar = functionName(FirebaseAuth.instance.currentUser);
group.add(streamvar)
then created:
Stream<FirestoreDocuments> providerFunc(){
return group.stream.map((event) {
if(isUserChangetest){
group.remove(streamvar);
streamvar = newStreamvar;
group.add(newStreamvar)
fetch = newFirestoreDocs;
return fetch;
}else{
return sth;
}
})
}
Now streamVar always holds a reference to the last firestore stream input to group.
Swap stream from Firestore and you can listen to both kind of changes Firestore and Auth.
Incase I missed something:
class Wallet {
static final Wallet _singleton = Wallet._internal();
factory Wallet() {
return _singleton;
}
Wallet._internal();
Stream coins;
create() {
Stream<CrossOver> signin = FirebaseAuth.instance
.authStateChanges()
.map((event) => CrossOver(user: event, isnum: false));
group.add(signin);
coins = replace(FirebaseAuth.instance.currentUser);
group.add(coins);
}
Stream<CrossOver> replace(User user) async* {
if (user != null) {
await for (var x in FirebaseDatabase.instance
.reference()
.child('users/' + user.uid + '/scans')
.onValue) {
yield CrossOver(coins: Coins.load(x.snapshot.value), isnum: true);
}
} else {
yield CrossOver(coins: Coins.load(0), isnum: true);
}
}
Stream<Coins> real() {
return group.stream.map((event) {
Coins val;
if (event.isnum == true) {
print('new money');
val = event.coins;
}
if (event.isnum == false) {
Stream<CrossOver> nStream = replace(event.user);
print('new user');
group.remove(coins);
coins = nStream;
group.add(nStream);
FirebaseDatabase.instance
.reference()
.child('users/' + event.user.uid + '/scans')
.once()
.then((value) => val = Coins.load(value.value));
}
return val;
});
}
StreamGroup<CrossOver> group = StreamGroup();
}
class Coins {
int money;
Coins(this.money);
factory Coins.load(int i) {
return Coins(i);
}
}
class CrossOver {
bool isnum;
User user;
Coins coins;
CrossOver({this.user, this.coins, this.isnum});
}
I used flatMap() in rxdart to merge and combine the auth user stream and the firestore user stream so the resulting stream listens to both auth and firestore changes.
I wrote a more detailed answer with the code here: https://stackoverflow.com/a/66234728/10394353

Unhandled Exception: PlatformException(Error performing get, PERMISSION_DENIED: Missing or insufficient permissions., null)

Whenever the app starts this error comes. i think the error mainly is in the saveUserInfoFirestore function.. i saw that many other people have this same problem cause of their firestore security rules but i tried differenct rules it still shows same error..
final GoogleSignIn gSignIn = GoogleSignIn();
final userReference = Firestore.instance.collection("Users");
void initState(){
super.initState();
pageController=PageController();
gSignIn.onCurrentUserChanged.listen((gSigninAccount) { ControlSingIn(gSigninAccount); },
onError: (gError){
print("Google signin error"+ gError);
});
gSignIn.signInSilently(suppressErrors: false).then((gSigninAccount) { ControlSingIn(gSigninAccount); }).
catchError((onError){
print("Signin silently error"+ onError);
});
}
ControlSingIn(GoogleSignInAccount signInAccount) async{
if(signInAccount != null){
await saveUserInfotoFirestore();
setState(() {
isSignin = true;
});
}else{
setState(() {
isSignin = false;
});
}
}
saveUserInfotoFirestore() async{
final GoogleSignInAccount gCurrentuser = gSignIn.currentUser;
DocumentSnapshot documentSnapshot = await userReference.document(gCurrentuser.id).get();
if(!documentSnapshot.exists){
final username = await Navigator.push(context, MaterialPageRoute(builder: (context)=>CreateAccountPage()));
userReference.document(gCurrentuser.id).setData({
"id": gCurrentuser.id,
"profileName": gCurrentuser.displayName,
"username": username,
"url": gCurrentuser.photoUrl,
"email": gCurrentuser.email,
"bio": "",
"timestamp":timestamp
});
documentSnapshot = await userReference.document(gCurrentuser.id).get();
}
currentUser = User.fromDocument(documentSnapshot);
}
heres the firestore security rule that i am using:
rules_version = '2';
// Allow read/write access on all documents to any user signed in to the application
service cloud.firestore {
match /databases/{database}/documents {
match /messages/{document=**} {
allow read, write: if true;
}
}
}
Your security rules only allow access to a single collection called "messages" using this match:
match /messages/{document=**} {
allow read, write: if true;
}
However, your code is trying to write a different collection called "Users". You are seeing a permission error because your rules don't allow any access at all to that collection. You will have to write another rule to allow enough access to that collection as well.
I strongly suggest fully reviewing the documentation for security rules to understand how best to protect your app.
check with your googleservices.json file if it is in the right location. It should be in your project>android folder> app folder>

Updating User Data in Cloud Firestore

I've been having a problem trying to update data from a logged in user. I have the uid, but there has to be a connection between the uid and the collection of users, so that the program picks the right user to update, but I don't know how to make it.
Here's what I have:
FirebaseUser loggedInUser;
final _firestore = Firestore.instance;
//
double _latitude;
double _longitude;
void getCurrentLocation() async {
try {
Position position = await Geolocator()
.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
setState(() {
_latitude = position.latitude;
_longitude = position.longitude;
});
_firestore
.collection('users')
.document('${loggedInUser.uid}')
.updateData({'location': GeoPoint(_latitude, _longitude)});
} catch (e) {
print(e);
}
}
Here's what I've been getting:
E/flutter ( 9187): [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: PlatformException(Error performing updateData, NOT_FOUND: No document to update: projects/app-#####/databases/(default)/documents/users/CGyByl58ELc0zirlVjJpv5OWAc42, null)
So it is using the right uid ("CGyByl58ELc0zirlVjJpv5OWAc42")
Here's a screenshot from the Authentication tab of Firebase:
But what I'm trying to get is the name of the collection of this user in the database:
The user id is different than the document id, that's why you get that error since no document exists with the userId. You need to use the userId as the document id:
void addUser() async{
var firebaseUser = await FirebaseAuth.instance.currentUser();
Firestore.instance.collection("users").document(firebaseUser.uid).setData(
{
"age" : 38,
}).then((_){
print("success!");
});
}
Now you will have the userId as the document id and you can update the document using the userId

Firebase Authentication and Database Rules

I am new to Flutter/Dart but I have been trying to figure out the DB rules for a project that I am working on. I know that there are numerous posts on SO about Firebase DB rules but everything that I have found and tried has not worked. In the particular case below I want anyone to be able to read the data but only the author to be able to write and edit it. I set the Doc ID to be the uid and that works fine with permissions removed but I cannot get anything to work when I add restricted rules. What am I doing wrong?
Stack Trace Message
I/flutter (17900): PlatformException(Error performing setData, PERMISSION_DENIED: Missing or insufficient permissions., null)
E/flutter (17900): [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: setState() called after dispose(): _RegisterState#1360e(lifecycle state: defunct, not mounted)
My Firebase Rules:
service cloud.firestore {
match /databases/{database}/documents {
match /brews/{userId} {
allow read: if request.auth.uid != null;
allow write: if request.auth.uid == userId;
}
}
}
Database Code
class DatabaseService {
final String uid;
DatabaseService({this.uid});
//Collection reference
final CollectionReference brewCollection =
Firestore.instance.collection('brews');
Future updateUserData(String sugars, String name, int strength) async {
return await brewCollection.document(uid).setData({
'sugars': sugars,
'name': name,
'strength': strength,
});
}
//brew list from snapshot
List<Brew> _brewListFromSnapshot(QuerySnapshot snapshot) {
return snapshot.documents.map((doc) {
return Brew(
name: doc.data['name'] ?? '',
strength: doc.data['strength'] ?? 0,
sugars: doc.data['sugars'] ?? '0',
);
}).toList();
}
//userData from snapshot
UserData _userDataFromSnapshot(DocumentSnapshot snapshot) {
return UserData(
uid: uid,
name: snapshot.data['name'],
sugars: snapshot.data['sugars'],
strength: snapshot.data['strength'],
);
}
//Get the collection stream
Stream<List<Brew>> get brews {
return brewCollection.snapshots().map(_brewListFromSnapshot);
}
//get user doc stream
Stream<UserData> get userData {
return brewCollection.document(uid).snapshots().map(_userDataFromSnapshot);
}
}
Authentication Code:
class Authenticate extends StatefulWidget {
#override
_AuthenticateState createState() => _AuthenticateState();
}
class _AuthenticateState extends State<Authenticate> {
bool showSignIn = true;
void toggleView() {
setState(() {
showSignIn = !showSignIn;
});
}
#override
Widget build(BuildContext context) {
if (showSignIn) {
return SignIn(toggleView: toggleView);
} else {
return Register(toggleView: toggleView);
}
}
}
Your rules are correct, and your update function is correct. Your authentication code doesn't show us if you perform a firebase auth login though.
The only thing that could stop it from working is if you are not logged in (authenticated via firebase authentication) OR you passed the wrong uid String to your DatabaseService class. If that's the case you wont have a request.auth.uid so the rule will fail.
You can test your rules in the console, or you can try isolate the exact payload in your code to provide us more info.

Flutter: How to listen to the FirebaseUser is Email verified boolean?

My Idea:
I want to use the Firebase Auth Plugin in Flutter to register the users.
But before they can access the App, they have to verify their Email address.
Therefor I push the Firebase users after registration to a verification screen. This is just a loading screen which tells the user that he has to verify his email.
But now: How can I continuously listen, if the users email is verified or not and send him (when true) to the Homescreen?
I'm new to Flutter and I don't know if I have to use a Streams or Observables or a while Loop or setState() or something else for such a boolean check. And I also don't know how to setup a solution.
This is my basic code for register a user:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:async';
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
final Firestore _db = Firestore.instance;
Future<FirebaseUser> get getUser => _auth.currentUser();
Stream<FirebaseUser> get user => _auth.onAuthStateChanged;
Future<FirebaseUser> edubslogin(String email, String password) async {
try {
final FirebaseUser user = await _auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
await user.sendEmailVerification();
//email verification somewhere here
updateUserData(user);
return user;
} catch (error) {
print(error);
return null;
}
}
I've tried this:
if (user.isEmailVerified == true) {
//go to Homescreen
return true;
} else {
//show verification screen(loading spinner)
return false;
}
But I don't get a boolean value true out of isEmailVerified.
What do I have to do?
I faced the same situation in my app. My solution was to create a periodic timer into the initState method of a strategic route to hold the app until the e-mail is verified. It is not so elegant as using a listener but works fine.
import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart';
class _AccountConfirmationState extends State<AccountConfirmation> {
late Timer _timer;
#override
void initState() {
super.initState();
_timer = Timer.periodic(const Duration(seconds: 5), (timer) async {
await FirebaseAuth.instance.currentUser?.reload();
final user = FirebaseAuth.instance.currentUser;
if (user?.emailVerified ?? false) {
timer.cancel();
Navigator.pop(context, true);
}
});
}
#override
void dispose() {
super.dispose();
_timer.cancel();
}
#override
Widget build(BuildContext context) {
//TODO: Implement your amazing waiting screen here
}
}
This verification isn't as straightforward as you'd hope. First, there is the problem of recognizing that the user has verified their email. Second, there is the issue that there isn't any sort of a notification you can listen to that will automatically trigger a change in your app.
Check this thread for info about emailVerified: https://github.com/flutter/flutter/issues/20390#issuecomment-514411392
I was only able to verify the user if I 1) Created their account, 2) Signed them in, 3) Then checked to make sure they verified their email.
final FirebaseAuth _auth = FirebaseAuth.instance;
var _authenticatedUser = await _auth.signInWithEmailAndPassword(email: _email, password: _password);
//where _email and _password were simply what the user typed in the textfields.
if (_authenticatedUser.isEmailVerified) {
//Verified
} else {
//Not verified
}
Part 2: How do you get your app to recognize that the user has confirmed their email? Find a way to trigger the function that checks confirmation. A button would be easy enough. If you want it to see "automatic" then I guess you could create a timer that checks for email verification every 10 seconds or so.
Well I created a stream to handle this. Not so elegant but works. Use a StreamProvider.value() to handle events.
Stream<userVerificationStatus> checkUserVerified() async* {
bool verified = false;
yield userVerificationStatus(status: Status.LOADING);
while (!verified) {
await Future.delayed(Duration(seconds: 5));
FirebaseUser user = await _auth.currentUser();
if(user!=null)await user.reload();
if (user == null) {
yield userVerificationStatus(status: Status.NULL);
} else {
print("isemailverified ${user.isEmailVerified}");
await user.reload();
verified = user.isEmailVerified;
if(verified)
yield userVerificationStatus(status: Status.VERIFIED);
else
yield userVerificationStatus(status: Status.NOT_VERIFIED);
}
}
}
True. None of the FirebaseAuth idTokenChanges() , authStateChanges() or userChanges() will send you an event if the user verifies their email. I'm using a combination of the methods to get an email verification update in my app and it seems to be working well.
First I check the status in the initState() method and start a timer if email is not verified
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
//Get Authenticated user
user = context.read<AuthenticationService>().currentUser();
_isEmailVerified = user.emailVerified;
if (!_isEmailVerified) _startEmailVerificationTimer();
}
I also listen for app background/foreground events in case the user happens to leave the app to confirm their email ( If you also do this, add WidgetsBindingObserver to your class)
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
user = context.read<AuthenticationService>().reloadCurrentUser();
if (user.emailVerified) {
setState(() {
_isEmailVerified = user.emailVerified;
});
timer?.cancel();
} else {
if (!timer.isActive) _startEmailVerificationTimer();
}
}
}
This is the _startEmailVerificationTimer() method
_startEmailVerificationTimer() {
timer = Timer.periodic(Duration(seconds: 5), (Timer _) {
user = context.read<AuthenticationService>().reloadCurrentUser();
if (user.emailVerified) {
setState(() {
_isEmailVerified = user.emailVerified;
});
timer.cancel();
}
});
}
Don't forget to dispose the timer
#override
void dispose() {
timer?.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
My Firebase User methods in case anyone is interested:
User currentUser() {
return _firebaseAuth.currentUser;
}
User reloadCurrentUser() {
User oldUser = _firebaseAuth.currentUser;
oldUser.reload();
User newUser = _firebaseAuth.currentUser;
return newUser;
}
In order for the app to recognise if the user has verified their email you can achieve this with a simple user.reload.
In order to test it yourself implement a button with onPressed code:
FlatButton(
child: Text("check"),
textColor: Colors.white,
onPressed: () async {
try {
FirebaseUser user = await _firebaseAuth.currentUser();
await user.reload();
user = await _firebaseAuth.currentUser();
print( user.isEmailVerified);
} catch (e) {
return e.message;
}
}),
I had the same problem with the latest version of firebase auth.
But I found out there is a function for reloading the current user which signed in
Future<bool> get userVerified async {
await FirebaseAuth.instance.currentUser.reload();
return FirebaseAuth.instance.currentUser.emailVerified;
}
referesh token after checking current user emailVerified is true
var user = FirebaseAuth.instance.currentUser;
await user?.reload();
if (user?.emailVerified == true) {
await FirebaseAuth.instance.currentUser?.getIdToken(true);
//rest code..
}
also please let me know if this a correct way of doing things.
I have found a way by updating firebase user profile and calling it in init() like below function.
void _checkEmailVerification() async {
await widget.auth.getCurrentUser().then((user) {
UserUpdateInfo userUpdateInfo = new UserUpdateInfo();
userUpdateInfo.displayName = user.displayName;
user.updateProfile(userUpdateInfo).then((onValue) {
setState(() {
_isEmailVerified = user.isEmailVerified;
});
});
});
}
Auth state change listener didn't work for me. Field isEmailVerified remains false even after user verifies his email.
My workaround:
Started from the assumption that user leaves the app to verify his email (which mean app is paused), and he returns to the app after verifying it (app resumes).
What I did was attach a WidgetsBinding to a relevant stateful widget where I wanted to display if email was verified (but can be done elsewhere). This involves two steps.
First step is to attach the binding:
#override
void initState() {
WidgetsBinding.instance.addObserver(this);
super.initState();
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
Second step is to override the didChangeAppLifecycleState to reload the user. I created a function that does the reload and sets a new firebaseUser object
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed && !firebaseUser.isEmailVerified)
refreshFirebaseUser().then((value) => setState(() {}));
super.didChangeAppLifecycleState(state);
}
Future<void> refreshFirebaseUser() async {
await firebaseUser.reload();
firebaseUser = FirebaseAuth.instance.currentUser;
}
So what this is basically doing is to reload firebase user object everytime the user returns to the app, while its email is not verified. I chose this solution over setting and cancelling a timer as it avoided setting a recurrent action through a timer which could be overkill for this particular problem.
Since authOnChanged only listens for sign in and sign out actions, in your sign in method, first sign out then try to sign in.
await _firebaseAuth.signOut();
authResult = await _firebaseAuth.signInWithEmailAndPassword(email: email, password: password);
return authResult.user;
In the onAuthChanged, when you control if user.isEmailVerified, it will work since you have signed out and it will update the user even if you haven't signed in yet because sign out will trigger your onAuthChanged even if you haven't signed in.
It is like cheating but the only way that I have found without timeout is this.

Resources