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

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();

Related

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.

Cloud Functions, deleting Firestore SubCollections, is AdminToken necessary?

I am trying to build callable cloud functions, when users delete a post, it also try to delete the comments, which is a sub-collection of the post. so I saw the example and implemented just like a documentation example
const admin = require('firebase-admin');
const firebase_tools = require('firebase-tools');
const functions = require('firebase-functions');
admin.initializeApp({
serviceAccountId: 'xxxxxx-xxxxx#appspot.gserviceaccount.com'
}
);
exports.mintAdminToken = functions.https.onCall(async (data: any, context: any) => {
const uid = data.uid;
const token = await admin
.auth()
.createCustomToken(uid, { admin: true });
return { token };
});
exports.recursiveDelete = functions
.runWith({
timeoutSeconds: 540,
memory: '2GB'
})
.https.onCall(async (data: any, context: any) => {
// Only allow admin users to execute this function.
if (!(context.auth && context.auth.token && context.auth.token.admin)) {
throw new functions.https.HttpsError(
'permission-denied',
'Must be an administrative user to initiate delete.'
);
}
const path = data.path;
console.log(
`User ${context.auth.uid} has requested to delete path ${path}`
);
await firebase_tools.firestore
.delete(path, {
project: process.env.GCLOUD_PROJECT,
recursive: true,
yes: true,
token: functions.config().fb.token
});
return {
path: path
};
});
and I succeeded in receiving the custom token to the client. but what I have to do now? after getting token I called the "recursiveDelete" function from client but it occurs error PERMISSION_DENIED
Should the user who received the token be initialized with a new custom admin token? (if I misunderstand let me know)
Is the admin token really necessary when deleting a sub collection like this? It's difficult to use, so I ask.
I don't think that you really need a custom token for this use case and I suggest that you use firebase firestore rules rather than implementing your own role based authentication.
Steps to follow:
1- create a collection that you may call "users" and include in it a field of the role that this user may have such as "ADMIN". every document id in this collection can be the auth uid of users that firebase auth generates. you can get this uid from your frontend by using the currentUser prop and it's all explained here
2- protect your database with firestore rules as such:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// only admins can remove posts
match /posts/{postID} {
allow read, write: if isAdmin();
}
// only admins can remove comments
match /comments/{commentID} {
allow read, write: if isAdmin();
}
// this function will check if the caller has an admin role and allow or disallow the task upon that
function isAdmin() {
return get(/databases/$(database)/documents/
users/$(request.auth.uid)).data.role == "ADMIN";
}
}
}
3- after you succefully deletes a post document you can create a function with onDelete trigger that get invoked and deletes the comments subcollection recursivley and to do that you should include this bit of code:
const client = require('firebase-tools');
exports.recursiveDelete = functions.firestore
.document('posts/{postID}')
.onDelete((snap, context) => {
.....
await client.firestore
.delete(collectionPath, {
project: process.env.GCLOUD_PROJECT,
recursive: true,
yes: true
});
}

Firestore Permission denied. Connecting firestore to react native

addPost = async ({ text, localUri }) => {
const remoteUri = await this.uploadPhotoAsync(localUri);
return new Promise((res, rej) => {
this.firestore
.collection("posts")
.add({
text,
uid: this.uid,
timestamp: this.timestamp,
image: remoteUri
})
.then(ref => {
res(ref);
})
.catch(error => {
rej(error);
});
});
};
And
rules_version = '2';
service firebase.storage {
match /posts/{doc} {
allow read, write: if true;
}
}
Above is my react-native code and below it is the firebase rules for my database so far. Still getting
FirebaseError:[code=permission-denied]: Missing or insufficient permissions.
Any help as to how to fix this code or to make my rules more secure? At this point in the code the user has been authenticated.
The rules in the question are for firebase storage, you need to change the rule for firestore to true:
// Allow read/write access to all users under any conditions
// Warning: **NEVER** use this rule set in production; it allows
// anyone to overwrite your entire database.
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
Check here for more information:
https://firebase.google.com/docs/firestore/security/insecure-rules

Firebase Rules are not working. Returning data even when I set up rules in Firestore

I set up Firebase Functions to make calls to my Firestore. I'm using admin.auth() and returning data. I set up custom rules in the Firestore Rules section, but the Functions are not following the Rules i.e. when I use the URL in Postman, I shouldn't be getting the data because it doesn't fulfill "if read, write: if request.auth != null". How do I address this?
Here is my Firebase Function Code:
const admin = require('firebase-admin');
module.exports = function(req, res) {
const uid = req.body.uid;
admin
.auth()
.getUser(uid)
.then(user => {
admin
.firestore()
.collection('discover')
.get()
.then(snapshot => {
res.send(
snapshot.docs.map(doc => {
const data = Object.assign({ doc_id: doc.id }, doc.data());
return data;
})
);
})
.catch(err => {
res.send({ message: 'Something went wrong!', success: false });
});
})
.catch(err => {
res.send({ error: 'Something went wrong!', success: false });
});
};
Firestore Rules:
service cloud.firestore {
match /databases/{database}/documents {
match /users/{users} {
allow read: if request.auth != null;
}
match /discover/{discover} {
allow read: if request.auth != null;
}
match /favorites/{favorite} {
allow read, write: if request.auth != null;
}
}
}
I shouldn't be able to get this data from Postman (Since I'm not authenticated) but I'm still getting the data. I don't want the data to be accessible if the user is not logged in.
Security rules don't apply when you access the database via the Admin SDK, or any other time you use a service account. It doesn't matter at all that you're using postman (or any other HTTP client). The thing actually doing the database access here is the Admin SDK.

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
}
}

Resources