How to hide information in Firebase? - firebase

I have these requirements:
User will log in via email and multiple Oauth providers. I use function like User.findByEmail('1#1.com'). So I need to have permision to see the list of users before being authenticated.
User's email address, geolocation and age should be kept secretly from other users.
My first plan was:
users:{
$user-id:{
// public
".read" : true,
name,
created_at,
private: {
".read" : "auth.email === data.child('email').val()",
age,
email,
geolocation,
}
$piority = email
}
}
Then I realized no, it just doesn't work that way. So would anyone please tell me how to do it correctly? Thanks in advance.
P.S. Firebase really needs a filter/serialize method.

There are really several questions in this post. The first is how to store private data. One simple change you can make is to invert the public/private and user keys--they don't have to be nested in the same user record.
/users/$user_id/...public data...
/private/$user_id/...private data...
This makes securing the data quite a bit simpler.
Another question is regarding duplicate email detection. If we assume you are using simple login here, this is all pretty moot. You can check to see if an email address exists by simply trying to create the account. An error will be returned if the email is already registered.
If that doesn't solve it, you can still check this manually, without providing a list of users' emails. This is typically done with an index. When a new account is created, write something like this:
/email_index/$escaped_email/$userid ($userid is the value)
Now when you want to check if the email is available, you do a read like this:
var ref = new Firebase(URL);
function checkEmail(emailAddress, callback) {
ref.child('email_index/'+escapeEmail(emailAddress)).once('value', function(snap) {
callback(snap.val() !== null);
});
}
function escapeEmail(emailAddress) {
return (email || '').replace('.', ',');
}
checkEmail(EMAIL_ADDRESS, function(exists) {
console.log(EMAIL_ADDRESS + (exists? 'does' : 'does not') + ' exist!');
});
To prevent someone from listing the emails, you do something like this in your (amazingly flexible and really quite sophisticated, even for enterprise apps) security rules:
"email_index": {
// no .read rule here means that the data cannot be listed; I have to know the email address to check it
"$email_address": {
".read": true,
// it can only be claimed once and the value must be my user id
".write": "auth.uid === newData.val() && !data.exists()"
}
}

Related

Firestore rules and query for document map with email keys to share data with users

Use Case Summary
User A creates a story
User A shares story with unknown (to the app) User B via email (sent via cloud function)
User B receives an email about the story
User B visits app and creates a new account
User B sees/reads story create by User A
Note: stories can only be seen by whom they been shared with or created by
I'm building a role based access system. I've been looking at the role based access firestore documentation and I'm missing one piece.
Consider a story that can only be read by a user for which that story has been shared. Most examples including the firestore example use the UID has the key to identify shared users. However, that user may not currently be a user of the firebase app additionally how does a user assign that UID.
Story Data
{
title: "A Great Story",
roles: {
aliceUID: {
hasRole: true,
type: "owner",
},
bobUID: {
hasRole: true,
type: "reader",
}
}
}
Story Query
firebase.firestore().collection('stories').where(`roles.${user.uid}.hasRole`, '==', true)
The second part could potentially be solved by maintaining a separate user collection then you could find the user from their email address, but this doesn't address users that have never logged in.
The user who intends to share a story could add the user with an email address. Then using firebase functions we could send an email to notify the user of the shared story and the user could login to the app and read that story.
If we proceed with this method then you would not have a UID but only an email address as the key.
Story Data
{
title: "A Great Story",
roles: {
alice#yahoo.com: {
hasRole: true,
type: "owner",
},
bob#gmail.com: {
hasRole: true,
type: "reader",
}
}
}
Story Query
firebase.firestore().collection('stories').where(`roles.${user.email}.hasRole`, '==', true)
Updated Firestore Rule - from documentation
function getRole(rsc) {
// Read from the "roles" map in the story document.
return rsc.data.roles[request.auth.uid] || rsc.data.roles[request.auth.token.email];
}
I can not get the email query to work. This SO issue mentions that
Unfortunately dots are not allowed as a map key. So email addresses won't work.
I don't see why this would be a conflict on the rules side. It does make for a likely invalid where clause
e.g.
.where(`roles.${user.email}.hasRole`, '==', true) -> .where(`roles.bob#gmail.com.hasRole`, '==', true)
That looks like invalid JS and unfortunately [ and ] are invalid characters so we can't do
.where(`roles[${user.email}]hasRole`, '==', true)
The final thing I've seen is using for this Firebase talk is to escape the email address using something like
function encodeAsFirebaseKey(string) {
return string.replace(/\%/g, '%25')
.replace(/\./g, '%2E')
.replace(/\#/g, '%23')
.replace(/\$/g, '%24')
.replace(/\//g, '%2F')
.replace(/\[/g, '%5B')
.replace(/\]/g, '%5D');
};
This appears to fix the query where clause and it's a valid data structure, but it's not a valid Firestore rule meaning it has no true security enforcement.
Any ideas on how to implement this? Please include valid data structure, firestore rules, and query. I've shown and seen many examples that get two out of the three which are all non-working solutions.
Thanks!
The basic issue was that I did not know how to properly formulate a valid query. It turns out that you don't need to create a query in one line.
You can use FieldPath to construct your query parameter.
var path = new firebase.firestore.FieldPath('roles', email ,'hasRole');
firebase.firestore().collection('stories').where(path, '==', true)
This solves for the original missing piece.
This is a use case for Control Access with Custom Claims and Security Rules.
The Firebase Admin SDK supports defining custom attributes on user
accounts. This provides the ability to implement various access
control strategies, including role-based access control, in Firebase
apps. These custom attributes can give users different levels of
access (roles), which are enforced in an application's security rules.
User roles can be defined for the following common cases:
Giving a user administrative privileges to access data and resources.
Defining different groups that a user belongs to.
Providing multi-level access:
Differentiating paid/unpaid subscribers.
Differentiating moderators from regular users.
Teacher/student application, etc.
You'll need to stand up a node server (skill level low). A script like below works to generate the claims.
var admin = require('firebase-admin');
var serviceAccount = require("./blah-blah-blah.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://my-app.firebaseio.com"
});
admin.auth().setCustomUserClaims("9mB3asdfrw34ersdgtCk1", {admin: true}).then(() => {
console.log("Custom Claim Added to UID. You can stop this app now.");
});
Then on your client side, do something like:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
//is email address up to date? //do we really want to modify it or mess w it?
switch (user.providerData[0].providerId) {
case 'facebook':
case 'github':
case 'google':
case 'twitter':
break;
case 'password':
// if (!verifiedUser) {
// }
break;
}
//if admin
firebase.auth().currentUser.getIdToken().then((idToken) => {
// Parse the ID token.
const payload = JSON.parse(window.atob(idToken.split('.')[1]));
// Confirm the user is an Admin or whatever
if (!!payload['admin']) {
switch (thisPage) {
case "/admin":
showAdminStuff();
break;
}
}
else {
if(isAdminPage()){
document.location.href="/";
}
}
})
.catch((error) => {
console.log(error);
});
}
else {
//USER IS NOT SIGNED IN
}
});
From what I have gathered, you want to make a story private but shareable with anyone. Your biggest concern is for users who do not have the app but have the share link.
And therefore your biggest problem is that the way firebase works means that you cant limit access to your data without using some sort of login.
If you are ok with requiring new users to login, then your answer should just be Dynamic Links. These links are persistent all the way though installation and login which means that anyone can be given a dynamic link that has story access data attached. You would merely need to add a listener to your app's mainActivity or AppDelegate equivalent to record the dynamic link data and run a specif task after login.
If you wish to stay away from the login completely, then you set up the dynamic link to bypass the login process and direct the new-install-user directly to the story. This second option however, requires a bit more work and is less secure because you will probably be forced to duplicate the story data for open access to anyone with the proper link to the story.

How to check if users owns the object in Firebase bolt compiler

Supposing I have a simple schema with two collections users & posts. Each post object has a key, value pair (ownerId:userId) to find out which users owns the posts object.
users/{1,2,3...}
posts/{a,b,c...}/ownerId:userId
I am trying to write the rules where user can only read/write his user data and his posts.
For this the bolt rules for the user is quite straightforward:
isUser(uid) = auth != null && auth.uid == uid;
path /users/$uid {
read() = isUser($uid);
write() = isUser($uid);
}
My Question is how can I secure the posts collection to be only accessed by the user. Can I check the ownderId property of the posts collection in rules? If so how, If not they how can I structure my schema to do it?
EDIT
I am trying to secure the posts path like this:
path /posts/$pid {
read() = isUser(this.ownerId);
write() = isUser(this.ownerId);
}
Is this the correct way to do this?
We can do this by adding a owner Id property to the collection and then checking if the user is authenticated.
path /posts/$pid {
read() = isUser(this.ownerId);
write() = isUser(this.ownerId);
}

Meteor: How to assign different roles to users during sign up process

I am using the meteor package ian:accounts-ui-bootstrap-3 for accounts and alanning:roles for assigning roles.
On the sign up form I have two options one for Doctor and one for Advisor. I want to assign the selected option as a role to that user. Can someone let me know how to do this?
I have just started learning meteor and don't know much about its flow. I can assign roles to a user if I create the user manually like this:
var adminUser = Meteor.users.findOne({roles:{$in:["admin"]}});
if(!adminUser){
adminUser = Accounts.createUser({
email: "mohsin.rafi#mail.com",
password: "admin",
profile: { name: "admin" }
});
Roles.addUsersToRoles(adminUser, [ROLES.Admin]);
}
But I want to assign a roll automatically as a user signs up and select one of the options and that option should be assigned as his role.
You shouldn't need a hack for this. Instead of using Accounts.onCreateUser you can do it with the following hook on the Meteor.users collection. Something along the lines of the following should work:
Meteor.users.after.insert(function (userId, doc) {
if (doc.profile.type === "doctor") {
Roles.addUsersToRoles(doc._id, [ROLES.Doctor])
} else if (doc.profile.type === "advisor") {
Roles.addUsersToRoles(doc._id, [ROLES.Advisor])
}
});
To get around having to check on login every time it's possible to directly set the roles on the user object instead of using the Roles API.
A hack? Yep, you probably need to make sure the roles have already been added to roles... not sure if there's anything else yet.
if(Meteor.isServer){
Accounts.onCreateUser(function(options, user){
if(options.roles){
_.set(user, 'roles.__global_roles__', ['coach', options.roles]);
}
return user;
});
}
Note: _.set is a lodash method not in underscorejs.
There's no pretty solution because:
There's no server side meteor callback post complete account creation.
In onCreateUser the user hasn't been added to the collection.
Accounts.createUser's callback is currently only for use on the client. A method could then be used from that callback but it would be insecure to rely on it.
The roles package seems to grab the user from the collection and in onCreateUser it's not there yet.
you can use the Accounts.onCreateUser hook to manage that.
Please keep in mind the code below is fairly insecure and you would probably want to do more checking beforehand, otherwise anyone can assign themselves admin. (from docs):
options may come from an untrusted client so make sure to validate any values you read from it.
Accounts.onCreateUser(function (options, user) {
user.profile = options.profile || {};
if (_.has(options, 'role')) {
Roles.addUserToRoles(user._id, options.role);
}
return user;
});
Thanks for your response. I tried but it doesn't work for me.
I used Accounts.onLogin hook to to manage this. Below code works for me:
Accounts.onLogin(function (info) {
var user = info.user;
if(user.profile.type === "doctor"){
Roles.addUsersToRoles(user, [ROLES.Doctor])
}
else
if(user.profile.type === "advisor"){
Roles.addUsersToRoles(user, [ROLES.Advisor])
}
return user;
});

Best way to compare two authentication tokens

Consider the following example: http://symfony.com/doc/current/cookbook/security/custom_authentication_provider.html#the-listener
I want to see if the new authenticated token is the same as for the currently logged in user. See below.
try {
$currentToken = $this->securityContext->getToken();
$authToken = $this->authenticationManager->authenticate($token);
// What is the best way to compare these two? Both implement TokenInterface.
if ([compare tokens]) {
// already logged in
return;
}
$this->tokenStorage->setToken($authToken);
return;
} catch (AuthenticationException $failed) {
}
I considered comparing UserInterface->getUsername():
if ($currentToken->getUser()->getUsername() === $authToken->getUser()->getUsername()) {
But wonder if there is a better solution...
I assume both user object aren't the same reference. Because if they are you could compare them using === to check if they are the same.
Given that username is unique than this is one way to check if it is the same user.
But in some systems the username doesn't have to be unique. So comparing the id's might be better since they should alway be unique.
What about leveraging the code they show in the authenticate() function?
$currentUser = $this->userProvider->loadUserByUsername($currentToken->getUsername());
then
$tokenUser = $this->userProvider->loadUserByUsername($authToken->getUsername());
then
if ($currentUser == $tokenUser) {

firebase security api read permission denied

I found a neat little example for permission based chat rooms using firebase security api here
Notice the "chat": {
// the list of chats may not be listed (no .read permissions here)
I actually need to list the chats a user belongs to when I load their inbox, however I can't seem to get the .read rule correctly.
Ive tried using the following rule which makes total sense but doesn't work:
"convos": {
".read" : "auth != null && data.child('users').hasChild(auth.id)",
I suspect the problem is that there is still a level between convo and users.. aka would make more sense to do:
"convos": {
".read" : "auth != null && data.child($key + '/users').hasChild(auth.id)",
$key : { ... }
But that's not allowed is complains about $key not existing yet.
How can I allow a user to pull all the convos they belongs to using this setup?
You can't use security rules to filter data. Generally, your data structure will be fairly dependent on your specific use case--most directly on how the data will be read back.
A general solution is to list the chats your user belongs to separate from the bulk chat data, i.e. to heavily denormalize, and access the chats individually.
/messages/$chat_id/... (messages chronologically ordered using push() ids)
/chats/$chat_id/... (meta data)
/my_chats/$user_id/$chat_id/true (the value here is probably not important)
Now to access all of my chats, I could do something like the following:
var fb = new Firebase(URL);
fb.child('my_chats/'+myUserId).on('child_added', function(snap) {
var chatID = snap.name());
loadChat(chatID);
});
function loadChat(chatID) {
fb.child('messages/'+chatID).on('child_added', function(snap) {
console.log('new message', chatID, snap.val());
});
}
You would still want security rules to validate the structure of chat messages, and access to a users' chat list, et al. But the functionality of filtering would be done with an index like this, or by another creative data structure.
I'm not completely sure how you're structuring your Firebase, but this might work:
"convos": {
$key : {
".read" : "auth != null && data.child('users').hasChild(auth.id)",
...
}

Resources