Cloud Functions, deleting Firestore SubCollections, is AdminToken necessary? - firebase

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

Related

Firebase delete method with insufficient permissions

I have the following rules for a Firestore database:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{user} {
allow create, update: if request.auth != null;
allow read, delete, list: if request.auth.token.email == resource.data.email;
}
}
}
In my app I can read the data by using getDocs(), but even though the same security is on the delete function, the following does not work:
const removeUser = async(number) => {
console.log(number);
console.log(auth.currentUser?.email || "No User");
const usersRef = collection(db, 'users');
const qU = query(usersRef, where('number', '==', number));
const usersQuerySnapshot = await getDocs(qU);
console.log(usersQuerySnapshot.docs.map(d => d.data().email);
usersQuerySnapshot.forEach((user) => {
deleteDoc(doc);
});
};
After some debugging, I know that deleting the deleteDoc() lines still gives me an insufficient permissions error.
This function is called from another function, update(). This function also calls a getUsersOfTeacher() (a teacher has its own users) which uses the same code as removeUser() but without the deleting part. This function works with no errors.
Does anyone know what's going on here?
Firebase's security rules do not filter the data. Instead they merely ensure that the code doesn't request more data than it's authorized for.
Since your rules require that the user's email address is in the email field of the document, your query must do the same. So something like:
query(
usersRef,
where('number', '==', number),
where('email', '==', auth.currentUser?.email) // 👈
);

Integrate custom Oauth provider with firebase.auth().signInWithRedirect?

I setup a Twitch OAuth integration using the Instagram example, now I can login into my app by opening the popup.html page that the example gave me.
Here's my adapted code:
'use strict';
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');
const { AuthorizationCode } = require('simple-oauth2');
const fetch = require('node-fetch');
// Firebase Setup
const admin = require('firebase-admin');
// #ts-ignore
const serviceAccount = require('./service-account.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: `https://${process.env.GCLOUD_PROJECT}.firebaseio.com`,
});
const OAUTH_REDIRECT_URI = `https://${process.env.GCLOUD_PROJECT}.firebaseapp.com/popup.html`;;
const OAUTH_SCOPES = 'user:read:email';
/**
* Creates a configured simple-oauth2 client for Twitch.
*/
function twitchOAuth2Client() {
// Twitch OAuth 2 setup
// TODO: Configure the `twitch.client_id` and `twitch.client_secret` Google Cloud environment variables.
const credentials = {
client: {
id: functions.config().twitch.client_id,
secret: functions.config().twitch.client_secret,
},
auth: {
tokenHost: 'https://id.twitch.tv',
tokenPath: '/oauth2/token',
authorizePath: '/oauth2/authorize',
},
options: {
bodyFormat: 'json',
authorizationMethod: 'body',
},
};
return new AuthorizationCode(credentials);
}
/**
* Redirects the User to the Twitch authentication consent screen. Also the 'state' cookie is set for later state
* verification.
*/
exports.redirect = functions.https.onRequest((req, res) => {
const authorizationCode = twitchOAuth2Client();
cookieParser()(req, res, () => {
const state = req.cookies.__session || crypto.randomBytes(20).toString('hex');
console.log('Setting verification state:', state);
res.cookie('__session', state.toString(), { maxAge: 3600000, httpOnly: true });
const redirectUri = authorizationCode.authorizeURL({
redirect_uri: OAUTH_REDIRECT_URI,
scope: OAUTH_SCOPES,
state: state,
});
console.log('Redirecting to:', redirectUri);
res.redirect(redirectUri);
});
});
/**
* Exchanges a given Twitch auth code passed in the 'code' URL query parameter for a Firebase auth token.
* The request also needs to specify a 'state' query parameter which will be checked against the 'state' cookie.
* The Firebase custom auth token, display name, photo URL and Twitch acces token are sent back in a JSONP callback
* function with function name defined by the 'callback' query parameter.
*/
exports.token = functions.https.onRequest((req, res) => {
const authorizationCode = twitchOAuth2Client();
try {
cookieParser()(req, res, async () => {
try {
console.log('Received verification state:', req.cookies.__session);
console.log('Received state:', req.query.state);
if (!req.cookies.__session) {
throw new Error(
'State cookie not set or expired. Maybe you took too long to authorize. Please try again.'
);
} else if (req.cookies.__session !== req.query.state) {
throw new Error('State validation failed');
}
} catch (error) {
return res.jsonp({ error: error.toString() });
}
let accessToken;
try {
console.log('Received auth code:', req.query.code);
const options = {
client_id: functions.config().twitch.client_id,
client_secret: functions.config().twitch.client_secret,
code: req.query.code,
grant_type: 'authorization_code',
redirect_uri: OAUTH_REDIRECT_URI,
};
console.log('Asking token with options', JSON.stringify(options));
accessToken = await authorizationCode.getToken(options);
console.log('Auth code exchange result received');
const twitchUser = await getTwitchUser(accessToken.toJSON().access_token);
// Create a Firebase account and get the Custom Auth Token.
const firebaseToken = await createFirebaseAccount(twitchUser);
// Serve an HTML page that signs the user in and updates the user profile.
return res.jsonp({ token: firebaseToken });
} catch (error) {
return res.jsonp({ error: error.toString() });
}
});
} catch (error) {
return res.jsonp({ error: error.toString() });
}
});
/**
* Creates a Firebase account with the given user profile and returns a custom auth token allowing
* signing-in this account.
*
* #returns {Promise<string>} The Firebase custom auth token in a promise.
*/
async function createFirebaseAccount(twitchUser) {
// The UID we'll assign to the user.
const uid = `twitch:${twitchUser.id}`;
// Save the access token to the Firebase Database.
const db = admin.firestore();
const databaseTask = db.collection('users').doc(uid).set(twitchUser);
// Create or update the user account.
const userCreationTask = admin
.auth()
.updateUser(uid, {
displayName: twitchUser['display_name'],
photoURL: twitchUser['profile_image_url'],
email: twitchUser['email'],
})
.catch((error) => {
// If user does not exists we create it.
if (error.code === 'auth/user-not-found') {
return admin.auth().createUser({
uid: uid,
displayName: twitchUser['display_name'],
photoURL: twitchUser['profile_image_url'],
email: twitchUser['email'],
});
}
throw error;
});
// Wait for all async task to complete then generate and return a custom auth token.
await Promise.all([userCreationTask, databaseTask]);
// Create a Firebase custom auth token.
const token = await admin.auth().createCustomToken(uid);
console.log('Created Custom token for UID "', uid, '" Token:', token);
return token;
}
async function getTwitchUser(accessToken) {
console.log('Fetching Twitch user with access_token', accessToken);
try {
const response = await fetch('https://api.twitch.tv/helix/users', {
method: 'GET',
headers: {
'Client-Id': functions.config().twitch.client_id,
Authorization: 'Bearer ' + accessToken,
},
});
const data = await response.json();
return { ...data.data[0], access_token: accessToken };
} catch (error) {
console.error(error);
}
}
I'd like, though, to login into Twitch using the firebase.auth().signInWithRedirect() method that I already use for Facebook and Google, unfortunately I can't find any documentation about this, and the Facebook provider source code refers to some externs.* resources so I'm not sure how to adapt it for my own needs.
Right now I have two endpoints/cloud functions: _twitchRedirect and _twitchToken, what should I do to integrate them with signInWithRedirect?
I was similarly curious, so spent a little time playing around with things today.
In short, when using Firebase Auth, I believe the providerId will need to be one of the existing supported providers.
If you upgrade to using the Google Cloud Identity Platform though, I believe you will be able to configure custom providers, and then use this function to authenticate:
https://cloud.google.com/identity-platform
We can see that firebase.auth.OAuthProvider and firebase.auth().signInWithPopup (or firebase.auth().signInWithRedirect) are used with a number of the providers here, eg.
https://cloud.google.com/identity-platform/docs/web/apple
https://cloud.google.com/identity-platform/docs/web/microsoft
In addition to these provider choices that we get with the standard Firebase Auth, Google Cloud Identity Platform allows us to also add SAML and OpenID Connect (OIDC) integrations:
https://cloud.google.com/identity-platform/docs/web/saml
https://cloud.google.com/identity-platform/docs/web/oidc
When adding a new identity provider using either of these, we are able to specify the 'Provider ID' to use (prefixed with either saml. or oidc.). This custom provider ID is then used with firebase.auth.OAuthProvider and firebase.auth().signInWithPopup (or firebase.auth().signInWithRedirect) as described above.
For example, if I created a new identity provider with an ID of oidc.foo, my integration code would end up looking like:
const provider = new firebase.auth.OAuthProvider('oidc.foo');
firebase.auth().signInWithPopup(provider)
.then((result) => {
// result.credential is a firebase.auth.OAuthCredential object.
// result.credential.providerId is equal to 'oidc.foo'.
// result.credential.idToken is the OIDC provider's ID token.
})
.catch((error) => {
// Handle error.
});
Based on my understanding of this, I believe we will only currently be able to add custom providers this way if they conform to the OpenID Connect (OIDC) standard (including the OIDC Discovery part, which uses a /.well-known/openid-configuration URL):
Note: If your OIDC provider doesn't comply with the OIDC specification for discovery, it won't work with Identity Platform.
So to my knowledge, the best way to implement 'normal' OAuth2 providers currently is the custom backend function flow you used above (based on the Firebase Auth examples).
As part of figuring this out, I decided to see what would happen if I used a provider ID that didn't match anything configured in my account (this is a fairly verbose step by step, and the main answer is already included above, but this may help provide some more context/help someone out, so including it here)
var provider = new firebase.auth.OAuthProvider("foo.example.com");
firebase
.auth()
.signInWithRedirect(provider)
.then((result) => console.log("OAuthProvider:", result))
.catch((error) => console.log("OAuthProvider::error:", error));
firebase
.auth()
.getRedirectResult()
.then((result) => console.log("RedirectResult:", result))
.catch((error) => console.log("RedirectResult::error:", error));
At first I go this auth/auth-domain-config-required error:
OAuthProvider::error: {
"code": "auth/auth-domain-config-required",
"message": "Be sure to include authDomain when calling firebase.initializeApp(), by following the instructions in the Firebase console."
}
I figured maybe this should be set to the OAuth provider I was wanting to login to, so I set authDomain in my firebase config to foo.myauthprovider.com, but when I called signInWithRedirect, it tried to load the following URL (where the apiKey is the API key of my firebase project), which failed to load:
https://foo.myauthprovider.com/__/auth/handler?apiKey=REDACTED&appName=%5BDEFAULT%5D&authType=signInViaRedirect&providerId=foo.example.com&redirectUrl=http%3A%2F%2Flocalhost%3A3000%2F&v=7.14.5
This /__/auth/handler URL is part of Firebase Auth's reserved URLs, which you can read more about at:
https://firebase.google.com/docs/hosting/reserved-urls#auth_helpers
And is explained a little better in this StackOverflow answer, but is basically what Firebase Auth uses to handle OAuth callbacks to avoid needing to expose sensitive credentials on the frontend, and so users don't need to implement their own handlers all the time):
Why does Firebase auth uses a "middleware" redirect before returning to my app?
Changing authDomain to the actual custom domain of my firebase project fixed that issue, and then resulted in the following auth/operation-not-allowed error when I tried to redirect:
RedirectResult::error: u {code: "auth/operation-not-allowed", message: "The identity provider configuration is not found.", a: null}

How to get users from Firebase auth based on custom claims?

I'm beginning to use custom claims in my Firebase project to implement a role-based authorization system to my app.
I'll have a firebase-admin script which is going to set {admin: true} for a specific user's uid. This will help me write better and clearer Firestore security rules.
admin.auth().setCustomUserClaims(uid, {admin: true})
So far, so good. My problem is that I'll also need a dashboard page to let me know which users are currently admins inside my app.
Basically I'll need a way to query/list users based on custom claims. Is there a way to do this?
From this answer, I can see that it's not possible to do this.
But maybe, Is there at least a way to inspect (using Firebase Console) the customUserClaims that were set to a specific user?
My current solution would be to store that information (the admins uid's) inside an admin-users collection in my Firestore and keep that information up-to-date with the any admin customClaims that I set or revoke. Can you think of a better solution?
I solved this use case recently, by duplicating the custom claims as "roles" array field into the according firestore 'users/{uid}/private-user/{data}' documents. In my scenario I had to distinguish between two roles ("admin" and "superadmin"). The documents of the firestore 'users/' collection are public, and the documents of the 'users/{uid}/private-user/' collection are only accessible from the client side by the owning user and "superadmin" users, or via the firestore Admin SDK (server side) also only as "superadmin" user.
Additionally, I only wanted to allow "superadmin" users to add or remove "superadmin" or "admin" roles/claims; or to get a list of "superadmin" or "admin" users.
Data duplication is quite common in the NoSQL world, and is NOT considered as a bad practice.
Here is my code (Node.js/TypeScript)
First, the firebase cloud function implementation (requires Admin SDK) to add a custom user claim/role.
Note, that the "superadmin" validation line
await validateUserClaim(functionName, context, "superadmin")
must be removed until at least one "superadmin" has been created that can be used later on to add or remove additional roles/claims to users!
const functionName = "add-admin-user"
export default async (
payload: string,
context: CallableContext,
): Promise<void> => {
try {
validateAuthentication(functionName, context)
validateEmailVerified(functionName, context)
await validateUserClaim(functionName, context, "superadmin")
const request = parseRequestPayload<AddAdminUserRoleRequest>(
functionName,
payload,
)
// Note, to remove a custom claim just use "{ [request.roleName]: null }"
// as second input parameter.
await admin
.auth()
.setCustomUserClaims(request.uid, { [request.roleName]: true })
const userDoc = await db
.collection(`users/${request.uid}/private-user`)
.doc("data")
.get()
const roles = userDoc.data()?.roles ?? []
if (roles.indexOf(request.roleName) === -1) {
roles.push(request.roleName)
db.collection(`users/${request.uid}/private-user`)
.doc("data")
.set({ roles }, { merge: true })
}
} catch (e) {
throw logAndReturnHttpsError(
"internal",
`Firestore ${functionName} not executed. Failed to add 'admin' or ` +
`'superadmin' claim to user. (${(<Error>e)?.message})`,
`${functionName}/internal`,
e,
)
}
}
Second, the firebase cloud function implementation (requires Admin SDK) that returns a list of "superadmin" or "admin" users.
const functionName = "get-admin-users"
export default async (
payload: string,
context: CallableContext,
): Promise<GetAdminUsersResponse> => {
try {
validateAuthentication(functionName, context)
validateEmailVerified(functionName, context)
await validateUserClaim(functionName, context, "superadmin")
const request = parseRequestPayload<GetAdminUsersRequest>(
functionName,
payload,
)
const adminUserDocs = await db
.collectionGroup("private-user")
.where("roles", "array-contains", request.roleName)
.get()
const admins = adminUserDocs.docs.map((doc) => {
return {
uid: doc.data().uid,
username: doc.data().username,
email: doc.data().email,
roleName: request.roleName,
}
})
return { admins }
} catch (e) {
throw logAndReturnHttpsError(
"internal",
`Firestore ${functionName} not executed. Failed to query admin users. (${
(<Error>e)?.message
})`,
`${functionName}/internal`,
e,
)
}
}
And third, the validation helper functions (require the Admin SDK).
export type AdminRoles = "admin" | "superadmin"
export const validateAuthentication = (
functionName: string,
context: CallableContext,
): void => {
if (!context.auth || !context.auth?.uid) {
throw logAndReturnHttpsError(
"unauthenticated",
`Firestore ${functionName} not executed. User not authenticated.`,
`${functionName}/unauthenticated`,
)
}
}
export const validateUserClaim = async (
functionName: string,
context: CallableContext,
roleName: AdminRoles,
): Promise<void> => {
if (context.auth?.uid) {
const hasRole = await admin
.auth()
.getUser(context.auth?.uid)
.then((userRecord) => {
return !!userRecord.customClaims?.[roleName]
})
if (hasRole) {
return
}
}
throw logAndReturnHttpsError(
"unauthenticated",
`Firestore ${functionName} not executed. User not authenticated as ` +
`'${roleName}'. `,
`${functionName}/unauthenticated`,
)
}
export const validateEmailVerified = async (
functionName: string,
context: CallableContext,
): Promise<void> => {
if (context.auth?.uid) {
const userRecord = await auth.getUser(context.auth?.uid)
if (!userRecord.emailVerified) {
throw logAndReturnHttpsError(
"unauthenticated",
`Firestore ${functionName} not executed. Email is not verified.`,
`${functionName}/email-not-verified`,
)
}
}
}
Finally, custom claims can be added or removed only on the server side as the according "setCustomUserClaims" function belong to the firebase Admin SDK, whereas the "get-admin-users" function could be implemented also on the client side. Here and here you will find more information about custom claims, including firestore rules for client side queries protected by a custom user claim/role.

Generating a custom auth token with a cloud function for firebase using the new 1.0 SDK

As of firebase-admin#5.11.0 and firebase-functions#1.0.0 firebase-admin no longer takes in an application config when the app initializes.
I had a firestore function that would generate a custom token using firebase-admin’s createCustomToken. Calling that function would generate a credential that I would pass into initializeApp in the credential attribute. How would I go about doing that now?
Do I need to edit process.env.FIREBASE_CONFIG somehow and put the serialized credential there before calling initializeApp?
Based on this issue in Github, it still works.
https://github.com/firebase/firebase-admin-node/issues/224
The following example worked for me:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const serviceAccount = require('./serviceAccountKey.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: 'https://yourapplication.firebaseio.com/'
});
exports.createToken = functions.https.onCall((data, context) => {
const uid = context.auth.uid;
return admin.auth()
.createCustomToken(uid)
.then(customToken => {
console.log(`The customToken is: ${customToken}`);
return {status: 'success', customToken: customToken};
})
.catch(error => {
console.error(`Something happened buddy: ${error}`)
return {status: 'error'};
});
});
Michael Chen's cloud function appears to trigger from a HTTP request from somewhere (an external server?). My employee wrote a cloud function that triggers when the user logs in:
// this watches for any updates to the user document in the User's collection (not subcollections)
exports.userLogin = functions.firestore.document('Users/{userID}').onUpdate((change, context) => {
// save the userID ubtained from the wildcard match, which gets put into context.params
let uid = context.params.userID;
// initialize basic values for custom claims
let trusted = false;
let teaches = [];
// check the Trusted_Users doc
admin.firestore().collection('Users').doc('Trusted_Users').get()
.then(function(doc) {
if (doc.data().UIDs.includes(uid)) {
// if the userID is in the UIDs array of the document, set trusted to true.
trusted = true;
}
// Get docs for each language in our dictionary
admin.firestore().collection('Dictionaries').get()
.then(function(docs) {
// for each of those language docs
docs.forEach(function(doc) {
// check if the userID is included in the trustedUIDs array in the doc
if (doc.data().trustedUIDs.includes(uid)) {
// if it is, we push the 2-letter language abbreviation onto the array of what languages this user teaches
teaches.push(doc.data().shortLanguage);
}
});
// finally, set custom claims as we've parsed
admin.auth().setCustomUserClaims(uid, {'trusted': trusted, 'teaches': teaches}).then(() => {
console.log("custom claims set.");
});
});
});
});
First, we put in a lastLogin property on the user object, which runs Date.now when a user logs in and writes the time to the database location, triggering the cloud function.
Next, we get the userID from the cloud function response context.params.userID.
Two variables are then initialized. We assume that the user is not trusted until proven otherwise. The other variable is an array of subjects the user teaches. In a roles-based data security system, these are the collections that the user is allowed to access.
Next, we access a document listing the userIDs of trusted users. We then check if the recently logged in userID is in this array. If so, we set trusted to true.
Next, we go to the database and traverse a collection Dictionaries whose documents include arrays of trusted userIDs (i.e., users allowed to read and write those documents). If the user is in one or more of these arrays, he or she gets that document added to the teaches property on his or her user data, giving the user access to that document.
Finally, we're ready to run setCustomUserClaims to customize the token claims.
Here's a variation for a Callable Cloud Function, thanks to Thomas's answer
Once the custom claim is set, you can access the field in/from .. say, a firebase storage rule.
For example:
allow write: if request.auth.token.isAppAdmin == true;
With a Callable Cloud Function, as long as the admin.auth().setCustomUserClaims(..) function is returned somewhere along the promise chain, the claim field will be added to the request.auth.token object:
const functions = require('firebase-functions');
exports.setIsAdminClaim = functions.https.onCall((data, context) => {
var uid = context.auth.uid;
return admin.auth().setCustomUserClaims(
uid, {
isAppAdmin: true
}
)
.then(() => {
var msg = 'isAppAdmin custom claim set';
console.log(msg);
return new Promise(function (resolve, reject) {
var resolveObject = {
message : msg
};
resolve(resolveObject);
});
});
});

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