Persona.verify return error 500

Hi all i need some help with Persona.
in my controller i’m trying to use the method Persona.verify

 async login({request, response, auth}) {
    const payload = request.only(['uid', 'password'])
    const user = await Persona.verify(payload)
    console.log(user)
    const token = await auth.generate(user)
    response.send(token)

i’m using vue.js and nuxt.js for the frontend.

what i’m not understand it’s when i enter the right information in the DB. it return me the token an all it’s fine.

but when i try with false uid , i receive the message: {message: “Cannot read property ‘uid.exists’ of undefined”, name: “TypeError”, status: 500,…}

or when i made with the right uid but the wrong password it return :
{message: “Cannot read property ‘password.mis_match’ of undefined”, name: “TypeError”, status: 500,…}

what it supposed to return is an validationException, i tried to search in the initial code of Persona.js but i’ dont see anything seem’s wrong.

1 Like

i know why it made this error.

in my config/persona.js file.
i think i wrote the bad method of validationMessages

validationMessages: (action) => {
    switch (action) {
      case 'register':
        return {
          'email.unique': 'This email is already used'
        }
      case 'login':
        return {
          'uid.exists': 'Invalid email or password',
          'password.mis_match': 'Invalid email or password'
        }
    }
  }

that’s the reason why it return error 500 i just need to change this code .

2 Likes

i have now a problem with the validationMessage function.

in the documentation the action method can take one of those list of actions

  1. register
  2. login
  3. emailUpdate
  4. passwordUpdate

but when i try this code

'password.mis_match': action === 'login' ? 'test A' : 'test B'

it return ‘test B’ so i tried a console.log('action = ’ + action)

and it return 2 values.

action = verify ( of course i tried to change ‘login’ by ‘verify’ but the result is the same )
action = undefined

can someone help me please ?

1 Like

Does it run validationMessages two times when trying to log in? :thinking:

You can see what’s calling validationMessage using console.trace() instead of console.log()

this the message i received with console.trace.

Trace: verify
    at Object.validationMessages (F:\dev\server\config\persona.js:96:13)
    at Persona.getMessages (F:\dev\server\node_modules\@adonisjs\persona\src\Persona.js:265:81)
    at Persona.runValidation (F:\dev\server\node_modules\@adonisjs\persona\src\Persona.js:389:78)
    at Persona.verify (F:\dev\server\node_modules\@adonisjs\persona\src\Persona.js:518:16)
    at AuthController.login (F:\dev\server\app\Controllers\Http\AuthController.js:23:32)
    at Server._routeHandler (F:\dev\server\node_modules\@adonisjs\framework\src\Server\index.js:121:31)
    at MiddlewareBase._resolveMiddleware (F:\dev\server\node_modules\@adonisjs\middleware-base\index.js:195:28)
    at Runnable._invoke (F:\dev\server\node_modules\co-compose\src\Runnable.js:76:42)
    at F:\dev\server\node_modules\co-compose\src\Runnable.js:73:34
    at f (F:\dev\server\node_modules\once\once.js:25:25)
    at AllowGuestOnly.handle (F:\dev\server\node_modules\@adonisjs\auth\src\Middleware\AllowGuestOnly.js:18:11)
    at processTicksAndRejections (internal/process/task_queues.js:93:5)


Trace: undefined
    at Object.validationMessages (F:\dev\server\config\persona.js:96:13)
    at Persona.getMessages (F:\dev\server\node_modules\@adonisjs\persona\src\Persona.js:265:81)
    at Persona._makeCustomMessage (F:\dev\server\node_modules\@adonisjs\persona\src\Persona.js:136:32)
    at Persona.getUserByUids (F:\dev\server\node_modules\@adonisjs\persona\src\Persona.js:449:25)
    at async Persona.verify (F:\dev\server\node_modules\@adonisjs\persona\src\Persona.js:519:18)
    at async AuthController.login (F:\dev\server\app\Controllers\Http\AuthController.js:23:18)
    at async Server._routeHandler (F:\dev\server\node_modules\@adonisjs\framework\src\Server\index.js:121:25)
    at async AllowGuestOnly.handle (F:\dev\server\node_modules\@adonisjs\auth\src\Middleware\AllowGuestOnly.js:18:5)
    at async ConvertEmptyStringsToNull.handle (F:\dev\server\app\Middleware\ConvertEmptyStringsToNull.js:13:5)
    at async BodyParser.handle (F:\dev\server\node_modules\@adonisjs\bodyparser\src\BodyParser\index.js:284:7)

still not resolve yet, i tried to console.log the request in the authController to see if i received from the client 2 request, but i received only one time the request. so i really don’t know how to do

when i login with the right ID, the validationMessage run one time only, i really don’t know what is calling the second ValidationMessage.

Finally Found it . the _makecustomMessage function doesn’t return the action of get.messages cause is not set.

I made modifications on :

_makeCustomMessage, verify, getUsersByUid , verifyPassword and updatePassword functions

here my Persona.js file modified and it know work great.

Hope can help :slight_smile:

'use strict'

/**
 * adonis-persona
 *
 * (c) Harminder Virk <virk@adonisjs.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

const moment = require('moment')
const randtoken = require('rand-token')
const GE = require('@adonisjs/generic-exceptions')

/**
 * Raised when token is invalid or expired
 *
 * @class InvalidTokenException
 */
class InvalidTokenException extends GE.LogicalException {
  static invalidToken () {
    return new this('The token is invalid or expired', 400)
  }
}

/**
 * The personna class is used to manage the user profile
 * creation, verification and updation with ease.
 *
 * @class Persona
 *
 * @param {Object} Config
 * @param {Object} Validator
 * @param {Object} Event
 * @param {Object} Hash
 */
class Persona {
  constructor (Config, Validator, Event, Encryption, Hash) {
    this.config = Config.merge('persona', {
      uids: ['email'],
      email: 'email',
      password: 'password',
      model: 'App/Models/User',
      newAccountState: 'pending',
      verifiedAccountState: 'active',
      dateFormat: 'YYYY-MM-DD HH:mm:ss'
    })

    /**
     * Varients of password fields
     */
    this._oldPasswordField = `old_${this.config.password}`
    this._passwordConfirmationField = `${this.config.password}_confirmation`

    this.Hash = Hash
    this.Event = Event
    this.Validator = Validator

    this._encrypter = Encryption.getInstance({ hmac: false })
    this._model = null
  }

  /**
   * Returns the email value from an object
   *
   * @method _getEmail
   *
   * @param  {Object}      payload
   *
   * @return {String}
   *
   * @private
   */
  _getEmail (payload) {
    return payload[this.config.email]
  }

  /**
   * Returns the password value from an object
   *
   * @method _getPassword
   *
   * @param  {Object}         payload
   *
   * @return {String}
   *
   * @private
   */
  _getPassword (payload) {
    return payload[this.config.password]
  }

  /**
   * Updates email field value on an object
   *
   * @method _setEmail
   *
   * @param  {Object}  payload
   * @param  {String}  email
   *
   * @private
   */
  _setEmail (payload, email) {
    payload[this.config.email] = email
  }

  /**
   * Sets password field value on an object
   *
   * @method _setPassword
   *
   * @param  {Object}     payload
   * @param  {String}     password
   *
   * @private
   */
  _setPassword (payload, password) {
    payload[this.config.password] = password
  }

  /**
   * Makes the custom message for a given key
   *
   * @method _makeCustomMessage
   *
   * @param  {String}           key
   * @param  {Object}           data
   * @param  {String}           defaultValue
   *@param {string}             action
   * @return {String}
   *
   * @private
   */
  _makeCustomMessage (key, data, defaultValue, action) {
    const customMessage = this.getMessages(action)[key]
    if (!customMessage) {
      return defaultValue
    }

    return customMessage.replace(/{{\s?(\w+)\s?}}/g, (match, group) => {
      return data[group] || ''
    })
  }

  /**
   * Adds query constraints to pull the right token
   *
   * @method _addTokenConstraints
   *
   * @param  {Object}            query
   * @param  {String}            type
   *
   * @private
   */
  _addTokenConstraints (query, type) {
    query
      .where('type', type)
      .where('is_revoked', false)
      .where('updated_at', '>=', moment().subtract(24, 'hours').format(this.config.dateFormat))
  }

  /**
   * Generates a new token for a user and given type. Ideally
   * tokens will be for verifying email and forgot password
   *
   * @method generateToken
   *
   * @param  {Object}      user
   * @param  {String}      type
   *
   * @return {String}
   *
   * @example
   * ```
   * const user = await User.find(1)
   * const token = await Persona.generateToken(user, 'email')
   * ```
   */
  async generateToken (user, type) {
    const query = user.tokens()
    this._addTokenConstraints(query, type)

    const row = await query.first()
    if (row) {
      return row.token
    }

    const token = this._encrypter.encrypt(randtoken.generate(16))
    await user.tokens().create({ type, token })
    return token
  }

  /**
   * Returns the token instance along with releated
   * users
   *
   * @method getToken
   *
   * @param  {String} token
   * @param  {String} type
   *
   * @return {Object|Null}
   *
   * @example
   * ```
   * const token = request.input('token')
   * const tokenRow = await Persona.getToken(token, 'email')
   *
   * if (!tokenRow) {
   *   // token is invalid or expired
   * }
   *
   * const user = tokenRow.getRelated('user')
   * ```
   */
  async getToken (token, type) {
    const query = this.getModel().prototype.tokens().RelatedModel.query()
    this._addTokenConstraints(query, type)

    const row = await query.where('token', token).with('user').first()
    return row && row.getRelated('user') ? row : null
  }

  /**
   * Remvoes the token from the tokens table
   *
   * @method removeToken
   *
   * @param  {String}    token
   * @param  {String}    type
   *
   * @return {void}
   */
  async removeToken (token, type) {
    const query = this.getModel().prototype.tokens().RelatedModel.query()
    await query.where('token', token).where('type', type).delete()
  }

  /**
   * Returns the model class
   *
   * @method getModel
   *
   * @return {Model}
   */
  getModel () {
    if (!this._model) {
      this._model = use(this.config.model)
    }
    return this._model
  }

  /**
   * Returns an object of messages to be used for validation
   * failures
   *
   * @method getMessages
   *
   * @param {String} action
   *
   * @return {Object}
   */
  getMessages (action) {
    return typeof (this.config.validationMessages) === 'function' ? this.config.validationMessages(action) : {}
  }

  /**
   * Returns the table in user
   *
   * @method getTable
   *
   * @return {String}
   */
  getTable () {
    return this.getModel().table
  }

  /**
   * Returns an object of registeration rules
   *
   * @method registerationRules
   *
   * @return {Object}
   */
  registerationRules () {
    return this.config.uids.reduce((result, uid) => {
      const rules = ['required']
      if (uid === this.config.email) {
        rules.push('email')
      }

      rules.push(`unique:${this.getTable()},${uid}`)

      result[uid] = rules.join('|')
      return result
    }, {
      [this.config.password]: 'required|confirmed'
    })
  }

  /**
   * Returns the validation rules for updating email address
   *
   * @method updateEmailRules
   *
   * @param  {String}         userId
   *
   * @return {Object}
   */
  updateEmailRules (userId) {
    if (!userId) {
      throw new Error('updateEmailRules needs the current user id to generate the validation rules')
    }

    return {
      [this.config.email]: `required|email|unique:${this.getTable()},${this.config.email},${this.getModel().primaryKey},${userId}`
    }
  }

  /**
   * Returns the validation rules for updating the passowrd
   *
   * @method updatePasswordRules
   *
   * @param {Boolean} enforceOldPassword
   *
   * @return {Object}
   */
  updatePasswordRules (enforceOldPassword = true) {
    const rules = {
      [this.config.password]: 'required|confirmed'
    }

    /**
     * Enforcing to define old password
     */
    if (enforceOldPassword) {
      rules[this._oldPasswordField] = 'required'
    }

    return rules
  }

  /**
   * Returns an object of loginRules
   *
   * @method loginRules
   *
   * @return {String}
   */
  loginRules () {
    return {
      'uid': 'required',
      [this.config.password]: 'required'
    }
  }

  /**
   * Mutates the registeration payload in the shape that
   * can be inserted to the database
   *
   * @method massageRegisterationData
   *
   * @param  {Object}                 payload
   *
   * @return {void}
   */
  massageRegisterationData (payload) {
    delete payload[this._passwordConfirmationField]
    payload.account_status = this.config.newAccountState
  }

  /**
   * Runs validations using the validator and throws error
   * if validation fails
   *
   * @method runValidation
   *
   * @param  {Object}      payload
   * @param  {Object}      rules
   * @param  {String}      action
   *
   * @return {void}
   *
   * @throws {ValidationException} If validation fails
   */
  async runValidation (payload, rules, action) {
    const validation = await this.Validator.validateAll(payload, rules, this.getMessages(action))

    if (validation.fails()) {
      throw this.Validator.ValidationException.validationFailed(validation.messages())
    }
  }

  /**
   * Verifies two password and throws exception when they are not
   * valid
   *
   * @method verifyPassword
   *
   * @param  {String}       newPassword
   * @param  {String}       oldPassword
   * @param  {String}       [field = this.config.password]
   * @param {string}        action
   *
   * @return {void}
   */
  async verifyPassword (newPassword, oldPassword, field = this.config.password, action) {
    if (typeof action === "undefined") {
      action = field
      field = this.config.password
    }
    const verified = await this.Hash.verify(newPassword, oldPassword)
    if (!verified) {
      const data = { field, validation: 'mis_match', value: newPassword }
      throw this.Validator.ValidationException.validationFailed([
        {
          message: this._makeCustomMessage(`${field}.mis_match`, data, 'Invalid password', action),
          field: field,
          validation: 'mis_match'
        }
      ])
    }
  }

  /**
   * Finds the user by looking for any of the given uids
   *
   * @method getUserByUids
   *
   * @param  {String}      value
*   @param {string}        action = 'undefined'
   * @return {Object}
   */
  async getUserByUids (value, action = 'undefined') {
    const userQuery = this.getModel().query()

    /**
     * Search for all uids to allow login with
     * any identifier
     */
    this.config.uids.forEach((uid) => userQuery.orWhere(uid, value))

    /**
     * Search for user
     */
    const user = await userQuery.first()

    if (!user) {
      const data = { field: 'uid', validation: 'exists', value }

      throw this.Validator.ValidationException.validationFailed([
        {
          message: this._makeCustomMessage('uid.exists', data, 'Unable to locate user', action),
          field: 'uid',
          validation: 'exists'
        }
      ])
    }


    return user
  }

  /**
   * Creates a new user account and email verification token
   * for them.
   *
   * This method will fire `user::created` event.
   *
   * @method register
   *
   * @param  {Object}   payload
   * @param  {Function} callback
   *
   * @return {User}
   *
   * @example
   * ```js
   * const payload = request.only(['email', 'password', 'password_confirmation'])
   * await Persona.register(payload)
   * ```
   */
  async register (payload, callback) {
    await this.runValidation(payload, this.registerationRules(), 'register')
    this.massageRegisterationData(payload)

    if (typeof (callback) === 'function') {
      await callback(payload)
    }

    const user = await this.getModel().create(payload)

    /**
     * Get email verification token for the user
     */
    const token = await this.generateToken(user, 'email')

    /**
     * Fire new::user event to app to wire up events
     */
    this.Event.fire('user::created', { user, token })

    return user
  }

  /**
   * Verifies user credentials
   *
   * @method verify
   *
   * @param  {Object} payload
   * @param  {Function} callback
   *
   * @return {User}
   *
   * @example
   * ```js
   * const payload = request.only(['uid', 'password'])
   * await Persona.verify(payload)
   * ```
   */
  async verify (payload, callback) {
    await this.runValidation(payload, this.loginRules(), 'verify')
    const user = await this.getUserByUids(payload.uid, 'verify')

    const enteredPassword = this._getPassword(payload)
    const userPassword = this._getPassword(user)

    if (typeof (callback) === 'function') {
      await callback(user, enteredPassword)
    }

    await this.verifyPassword(enteredPassword, userPassword, 'verify')

    return user
  }

  /**
   * Verifies the user email address using a unique
   * token associated to their account
   *
   * @method verifyEmail
   *
   * @param  {String}    token
   *
   * @return {User}
   *
   * @example
   * ```js
   * const token = request.input('token')
   * await Persona.verifyEmail(token)
   * ```
   */
  async verifyEmail (token) {
    const tokenRow = await this.getToken(token, 'email')
    if (!tokenRow) {
      throw InvalidTokenException.invalidToken()
    }

    const user = tokenRow.getRelated('user')

    /**
     * Update user account only when in the newAccountState
     */
    if (user.account_status === this.config.newAccountState) {
      user.account_status = this.config.verifiedAccountState
      await user.save()
      await this.removeToken(token, 'email')
    }

    return user
  }

  /**
   * Updates the user email address and fires an event for same. This
   * method will fire `email::changed` event.
   *
   * @method updateEmail
   *
   * @param  {Object}    user
   * @param  {String}    newEmail
   *
   * @return {User}
   *
   * @example
   * ```js
   * const user = auth.user
   * const newEmail = request.input('email')
   *
   * if (user.email !== newEmail) {
   *   await Persona.updateEmail(user, newEmail)
   * }
   * ```
   */
  async updateEmail (user, newEmail) {
    await this.runValidation({ [this.config.email]: newEmail }, this.updateEmailRules(user.primaryKeyValue), 'emailUpdate')

    const oldEmail = this._getEmail(user)

    /**
     * Updating user details
     */
    user.account_status = this.config.newAccountState
    this._setEmail(user, newEmail)
    await user.save()

    /**
     * Getting a new token for verifying the email and firing
     * the event
     */
    const token = await this.generateToken(user, 'email')
    this.Event.fire('email::changed', { user, oldEmail, token })

    return user
  }

  /**
   * Update user profile. Updating passwords is not allowed here. Also
   * if email is provided, then this method will internally call
   * `updateEmail`.
   *
   * @method updateProfile
   *
   * @param  {Object}      user
   * @param  {Object}      payload
   *
   * @return {User}
   *
   * @example
   * ```js
   * const user = auth.user
   * const payload = request.only(['firstname', 'lastname', 'email'])
   *
   * await Persona.updateProfile(user, payload)
   * ```
   */
  async updateProfile (user, payload) {
    /**
     * Do not allow changing passwords here. Password flow needs
     * old password to be verified
     */
    if (this._getPassword(payload)) {
      throw new Error('Changing password is not allowed via updateProfile method. Instead use updatePassword')
    }

    const newEmail = this._getEmail(payload)
    const oldEmail = this._getEmail(user)

    /**
     * Update new props with the user attributes
     */
    user.merge(payload)

    if (newEmail !== undefined && oldEmail !== newEmail) {
      /**
       * We need to reset the user email, since we are calling
       * updateEmail and it needs user old email address
       */
      this._setEmail(user, oldEmail)
      await this.updateEmail(user, newEmail)
    } else {
      await user.save()
    }

    return user
  }

  /**
   * Updates the user password. This method will emit `password::changed` event.
   *
   * @method updatePassword
   *
   * @param  {Object}       user
   * @param  {Object}       payload
   *
   * @return {User}
   *
   * @example
   * ```js
   * const user = auth.user
   * const payload = request.only(['old_password', 'password', 'password_confirmation'])
   *
   * await Persona.updatePassword(user, payload)
   * ```
   */
  async updatePassword (user, payload) {
    await this.runValidation(payload, this.updatePasswordRules(), 'passwordUpdate')

    const oldPassword = payload[this._oldPasswordField]
    const newPassword = this._getPassword(payload)
    const existingOldPassword = this._getPassword(user)

    await this.verifyPassword(oldPassword, existingOldPassword, this._oldPasswordField, 'passwordUpdate')

    this._setPassword(user, newPassword)
    await user.save()

    this.Event.fire('password::changed', { user })

    return user
  }

  /**
   * Finds the user using one of their uids and then fires
   * `forgot::password` event with a temporary token
   * to update the password.
   *
   * @method forgotPassword
   *
   * @param  {String}       email
   *
   * @return {void}
   *
   * @example
   * ```js
   * const email = request.input('email')
   * await Persona.forgotPassword(email)
   * ```
   */
  async forgotPassword (uid) {
    const user = await this.getUserByUids(uid)
    const token = await this.generateToken(user, 'password')

    this.Event.fire('forgot::password', { user, token })
  }

  /**
   * Updates the password for user using a pre generated token. This method
   * will fire `password::recovered` event.
   *
   * @method updatePasswordByToken
   *
   * @param  {String}              token
   * @param  {Object}              payload
   *
   * @return {User}
   *
   * @example
   * ```js
   * const token = request.input('token')
   * const payload = request.only(['password', 'password_confirmation'])
   *
   * await Persona.updatePasswordByToken(token, payload)
   * ```
   */
  async updatePasswordByToken (token, payload) {
    await this.runValidation(payload, this.updatePasswordRules(false), 'passwordUpdate')

    const tokenRow = await this.getToken(token, 'password')
    if (!tokenRow) {
      throw InvalidTokenException.invalidToken()
    }

    const user = tokenRow.getRelated('user')
    this._setPassword(user, this._getPassword(payload))

    await user.save()
    await this.removeToken(token, 'password')

    this.Event.fire('password::recovered', { user })
    return user
  }
}

module.exports = Persona



EDIT : I don’t know if i have the right to share this modifications so if admins thinks not you can delete my post .

1 Like