0.1.4 • Published 6 years ago

node-devise v0.1.4

Weekly downloads
4
License
MIT
Repository
github
Last release
6 years ago

node-devise

Another flexible authentication solution for mongoosejs. Inspired by irina.

standard travis Code Coverage npm

See complete MVC solution based on Restify engines;

Restify Devise

Installation:

npm install node-devise --save

Requires:

Node >8.0.0.

Mongoose >5.0.0.

Summary

Usage

Standard installation of the plugin:

const mongoose = require('mongoose')
const devise = require('node-devise')

const UserSchema = new mongoose.Schema({})
UserSchema.plugin(devise)

mongoose.model('User', UserSchema)

Overview

Authenticable

authenticatable Module, responsible for hashing the password and validating the authenticity of a user while signing in.

Options

authenticatable will set the following options to schema:

* +authenticationField+: parameter used to set the name of the authentication field

* +authenticationFieldType+: by default, `node-devise` will set the authentication field a Email type

* +passwordField+: parameter used to set the field that triggers the password hash

* +hashedPasswordField+: parameter used to set the name of the hashed field

* +maximumAllowedFailedAttempts+: number of attempts allowed before lockout of account. By default 3
Examples
UserSchema.plugin(devise, {
  authenticationField: 'username',
  authenticationFieldType: String,
  maximumAllowedFailedAttempts: 5
})
Custom message options
KeyDescription
authenticatorErrorMessageparameter used to change the authentication error message
passwordErrorMessagewhen the password is not entered
passwordNotMatchErrorMessageused in the comparePassword method to inform that the password does not match
hashedPasswordErrorMessageused in the comparePassword method to inform that the current instance does not have a password hash
authenticatorNotExistErrorMessageused in the authenticate static method to inform that the string does not have a match in the database
credentialsNotExistErrorMessageused in the authenticate static method to report that the credential is incorrect

Instance Methods

#encryptPassword(password) :

instance method used to encrypt the current password. This method is called when the instance receives any value in the password field or when it makes a manual call.

Examples
// encypt instance password
await user.encryptPassword('secret')

#comparePassword(password) :

an instance method which takes in a plain string password and compare with the instance hashed password to see if they match.

Examples
// after having model instance, returns true or an exception
const res = await user.comparePassword('secret')

#changePassword(newPassword) :

an instance method that uses a single string in the newPassword parameter and sets it to the current password of that authenticated instance.

Examples
// after having model instance, returns true or an exception
const res = await user.changePassword('new_secret')

#authenticate(password) :

authenticates the user's instance from a password passed by parameter.

Examples
// after having model instance, returns true or an exception
await user.authenticate(credentials.password)

Static Methods

#authenticate(credentials, opts) :

a static method which takes in credentials in the format below:

const faker = require('faker')

const credentials = {
  email: faker.internet.email(),
  password: faker.internet.password()
}

where a valid email and password corresponding to a database user must be reported. At the end, the user of the provided credentials will be returned, otherwise the corresponding errors will be thrown.

Examples
// returns current object or an exception
const user = await User.authenticate(credentials)

internally the authenticate static method calls an authenticate instance method that can also be ivoked by the instance object receiving in the parameter the current password.

Confirmable

confirmable's main responsability is to verify if an account is already confirmed to sign in, and to send emails with confirmation instructions. Confirmation instructions are sent to the user email after creating a record and when manually requested by a new confirmation instruction request.

Confirmable adds the following columns:
  • confirmationToken is a unique random token
  • confirmedAt is a timestamp when the user clicked the confirmation link
  • confirmationSentAt is a timestamp when the confirmationToken was generated (not sent)
  • confirmationTokenExpiryAt is an attribute that keep tracks of when the confirmation token will expiry. Beyond that, new confirmation token will be generated and notification will be send.
Examples
const user = await User.findOne({ email: credentials.email })
try {
  await user.confirm()          // returns true or an exception
  await user.isConfirmed()      // returns true or an exception
  await user.sendConfirmation() // manually send instructions
} catch (error) {
  console.log(error.stack)
}
Options

confirmable will set the following options:

* +confirmable.tokenLifeSpan+: specifies the lifetime of the token, by default 3 days.

Custom message options

KeyDescription
invalidConfirmationTokenErrorMessageparameter used to tell that the commit token is invalid
confirmationTokenExpiredErrorMessageinforms you that the confirmation token has expired
accountNotConfirmedErrorMessagewhen the account is not confirmed
checkConfirmationTokenExpiredErrorMessagewhen the confirmation token has expired and a new email for verification is sent

Instance Methods

#generateConfirmationToken(opts = { save: true }) :

an attribute that keep tracks of when the confirmation token will expiry. Beyond that, new confirmation token will be generated and notification will be send.

Examples
const res = await user.generateConfirmationToken({ save: false })

// when `save = false`
await res.save()

#sendConfirmation(opts = { save: true }) :

This instance method is utilized by model.send and sends the confirmation notification. If sent successfully, it will update confirmationSentAt instance attribute with the current time stamp and persist the instance before return it.

Examples
// sends a confirmation notification and saves the send date in the `ConfirmationSentAt` parameter
const res = await user.sendConfirmation({ save: false })

// when `save = false`
await res.save()

#isConfirmed() :

verifies whether a user is confirmed or not.

Examples
// after having model instance, returns true or an exception
const res = await user.isConfirmed()

#confirm() :

confirm a user by setting it's confirmedAt to actual time and persist the instance before return it.

Examples
// after having model instance, returns true or an exception
const res = await user.confirm()

Static Methods

#confirm(confirmationToken) :

this static method takes the given confirmationToken and confirms un-confirmed registration which matches the given confirmation token.

Examples
// returns current object or an exception
const res = await User.confirm('confirmationToken')

Lockable

provide a means of locking an account after a specified number of failed sign-in attempts (defaults to 3 attempts). user can unlock account through unlock instructions sent. It extend the model with the following.

Lockable adds the following columns:
  • failedAttempt is an attribute which keeps track of failed login attempts.
  • lockedAt is an attribute which keeps track of the moment account is locked.
  • unlockedAt is an attribute which keeps track of the moment account is unlocked.
  • unlockToken is an attribute which store the current unlock token of the locked account.
  • unlockTokenSentAt is an attribute which keeps track of when the unlock token notification sent.
  • unlockTokenExpiryAt is an attribute which keep track of unlockToken expiration. If unlockToken is expired, a new token will get generated and set.
Examples
const user = await User.findOne({ email: credentials.email })
try {
  // block account
  await user.lock()                     // returns true or an exception
  await user.isLocked()                 // returns true or an exception
  await user.sendUnlock()               // manually send instructions

  // unlocks account
  await User.unlock(user.unlockToken)   // returns current account or an exception
  await user.resetFailedAttempts()      // reset invalid login attempts
} catch (error) {
  console.log(error.stack)
}
Options

lockable will set the following options:

* +lockable.tokenLifeSpan+: specifies the lifetime of the token, by default 3 days.

Custom message options

KeyDescription
accountLockedErrorMessageparameter used to change the account lockout error message.
invalidUnlockTokenErrorMessageinforms that the unlock token is invalid.
unlockTokenExpiredErrorMessageunlocking token already expired.

Instance Methods

#generateUnlockToken(opts = { save: true }) :

an instance method that generate unlockToken and unlockTokenExpiryAt. Instance will get persisted before returned otherwise corresponding errors will get returned.

Examples
const res = await user.generateUnlockToken({ save: false })

// when `save = false`
await res.save()

#sendUnlock(opts = { save: true }) :

an instance method which send account locked notification to the owner. It will set unlockTokenSentAt to track when the lock notification is sent. Instance will get updated before it returns otherwise corresponding errors will get returned.

Examples
// sends a unlock notification and saves the send date in the `unlockTokenSentAt` parameter
const res = await user.sendUnlock({ save: false })

// when `save = false`
await res.save()

#isLocked() :

verifies whether a user is locked or not.

Examples
// after having model instance, returns false if not locked or an exception
const res = await user.isLocked()

#resetFailedAttempts() :

clear previous failed attempts.

Examples
// set failedAttempts = 0
const res = await user.resetFailedAttempts()

#lock(opts) :

an instance method that is used to lock an account. When invoked, it will check if the number of failedAttempts is greater than the configured maximum allowed login attempts, if so the account will get locked by setting lockedAt to the current timestamp of lock invocation. Instance will get persisted before it returns otherwise corresponding errors will get returned.

Examples
// after having model instance, returns true or an exception
const res = await user.lock()

Static Methods

#unlock(unlockToken) :

a model static method which unlock a locked account with the provided unlockToken. If the token is expired the new unlockToken will get generated. If token is valid, locked account will get unlocked and unlockedAt attribute will be set to current timestamp and failedAttempts will get set to 0. Instance unlocked will get persisted before it returns otherwise corrensponding errors will get returned.

Examples
// returns current object or an exception
const res = await User.unlock('unlockToken')

Recoverable

lays out infrastructure of resets of the user passwords and sends reset instructions. It extends model with the following.

Recoverable adds the following columns:
  • recoveryToken is an attribute that store recovery token.
  • recoveryTokenExpiryAt is an attribute that track when the recoverable token is expiring.
  • recoverySentAt is an attribute that keep track of the moment the recovery notification is sent.
  • recoveredAt is an attribute which keeps track of the moment the password was recovered.
Examples
try {
  // creates a new token and send it with instructions about how to reset the password
  const user = await User.requestRecover({ email: credentials.email })

  // resets the user password and save the record
  await User.recover(user.recoveryToken, 'new_secret')
} catch (error) {
  console.log(error.stack)
}
Options

recoverable will set the following options:

* +recoverable.tokenLifeSpan+: specifies the lifetime of the token, by default 3 days.

Custom message options

KeyDescription
invalidRecoveryDetailsErrorMessageparameter used to change the recovery details message is invalid.
invalidRecoveryTokenErrorMessageinforms message to invalid recovery token.
recoveryTokenExpiredErrorMessageused to change expired recovery token message.

Instance Methods

#generateRecoveryToken(opts = { save: true }) :

an instance method which is used to generate recoveryToken and set recoveryTokenExpiryAt timestamp. Instance will get persisted before it returns.

Examples
const res = await user.generateRecoveryToken({ save: false })

// when `save = false`
await res.save()

#sendRecovery(opts = { save: true }) :

an instance method which is used to send recovery notification to the user. It will set recoveryTokenSentAt timestamp. Instance will get persisted before it returns.

Examples
const res = await user.sendRecovery({ save: false })

// when `save = false`
await res.save()

#recover(recoveryToken, newPassword) :

a model static method which is used to recover an account with the matched recoverToken. The newPassword provided will get encrypted before set as user password. It will set recoveredAt before it persists the model.

Examples
// returns current object or an exception
const res = await User.recover('recoveryToken', 'new_password')

Static Methods

#requestRecover(credentials, opts) :

a model static method which is used to request account password recovery. It utilize generateRecoveryToken and sendRecovery to generate recovery token and send it.

Examples
// returns current object or an exception
const res = await User.requestRecover({ email })

Registerable

handles signing up users through a registration process, also allowing them to edit and destroy their account.

Registerable adds the following columns:
  • registeredAt is an attribute which keeps track of when an account is registered.
  • unregisteredAt is an attribute which keep tracks of when an account is unregistered.
Examples
const faker = require('faker')
try {
  const user = await User.register({
    email: faker.internet.email().toLowerCase(),
    password: faker.internet.password()
  })
  const res = await user.unregister()
} catch (error) {
  console.log(error.stack)
}
Options

recoverable will set the following options:

* +registerable.autoConfirm+: allows registered accounts to be pre-confirmed.

Custom message options

KeyDescription
credentialsNotExistErrorMessageparameter used to change the bad credential message.
authenticatorAlreadyExistErrorMessagechange the message for existing accounts.

Instance Methods

#unregister() :

an instance method which allows unregistering (destroy a user). The implementation currently is to set unregiesteredAt to current timestamp of the invocation. Instance will get persisted before is returned otherwise corresponding errors will be returned.

// returns true or an exception
const res = await user.unregister()

Static Methods

#register(credentials, opts) :

a model static method which is used to register provided credentials. It takes care of checking if then email is taken and validating credentials. It will return registered user otherwise corresponding registration errors.

const faker = require('faker')

const credentials = {
  email: faker.internet.email(),
  password: faker.internet.password()
}

// returns current object or an exception
const user = await User.register(credentials)

Trackable

provide a means of tracking user signin activities. It extends provided model with the following.

Trackable adds the following columns:
  • signInCount increased every time a sign in is made (by form).
  • currentSignInAt a timestamp updated when the user signs in.
  • currentSignInIpAddress keeps track of the latest IP address a user used to log.
  • lastSignInAt holds the timestamp of the previous sign in.
  • lastSignInIpAddress holds the remote ip of the previous sign in.
Examples
const user = await User.findOne({ email: credentials.email })

try {
  await user.track('ipAddress')
} catch (error) {
  console.log(error.stack)
}

Instance Methods

track(ipAddress) :

this is model instance method, which when called with the IP address, it will update current tracking details and set the provided IP address as the currentSignInIpAddress.

Examples
const faker = require('faker')
const user = await User.findOne({ email: faker.internet.email() })

// returns true or an exception
await user.track(faker.internet.ip())

Sending Notifications

the default implementation of node-devise to send notifications is noop. This is because there are different use case(s) when it comes on sending notifications.

due to that reason, node-devise requires that your model implements send method which accept record, action, done as its argurments.

record :

refer to the current user model instance.

action :

refer to the type of notifcation to be sent. There are just three types which are account_confirmation, account_recovery and password_recovery which are sent when new account is registered, when an account is locked and need to be unlocked and when account is requesting to recover the password respectively.

done :

is the callback that you must call after finishing sending the notification. By default this callback will update the notification send details based on the usage.

How to implement a send

simple add send into your model as instance methods.

Examples
const UserSchema = new Schema({})

// override send method i.e your notification sending: email, sms, etc
schema.methods.send = function (record, action, done) {

  // below are the methods that dispatch the data to send via method `done`
  //
  // await User.requestRecover({ email }, { req })
  // await User.register({ email, password }, { req })
  // await User.unlock(req.user.unlockToken, { req })
  // await User.authenticate({ email, password }, { req })

  done((opts) => {
    // const host = isObject(opts) && (isObject(opts.req) && opts.req.isSecure())
    //   ? 'https'
    //   : 'http' + '://' + uri

    switch (action) {
      // if we send `confirmation email`
      case 'account_confirmation':
        console.log('Action type: %s.\nRecord: %s \n', action, JSON.stringify(record))
        break

      // if we send `account recovery`
      case 'password_recovery':
        console.log('Action type: %s.\nRecord: %s \n', action, JSON.stringify(record))
        break

      // if we send `account locked` information
      case 'account_recovery':
        console.log('Action type: %s.\nRecord: %s \n', action, JSON.stringify(record))
        break

      default:
        console.log('mailer', 'Template not found')
    }
  })
}

Sending issues

It is recommended to use job queue like kue when implementing your send to reduce your API response time.

i18n

it is recommended to use an internalizer such as the i18nex or node-i18n when implementing the foreign language support layer.

the node-devise uses messages with the i18n. To customize your application, you can configure your locale file and use the i18n key in the installation plug-in:

{
  'en-US': {
    // authenticable
    authenticatorErrorMessage: 'No {{field}} provided',
    passwordErrorMessage: 'No password provided',
    passwordNotMatchErrorMessage: 'Incorrect password',
    hashedPasswordErrorMessage: 'Hashed password not found',
    authenticatorNotExistErrorMessage: 'Incorrect {{field}}',
    credentialsNotExistErrorMessage: 'Incorrect credentials',

    // confirmable
    invalidConfirmationTokenErrorMessage: 'Invalid confirmation token',
    confirmationTokenExpiredErrorMessage: 'Confirmation token expired',
    accountNotConfirmedErrorMessage: 'Account not confirmed',
    checkConfirmationTokenExpiredErrorMessage: 'Confirmation token expired. Check your email for confirmation instructions.',

    // lockable
    accountLockedErrorMessage: 'Account locked. Check unlock instructions sent to you.',
    invalidUnlockTokenErrorMessage: 'Invalid unlock token',
    unlockTokenExpiredErrorMessage: 'Unlock token expired',

    // recoverable
    invalidRecoveryDetailsErrorMessage: 'Invalid recovery details',
    invalidRecoveryTokenErrorMessage: 'Invalid recovery token',
    recoveryTokenExpiredErrorMessage: 'Recovery token expired',

    // registerable
    authenticatorAlreadyExistErrorMessage: 'Account of {{field}} "{{value}}" already exist'
  }
}
Examples
UserSchema.plugin(devise, { i18n: i18n })

Features & Roadmap

Test

  • Install all development dependencies
npm install
  • Then run test
npm test

License

MIT

Copyright (c) 2018-present