0

I have been able to get Firebase Authentication to work for Google sign in, Anonymous sign in and from Email and Password sign in, including sending a verification email during email and password sign in thanks to help on stackoverflow.  Everything works as intended.  Now for my next step I am trying to create a user collection in Firestore using the uid created by Firebase Authentication.  I am confident my code is written correctly because I have tested it with (unsecure) Security Rules and the process worked exactly as desired. I have reviewed the Firebase documentation several times but I cannot figure out what is wrong with my Security Rules code. How can I fix my Security rules to allow a new user to create a Screen name that will be added to the user collection in Firestore?  Thanks in advance for the help. 

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{uid}/jobs/{document=**} {
       allow read, write: if request.auth.uid == uid;
  }

  match /users/{uid}/{document=**} {
       allow read, write: if request.auth.uid == uid;
  }

  }
  
  }


   class HomePage extends StatefulWidget {
      const HomePage({
        Key? key,
        
      }) : super(key: key);
    
      
    
      @override
      State<HomePage> createState() => _HomePageState();
    }
    
    class _HomePageState extends State<HomePage> {
      @override
      void initState() {
        super.initState();
        createUserInFirestore();
      }
    
      Future<void> createUserInFirestore() async {
        final GoogleSignIn googleSignIn = GoogleSignIn();
        final GoogleSignInAccount? user = googleSignIn.currentUser;
        final usersRef = FirebaseFirestore.instance.collection('users');
        final DocumentSnapshot doc = await usersRef.doc(user?.id).get();
        if (!doc.exists) {
          final userName = await Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) => const CreateAccountPage(),
            ),
          );
          usersRef.doc(user?.id).set({
            'id': user?.id,
            'userName': userName,
            'photoUrl': user?.photoUrl,
            'email': user?.email,
            'displayName': user?.displayName,
            'bio': '',
            'timestamp': documentIdFromCurrentDate(),
          });
        doc = await usersRef.doc(user?.id).get();
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return AdaptiveLayoutScaffold(
          drawer: const SideSheet(
            userImage: FakeUserAvatars.stacy,
            userName: 'Stacy James',
          ),
          landscapeBodyWidget: Container(),
          portraitBodyWidget: Container(),
        );
      }
    }

class CreateAccountPage extends StatefulWidget {
  const CreateAccountPage({
    Key? key,
  }) : super(key: key);

  @override
  _CreateAccountPageState createState() => _CreateAccountPageState();
}

class _CreateAccountPageState extends State<CreateAccountPage> {
  final _formKey = GlobalKey<FormState>();
  late String userName;

  void submit() {
    _formKey.currentState?.save();
    Navigator.pop(context, userName);
  }

  @override
  Widget build(BuildContext context) {
    return AdaptiveLayoutScaffold(
      appBar: const Header(
        automaticallyImplyLeading: false,
        pageName: 'User Name',
      ),
      landscapeBodyWidget: Container(),
      portraitBodyWidget: ListView(
        children: [
          Column(
            children: [
              const Padding(
                padding: EdgeInsets.only(top: 16.0),
                child: Center(
                  child: Text('Create a User Name'),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: Form(
                  key: _formKey,
                  child: TextFormField(
                    autovalidateMode: AutovalidateMode.always,
                    decoration: InputDecoration(
                      hintText: 'Must be between 3 and 20 characters',
                      labelText: 'User Name',
                      prefixIcon: Icon(
                        Icons.person,
                        color: Theme.of(context).iconTheme.color,
                      ),
                    ),
                    keyboardType: TextInputType.text,
                    onSaved: (val) => userName = val as String,
                  ),
                ),
              ),
              PlatformElevatedButton(
                onPressed: submit,
                buttonText: 'Create User Name',
              ),
            ],
          ),
        ],
      ),
    );
  }
}
Carleton Y
  • 289
  • 5
  • 17
  • Firestore creates collection automatically when you add data by naming any collection. Are you getting any permission error? – Anuj Raghuvanshi Nov 28 '21 at 20:21
  • The new user is being created in Firebase but the console says 'failed: Status{code=PERMISSION_DENIED, description=Missing or insufficient permissions., cause=null. Unhandled Exception: [cloud_firestore/permission-denied] The caller does not have permission to execute the specified operation. @Anuj Raghuvanshi – Carleton Y Nov 28 '21 at 20:38
  • Yes, Right. This is what you have added for your security rules. that Collection is allowed to be accessed when you have logged in. But If have not logged in, you will not be able to access any collection to perform operations. You can Add `match /users/{uid}/users/{document=**} { allow read, write: if true; } ` to allow users collection always readable and writable and try by doing so. – Anuj Raghuvanshi Nov 28 '21 at 21:12
  • Add as replacement for the rule I had or add in addition to? @AnujRaghuvanshi. I tried it both ways (I think) and the same error appears which is very strange. The new user is created in Firebase but no collection is created in Firestore and the app goes straight to the Home page instead of the Create Account Page. – Carleton Y Nov 28 '21 at 21:51
  • Could you try this instead ```match /users/{uid}/{document=**} { function isAuthenticated() { return request.auth.uid != null; } allow read, write: if isAuthenticated() && request.auth.uid == resource.data.uid }``` – Marc Anthony B Nov 29 '21 at 07:08
  • Thank you @MarcAnthonyB. I tried your suggestion and unfortunately for me for some reason that didn't work either. I did a new test (unsecure) to make sure my Flutter code and the flow was correct and the user was authenticated and the data was correctly added to the database. I cannot leave access for anyone as 'true' but doing that allowed me to confirm that the issue is definitely the way I have the Security Rules written and not my code. So at least I know my focus is in the right place. I appreciate the help. – Carleton Y Nov 29 '21 at 07:40
  • 1
    @CarletonY, Could you check this [thread](https://stackoverflow.com/questions/47808698/firestore-security-rule-request-auth-uid-is-not-working) and see if it helps. Thank you. – Marc Anthony B Nov 29 '21 at 08:31
  • @MarcAnthonyB I wanted to say thanks for the link to the thread. I needed to read and learn more. I ended up rewriting and simplifying my code above and now I securely have the exact behavior that I wanted. The Rules Playground was my friend. – Carleton Y Nov 30 '21 at 18:07
  • Hi @CarletonY, Can you post it as answer so it would help the community. Thank you. – Marc Anthony B Dec 01 '21 at 05:45

1 Answers1

2

After reading what has been suggested and a few other things I used the Firestore Rules Playground to fix my code and then updated my Auth class to include a new method called createUserInFirestore() to handle the creation of a new user in Firestore using the uid after the user is created by Firebase Authentication.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
  
    match /users/{uid}/{document=**} {
       allow read, create, update, delete: if request.auth.uid == uid;  
    }
  } 
}

abstract class AuthBase {
  User? get currentUser;

  Stream<User?> authStateChanges();
  Future<User?> signInWithGoogle();
  Future<User?> createUserWithEmailAndPassword(
    String email,
    String password,
  );
  Future<void> checkEmailVerified(BuildContext context, Timer timer);
  Future<User?> signInWithEmailAndPassword(String email, String password);
  Future<User?> signInAnonymously();
  Future<void> resetPassword(BuildContext context, String email);
  Future<void> confirmSignOut(BuildContext context);
  Future<void> signOut();
}

class Auth implements AuthBase {
  final _firebaseAuth = FirebaseAuth.instance;

  @override
  User? get currentUser => _firebaseAuth.currentUser;

  @override
  Stream<User?> authStateChanges() => _firebaseAuth.authStateChanges();

  void _createNewUserInFirestore() {
    final User? user = currentUser;
    final CollectionReference<Map<String, dynamic>> usersRef =
        FirebaseFirestore.instance.collection('users');
    usersRef.doc(user?.uid).set({
      'id': user?.uid,
      'screenName': '',
      'displayName': user?.displayName,
      'photoUrl': user?.photoURL,
      'bio': '',
      'darkMode': false,
      'timestamp': documentIdFromCurrentDate(),
    });
  }

  @override
  Future<User?> signInWithGoogle() async {
    final GoogleSignIn googleSignIn = GoogleSignIn();
    final GoogleSignInAccount? googleUser = await googleSignIn.signIn();
    if (googleUser != null) {
      final googleAuth = await googleUser.authentication;
      if (googleAuth.idToken != null) {
        final UserCredential userCredential =
            await _firebaseAuth.signInWithCredential(
          GoogleAuthProvider.credential(
            idToken: googleAuth.idToken,
            accessToken: googleAuth.accessToken,
          ),
        );
        _createNewUserInFirestore();
        return userCredential.user;
      } else {
        throw FirebaseAuthException(
          code: FirebaseExceptionString.missingGoogleIDTokenCode,
          message: FirebaseExceptionString.missingGoogleIDTokenMessage,
        );
      }
    } else {
      throw FirebaseAuthException(
        code: FirebaseExceptionString.abortedByUserCode,
        message: FirebaseExceptionString.canceledByUserMessage,
      );
    }
  }

  @override
  Future<User?> createUserWithEmailAndPassword(
    String email,
    String password,
  ) async {
    final UserCredential userCredential =
        await _firebaseAuth.createUserWithEmailAndPassword(
      email: email,
      password: password,
    );
    _createNewUserInFirestore();
    return userCredential.user;
  }

  @override
  Future<void> checkEmailVerified(BuildContext context, Timer timer) async {
    final User? user = currentUser;
    await user?.reload();
    final User? signedInUser = user;
    if (signedInUser != null && signedInUser.emailVerified) {
      timer.cancel();
      Navigator.pushReplacement(
        context,
        MaterialPageRoute(
          builder: (context) => const HomePage(),
        ),
      );
    }
  }

  @override
  Future<User?> signInWithEmailAndPassword(
    String email,
    String password,
  ) async {
    final UserCredential userCredential =
        await _firebaseAuth.signInWithCredential(
      EmailAuthProvider.credential(
        email: email,
        password: password,
      ),
    );
    return userCredential.user;
  }

  @override
  Future<void> resetPassword(
    BuildContext context,
    String email,
  ) async {
    try {
      await _firebaseAuth.sendPasswordResetEmail(email: email);
      Navigator.of(context).pop();
    } catch (e) {
      print(
        e.toString(),
      );
    }
  }

  @override
  Future<User?> signInAnonymously() async {
    final UserCredential userCredential =
        await _firebaseAuth.signInAnonymously();
    return userCredential.user;
  }

  Future<void> _signOut(BuildContext context) async {
    try {
      final AuthBase auth = Provider.of<AuthBase>(
        context,
        listen: false,
      );
      await auth.signOut();
      Navigator.pushAndRemoveUntil<dynamic>(
        context,
        MaterialPageRoute<dynamic>(
          builder: (BuildContext context) => const LandingPage(),
        ),
        (route) => false,
      );
    } catch (e) {
      print(
        e.toString(),
      );
    }
  }

  @override
  Future<void> confirmSignOut(BuildContext context) async {
    final bool? didRequestSignOut = await showAlertDialog(
      context,
      cancelActionText: DialogString.cancel,
      content: DialogString.signOutAccount,
      defaultActionText: DialogString.signOut,
      title: DialogString.signOut,
    );
    if (didRequestSignOut == true) {
      _signOut(context);
    }
  }

  @override
  Future<void> signOut() async {
    final GoogleSignIn googleSignIn = GoogleSignIn();
    await googleSignIn.signOut();
    await _firebaseAuth.signOut();
  }
}
Carleton Y
  • 289
  • 5
  • 17