Dynamic Google Cloud Firestore rules on document ID - firebase

Thanks for taking the time to read!
What I'm trying to do is dynamically compare the requested document ID against another string saved in the custom claims of a user, using Google Firestore.
I've added the code below (removed the unimportant rules for this question) which from reading the docs seems to be correct, but when I try and use these rules it always returns to false. In fact, when I compare orgId to a string of the document like orgId == 'T90101' that also returns false.
What am I missing?
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Organization ID saved on the custom claims for this user
function getMyOrgId() {
return request.auth.token.organizationId
}
// BUG: This equates to false but should be true
function verifyOrganizationClaim(orgId) {
return getMyOrgId() == orgId;
}
match /organizations/{orgId} {
allow create: if false;
allow delete: if false;
allow update: if verifyOrganizationClaim(orgId);
allow read: if verifyOrganizationClaim(orgId);
}
}
}
In summary - how do I compare a document ID against a saved custom claim ID, to restrict access to documents?
Any help would be appreciated!
Here's the code I'm using to access this data on the client side using React + TypeScript. The error I get is Uncaught (in promise) FirebaseError: Missing or insufficient permissions.
import React from "react";
import firebase from "firebase";
import { useDocumentDataOnce } from "react-firebase-hooks/firestore";
export const firestore = firebase.firestore();
const MY_ORG_ID = "T01JLKU9W7L"; // This is the same value saved in the custom claims for this user
const EXTERNAL_ORG_ID = "T01JLKU9W7L";
interface Organization {
id: string;
displayName: string;
}
const Example: React.FC = () => {
const [myOrg] = useDocumentDataOnce<Organization>(
firestore.doc(`organizations/${MY_ORG_ID}`)
);
const [externalOrg] = useDocumentDataOnce<Organization>(
firestore.doc(`organizations/${EXTERNAL_ORG_ID}`)
);
return (
<>
{/** Should succeed */}
<div>
<h1>My Organization</h1>
<p>Organization Name: {myOrg?.displayName}</p>
<p>Organization Id: {myOrg?.id}</p>
</div>
{/** Should fail */}
<div>
<h1>Other Organization</h1>
<p>Organization Name: {externalOrg?.displayName}</p>
<p>Organization Id: {externalOrg?.id}</p>
</div>
</>
);
};
export default Example;

I've tried your Security Rules in one of my test environment and I can confirm that the if verifyOrganizationClaim(orgId); statement does work (together with the two functions).
So I can see three possible reasons why it does not work on your side:
Your user is not authenticated
You incorrectly assigned the Custom Claim to the user. You can check the custom claims with the following CLI command: $ firebase auth:export users.csv
You try to create a document, instead of updating or reading it. As a matter of fact your rules only set access rights for updating or reading. (I'm not verse in React and I cannot deduce from the code in your question which operation you execute...)

Related

Detecting new user signup in [...nextauth].js

I'm building an app with NextAuth's email provider as the sole means of user registration and login.
In local development everything worked fine, but when deploying the app to Vercel I keep getting Unhandled Promise Rejection errors from Mongo, so I decided to switch to NextAuth's unstable_getServerSession and everything is running very smoothly now.
Before making the switch I used an extra parameter to communicate whether the user was just logging in or newly signing up. Based on this, I sent different emails (a welcome email with login link vs. a shorter message with just the login link).
Here is how the [...nextauth].js file looked before:
export default async function auth(req, res) {
const signup = req.body.signup || false
return await NextAuth(req, res, {
adapter: MongoDBAdapter(clientPromise),
...
providers: [
EmailProvider({
...
sendVerificationRequest(server) {
customLogin(
server.identifier,
server.url,
server.provider, // passing the above data for email content
signup // custom parameter to decide which email to send
)
}
})
],
...
})
}
I adjusted the code sample from the documentation, but wasn't able to pass a custom parameter to it.
Here is my new code:
export const authOptions = {
adapter: MongoDBAdapter(promise),
...
providers: [
EmailProvider({
...
sendVerificationRequest(server) {
customLogin(
server.identifier,
server.url,
server.provider,
)
}
})
],
...
}
export default NextAuth(authOptions)
The customLogin function doesn't do anything except construction the different email options.
Among the things I have tried are wrapping authOptions in a handler function with req and res, setting up a separate API route, and passing parameters to the NextAuth function, but none of those worked.
Any advise on how to implement a custom parameter here would be highly appreciated.

NuxtJS store returning local storage values as undefined

I have a nuxt application. One of the components in it's mounted lifecycle hook is requesting a value from the state store, this value is retrieved from local storage. The values exist in local storage however the store returns it as undefined. If I render the values in the ui with {{value}}
they show. So it appears that in the moment that the code runs, the value is undefined.
index.js (store):
export const state = () => ({
token: process.browser ? localStorage.getItem("token") : undefined,
user_id: process.browser ? localStorage.getItem("user_id") : undefined,
...
Component.vue
mounted hook:
I'm using UserSerivce.getFromStorage to get the value directly from localStorage as otherwise this code block won't run. It's a temporary thing to illustrate the problem.
async mounted() {
// check again with new code.
if (UserService.getFromStorage("token")) {
console.log("user service found a token but what about store?")
console.log(this.$store.state.token, this.$store.state.user_id);
const values = await ["token", "user_id"].map(key => {return UserService.getFromStorage(key)});
console.log({values});
SocketService.trackSession(this, socket, "connect")
}
}
BeforeMount hook:
isLoggedIn just checks that the "token" property is set in the store state.
return !!this.$store.state.token
beforeMount () {
if (this.isLoggedIn) {
// This runs sometimes??? 80% of the time.
console.log("IS THIS CLAUSE RUNNING?");
}
}
video explanation: https://www.loom.com/share/541ed2f77d3f46eeb5c2436f761442f4
OP's app is quite big from what it looks, so finding the exact reason is kinda difficult.
Meanwhile, setting ssr: false fixed the errors.
It raised more, but they should probably be asked into another question nonetheless.

firebase onSnapshot gets update before create is complete

I have a "post" that listens to changes on its comments in react like so:
// React hook state
const [comments, setComments] = useState([])
// My listener in useEffect
db.collection(`users/${userId}/posts/${postId}/comments`)
.onSnapshot((querySnapshot) => {
let newComments = []
querySnapshot.forEach(function (doc) {
newComments.push({
id: doc.id,
...doc.data()
})
})
setComments(newComments)
})
When the user creates a new comments, I set a loading state and disable the comment section
// React hook
const [isLoading, setLoading] = useState(false)
// Add comment
const addComment = () => {
const comment = {text:"hello"}
setSaving(true)
db.collection(`users/${postUid}/posts/${postId}/comments`).doc()
.set(comment)
.then(()=>{
setSaving(false)
})
}
My problem is (a good problem to have), the subscription onSnapshot gets the new comment before my addComment callback is completed, creating some visual issues:
- Makes the app look buggy when the comment input is still loading but the comment already there
- If there is an error (ex: database permission issue), the comment shows up in the list and then disappears...
Any idea what I can change to not have the onSnapshot update before the create is done?
As explained here in the doc:
Local writes in your app will invoke snapshot listeners immediately.
This is because of an important feature called "latency compensation."
When you perform a write, your listeners will be notified with the new
data before the data is sent to the backend.
Retrieved documents have a metadata.hasPendingWrites property that
indicates whether the document has local changes that haven't been
written to the backend yet.
See also the following remark in the "Listen to multiple documents in a collection" section:
As explained above under Events for local changes, you will receive
events immediately for your local writes. Your listener can use the
metadata.hasPendingWrites field on each document to determine whether
the document has local changes that have not yet been written to the
backend.
So you can use this property to display the change only if it has been written to the back-end, something along the following lines (untested):
db.collection(`users/${userId}/posts/${postId}/comments`)
.onSnapshot((querySnapshot) => {
let newComments = []
querySnapshot.forEach(function (doc) {
if (!doc.metadata.hasPendingWrites) {
newComments.push({
id: doc.id,
...doc.data()
})
}
})
setComments(newComments)
})

Vue Firebase / Firestore Duplicates

I am trying to go through a tutorial (link below) to learn vue and firebase. There is a main dashboard page with a list of components, and I have gotten that to display a list of employees. Then there is a view employee component. When I started to build that, and just loaded data, I started getting this error:
Uncaught FirebaseError {code: "app/duplicate-app", message: "Firebase:
Firebase App named '[DEFAULT]' already exists (app/duplicate-app).",
name: "[DEFAULT]", stack: "[DEFAULT]: Firebase: Firebase App named
'[DEFAULT]…0)↵ at fn (http://localhost:8081/app.js:89:20)"}
The firebase code I added to view employee is as follows:
import db from "./firebaseInit.js";
export default {
name: "view-employee",
data() {
return {
employee_id: null,
name: null,
dept: null,
position: null
};
},
beforeRouteEnter(to, from, next) {
db
.collection("employees")
.where("employee_id", "==", to.params.employee_id),
get().then(querySnapShot => {
querySnapShot.forEach(doc => {
next(vm => {
vm.employee_id = doc.data().employee_id
vm.name = doc.data().name
vm.dept = doc.data().dept
vm.position = doc.data().position
})
});
});
}
};
When I comment out this script on the view employee page, the error goes away. From what I can tell, I have done everything the same as the tutorial in the video, and as my buddy who did the same project.
There is also a warning, which may be related, which states as follows:
There are multiple modules with names that only differ in casing. This
can lead to unexpected behavior when compiling on a filesystem with
other case-semantic. Use equal casing. Compare these module
identifiers: *
/Users/jdurell/code/employeemanager/node_modules/babel-loader/lib/index.js!/Users/jdurell/code/employeemanager/src/components/FirebaseInit.js
I am working on this tutorial / project:
https://www.youtube.com/watch?v=cjEzK4me1k8&index=4&list=PLillGF-RfqbYsOOycB67Raf9dwmL6Y31M
Nemesv had the correct answer. It was a casing issue. I had the issue FirebaseInit on the other component. I changed that to firebaseInit so it was the same case on both components, and the error resolved. Thanks!

How to check Item exists in Firebase Database? - react-native-firebase

Currently, I am using https://github.com/invertase/react-native-firebase for my project. I have a custom database for users and I want to check if the user exists or not by email.
Here is a screenshot of the database:
Here's a generic firebase method but you may need to reconfigure the method to suit your data structure. Please refer to the official docs if you wish to know more.
firebase.database()
.ref(`/users`)
.orderByChild("email")
.equalTo(email)
.once("value")
.then(snapshot => {
if (snapshot.val()) {
// data exist, do something else
}
})
You can also query the registration status with hasChild method. Refer to your root path and query with .once and check the result returned.
export function checkUserExist(email) {
return(dispatch) => {
firebase.database().ref(`/ExistingUser/`)
.once('value', snapshot => {
if(snapshot.hasChild(email)) {
dispatch({
type: FIREBASE_USER_EXISTED
});
} else {
dispatch({
type: FIREBASE_USER_NOT_EXISTED,
});
}
});
}
}
Another preferred method would be using the fetchProvidersForEmail method provided by Firebase. It takes an email and returns a promise that resolves with the list of providers linked to that email if it is already registered, refer here.
Is there a good reason to store users credential in your database? In my daily practice, I would use the createUserWithEmailAndPassword provided by Firebase for security purposes, refer here. Just make sure rules are defined properly to prevent unauthorized access.

Resources