perfood / couch-auth

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

Unexpected behavior when email has uppercase letters #67

Closed ErikGoH closed 1 year ago

ErikGoH commented 1 year ago

I was creating some test users when I found the following behavior:

  1. Create a user with an email that has an uppercase letter
        curl --request POST \
          --url http://localhost:3000/auth/register \
          --header 'Content-Type: application/json' \
          --data '{
          "email": "testAdmin@example.com",
          "password": "bigsecret",
          "confirmPassword": "bigsecret",
        }'
  2. Try to login
    curl --request POST \
      --url http://localhost:3000/auth/login \
      --header 'Content-Type: application/json' \
      --data '{
      "email": "testAdmin@example.com", 
      "password": "bigsecret"
    }
    '
  3. Receive an unauthorized error message
      {
        "error": "Unauthorized",
        "message": "Invalid username or password"
      }
  4. Change the email to all lowercase letters
    curl --request POST \
      --url http://localhost:3000/auth/login \
      --header 'Content-Type: application/json' \
      --data '{
      "email": "testadmin@example.com", 
      "password": "bigsecret"
    }
    '
  5. Everything works normally
      {
        "issued": 1676830866748,
        "expires": 1676917266748,
        "provider": "local",
        "token": "NlKFvdkRSh-Z4Zf-z5QmbA",
        "password": "ZsO0x9N4Q6mUaOs_XHF7Jg",
        "user_id": "re46jmvr",
        "roles": [
          "user",
          "Admin",
          "user"
        ],
        "userDBs": {
          "interviewer": "http://NlKFvdkRSh-Z4Zf-z5QmbA:ZsO0x9N4Q6mUaOs_XHF7Jg@devf.local:5985/interviewer_d31c511c86334591bcae45d9a5b6086e"
        }
      }

It took me a while to figure out why I was getting the Unauthorized message if I had just created the user, after checking that my configuration was correct (I disabled everything related to email confirmation) I had to start adding console.logs in the node_modules > couch-auth files

Finally realized the thing that was happening when I checked node_modules/@perfood/couch-auth/lib/user/DbManager.js that everything was correct but when querying the auth.email view it wasn't returning anything thats when I realized that the emails are saved in lower case.

I don't know if the login route should also lowercase the email or the create should save the email as is sent, my quick fix will be to lowercase the email before sending the login request as I have had some users report that they couldn't login even when the browser password manager saved their credentials.

PS: I am currently using my fork https://github.com/ErikGoH/superlogin-next/tree/main-keycode but I don't think there are any significant changes My package.json looks like this

...
"dependencies": {
   ...,
   "@perfood/couch-auth": "github:ErikGoH/superlogin-next#main-keycode",
   ...
}
My config ``` const config = { security: { loginOnRegistration: true, }, testMode: { debugEmail: this.configService.get('TEST_MODE_DEBUG_EMAIL') === 'true', noEmail: this.configService.get('TEST_MODE_NO_EMAIL') === 'true', }, dbServer: { protocol: this.configService.get<'http://' | 'https://'>('DB_PROTOCOL'), host: this.configService.get('DB_HOST'), user: this.configService.get('DB_USER'), password: this.configService.get('DB_PASSWORD'), userDB: this.configService.get('DB_USERDB_SL'), couchAuthDB: this.configService.get('DB_COUCH_AUTHDB'), publicURL: this.configService.get('DB_PUBLIC_URL'), }, mailer: { fromEmail: this.configService.get('MAILER_FROMUSER'), transport: this.configService.get('MAILER_SENDGRID_APIKEY') ? nodemailerSendgrid : undefined, options: this.configService.get('MAILER_SENDGRID_APIKEY') ? { apiKey: this.configService.get( 'MAILER_SENDGRID_APIKEY', ) as string, } : { host: this.configService.get('MAILER_HOST'), port: this.configService.get('MAILER_PORT'), secure: this.configService.get('MAILER_PORT') === '465' ? true : false, auth: { user: this.configService.get('MAILER_AUTHUSER'), pass: this.configService.get('MAILER_PASSWORD'), }, }, }, local: { // Custom names for the username and password fields in your sign-in form usernameField: 'email', passwordField: 'password', emailUsername: true, usernameLogin: false, // Send out a confirm email after each user signs up with local login sendConfirmEmail: false, // Require the email be confirmed before the user can login or before his changed email is updated requireEmailConfirm: false, }, providers: { google: { credentials: { clientID: this.configService.get('GOOGLE_CLIENT_ID'), clientSecret: this.configService.get('GOOGLE_CLIENT_SECRET'), audience: [ this.configService.get('GOOGLE_CLIENT_ID'), this.configService.get('GOOGLE_CLIENT_ID_CORDOVA'), ], }, options: { scope: ['email'], }, template: path.join( __dirname, './templates/oauth/my-custom-secure-auth-callback.ejs', ), templateTest: path.join( __dirname, './templates/oauth/my-custom-secure-auth-callback-test.ejs', ), }, facebook: { credentials: { clientID: this.configService.get('FACEBOOK_APP_ID'), clientSecret: this.configService.get('FACEBOOK_APP_SECRET'), profileFields: ['id', 'displayName', 'name', 'emails'], fbGraphVersion: 'v3.2', }, options: { scope: ['email', 'public_profile'], }, template: path.join( __dirname, './templates/oauth/my-custom-secure-auth-callback.ejs', ), }, }, userDBs: { defaultDBs: { private: ['interviewer'], }, model: { _default: { designDocs: [], }, interviewer: { type: 'private', adminRoles: ['admin', 'AdminActive'], appendSeparator: this.configService.get('DB_APPEND_SEPARATOR'), designDocs: [], //'consumos', 'sesiones', 'seguimientos' }, }, designDocDir: path.join(__dirname, './ddocs'), }, userModel: { whitelist: ['roles'], customValidators: { roleValidator: function (value: string[], validRoles: string[]) { if (!value || value.length < 1) { return 'El rol es requerido'; } try { value.forEach((role) => { if (!validRoles.includes(role)) { throw new Error(`: El rol no puede ser ${role}`); } }); } catch (error) { type validatorError = { message?: string }; const posibleError = error as validatorError; return posibleError?.message ?? 'Ocurrio un error'; } return null; }, }, validate: { roles: { roleValidator: ['user', 'Interviewer', 'Admin'], }, }, }, }; ```
fynnlyte commented 1 year ago

I don't know if the login route should also lowercase the email or the create should save the email as is sent, my quick fix will be to lowercase the email before sending the login request as I have had some users report that they couldn't login even when the browser password manager saved their credentials.

Yeah, lowercasing all emails before sending them to login is the correct workaround. I guess I'm always doing that on the client side, so I forgot about it. I've also been bitten by email casing issues in superlogin and moved towards lowercasing everything, e.g. in:

The .toLowerCase().trim() isn't yet included in the local strategy. Would be a good idea to add it there, too:

https://github.com/perfood/couch-auth/blob/master/src/local.ts#L52

Should be also fine when using username as login, since that's validated with /^[a-z0-9_-]{3,16}$/