How to protect some data in firebase firestore? - reactjs

Im trying to make chat rooms in my web app. That chat rooms will be protected by password. How to securly store that passwords?
I figure out that way:
validation through my own api which only has access to that passwords.
I know there are firebase security rules that i can change but:
do i have to make new collection? or do i am able to change security rules just for one field in document?
how to make collection accesible just for api? I know that i can change in security rules that some data is only accesible for it's creator by how to make it for api?
Or is there better way to do that?

The simplest way I found is that you don’t have to store passwords in the Firestore, instead you can use Firebase Authentication for it.
You can create it by calling createUserWithEmailAndPassword, or you can also create new password-authenticated users from the Authentication section as mentioned in Manage Users in Firebase.

I managed to do that by using firebase functions. I created collection that matches rooms with users.
One function for:
If password passed in request is correct for room which id was also passed in request, add document to that collection that match this room with passed user id.
And another one:
Return data about all rooms which ids are connected with user id.
Code below i help it will help anyone:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const cors = require('cors')({ origin: true });
admin.initializeApp(functions.config().firebase);
exports.passwordCheck = functions.region("europe-central2").https.onRequest((req, res) => {
cors(req, res, () => {
const { uid, password, id } = req.body;
// refs to room with given args
const roomRef = admin.firestore()
.collection('rooms').doc(id)
roomRef.get().then((doc) => {
if (doc.data().password == password) {
// ref to room_members list in requested room
const room_members = admin.firestore().collection('room_members');
const is_member = room_members.where("user_uid", "==", uid).where("room_id", "==", id);
is_member.get().then((docs) => {
if (docs.docs.length == 0) {
room_members.add({
user_uid: uid,
room_id: id,
}).then(() => {
res.status(200).end();
})
}
}).catch(err => {
// user already in members
});
// Success status
}
else {
// wrong password
res.status(400).end();
}
}).catch(err => {
// not found room
res.status(404).end();
});
});
});
/* Example request for that function
curl -X POST https://europe-central2-xxx.cloudfunctions.net/passwordCheck
-H 'Content-Type: application/json'
-d '{"uid": "B2Xidd3Vw1PL9Kyt5ERFXCjniuF3","id":"Sjucgsyuw2723"
"password": "xxxx"}'
*/
exports.rooms = functions.region("europe-central2").https.onRequest((req, res) => {
cors(req, res, () => {
const { uid } = req.body;
// refs to rooms members list objects with given user id
const membersRef = admin.firestore()
.collection('room_members').where("user_uid", "==", uid);
membersRef.get().then((doc) => {
if (doc) {
// get all ids of rooms that user is in
const rooms_ids = doc.docs.map((docx) => { return docx.data().room_id });
// get all datas from that rooms
const roomsrefs = rooms_ids.map(id => admin.firestore().collection("rooms").doc(id));
admin.firestore().getAll(...roomsrefs).then((rooms) => {
res.json(rooms.map(room =>
Object.assign({}, { "id": room.id }, room.data())))
res.status(200).end();
})
}
else {
// not found resurces
res.json([]);
res.status(401).end();
}
}).catch(err => {
//Internal server error
res.status(500).end();
});
})
});
/* Example request for that function
curl -X POST https://europe-central2-xxxx.cloudfunctions.net/rooms
-H 'Content-Type: application/json'
-d '{"uid": "B2Xidd3Vw1PL9Kyt5ERFXCjniuF3"}'
// */
Response time of the functions is like 500ms so im sure that it's not the best solution but it works :)
It's also possible to do with firestore rules but its neccesary to have matching strucure of data.

Related

NextJS-Auth0: How can I assign a role on signup with Auth0?

Using the library nextjs-auth0 (https://github.com/auth0/nextjs-auth0) I've been trying to make use of the handleAuth hook to grab a query arg to specify which role should be assigned to the user on signup.
An example of what I'm trying to do:
//pages/api/auth/[...auth0].js
const getLoginState = (req, loginOptions) => {
const { role } = req.query;
return { role: role };
};
export default handleAuth({
async login(req, res) {
try {
await handleLogin(req, res, { getLoginState } );
} catch (error) {
res.status(error.status || 500).end(error.message);
}
}
});
The documentation for handleAuth makes it seem like it's possible to do this ( https://auth0.github.io/nextjs-auth0/modules/handlers_login.html#getloginstate )
// from the documentation
const getLoginState = (req, loginOptions) => {
return { basket_id: getBasketId(req) };
};
From that doc - it looks like basket_id is the custom property to be saved against the user "before they visit the Identity Provider to login".
This sounds, to me, that basked_id will be saved somewhere against the users metadata once they've logged in. Is the documentation misleading, or am I misunderstanding?
How can I set the role during (or even slightly after) signup?
I managed to achieve what I wanted with the following Auth0 "rule":
function (user, context, callback) {
const count = context.stats && context.stats.loginsCount ? context.stats.loginsCount : 0;
if (count > 1) {
return callback(null, user, context);
}
const ManagementClient = require('auth0#2.27.0').ManagementClient;
const management = new ManagementClient({
token: auth0.accessToken,
domain: auth0.domain
});
let roles = [context.request.query.role];
const params = { id : user.user_id};
const data = { "roles" : roles};
management.users.assignRoles(params, data, function (err, user) {
if (err) {
// Handle error.
console.log(err);
}
callback(null, user, context);
});
}
Notice that the role is being read in from context.request.query.role. This pulls the query param role key off the login URL which more-or-less works how I wanted it to.
Then forward the role along from the auth in the backend:
const getLoginState = (req, loginOptions) => {
const { role } = req.query;
loginOptions.authorizationParams.role = role;
return { role: role };
};
export default handleAuth({
async login(req, res) {
try {
await handleLogin(req, res, { getLoginState });
} catch (error) {
res.status(error.status || 500).end(error.message);
}
}
});
Notice the loginOptions.authorizationParams.role = role;
So the login link can be set to: /api/auth/login?role=somerole and the rule will pick up the role and set it in the metadata part of the users info.
However: I wasn't able to get this to actually properly set the role on the user but it's enough for me, as it appears in the session.

How to send properly a message in twilio conversation with react?

I have a nextjs project and I want to replace the twilio programmable chat with twilio conversations.
I did the following steps:
I did one API which creates or gets (if it is already created) the conversation and returns to the client the conversation unique name and the token
Once I have the conversation unique name and the token I want to send client side one message.
To do so I did the following function:
import { Client, State } from '#twilio/conversations';
import toast from 'react-hot-toast';
const sendMessageToConversation = async (
token: string,
room: string,
message: string
) => {
const client = new Client(token);
client.on('stateChanged', async (state: State) => {
if (state === 'initialized') {
try {
const conversation = await client.getConversationByUniqueName(room);
await conversation.join();
if (message && String(message).trim()) {
await conversation.sendMessage(message);
}
} catch {
toast.error('Unable to create conversation, please reload this page');
}
}
});
};
the problem seems to be const conversation = await client.getConversationByUniqueName(room); which gives the following error:
What do you think I did wrong?
Also is it a better idea to build an API to send messages in this way? I would avoid this because of the possible overhead of the server
UPDATE
I tried to send a message through API. It works and it returns what I expect. For more detail I will put also the code I have on backend side which generates the tokens and the conversations.
I generate tokens for client side with:
import Twilio from 'twilio';
import { config } from '../config';
const client = require('twilio')(
config.TWILIO_ACCOUNT_SID,
config.TIWLIO_AUTH_TOKEN
);
const AccessToken = Twilio.jwt.AccessToken;
const ChatGrant = AccessToken.ChatGrant;
const SyncGrant = AccessToken.SyncGrant;
export const tokenGenerator = (identity: string) => {
const token = new AccessToken(
config.TWILIO_ACCOUNT_SID,
config.TWILIO_API_KEY,
config.TWILIO_API_SECRET
);
token.identity = identity || 'unknown';
if (config.TWILIO_CHAT_SERVICE_SID) {
const chatGrant = new ChatGrant({
serviceSid: config.TWILIO_CHAT_SERVICE_SID,
pushCredentialSid: config.TWILIO_FCM_CREDENTIAL_SID,
});
token.addGrant(chatGrant);
}
if (config.TWILIO_SYNC_SERVICE_SID) {
const syncGrant = new SyncGrant({
serviceSid: config.TWILIO_SYNC_SERVICE_SID || 'default',
});
token.addGrant(syncGrant);
}
return {
identity: token.identity,
token: token.toJwt(),
};
};
I create conversations with:
const client = require('twilio')(
config.TWILIO_ACCOUNT_SID,
config.TIWLIO_AUTH_TOKEN
);
export const createTwilioConversation = async (
partecipantsProfiles: Partial<User>[],
identity: string
) => {
const friendlyName: string = partecipantsProfiles
.map((el) => `${el.first_name} ${el.last_name}`)
.join(' - ');
const conversation = (await client.conversations.conversations.create({
friendlyName,
uniqueName: uuidv4(),
})) as TwilioConversationResponse;
await client.conversations
.conversations(conversation.sid)
.participants.create({ identity });
return conversation;
};
The flow I do in order to send messages is:
If I want to send a message I create a conversation by calling an API, executes the functions above and returns the room unique name and the token. I also store into my DB the room unique name, participants and other infos. I do this only for the first message. If a user has already chatted with another user, then I don't create a conversation anymore, but I return the unique name stored + token generated and I get/send the message client side
I have also tried to send a message through API and it works. I can't figure out why I still can't get the conversation by unique name client side. Maybe I should generate the token differently?
This is the method that sends messages server side:
export const sendMessage = async (
conversationSid: string,
author: string,
body: string
) => {
return await client.conversations
.conversations(conversationSid)
.messages.create({ author, body });
};
Your issue is that you are not using the default Conversations service, but you are not scoping your API calls on the server side to the service you need.
So in pages/api/utils/conversations you need to add .services(config.TWILIO_CHAT_SERVICE_SID) into all your API calls. See below:
export const createTwilioConversation = async (
chatTo: string,
myIdentity: string
) => {
const uniqueName = uuidv4();
const conversation = (await client.conversations
.services(config.TWILIO_CHAT_SERVICE_SID)
.conversations.create({
friendlyName: `Chat created by ${myIdentity}`,
uniqueName,
})) as TwilioConversationResponse;
await client.conversations
.services(config.TWILIO_CHAT_SERVICE_SID)
.conversations(conversation.sid)
.participants.create({ identity: chatTo });
await client.conversations
.services(config.TWILIO_CHAT_SERVICE_SID)
.conversations(conversation.sid)
.participants.create({ identity: myIdentity });
return conversation;
};
export const sendMessage = async (
conversationSid: string,
author: string,
body: string
) => {
return await client.conversations
.services(config.TWILIO_CHAT_SERVICE_SID)
.conversations(conversationSid)
.messages.create({ author, body });
};
Once you've done that, you need to one other thing. Because you add your participant to the conversation using the REST API, you don't need to join the conversation in the front-end. So you can remove the line
await conversation.join();
from src/twilio/index.ts.
One last thing, you can get better error messages in the front-end if you log out error.body rather than just error or error.message.
try {
const conversation = await client.getConversationByUniqueName(room);
if (message && String(message).trim()) {
await conversation.sendMessage(message);
}
} catch (error) {
console.log("error", error);
console.log("error body", error.body);
toast.error("Unable to create conversation, please reload this page");
}
Twilio have an official blog for Build a Chat App with Twilio Programmable Chat and React.js, please check it out once,
Here is the link - https://www.twilio.com/blog/build-a-chat-app-with-twilio-programmable-chat-and-react

React, connect-mongoose & Express session/user login

I'm trying to get sessions to work with a React front-end and an express + connect-mongo using MongoStore back-end.
Handle Register Function
async function handleRegister(evt){
//Prevent default form redirect.
evt.preventDefault();
//Create a new user objec to pass into axios
const user = {
username: username,
password: password
}
//Send axios post request to nodeJS API.
await axios.post("http://localhost:5000/users/register",user)
.then((res) => {
history.push({
pathname: '/',
state: res.data
});
})
.catch((err) => {
console.log(err);
});
//Push react history back to index page.
}
Handle Login function
const handleLogin = async (evt) => {
//Prevent default form submission
evt.preventDefault();
const loginDetails = {
username: username,
password: password,
}
//send login request to api
await axios.post('http://localhost:5000/users/login', loginDetails)
.then((res) => {
})
.catch((err) => {
})
}
I'm stuck on trying to figure out how to make the data be sent back to react after either of the above functions. In the register function I've sent back the res.data which contains the session. See blow route for express
router.post('/register', async (req, res) => {
//Destructure req.body.
const {username,password} = req.body;
//hash password.
const hashedPassword = await hashPassword(password);
//Create new user to store in mongodb.
const newUser = {
username: username,
password: hashedPassword
}
//Create new user document
await User.create(newUser, (err, newlyAddedUser) => {
if(err){
console.log(err);
}else{
req.session.username = newlyAddedUser.username;
console.log(req.session);
res.send(req.session);
}
})
});
With the console.log(req.session) it outputs the cookie and the username I added in the session itself.
Should I make a user object on the react side and store the username and password inside?
Should I be passing back the session itself to the route with history.push({ pathname: '/',state: res.data});
How can I verify that the session is valid for the user using connect-mongo?
I spent 10 minutes trying to understand what is your goal. Didn't find.
But whatever you need to use a jsonwebtoken if you want to verify that the session is valid like you said
Enjoy https://jwt.io/
https://www.npmjs.com/package/jsonwebtoken
I wouldn't store the session in the History state API like you do.
history.push({
pathname: '/',
state: res.data
});
You better use a sessionStorage and/or localStorage. The name just talks by itself.
Give me one point please

Connect Firebase Auth with Firestore in React

I'm trying to figure out how to connect users with data in my firestore. I didn't find anything about how to do it in React.
My idea, in the register page of my app, is:
async function register() {
try {
await auth.createUserWithEmailAndPassword(email, password).then(data => {
auth.currentUser.updateProfile({
displayName: name
})
db.collection('usersdata').doc(data.user.uid).set({nome: name})
props.history.replace('/success')
})
} catch(error) {
alert(error.message)
}
}
In this way I'm creating a new user and also a new entry in my "usersdata" collection. That entry has the same ID and the same name of the user.
After the login I can get the active user name using:
const userName = getCurrentUsername()
function getCurrentUsername() {
return auth.currentUser && auth.currentUser.displayName
}
All of this is working. But here it comes my problem:
I would like to add new data to the current user, for example "age" and "nationality".
But I don't know how to exactly get access to that user from firestore instead of auth.
And I also need to return only his data. Then, after a research, I need to return data from all users, and I guess that is the method:
const [datidb, setDatidb] = useState([])
useEffect(() => {
const fetchData = async () => {
const data = await db.collection('usersdata').get()
setDatidb(data.docs.map(doc => doc.data()))
}
fetchData()
}, [])

Microsoft Graph API - Create user in Azure AD B2C with local account identity gives Request_ResourceNotFound error

My goal is to create a local account, like michele#gmail.com, in Azure Active Directory B2C.
I want to use my own UI, so I started to look at how to implement the API. After some research, it looks like the best way to do it should be via Microsoft Graph.
I started by following Manage Azure AD B2C user accounts with Microsoft Graph, and I properly created an app (not sure if I need to choose the third options, but looks like the broader one):
with the following permissions (updated following also #Tony Ju screenshot):
Then I created the client secret and coded my own auth provider
const { AuthenticationContext } = require('adal-node');
class AuthProvider {
async getAccessToken() {
return new Promise((resolve, reject) => {
const tenant = 'tenant';
const authority = `https://login.microsoftonline.com/${tenant}`;
const authenticationContext = new AuthenticationContext(authority);
const resource = 'https://graph.microsoft.com';
const clientId = 'clientId';
const clientSecret = 'clientSecret';
authenticationContext.acquireTokenWithClientCredentials(
resource,
clientId,
clientSecret,
(err, tokenResponse) => {
if (err) {
console.error('error!', err);
return reject(err);
}
return resolve(tokenResponse.accessToken);
},
);
});
}
}
and initialized the client
require('isomorphic-fetch'); //needed for server side request with the client
const { Client } = require('#microsoft/microsoft-graph-client');
const options = {
authProvider: new AuthProvider(),
};
const client = Client.initWithMiddleware(options);
Following the official documentation, I create a local account
const user = {
displayName: 'John Smith',
identities: [
{
signInType: 'emailAddress',
issuer: 'MY_ISSUER.onmicrosoft.com',
issuerAssignedId: 'jsmith#yahoo.com',
},
],
passwordProfile: {
password: 'df42bfe2-8060-411f-b277-06b819874573',
},
passwordPolicies: 'DisablePasswordExpiration',
};
client
.api('/users')
.post(user)
.then(data => console.log(data))
.catch(e => console.error(e))
And I get back this "Request_ResourceNotFound" error
GraphError {
statusCode: 404,
code: 'Request_ResourceNotFound',
message:
'Resource \'User_30140fa1-ae7e-40b7-ad5a-ef4d0b4cd4dc\' does not exist or one of its queried reference-property objects are not present.',
requestId: 'fbf4c987-0383-472a-bc22-c94f98710344',
date: 2020-05-18T13:19:14.000Z,
body:
'{"code":"Request_ResourceNotFound","message":"Resource \'User_30140fa1-ae7e-40b7-ad5a-ef4d0b4cd4dc\' does not exist or one of its queried reference-property objects are not present.","innerError":{"request-id":"fbf4c987-0383-472a-bc22-c94f98710344","date":"2020-05-18T15:19:14"}}' }
The error does not help and I don't know how to continue. The basic configuration looks correct, because I'm able to get all the users and also create a user in the same tenant.
What am I missing? Feels so weird having this type of error by following the official docs. I'm starting to think that I need to use the invitation API, but I just want to create a user and don't go into the full email verification flow. Plus, what I exactly need is in the official docs and I would expect it to work. So maybe there is just something wrong on my side.
Your code works perfectly. I only updated tenant,clientId,clientSecret and MY_ISSUER in your code. Here is the whole code I ran.
const { AuthenticationContext } = require('adal-node');
class AuthProvider {
async getAccessToken() {
return new Promise((resolve, reject) => {
const tenant = '6b72a356-867d-4c35-bde6-959d99388ca8';
const authority = `https://login.microsoftonline.com/${tenant}`;
const authenticationContext = new AuthenticationContext(authority);
const resource = 'https://graph.microsoft.com';
const clientId = '270408b5-85a6-4e99-861e-7634853a5827';
const clientSecret = 'VYD_F_Rr~eHYqLtTXqa1~1KRS_932yNw35';
authenticationContext.acquireTokenWithClientCredentials(
resource,
clientId,
clientSecret,
(err, tokenResponse) => {
if (err) {
console.error('error!', err);
return reject(err);
}
return resolve(tokenResponse.accessToken);
},
);
});
}
}
require('isomorphic-fetch'); //needed for server side request with the client
const { Client } = require('#microsoft/microsoft-graph-client');
const options = {
authProvider: new AuthProvider(),
};
const client = Client.initWithMiddleware(options);
const user = {
displayName: 'John Smith',
identities: [
{
signInType: 'emailAddress',
issuer: 'tonyb2ctest.onmicrosoft.com',
issuerAssignedId: 'jsmith4#yahoo.com',
},
],
passwordProfile: {
password: 'df42bfe2-8060-411f-b277-06b819874573',
},
passwordPolicies: 'DisablePasswordExpiration',
};
client
.api('/users')
.post(user)
.then(data => console.log(data))
.catch(e => console.error(e))
The result:
I registered an application under Azure Active Directory and granted Directory.ReadWrite.All permission.

Resources