Remove/onDisconnect not working on log out - javascript

Using the documentation, I created the following presence function:
function setPresence() {
var myConnectionsRef = firebase.database().ref('users/' + currentUser.uid + '/connections');
var connectedRef = firebase.database().ref('.info/connected');
connectedRef.on('value', function(snap) {
if (snap.val() === true) {
var con = myConnectionsRef.push();
con.onDisconnect().remove();
con.set("profile");
}
});
}
I have the basic sign out function:
firebase.auth().signOut().then(() => {
// Sign-out successful.
}).catch((error) => {
// An error happened.
});
The issue is that onDisconnect does not seem to activate on sign out. I need to clear user state when they log out, but for some reason, I am getting permission denied error:
Using this, I tried the following when auth state changes such that user is undefined:
firebase.database().ref('users/' + currentUser.uid ).remove()
.then(function() {
console.log("Remove succeeded.");
window.location = "/";
})
.catch(function(error) {
console.log("Remove failed: " + error.message)
});
I am using the basic security rules:
{
"rules": {
"users": {
"$uid": {
".read": "auth != null",
".write": "auth != null && auth.uid == $uid"
}
}
}
}
Any idea what I am doing wrong?

Signing a user out from Authentication is unrelated to having a connection to the Realtime Database. While your security rules may require the user to be signed in, there is no such requirement on the product level: many apps allow part of their data to be used by unauthenticated users.
The .info/connected listener is true when the client can connect to the Firebase Realtime Database servers, and is not related to whether they can access specific data in that database.
Any onDisconnect handlers you define for a connection are executed when (the server detects that ) the connection is lost. This again is unrelated to the sign in state of the user.
If you want to mark the user as no longer present when they're signing out, you should remove their presence node before you sign them out. So your firebase.database().ref('users/' + currentUser.uid ).remove() need to run (and complete) before you call firebase.auth().signOut().
firebase.database().ref('users/' + currentUser.uid ).remove().then(() => {
firebase.auth().signOut().then(() => {
window.location = "/";
});
})

Related

Firestore/Firebase permissions denied using onDisconnect()

In this Javascript, everything is working except the presenceRef.onDisconnect().set('no more'); statement, which generates PERMISSION_DENIED: Permission denied in my console.
firebase.auth().onAuthStateChanged(function(user){
if (user){
uid=user.uid;
db.collection('user').doc(uid).set({
custnum: parseInt(custnum),
email: email,
password: password,
screenname: screenname,
admin: parseInt(admin),
uid: uid
})
.then(function(){
var docpath='/user/'+uid+'/email';
var presenceRef=firebase.database().ref(docpath);
presenceRef.onDisconnect().set('no more');
})
.catch(function(error){
console.log('Error writing document: ' + error);
});
} else {
db.collection('user').doc(uid).delete({
})
.then(function(){
//success
})
.catch(function(error){
console.log('Error writing document: ' + error);
});
uid='';
}
});
In the Rules tab when I am connected to this URL:
https://console.firebase.google.com/project/[Project Name]/database/firestore/rules
...I have everything opened up, as follows:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
I can use .set to write to the database. Why can I not use onDisconnect() to .set a different value? .db refers to a firebase.firestore() connection. Is there something going on here between Firebase and Firestore that I'm not understanding?
onDisconnect() is a method for the realtime database and not Firestore, it allows you to write or clear data when your client disconnects from the Database server.
Therefore if you meant to use it, then change the rules for the realtime database to the following:
{
"rules": {
".read": true,
".write": true
}
}
only use those rules in development.

Why is Firebase's "getRedirectResults()" returning "{user: null}"?

Currently, when the user goes through the Social auth (via redirects), it successfully creates a user under Firebase Authentication, but fails to retrieve the Google/Facebook API's data. Perplexed as to how it's failing to retrieve the data when the Auth is successful.
I used to only have the SignInWithPopup (which works perfectly fine), but am trying to get getRedirectResults to work too (for mobile).
Given the behavior I've been seeing, the problem is most likely with the getRedirectResults. Most likely not the social API setup, because otherwise the user would never get created in Authentication.
The following code resides in componentDidMount (this is a React.js app):
if (this.state.device === 'mobile') {
firebase.auth().getRedirectResult().then(function(result) {
console.log('login successful! Data is:');
console.log(result);
if (result.credential) {
var provider = result.credential.providerId;
self.handleSocialAuth(provider, result);
}
}).catch(function(error) {
var errorCode = error.code;
var errorMessage = error.message;
});
}
results is supposed to contain the user's data, keeps returning:
login successful! Data is:
{user: null}
I have not deviated from official docs examples, which is why I'm confused.
firebase.auth().getRedirectResult() - is called after the page loads.
Official doc link: https://firebase.google.com/docs/auth/web/google-signin
Use the below method to retrieve the user that is logged in.
firebase.auth().onAuthStateChanged(function(user) {
console.log(user);
});
If no user is logged in the user will be null.
Live example:
checkAuthStatus(){
firebase.auth().onAuthStateChanged(user => {
this.setState({ btnWithImg: user });
if(user !== null){
this.setState({ userIsLoggedIn: true });
this.props.toggleLogIn();
}
});
}
You should call firebase.auth().getRedirectResult() only when user has been authenticated. Example
firebase.auth().onAuthStateChanged(user => {
if (user) {
const result = await firebase.auth().getRedirectResult();
assert(result.user, 'user is empty')
}
});

Only authenticate with Google Auth Provider if user exists

I am using Firebase to authenticate users in our app using GoogleAuthProvider. But I don't want a new user to sign in if they are not already an authenticated user.
If the user exists then sign them in and console.log('user ' + user.email + ' does exist!');.
However, if the user does not exist. Then do not allow authentication and console.log('user ' + user.email + ' does not exist!')
var googleProvider = new firebase.auth.GoogleAuthProvider();
export const doSignInWithGoogle = () => auth.signInWithPopup(googleProvider);
googleLogin = () => {
auth
.doSignInWithGoogle()
.then(result => {
var user = result.user;
const userRef = db.collection('users').doc(user.uid);
userRef.get().then(docSnapshot => {
if (docSnapshot.exists) {
userRef.onSnapshot(() => {
console.log('user ' + user.email + ' does exist!');
});
} else {
console.log('user ' + user.email + ' does not exist!');
}
});
})
.catch(error => {
this.setState(updateByPropertyName('error', error));
});
};
I thought referencing the user records in Firestore would be a simple approach to this. However, perhaps Firebase Auth already have a way to do this. I cannot find documentation or any example.
In the above code, nothing gets logged and the user is either created or logged in.
How can I stop new users from signing up, whilst still allowing current users to sign in?
If you really want to use signInWithPopup method, you have this option,
but it's not the best way. when you are signing in with google, signInWithPopup method returns a promise. you can access the isNewUser property in additionalUserInfo from resulting object. then delete the user you just created.
firebase.auth().signInWithPopup(provider).then(
function (result) {
var token = result.credential.accessToken;
var user = result.user;
//this is what you need
var isNewUser = result.additionalUserInfo.isNewUser;
if (isNewUser) {
//delete the created user
result.user.delete();
} else {
// your sign in flow
console.log('user ' + user.email + ' does exist!');
}
}).catch(function (error) {
// Handle Errors here.
});
This is the easy way but deleting after creating is not the best practice. There is another option,
you can use, signInAndRetrieveDataWithCredential method for this. according to the docs,
auth/user-not-found will be
Thrown if signing in with a credential from
firebase.auth.EmailAuthProvider.credential and there is no user
corresponding to the given email.
function googleSignInWithCredentials(id_token) {
// Build Firebase credential with the Google ID token.
var credential = firebase.auth.GoogleAuthProvider.credential(id_token);
// Sign in with credential from the Google user.
firebase.auth().signInAndRetrieveDataWithCredential(credential)
.then(function (userCredential) {
//sign in
console.log(userCredential.additionalUserInfo.username);
}).catch(function (error) {
// Handle Errors here.
var errorCode = error.code;
if (errorCode === 'auth/user-not-found') {
//handle this
} else {
console.error(error);
}
});
}
here is an example from firebase github repo.
with Firebase security rules, can only check if keys exist - therefore searching in the users table is not an option:
"emails": {
"example1#gmail.com": true,
"example2#gmail.com": true
}
and then one can check with security rules, if the auth.token.email exists as a key:
{
"rules": {
".read": "root.child('emails').child(auth.token.email).exists(),
".write": false,
}
}
in the client, this should throw an "The read failed: Permission denied error" error then, to be handled accordingly. hooking into the Firebase sign-up isn't possible - but while they cannot log-in, this has the same effort (except that on has to clean up the user-database from time to time); eg. with a Cloud Function, which deletes users, which do not have their email as key in the emails "table".
in Firestore security rules, one can check with:
request.auth.token.email & request.auth.token.email_verified
for example, with a collection called emails and a collection called content:
match /databases/{database}/documents {
function userMatchesId(userId) {
return request.auth != null && request.auth.uid == userId
}
function readAllowed(email) {
return if get(/databases/$(database)/documents/emails/$(request.auth.token.email)).data != null
}
match /users/{userId} {
allow get: if userMatchesId(userId)
}
match /content {
allow get: if readAllowed(request.auth.token.email)
}
}
The object you receive from firebase after login has additionalUserInfo where you have the property isNewUser.
You can find the reference here: https://firebase.google.com/docs/reference/js/firebase.auth.html#.AdditionalUserInfo

If a database ref requires the user to be authenticated to read or write will signing out automatically cancel any listening at that location?

Say I have the default database rules.
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
And a listener.
firebase.database.ref('/foo').on('value', snap => {
console.log(snap);
});
If the user signs in and starts listening at 'foo', then signs out do I still need to call
firebase.database.ref('/foo').off()
Or will the listening be canceled automatically?
I just tested it with this code:
auth.signInAnonymously().then(user => {
console.log('signed in');
ref.on('value',
snapshot => console.log('got value: '+snapshot.val()),
error => console.error(error)
);
setTimeout(
() => auth.signOut().then(() => console.log('signed out')),
5000);
});
This prints:
signed in
got value: value
And then a few seconds later
signed out
[object Error] {
code: "PERMISSION_DENIED"
}
So it looks like authenticated listeners are cancelled when you sign out.

Parse Server / JS SDK, error 206 when saving a user object

I am having trouble using the Parse Server JS SDK to edit and save a user.
I am signing in, logging in and retrieving the user just fine, I can call without exception user.set and add/edit any field I want, but when I try to save, even when using the masterKey, I get Error 206: Can t modify user <id>.
I also have tried to use save to direcly set the fields, same result.
A interesting thing is that in the DB, the User's Schema get updated with the new fields and types.
Here is my update function:
function login(user, callback) {
let username = user.email,
password = user.password;
Parse.User.logIn(username, password).then(
(user) => {
if(!user) {
callback('No user found');
} else {
callback(null, user);
}
},
(error) => {
callback(error.message, null);
}
);
}
function update(user, callback) {
login(user, (error, user) => {
if(error) {
callback('Can t find user');
} else {
console.log('save');
console.log('Session token: ' + user.getSessionToken());
console.log('Master key: ' + Parse.masterKey);
user.set('user', 'set');
user.save({key: 'test'}, {useMasterKey: true}).then(
(test) => {
console.log('OK - ' + test);
callback();
}, (err) => {
console.log('ERR - ' + require('util').inspect(err));
callback(error.message);
}
);
}
});
}
And a exemple of the error:
update
save
Session token: r:c29b35a48d144f146838638f6cbed091
Master key: <my master key>
ERR- ParseError { code: 206, message: 'cannot modify user NPubttVAYv' }
How can I save correctly my edited user?
I had the exact same problem when using Parse Server with migrated data from an existing app.
The app was created before March 2015 when the new Enhanced Sessions was introduced. The app was still using legacy session tokens and the migration to the new revocable sessions system was never made. Parse Server requires revocable sessions tokens and will fail when encountering legacy session tokens.
In the app settings panel, the Require revocable sessions setting was not enabled before the migration and users sessions were not migrated to the new system when switching to Parse Server. The result when trying to edit a user was a 400 Bad Request with the message cannot modify user xxxxx (Code: 206).
To fix the issue, I followed the Session Migration Tutorial provided by Parse which explain how to upgrade from legacy session tokens to revocable sessions. Multiple methods are described depending on your needs like enableRevocableSession() to enable these sessions on a mobile app, if you're only having a web app, you can enforce that any API requests with a legacy session token to return an invalid session token error, etc.
You should also check if you're handling invalid session token error correctly during the migration to prompt the user to login again and therefore obtain a new session token.
I had the same error and neither useMasterKey nor sessionToken worked for me either. :(
Here's my code:
console.log("### attempt 1 sessionToken: " + request.user.getSessionToken());
var p1 = plan.save();
var p2 = request.user.save(null, {sessionToken: request.user.getSessionToken()});
return Parse.Promise.when([p1, p2]).then(function(savedPlan) {
...
}
I see the matching session token in log output:
2016-08-21T00:19:03.318662+00:00 app[web.1]: ### attempt 1 sessionToken: r:506deaeecf8a0299c9a4678ccac47126
my user object has the correct ACL values:
"ACL":{"*":{"read":true},"PC7AuAVDLY":{"read":true,"write":true}}
I also see a bunch of beforeSave and afterSave logs with user being "undefined". not sure whether that's related.
beforeSave triggered for _User for user undefined:
I'm running latest parser-server version 2.2.18 on Heroku (tried it on AWS and results are the same)
function login(logInfo, callback) {
let username = logInfo.email,
password = logInfo.password;
Parse.User.logIn(username, password).then(
(user) => {
if(!user) {
callback('No user found');
} else {
callback(null, user);
}
},
(error) => {
callback(error.message, null);
}
);
}
function update(userInfo, data, callback) {
login(userInfo, (error, user) => {
if(error) {
callback('Can t find user');
} else {
getUpdatedData(user.get('data'), data, (error, updateData) => {
if(error) {
callback(error);
} else {
user.save({data: updateData}, /*{useMasterKey: true}*/ {sessionToken: user.get("sessionToken")}).then(
(test) => {
callback();
}, (err) => {
callback(error.message);
}
);
}
});
}
});
}
For some reason, retrying to use sessionToken worked.
This is not how asynchronous functions work in JavaScript. When createUser returns, the user has not yet been created. Calling user.save kicks off the save process, but it isn't finished until the success or error callback has been executed. You should have createUser take another callback as an argument, and call it from the user.save success callback.
Also, you can't create a user with save. You need to use Parse.User.signUp.
The function returns long before success or error is called.

Categories