perfood / couch-auth

Powerful authentication for APIs and apps using CouchDB (or Cloudant) with Node >= 14
MIT License
68 stars 19 forks source link

User doc _rev field not returned on user creation when loginOnRegistration is set to false #25

Closed jbgtmartin closed 2 years ago

jbgtmartin commented 2 years ago

Hi, first all thanks for this updated library!

There is a bug I thing in the createUser() method. At the end, when the method resolves with the created user, the _rev field will only be included if it's returned by the second resolve() (i.e. if we create a user with loginOnRegistration set to false)

It can lead to CouchDB update conflicts, for instance if we try to update the user doc just after creation (as we will try to insert it without the _rev field).

// src/user.ts

// Let's say we have this.config.security.loginOnRegistration = false, and hasError = false.

return new Promise(async (resolve, reject) => {
  newUser = await this.prepareNewUser(newUser);
  if (hasError || !this.config.security.loginOnRegistration) {
    // 1. The promise resolves. It returns newUser instead of finalUser.
    resolve(hasError ? undefined : (newUser as SlUserDoc));
  }
  if (!hasError) {
    // 2. This is executed (even if the promise has already resolved).
    // The doc is inserted here, but resolve() has already been called so finalUser is not returned.
    const finalUser = await this.insertNewUserDocument(newUser, req);
    this.emitter.emit('signup', finalUser, 'local');
    if (this.config.security.loginOnRegistration) {
      resolve(finalUser);
    }
  }
});

Maybe we could change it like this?

if(hasError)
  return;

else {
  newUser = await this.prepareNewUser(newUser);
  const finalUser = await this.insertNewUserDocument(newUser, req);
  this.emitter.emit('signup', finalUser, 'local');
  return finalUser;
}

PS: I'm using the latest version (0.16.0)

fynnlyte commented 2 years ago

Hmm if I recalled correctly, I made the adjustments in order to prevent name guessing so simply removing it is most likely not the best choice.

But yeah, if this.config.security.loginOnRegistration is false, we can simply resolve with the final user or exit with the error.

jbgtmartin commented 2 years ago

What I meant is that no matter the value of this.config.security.loginOnRegistration, the user doc is returned:

So in both cases the user doc is returned, the only difference is that in the second case it's returned before calling insertNewUserDocument(). Is it what prevents name guessing?

fynnlyte commented 2 years ago

Yeah resolving before inserting the doc prevents time name guessing in case loginOnRegistration is switched off. But when it's switched on, name guessing doesn't matter anyways. So the fix would be to simply not resolve twice, i.e. first resolve only if !loginOnRegistration || hasError && loginOnRegistration

jbgtmartin commented 2 years ago

I'm not sure to know exactly what time name guessing is? Is it an attack where you try to register a user, and if the registration takes some time you deduce that the username you tried does not already exist ? So we want the method to return immediately in any case, and we can't wait for the doc to be inserted to get the _rev ?

fynnlyte commented 2 years ago

I meant time based name guessing, as in https://en.wikipedia.org/wiki/Timing_attack or https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#authentication-responses

So basically - yes, the function should return early regardless of whether the user is actually created or it fails because the email is already present (which is what an attacker would want to find out)

jbgtmartin commented 2 years ago

Ok now I understand better, thanks for the explanations! So I guess there's nothing to change in the library, and if I need to run some logic after user creation I can listen to the 'signup' event

fynnlyte commented 2 years ago

I'll look into this on the weekend. The 'signup' - event should only fire after the user has been inserted into the database, if the signup was successful.

jbgtmartin commented 2 years ago

Yes, after this event we can get the doc from the database as we know it has been inserted, so I think it's fine.

I'll let you close this issue this weekend, and thanks again for your work with this library!

fynnlyte commented 2 years ago

Yup, thanks for bringing this up, I've explained it in the docstring of createUser. What the code does is expected behaviour, but it is indeed a bit confusing that createUser doesn't return the correct revision.

jbgtmartin commented 2 years ago

Thank you!