Flutter - Your Cloud Firestore database has insecure rules - firebase

I have a collection called users where I am checking if new users mobile no is present or not. If It is present then I am performing phone authentication for that user then storing uid as a field in document.
If user is coming for the first time, he is not authenticated and I am performing read operation from users collection. Now every time I am getting Your Cloud Firestore database has insecure rules email from google.
Below is the rule I am using. Please let me know how can I make it secure.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read: if true;
allow write: if request.auth != null;
}
}
}

You can change your rule adding more security like this:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}
But, then your app won't be able to read from Firebase, since you are telling that even for read is necessary to be authenticated.
I solved this allowing users to authenticate anonymously in Firebase. For this go to:
https://console.firebase.google.com/project/[YOUR-PROJECT]/authentication/providers
and enable Anonymous method. Remember to change [YOUR-PROJECT] in the URL.
After this you will only need to add few lines of code in your main screen or whatever you want.
1) Import the Firebase Auth package:
import 'package:firebase_auth/firebase_auth.dart';
2) Add the following code at the beginning of your main StatefulWidget:
final FirebaseAuth firebaseAuth = FirebaseAuth.instance;
Future<FirebaseUser> signInAnon() async {
AuthResult result = await firebaseAuth.signInAnonymously();
FirebaseUser user = result.user;
print("Signed in: ${user.uid}");
return user;
}
void signOut() {
firebaseAuth.signOut();
print('Signed Out!');
}
3) And now you just have to call the function inside your initState:
signInAnon().then((FirebaseUser user){
print('Login success!');
print('UID: ' + user.uid);
});
And voilá! Now every user user will authenticate anonymously automatically in your Firebase database. The best part is the user persists in the app until you uninstall it or delete the cache data.
Here is a video explaining the steps, but using a login screen which I removed for my project and this example: https://www.youtube.com/watch?v=JYCNvWKF7vw

Related

How to set rules to users in firebase?

I want to create new rules for my client - one client can create one document in collection.
match /Users/{userId} {
allow update, delete: if request.resource.data.uid == uid;
allow create: if
request.data.uid != request.resource.data.uid;
if request uid == request.resource.data.uid; he cannot.
If you want each user to only be able to create a single document, that is easiest if you use the user's UID as the document ID. In JavaScript that'd be something like this:
import { doc, setDoc } from "firebase/firestore";
import { getAuth } from "firebase/auth";
const user = getAuth().currentUser;
await setDoc(doc(db, "Users", user.uid), {
displayName: user.displayName,
});
You can then enforce this content-owner only access in security rules with:
service cloud.firestore {
match /databases/{database}/documents {
// Allow only authenticated content owners access
match /Users/{userId}/{documents=**} {
allow write: if request.auth != null && request.auth.uid == userId
}
}
}
The main difference with your approach is that we don't check the data of the document, but instead check the document ID (which we set to the UID in our code) to the UID on the request.

React-Native + Apple sign-in + Firestore: permission-denied

I'm trying to add Apple Sign-In to my project which is based on react native and firestore. Authentication flow itself works fine but firestore security rules reject my request when I try to create a user profile afterwards.
Firebase security rules:
rules_version = '2';
service cloud.firestore {
match /users/{userId} {
allow create:
if request.auth != null;
...
}
...
}
Simplified React Native code:
import { firebase } from './config';
import { firebase as RNFBAuth } from '#react-native-firebase/auth';
// Step 1
const credential = RNFBAuth.auth.AppleAuthProvider.credential(token, nonce);
// Step 2
RNFBAuth.auth().signInWithCredential(credential).then((response) => {
if (response.additionalUserInfo.isNewUser) {
// Step 3
firebase.firestore()
.collection('users')
.doc(uid)
.set({
// profile details
})
.then(() => {
// update local state
})
.catch((_error) => {
console.log(_error + ": " + _error.code);
});
}
});
Step 3 is failing with error code FirebaseError: The caller does not have permission: permission-denied.
Error is gone when Firestore security rules are downgraded to "allow create: if true". Unfortunately it does not fly for me for obvious reasons.
My guess is firebase/firestore does not know that user completed authentication via firebase/auth package thus request in "Step 3" is being send as unauthenticated one. Any ideas how to sync them?
Other Auth Providers like Google and Facebook are located at the main firebase package instead of firebase/auth thus same problem does not apply for them:
const credential = firebase.auth.FacebookAuthProvider.credential(token);
const credential = firebase.auth.GoogleAuthProvider.credential(token);
const credential = RNFBAuth.auth.AppleAuthProvider.credential(token, nonce);
Any ideas how to solve it?
Eventually the problem has been found - incompatible package versions. I've upgraded all firebase packages and #invertase/react-native-apple-authentication to the latest versions and everything seems to work fine now.
As sugested in the comment you should use the onAuthStateChanged listener:
firebase.auth().onAuthStateChanged((user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
var uid = user.uid;
// ...
} else {
// User is signed out
// ...
}
});
That way you have a general solution for all auth providers. It would not matter wich one you use. The trigger will be fired asap a user is signed in. I also use that method in all of my apps to sync user data.
You can read more about it here.

How to set Firestore security rules for create operation

I am working on developing a mobile app in Flutter and using Firebase as backend.
Users will need to sign up to be able to use my app and I will save some user data in a user_profile collection in Firestore.
Since a new document will be added to the user_profile collection, I wonder how security rules should be set for creating a new document for the first time when the user registers.
Here is what happens during the sign up in my case:
// Step 1: Register user with email and password
FirebaseAuth _auth = FirebaseAuth.instance;
await _auth.createUserWithEmailAndPassword(email: email, password: password);
// Step 2: save user data in user_profile collection
FirebaseFirestore.instance
.collection('user_profile')
.doc(user.uid)
.set({
'uid': user.uid,
'email': email,
'name': name,
'date': DateTime.now,
})
Since I save the user data in the user_profile collection after the call for createUserWithEmailAndPassword(..) function is completed (meaning that the user is now authenticated), is it safe to define the security rule for create operation in Firebase as following? Or should I set it differently to make it secure in the right way?
// Rules for User Profile data
match /user_profile/{any} {
// Applies to writes to non-existent documents
allow create: if request.auth != null;
}
From my experience, this should be fine but you could additionally check that the uid contained in the document corresponds to the authenticated user.
allow create: if request.auth != null &&
request.auth.uid == request.resource.data.uid;

Flutter and Firestore does not have user information in request

Using flutter, I have installed the firebase-auth and firestore packages and am able to both authenticate with firebase auth and make a call into firestore as long as I don't have any rules around the user.
I have a button that calls _handleEmailSignIn and I do get a valid user back (since they are in the Firebase Auth DB)
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
final FirebaseAuth _auth = FirebaseAuth.instance;
void _handleEmailSignIn(String email, String password) async {
try {
FirebaseUser user = await _auth.signInWithEmailAndPassword(
email: email, password: password);
print("Email Signed in " + user.uid); // THIS works
} catch (err) {
print("ERROR CAUGHT: " + err.toString());
}
}
I then have another button that calls this function to attempt to add a record into the testing123 collection.
Future<Null> _helloWorld() async {
try {
await Firestore.instance
.collection('testing123')
.document()
.setData(<String, String>{'message': 'Hello world!'});
print('_initRecord2 DONE');
} catch (err) {
print("ERROR CAUGHT: " + err.toString());
}
}
Now this works as long as I don't have any rules around checking the request user. This works...
service cloud.firestore {
match /databases/{database}/documents {
match /testing123auth/{doc} {
allow read, create
}
}
}
This does not which gives PERMISSION_DENIED: Missing or insufficient permissions. when I want to make sure I have the authenticated user I did with _handleEmailSignIn.
service cloud.firestore {
match /databases/{database}/documents {
match /testing123auth/{doc} {
allow read, create: if request.auth != null;
}
}
}
I suspect that the firestore request is not including the firebase user. Am I meant to configure firestore to include the user or is this supposed to be automatic as part of firebase?
One thing to note that's not well documented is that firebase_core is the "Glue" that connects all the services together and when you're using Firebase Authentication and other Firebase services, you need to make sure you're getting instances from the same firebase core app configuration.
final FirebaseAuth _auth = FirebaseAuth.instance;
This way above should not be used if you're using multiple firebase services.
Instead, you should always get FirebaseAuth from FirebaseAuth.fromApp(app) and use this same configuration to get all other Firebase services.
FirebaseApp app = await FirebaseApp.configure(
name: 'MyProject',
options: FirebaseOptions(
googleAppID: Platform.isAndroid ? 'x:xxxxxxxxxxxx:android:xxxxxxxxx' : 'x:xxxxxxxxxxxxxx:ios:xxxxxxxxxxx',
gcmSenderID: 'xxxxxxxxxxxxx',
apiKey: 'xxxxxxxxxxxxxxxxxxxxxxx',
projectID: 'project-id',
bundleID: 'project-bundle',
),
);
FirebaseAuth _auth = FirebaseAuth.fromApp(app);
Firestore _firestore = Firestore(app: app);
FirebaseStorage _storage = FirebaseStorage(app: app, storageBucket: 'gs://myproject.appspot.com');
This insures that all services are using the same app configuration and Firestore will receive authentication data.
There shouldn't be any special configuration needed for the firestore to do this.
This is all you should need.
Modified from Basic Security Rules:
service cloud.firestore {
match /databases/{database}/documents {
match /testing123/{document=**} {
allow read, write: if request.auth.uid != null;
}
}
}
It seems they check if the uid is null rather than the auth itself. Try this out and see if it works. Also, it seemed that your code was inconsistent as the firestore rule had testing123auth and flutter had testing123. I'm not sure if that was intentional.
to check if the user is signed in you should use
request.auth.uid != null
I would have suggested to make the rule like:
service cloud.firestore {
match /databases/{database}/documents {
match /testing123auth/{documents=**} {
allow read, create: if true;
}
}
}
Or, better yet, limit the scope of the user:
service cloud.firestore {
match /databases/{database}/documents {
match /testing123auth/{userId} {
allow read, create:
if (request.auth.uid != null &&
request.auth.uid == userId); // DOCUMENT ID == USERID
} // END RULES FOR USERID DOC
// IF YOU PLAN TO PUT SUBCOLLECTIONS INSIDE DOCUMENT:
match /{documents=**} {
// ALL DOCUMENTS/COLLECTIONS INSIDE THE DOCUMENT
allow read, write:
if (request.auth.uid != null &&
request.auth.uid == userId);
} // END DOCUMENTS=**
} // END USERID DOCUMENT
}
}

Firestore permission denied when using signInWithCredential(), React Native Expo

firebase.initializeApp(config);
const db = firebase.firestore();
const googleSignIn = async () => {
return await Expo.Google.logInAsync({
androidClientId,
iosClientId,
scopes: ['profile', 'email'],
});
};
const firebaseLogin = async (accessToken) => {
const cred = firebase.auth.GoogleAuthProvider.credential(null, accessToken);
await firebase.auth().signInWithCredential(cred).catch(console.error);
const idToken = await firebase.auth().currentUser.getIdToken(true).catch(console.error);
};
await firebaseLogin(googleSignIn().accessToken);
db.collection("any").doc().set({test: "OK"})
I get a permission denied error when trying to write to Firestore using a request.auth.uid != null; security rule, but when I replace it with true it works.
It seems that the Firestore component of the web SDK does not send authentication details, even though the API on the client reports Firebase is logged in, and the user last login date appears in the web GUI.
Do I need to pass authentication details to the Firestore component when logging in directly with Google (instead of using the Firebase login APIs)?
The code is running in a React Native app via Expo.
Another example that gets a permission denied:
firebase.auth().onAuthStateChanged((user) => {
if (user) {
firebase.firestore().collection("any").doc().set({test: "OK"});
}
});
Rules
// This is OK:
service cloud.firestore {
match /databases/{database}/documents {
match /any/{doc} {
allow read, write: if true;
}
}
}
// Permission denied
service cloud.firestore {
match /databases/{database}/documents {
match /any/{doc} {
allow read, write: if request.auth.uid != null;
}
}
}
Related
Firebase Firestore missing or insufficient permissions using Expo (React Native)
https://forums.expo.io/t/firestore-with-firebase-auth-permissions-to-read-write-only-to-signed-in-users/5705
This solution, and possibly this whole issue, may be specific to React Native.
In a similar question, Jade Koskela noticed that requests were missing the Origin header, and applies a patch to React's XHRInterceptor to work around the missing auth object:
const xhrInterceptor = require('react-native/Libraries/Network/XHRInterceptor');
xhrInterceptor.setSendCallback((data, xhr) => {
if(xhr._method === 'POST') {
// WHATWG specifies opaque origin as anything other than a uri tuple. It serializes to the string 'null'.
// https://html.spec.whatwg.org/multipage/origin.html
xhr.setRequestHeader('Origin', 'null');
}
});
xhrInterceptor.enableInterception();

Resources