Next-auth: Mongoose: user.emailToken stays after sign in

Created on 22 Feb 2018  路  8Comments  路  Source: nextauthjs/next-auth

If in next-auth.functions.js pass Model instance of Mongoose rather then Object Collection instance of MongoClient, in this case user.emailToken stays after signing in till next email sign in.

//  User.js
const mongoose = require('mongoose')

const UserSchema = new mongoose.Schema({
  _id          : mongoose.Schema.Types.ObjectId,
  pass         : String,
  name         : String,
  email        : String,
  google       : Object,
  admin        : Boolean,
  emailVerified: Boolean,
  emailToken   : String
})

mongoose.model('User', UserSchema)
//  next-auth.functions.js
const mongoose = require('mongoose')

require('./models/User')
const User = mongoose.model('User')

module.exports = () => {
  new Promise((resolve, reject) => {
    if (!User) reject('new Error(\'\\n  connection error\')')
    resolve(User)
  }).then((User) => {
    return Promise.resolve({

      find: ({id, email, emailToken, provider} = {}) => {
        let query = {}

        if (id) {
          query = {_id: ObjectId(id)}
        } else if (email) {
          query = {email: email}
        } else if (emailToken) {
          query = {emailToken: emailToken}
        } else if (provider) {
          query = {[`${provider.name}.id`]: provider.id}
        }

        return new Promise((resolve, reject) => {
          User.findOne(query, (err, user) => {
            return err ? reject((err)) : resolve(user)
          })
        })
      },

      insert: (user, oAuthProfile) => {
        return new Promise((resolve, reject) => {
          User.insert(user, (err, response) => {
            if (err) return reject(err)
            if (!user._id && response._id) user._id = response._id
            return resolve(user)
          })
        })
      },

      update: (user, profile) => {
        return new Promise((resolve, reject) => {
          User.update({_id: ObjectId(user._id)}, user, {}, err => {
           **// it passes user.emailToken after sign in**
            return err ? reject(err) : resolve(user)
          })
        })
      },

      remove: (id) => {
        return new Promise((resolve, reject) => {
          User.remove({_id: ObjectId(id)}, (err) => {
            if (err) return reject(err)
            return resolve(true)
          })
        })
      },

      serialize: (user) => {
        if (user.id) {
          return Promise.resolve(user.id)
        } else if (user._id) {
          return Promise.resolve(user._id)
        } else {
          return Promise.reject(new Error("Unable to serialise user"))
        }
      },

      deserialize: (id) => {
        return new Promise((resolve, reject) => {
          User.findOne({_id: ObjectId(id)}, (err, user) => {
            !!err && reject(err)
            !user && resolve(null)

            return resolve({
              id           : user._id,
              name         : user.name,
              email        : user.email,
              emailVerified: user.emailVerified,
              admin        : user.admin || false,
            })
          })
        })
      },
      sendSignInEmail: ({
        email = null,
        url = null
      } = {}) => {
        nodemailer
          .createTransport(nodemailerTransport)
          .sendMail({
            to     : email,
            from   : process.env.EMAIL_FROM,
            subject: 'Sign in link',
            text   : `Use the link below to sign in:\n\n${url}\n\n`,
            html   : `<p>Use the link below to sign in:</p><p>${url}</p>`
          }, (err) => {
            if (err) {
              console.error('Error sending email to ' + email, err)
            }
          })
        if (process.env.NODE_ENV === 'development') {
          console.log('------>>>>> Generated sign in link ' + url + ' for ' + email)
        }
      }
    })
  })
}
}

or am i wrong somewhere?

bug enhancement

Most helpful comment

Thanks for sharing! Hope to get back to this (for 2.0) very soon!

All 8 comments

Thanks for sharing your config!

Hmm yes this might be a problem with how NextAuth currently works and about the design being too mongo specific. The email token SHOULD NOT be present on the user object on sign in.

To get this to work with Mongoose you will need to check for the property and explicitly do something like unset it and save the change (or call update with unset).

https://stackoverflow.com/questions/4486926/delete-a-key-from-a-mongodb-document-using-mongoose

  1. As an interim solution, I could add a third property to indicate which properties were removed.

e.g.

  • Removing an email token: update(user, profile, { delete: 'emailToken' })
  • Unlinking Facebook: update(user, profile, { delete: 'facebook' })
  • Unlinking Google: update(user, profile, { delete: 'google' })

This is a bit simple but is non breaking and would make it easier to support as you could just check for a options.delete and tell Mongoose to remove that key if it is present.

I am happy to add this today if you want.

  1. For 2.0 we will factor in this stuff into the API and make it much easier to write drivers (and will try and support with Mongo DB, Mongoose and MySQL by default).

Thank you again for sharing your config. If you don't mind, I'd like to include it in the examples as people ask for a Mongoose example quite a bit.

Thank you for a cool lib!
It would be nice to have at least a solution with a third property, like above!
Thank you anyway once again!
Greets!

Okies, I'm having problems with Mongoose not connecting to Mongo which is super weird.

I have published the above as 1.8.1 though (third option on update()).

I'd be really interested if it works for you. We still don't have a Mongoose reference example yet.

I'll probably try and figure out why Mongoose isn't working over the weekend.

It works for me.
Thank you!

Thanks so much for letting me know! :-)

I will figure out what's wrong with my local instance and use the config you've provided as a base for it so other folks have something they can copy/paste to get started (at least until we have 2.0).

Hello Iain.
I've found a couple 馃悶 in config i posted above. Here are som corrections if needed:


import mongoose from 'mongoose'
import User     from './server/models/User'

// Use Node Mailer for email sign in
import nodemailer                from 'nodemailer'
import nodemailerSmtpTransport   from 'nodemailer-smtp-transport'
import nodemailerDirectTransport from 'nodemailer-direct-transport'

// Load environment variables from a .env file if one exists
require( 'dotenv' ).load()

//  Connect mongoose to DB
const MONGO_URI = process.env.MONGO_URI

mongoose.connect( MONGO_URI, {dbName: '<BD_NAME>'} )
mongoose.connection.on( 'connected', () => {
  console.log( '馃敆 Mongoose successfully connected to DB' )
} )

const ObjectId = mongoose.Types.ObjectId

// Send email direct from localhost if no mail server configured
let nodemailerTransport = nodemailerDirectTransport()
if (process.env.EMAIL_SERVER && process.env.EMAIL_USERNAME && process.env.EMAIL_PASSWORD) {
  nodemailerTransport = nodemailerSmtpTransport( {
    host  : process.env.EMAIL_SERVER,
    port  : process.env.EMAIL_PORT || 25,
    secure: process.env.EMAIL_SECURE,
    auth  : {
      user: process.env.EMAIL_USERNAME,
      pass: process.env.EMAIL_PASSWORD
    }
  } )
}

module.exports = () => {
  return new Promise( ( resolve, reject ) => {
    return mongoose.connection ? resolve( User ) : reject( 'Error! No connection with DB' )
  } )
    .then( User => {
        return Promise.resolve( {
            // If a user is not found find() should return null (with no error).
            find           : ( {id, email, emailToken, provider} = {} ) => {
              let query = {}

              if (id) {
                query = {_id: ObjectId( id )}
              } else if (email) {
                query = {email: email}
              } else if (emailToken) {
                query = {emailToken: emailToken}
              } else if (provider) {
                query = {[ `${provider.name}.id` ]: provider.id}
              }

              return new Promise( ( resolve, reject ) => {
                User
                  .findOne( query )
                  .then( user => resolve( user ? user.toJSON() : null ) ) 
                  .catch( err => reject( err ) )
              } )
            },

            insert         : ( user, oAuthProfile ) => {
              return new Promise( ( resolve, reject ) => {
                // next-auth returns normalized user, so the rest of fields needed from oAuthProfile  must be added here
                if (user.google) user.google.avatarURL = oAuthProfile && oAuthProfile.photos ? oAuthProfile.photos[ 0 ].value : null
                User
                  .create( user )
                  .then( response => {
                    if (!user._id && response._id) user._id = response._id
                    resolve( user )
                  } )
                  .catch( err => reject( err ) )
              } )
            },

            update         : ( user, profile, field ) => {
              return new Promise( ( resolve, reject ) => {
                const mod = field ? {$unset: {[ field.delete ]: 1}} : user

                User
                  .update( {_id: ObjectId( user._id )}, mod, {new: true} )
                  .then( resp => resolve( user.toJSON() ) )
                  .catch( err => reject( err ) )
              } )
            },

            remove         : ( id ) => {
              return new Promise( ( resolve, reject ) => {
                User.remove( {_id: ObjectId( id )}, ( err ) => {
                  if (err) return reject( err )
                  return resolve( true )
                } )
              } )
            },

            serialize      :
              ( user ) => {
                // Supports serialization from Mongo Object *and* deserialize() object
                if (user.id) {
                  // Handle responses from deserialize()
                  return Promise.resolve( user.id )
                } else if (user._id) {
                  // Handle responses from find(), insert(), update()
                  return Promise.resolve( user._id )
                } else {
                  return Promise.reject( new Error( "Unable to serialise user" ) )
                }
              },

            deserialize    :
              ( id ) => {
                return new Promise( ( resolve, reject ) => {
                  User.findOne( {_id: ObjectId( id )}, ( err, user ) => {
                    if (err) return reject( err )

                    // If user not found (e.g. account deleted) return null object
                    if (!user) return resolve( null )

                    return resolve( {
                      id           : user._id,
                      name         : user.name,
                      email        : user.email,
                      emailVerified: user.emailVerified,
                      admin        : user.admin || false
                    } )
                  } )
                } )
              },


            sendSignInEmail:
              ( {email, url, req} ) => {
                nodemailer
                  .createTransport( nodemailerTransport )
                  .sendMail( {
                    to     : email,
                    from   : process.env.EMAIL_FROM,
                    subject: 'Sign in link',
                    text   : `Use the link below to sign in:\n\n${url}\n\n`,
                    html   : `<p>Use the link below to sign in:</p><p>${url}</p>`
                  }, ( err ) => {
                    if (err) {
                      console.error( 'Error sending email to ' + email, err )
                    }
                  } )
                if (process.env.NODE_ENV === 'development') {
                  console.log( 'Generated sign in link ' + url + ' for ' + email )
                }
              }
          }
        )
      }
    )
}

should work )

Thanks for sharing! Hope to get back to this (for 2.0) very soon!

Um, apparently 'very soon' has been 2 years. 馃檮

The good news is have actually been on it and version 2.0 is right around the corner!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

benoror picture benoror  路  3Comments

readywater picture readywater  路  3Comments

alex-cory picture alex-cory  路  3Comments

ryanbahan picture ryanbahan  路  3Comments

iaincollins picture iaincollins  路  3Comments