Meteor.userId() is not set to null after user is deleted

Hello,

I have recently come across this very strange error where when I:

  1. Have created and logged-in a user
  2. Decide to then delete said user
  3. Call Meteor.userId() in the developer console

Meteor.userId() then returns previous userId value and does not update.

'accounts.remove.currentUser'(digest){
        check(digest, String);
        if (this.userId) {
            var user = Meteor.user();
            //encrypt password when sending over wire
            var password = {digest: digest, algorithm: 'sha-256'};
            var result = Accounts._checkPassword(user, password);
            if(result.error==null){
                UserProfiles.remove({user:this.userId});
                Postings.remove({user:this.userId});
                Subscriptions.remove({user:this.userId});
                Meteor.users.remove(this.userId);
            }else{
                throw new Meteor.Error({section:'delete',error:'value-mismatch'});
            }
        } else {
            throw new Meteor.Error({section:'delete',error:'user-missing'});
        }
    },

Meteor.users.remove(this.userId);
… should be…
Meteor.users.remove({ _id: this.userId } );

Plus, not that it breaks anything, the first parameter of Meteor.Error is intended to be a string as follows:

A string code uniquely identifying this kind of error. This string should be used by callers of the method to determine the appropriate action to take, instead of attempting to parse the reason or details fields.

See https://docs.meteor.com/api/methods.html#Meteor-Error

I myself usually pass a context object in the 2. parameter.

There are two things:
1- deleting the current user (logged-in user)
2- deleting the user from the collection list

Now, if you are deleting current user, you need to call logout method of the meteor and remove relevant cookies:

 window.localStorage.removeItem("Meteor.loginToken");
 window.localStorage.removeItem("meteorUserId");
 window.localStorage.removeItem("Meteor.loginTokenExpires");
 window.localStorage.removeItem("Meteor.userId");

And if you are removing the user from the list then Meteor.userId() will return only the currently logged-in user’s Id.

Tried both but it didn’t seem to make much difference

Plus, not that it breaks anything, the first parameter of Meteor.Error is intended to be a string as follows:

A string code uniquely identifying this kind of error. This string should be used by callers of the method to determine the appropriate action to take, instead of attempting to parse the reason or details fields.

See https://docs.meteor.com/api/methods.html#Meteor-Error

I myself usually pass a context object in the 2. parameter.

I know it is not supposed to be done that way but I need this implementation for a bunch of error messages to display client side. Passing the context in the second parameter however seems like a pretty good solution.

1 Like

There are two things:
1- deleting the current user (logged-in user)
2- deleting the user from the collection list

Now, if you are deleting current user, you need to call logout method of the meteor and remove relevant cookies:

 window.localStorage.removeItem("Meteor.loginToken");
 window.localStorage.removeItem("meteorUserId");
 window.localStorage.removeItem("Meteor.loginTokenExpires");
 window.localStorage.removeItem("Meteor.userId");

And if you are removing the user from the list then Meteor.userId() will return only the currently logged-in user’s Id.

So, if I understand correctly, the correct order for deleting the current user account would be to first logout the user in the method and then delete the account ? Does deleting relevant cookies need to be done server-side or client-side ?

Yes. And note that Meteor.logout() is only available on the client. I consider this a serious design flaw in Meteor, by the way, because then when the Meteor method responsible for the actual deletion of the user is called , this.userId will not be present anymore, which makes the authentication of a highly sensitive operation rather difficult.

I think we’re left with the only chance as follows:

  1. SERVER: while the user is still logged in, mark the user document for deletion. For example:
import { Random } from 'meteor/random';
const deleteId = Random.id();
Meteor.users.update({ _id: this.userId },
  { $set: { deleteId } });
return deleteId;
  1. CLIENT:  Meteor.logout()
  2. SERVER:  in a method with a parameter deleteId execute Meteor.users.remove({ deleteId })

Note that in the final method call there is no way that the user’s _id could be passed as a parameter to be used in the selector for deletion, because then anyone could call this method from the browser console to arbitrarily delete users, provided that an attacker is aware of any userId in your system. I would definitely not take the risk.

Client side. Local Storage (which is not the same as cookies) lives in the browser.

One more thing: I don’t believe it is necessary to delete the Local Storage items. It may be nice to clean up a little, but even if you don’t, no harm would be done, since the user document will be gone at the end of this procedure anyway, so the userId and the login token are both useless and obsolete data.

Alright, so, I found a way of logging out in the server by deleting the user’s login token that meteor uses to identify if a user is logged in. However, things never seem to be this simple as for some incomprehensible reason, my meteor method callback is not being called so the client has no way to know if the operation was a success

'accounts.remove.currentUser'(digest){
        check(digest, String);
        if (this.userId) {
            var user = Meteor.user();
            //encrypt password when sending over wire
            var password = {digest: digest, algorithm: 'sha-256'};
            var result = Accounts._checkPassword(user, password);
            if(result.error==null){
                //fist delete all user data
                UserProfiles.remove({user:this.userId});
                Postings.remove({user:this.userId});
                Subscriptions.remove({user:this.userId});
                Roles.removeUsersFromRoles(user._id,Roles.getRolesForUser(user._id,{anyScope:true}),{anyScope:true})
                PermissionAssignment.remove({
                    user: user._id
                });
                Meteor.users.remove(this.userId);
                //then logout user
                Meteor.users.update({_id:this.userId}, {$set : { "services.resume.loginTokens" : [] }}, {multi:true});
            }else{
                throw new Meteor.Error({section:'delete',error:'value-mismatch'});
            }
        } else {
            throw new Meteor.Error({section:'delete',error:'user-missing'});
        }
    },

Here is the clientside code:

'click .js-remove-account': function(event,template){
        event.preventDefault();
        if($('#inputProfileConfirmationPassword').val()==null||$('#inputProfileConfirmationPassword').val()===''){
            template.errors.set('delete','empty-value');
        }else{
            var digest = Package.sha.SHA256($('#inputProfileConfirmationPassword').val());
            Meteor.call('accounts.remove.currentUser', digest, function(error){
                // For some awful reason, the callback is not called when no errors arise
                if(error){
                    callError = true;
                    console.error(error);
                    if(error.error.section&&error.error.error){
                        template.errors.set(error.error.section,error.error.error);
                    }
                }else{
                    // this is never called
                    console.log('done');
                }
            });
        }
    },

After further testing, I found out that the reason the callback is never being called is the user deletion itself. Honestly, any workaround would be a godsend. I think I might just go with the method you proposed but I would really like for this callback to work.

1 Like

If you wish to force a specific user to be logged out on each current connection, then one indirect way to do it from server side would be to use Accounts.setPassword and set the users password to some random string. See the logout option here:

Although just deleting the user document from the Meteor.users collection should do the trick as well…if I remember correctly. So not really sure what’s happening in your case here.

I use FlowRouter for routing, so basically, after deleting the user, I also need to reroute the path. Thing is, logging-out the user isn’t a problem anymore, the real problem is that the callback on the method doesn’t work so I don’t know that the user has been successfully deleted and as such can’t decide wether to reroute or not (account deletion is password protected so result is not always successful deletion)

In my case, when I tried to call Meteor.users.remove(...) in a method on the server of a user that was still logged in, the method has been called for a second time without calling the callback resulting from the original method invocation. During this repeated (second) and unexpected method call this.userId was always null. It took me hours to figure out that the method was called twice.

How did you then fix this double invocation? After testing a console.log, it seems that mine is only invoked once however, I feel that it may be the key to finally fixing this bug.

Relying on a callback would work when the user deletes itself. But you might also have situations when someone else deletes a user and then it would be nice to redirect the user to some login page as well.

In my app (Vue and Vue router) I decided that having such logic in the router is tricky since it is not reactive. So instead in my top component I have some if’s in the template that check for Meteor.userId() and only if the userId exists will the router content be shown. Otherwise a login page is shown. The advantage being that the userId in the template is reactive and thus if a user is logged out or deleted, either by itself or by some other user, the user will be immediately shown the login page.

That would seem like a good solution in theory, however, because of my original problem - Meteor.userId() is not set to null after user is deleted, this would sadly not work since I would not be able to know when userId changed… This problem is very frustrating to solve…

1 Like

More precisely this was my use-case: I do allow sign-ups by invitation only. The invitation is a document in a collection, and the new user identifies themselves with a token that matches the invitation document. If it does, their user document will be updated with a property that finalizes the registration.

The method that checks the validity of the invitation shall therefore either finalize the registration, or recognize it as being fraudulent, in which case…

  • the user shall be removed from MongoDB
  • and the method shall throw a specific Meteor.Error.

The method seems to be invoked twice when the user document of the logged in user is removed AND an error is thrown. It is not invoked twice if just the error is thrown, but no Meteor.users.remove(...) is called.

So I refrain from removing the user document at first, return to the client and remove the dangling user document in a second go. I don’t like this, I can tell you that much!

Oh, I see, this looks very different from my use case which is simply the user deleting their own account. At this point, the only solution I see is to directly look for if the user exists in the Meteor.users collection and put that in a Tracker. This seems like such an over-engineered and convoluted solution but I suppose I have no choice… Don’t know if I should mark this as solved. If this doesn’t work, I’m just going to have to live with the fact that the user doesn’t get redirected after their account gets deleted.

I think the bug stems from other design decisions and code that had unforeseen consequences because I cannot explain it otherwise.

Anyways, thanks for the help, really appreciate it.

1 Like

Maybe it helps if I mention that in my case all users are subscribed to some of the data in their Meteor.users document. So if the document gets deleted, then that subscription will update as well.

Althouh that should be irrelevant. The userId should change regardless when the user gets deleted and it should be reactive. So hard to say what’s the underlying cause here.