blowstack logo
  • BlowStack
  • ->
  • Blog
  • ->
  • Authentication in Nuxt app Part 2 - Auth module and Express.js implementation

Authentication in Nuxt app Part 2

Auth module and Express.js implementation

Last update

20 min.
  1. Change the existing User model.
  2. Set up email provider.
  3. Create email controller and test it.
  4. Create verifiaction tokens using Crypto and JWT.
  5. Verify tokens - create vue and controller.
  6. Resend verification tokens.
  7. Block unverified users.
  8. Password changing and resetting.

 

 

To follow this post you should first read 

Authentication in Nuxt part one.

 

In this post I cover how you can add email verification to your existent authentication system based on nuxt.js auth module, express.js and password.js. I also show you how to implement password changing and resetting.

 

Changing the existing User model

 

First change a little bit the user model by adding 4 new properties. You will need the following fields: 

  • verifiactionToken - to format unique url for the user so he can prove access to the provided email account
  • verifiactionTokenExpire - to restrict access to the url for a only specified time span
  • isVerified - to save the results of verification
  • resetPassword - to indicate that a user want to reset the password

 

//api/models/user.js

const UserSchema = new Schema({
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  verificationToken: { type: String, required: true},
  verificationTokenExpire: { type: Date, required: true },
  isVerified: { type: Boolean, required: true, default: false},
  resetPassword: { type: Boolean, required: true, default: false}
})

 

Set up email provider

 

You will send generated tokens nested in urls to the users but don't use SMPT config of your email. The better way is to go for professional email delivery services like SendGrid or MailGun. They both provide convinient API's and reasonable prices. SandGrid is totaly free up to 100 email per day (forever!) that's why I will continue this post based on it as it should be enough for testing puproses and even for many production web apps as well.

The registration process is easy after setting up your account create a single sender.

SendGrid single sender

Then follow the steps for Node.js environment in the API web integration guide (Email API tab). Don't accept verification yet when asked. First check if it works in your app!

 

SendGrid Node.js installation

 

Create email controller and test it

 

Create a new controller for managing emails sending process.

//api/controllers/mailer.controller.js

const sgMail = require('@sendgrid/mail')
sgMail.setApiKey(process.env.SENDGRID_API_KEY)

const from = 'dev@blowstack.com'

async function SendEmail (to, subject, text, html) {

  const msg = {
    to,
    from,
    subject,
    text,
    html
  }
  await sgMail
    .send(msg)
    .then(() => {
      console.log('Email sent')
    })
    .catch((error) => {
      console.error(error)
    })
}

export default {
  SendEmail
}

 

You should then test your newly implemented API. The good idea is to send a message everytime somebody is register ing in your app. Import the controller into the authentication.js file and send a message with information about successful registration.

//api/routes/authentication.js

...
import MailerController from '../controllers/mailer.controller'
...

router.post('/auth/register', async (req, res) => {
 ...
  await AuthenticationController.CreateUser(email, hashedPassword)

    .then(async () => {
      await MailerController.SendEmail(
      	email
      	'Registration confirmation',
      	'You has been registered correctly',
      	'<p>You has been registered correctly</p>'
      )
      res.send({ status: 'ok' })
    }).catch((err) => {
      throw err
    })
})

 

If every works as expected remember to accept the implementation in SendGrid!

 

Create verification tokens using Crypto and JWT

 

Knowing that the email service works and the user model is properly adjusted you can start creating verification tokens and send them after registration process. I noticed that many developers use a not maintained randomstring package to generate hashes. Don't do that! Use up to date build-in node.js package - Crypto

To continue you should implement 4 new functions responsible for:

  • generating a verifiaction token - random long string based
  • generating a verification token expire time
  • verifying signed verification token - verification of JWT string
  • signing a verification token - to alter token into JWT

 

Additionaly add constant with a default expire time. The presented one results in one day span. I also used defferent secret that when generating user password JWT string.

 

//api/controllers/authentication.controller

...
const crypto = require('crypto');
const expireSpan = 3600 * 1000 * 24
const authEmailVerificationSecret = process.env.AUTH_EMAIL_VERIFICATION_SECRET

async function CreateUser (email: string, password: string) {
  const verificationToken = generateVerificationToken()
  const verificationTokenExpire = generateVerificationTokenExpire()
  return await User.create({ email, password, verificationToken, verificationTokenExpire })
    .then((data) => {
      return data
    }).catch((error: Error) => {
      throw error
    })
}

function generateVerificationToken () {
  return crypto.randomBytes(30).toString('hex')
}

function generateVerificationTokenExpire () {
  return new Date(Date.now() + expireSpan)
}

function signVerificationToken (email, verificationToken) {
  return jwt.sign({
    email,
    verificationToken
  }, authEmailVerificationSecret)
}

function verifySignedVerificationToken (token) {
  return jwt.verify(token, authEmailVerificationSecret)
}


export default {
  ...
  generateVerificationToken,
  generateVerificationTokenExpire,
  signVerificationToken
  verifySignedVerificationToken
  ...
}

 

 

You can save a token in the database but you still need to send it to a newly created user as well. First add a new method to the mailer controller - SendRegistrationToken it should responsible for sending verification token message. Create an auxiliary method which will strip our template from html tags so we can pass it at once as a simple text to the SendEmail method. To make things more secure and performant use JWT to couple an user email and a token. This way you won't disclose a real token to a user and you will be able to make a fast query to the database when an user clicks a validation link.

// api/controllers/mailer.controller.js

function stripHtmlTags (html) {
  return html.replace(/(<([^>]+)>)/gi, "");
}

async function SendRegistrationToken (to, subject, verificationToken) {
  const html = `<p>Please click into the link below to confirm your email address and finish the registration</p><a href="http://localhost:3000/register/confirmation?token=${verificationToken}">Active your account</a>`
  const text = stripHtmlTags(html)
  await SendEmail(to, subject, text, html)
}

 

Now implement SendRegistrationToken in the authentication.js file in the routes folder. Notice that this time you have to use the user variable returned form the Promise to get our token. Remember to couple an email and a token into JWT. Consider using different secret than that used for the JWT user password creation. Overall you should get a similar code:

//api/routes/authentication.js

router.post('/auth/register', async (req, res) => {
 ...
  await AuthenticationController.CreateUser(email, hashedPassword)

    .then(async (user) => {
      const signedVerificationToken = AuthenticationController.signVerificationToken(user.email, user.verificationToken)
      await MailerController.SendRegistrationToken(
        email,
        'Registration confirmation',
        signedVerificationToken
      )
      res.send({ message: 'We send an email with verification link. Check your inbox!' })
    }).catch((err) => {
      throw err
    })
})

 

Test if it sends a token during registration. For now when a user clicks the link it goes nowhere. We should implement the logic for the verification of a token and a proper view in nuxt.js.

 

Verify tokens

 - create vue and controller

 

Create a view where user will be redirected after clicking the verification link. In the register folder create a folder named confirmation and then a new index.vue file. So when a user is redirected to this page a verification process will be initiated through mounted cycle. In case of a successful verification you can display a button to login. On the other hand if a user can't be verified you can display a button for resending a verification link.

 

// pages/register/confirmation/index.vue

<template>
  <div class="mt-12">
    <v-container>
      <v-row justify="center" class="mt-12 text-center">
        <v-col cols="12" v-if="confirmationStatus === 'verified'">
          <v-btn to="/login" class="ma-3" color="primary">Go to login page</v-btn>
        </v-col>
        <v-col cols="12" v-else-if="confirmationStatus === 'unverified'">
          <v-btn to="/register/confirmation/resend" class="ma-3" color="primary">Resend a verification token</v-btn>
        </v-col>
      </v-row>
    </v-container>
    <snack-bar :snackbar-message="snackbarMessage"></snack-bar>
  </div>
</template>

<script>
import SnackBar from '@/components/snackBar'

export default {
  name: 'index',
  components: { SnackBar },
  data: () => ({
    resend: false,
    confirmationStatus: false,
    snackbarMessage: ''
  }),
  methods: {
    async checkToken () {
      const token = this.$route.query.token
      const verification = await this.$axios.post('/api/auth/confirmation/', {
        token
      })
      this.confirmationStatus = verification.data.confirmationStatus
      this.snackbarMessage = verification.data.message
    }
  },
  mounted () {
    this.checkToken()
  }
}
</script>

 

Time for the backend. Create a new route in authentication.js file which will be responsible for the verification process. In the body write a code that get token from the params and extracts email and verification token from it. Find a user based on the extracted email. There are only two scenarios you should take into consideration:

  • an user with the email has been found, token and expiration span in valid
  • any other scenario not fullfiling the all above conditions

 

// api/routes/authentiaction.js

router.post('/auth/confirmation', async (req, res) => {
  const token = req.body.token
  const { email, verificationToken } = AuthenticationController.verifySignedVerificationToken(token)
  const user = await AuthenticationController.GetUser(email)

  if (user &amp;&amp; user.verificationToken === verificationToken &amp;&amp; user.verificationTokenExpire >= new Date()) {
    user.isVerified = true
    user.save()
    return res.send({
      confirmationStatus: 'verified',
      message: 'Your email has been verified.'
    })
  } else {
    return res.send({
      confirmationStatus: 'unverified',
      message: 'Email can\'t be verified!\n. The possible reason is expired token.'
    })
  }
})

 

 

Resend verification tokens

 

There is still one more important thing missing. Namely there are no verification email resend option available for expired or not delivered tokens. Let's fix it now!

Create a new route in authentication.js file with the following code. This should do the trick. It simply find a user by the email and generate for him a new token and an expire span. Finally sends the new token in an email. Now you have to add a new vue give a user possiblity to reach this endpoint.

// api/routes/authentication.js

router.post('/auth/confirmation/resend', async (req, res) => {
  const email = req.body.email
  const user = await AuthenticationController.GetUser(email)

  if (user &amp;&amp; user.isVerified === true) {
    return res.send('All ready verified.')
  } else if (user) {
    const verificationToken = AuthenticationController.generateVerificationToken()
    const verificationTokenExpire = AuthenticationController.generateVerificationTokenExpire()

    user.verificationToken = verificationToken
    user.verificationTokenExpire = verificationTokenExpire
    user.save()

    const signedVerificationToken = AuthenticationController.signVerificationToken(user.email, user.verificationToken)

    await MailerController.SendRegistrationToken(user.email, 'Registration confirmation - resend', signedVerificationToken)

    return res.send('Token has been resent.')
  } else {
    return res.send('Token can\'t be resent.')
  }
})

 

Create a form with only one input - email and two props: button title and email which can be used in the resending vue.

// forms/emailForm.vue

<template>
  <v-form
    ref="form"
    v-model="form.valid"
    lazy-validation
    class="div-center"
  >
    <v-text-field
      v-model="form.email"
      :rules="emailRules"
      validate-on-blur
      label="E-mail"
      required
    ></v-text-field>

    <v-btn
      :disabled="!form.valid"
      color="indigo lighten-1"
      class="mr-4"
      @click="validate"
    >
      {{ buttonTitle }}
    </v-btn>
  </v-form>
</template>

<script>
export default {
  name: 'emailForm',
  data: () => ({
    emailRules: [
      v => !!v || 'E-mail is required',
      v => /.+@.+\..+/.test(v) || 'E-mail must be valid'
    ]
  }),
  props: {
    buttonTitle: {
      required: true
    },
    form: {
      required: true
    }
  },
  mounted () {
    this.form.valid = false
  },
  methods: {
    validate () {
      if (this.$refs.form.validate()) {
        this.form.finish = true
        this.$emit('update:form', this.form)
      }
    }
  }
}
</script>

 

Use this form in a new vue responsible for resending a token. You could create it in a new folder named resend in the register folder

// pages/register/resend/index.vue

<template>
  <div class="mt-12">
    <v-container class="text-center">
      <v-row class="mt-12 text-center" justify="center">
        <h2>Verification token resending</h2>
      </v-row>
      <v-row :align="'center'"
             :justify="'center'" class="mt-12 ">
        <v-col cols="12" md="6" lg="3">
          <email-form button-title="Resend verification token" :form.sync="form"></email-form>
        </v-col>
      </v-row>
    </v-container>
    <snack-bar :snackbar-message.sync="snackbarMessage"></snack-bar>
  </div>
</template>
<script>

import EmailForm from '@/forms/emailForm'
import SnackBar from '@/components/snackBar'

export default {
  components: {
    SnackBar,
    EmailForm
  },
  data: () => ({
    snackbar: false,
    snackbarMessage: '',
    form: { finish: false }
  }),
  computed: {
    finish () {
      return this.form.finish
    }
  },
  watch: {
    finish (newVal) {
      if (newVal) {
        this.resendToken(this.form.email)
        this.form.finish = false
      }
    }
  },
  methods: {
    async resendToken (email) {
      const resendVerification = await this.$axios.post('/api/auth/confirmation/resend', {
        email
      })
      this.snackbarMessage = resendVerification.data
      this.snackbar = true
    }
  }
}
</script>

 

 

Block unverified users

 

You have basically a ready solution for verifing users emails. All works with resending option included. But still it doesn't matter for your authentication process because you haven't block unverified user access yet. It's super easy so let's add this blockade now. Go to the authentiaction controller, find the code responsible for LocalStrategy used by passport.js.

All you have to do is to add an another check this time for isVertified field and in case of fail give a proper message. It could look like this:

// api/routes/authentication.js

passport.use(
  new LocalStrategy(
    {
      usernameField: 'email',
      passwordField: 'password'
    },
    async function (email, password, done) {
      await GetUser(email)
        .then((user) => {
          return user
        }).then(async (user) => {
          if (!user) {
            return done(null, false, { message: 'Authentication failed' })
          }

          const validation = await comparePasswords(password, user.password)
          if (validation &amp;&amp; user.isVerified) {
            return done(null, user)
          } else if (validation) {
            return done(null, false, {
              message: 'You have to verify you email address',
              resendToken: true
            })
          } else {
            return done(null, false, { message: 'Authentication failed' })
          }
        }).catch((err) => {
          return done(err)
        })
    }
  )
)

 

Ok, users with not verified emails are blocked. Almost everything done. Let's finish it up and implement password changing and resetting.

 

Password changing and resetting

 

The both functions can be combined into one. When an user want to change a password an email with token is sent. Clicking a link an user is redirected to the form with possibility to input a new password. To implement this you need to more endpoints. One responsible for sending resetting tokens and the other responsible for verifying token from the url and accepting new password. Add also new method in mailer controller resposnible for sending resettign tokens.

// api/routes/authentication.js

...

router.post('/auth/password/reset', async (req, res) => {
  const email = req.body.email
  const user = await AuthenticationController.GetUser(email)

  if (user) {
    const verificationToken = AuthenticationController.generateVerificationToken()
    const verificationTokenExpire = AuthenticationController.generateVerificationTokenExpire()

    user.verificationToken = verificationToken
    user.verificationTokenExpire = verificationTokenExpire
    user.passwordReset = true
    user.save()

    const signedVerificationToken = AuthenticationController.signVerificationToken(user.email, user.verificationToken)

    await MailerController.SendPasswordChangeToken(user.email, 'Password resetting', signedVerificationToken)
    return res.send({ message: 'Link has been sent. Check you email.' })
  } else {
    return res.send({ message: 'Password can\'t be renew' })
  }
})

router.post('/auth/password/change', async (req, res) => {
  const token = req.body.token
  const password = req.body.password

  const { email, verificationToken } = AuthenticationController.verifySignedVerificationToken(token)
  const user = await AuthenticationController.GetUser(email)
  if (user &amp;&amp; user.verificationToken === verificationToken &amp;&amp; user.verificationTokenExpire >= new Date() &amp;&amp; user.passwordReset === true) {
    try {
      user.password = await AuthenticationController.generatePasswordHash(password)
      user.passwordReset = false
      user.save()
      return res.send({ message: 'Password has been changed' })
    } catch (err) {
      return res.send({ message: 'Some error happened. Contact administrator' })
    }
  } else {
    return res.send({ message: 'Token is invalid!. Re-send your request' })
  }
})

 

// api.controller/mailer.controller.js

async function SendPasswordChangeToken(to, subject, verificationToken) {
  const html = `<p>Please click into the link below to change your password</p><a href="http://localhost:3000/login/reset/confirmation?token=${verificationToken}">Change your password</a>`
  const text = stripHtmlTags(html)
  await SendEmail(to, subject, text, html)
}

export default {
 ...
  SendPasswordChangeToken
}

 

You have ready backend. Now you have to only create a new form with double password inputs and two new vues. One view will be responsible for possiblity to send a resetting token and the other to input new password. Let's start with the vue that give an user ability to send a request for password change.

It can be placed in pages/reset folder.

// pages/login/reset/index.vue

<template>
  <div class="mt-12">
    <v-cntainer>
      <v-row justify="center" class="mt-12">
        <v-col cols="12" lg=3 class="mt-12 text-center">
          <email-form button-title="Change/reset password" :form.sync="form"></email-form>
        </v-col>
      </v-row>
    </v-cntainer>
    <snack-bar :snackbar-message="snackbarMessage"></snack-bar>
  </div>
</template>

<script>
import EmailForm from '@/forms/emailForm'
import SnackBar from '@/components/snackBar'

export default {
  name: 'index',
  components: {
    SnackBar,
    EmailForm
  },
  data: () => ({
    form: { finish: false },
    snackbarMessage: ''
  }),
  computed: {
    finish () {
      return this.form.finish
    }
  },
  watch: {
    finish (newVal) {
      if (newVal) {
        this.sendPasswordToken()
        this.form.finish = false
      }
    }
  },
  methods: {
    async sendPasswordToken () {
      try {
        const email = this.form.email
        const response = await this.$axios.post('/api/auth/password/reset', {
          email
        })
        this.snackbarMessage = response.data.message
      } catch (err) {
        this.snackbarMessage = err.response.data.message
      }
    }
  }
}
</script>

 

You can now test if a request for resetting / changing password works. Then start creating the last form for changing password and implement it in a proper vue.

// forms/changePasswordForm.vue

<template>
  <v-form
    ref="form"
    v-model="form.valid"
    lazy-validation
  >

    <v-text-field
      v-model="form.password"
      :counter="20"
      :rules="[(form.password === form.passwordCheck) || 'Passwords must match', ...passwordRules]"
      :type="'password'"
      label="Password"
      required
    ></v-text-field>
    <v-text-field
      v-model="form.passwordCheck"
      :rules="[(form.password === form.passwordCheck) || 'Passwords must match', ...passwordRules]"
      :counter="20"
      :type="'password'"
      label="Password"
      required
    ></v-text-field>
    <v-btn
      :disabled="!form.valid"
      color="indigo lighten-1"
      class="mr-4"
      @click="validate"
    >
      {{ buttonTitle }}
    </v-btn>
  </v-form>
</template>

<script>
export default {
  name: 'changePasswordForm',
  data: () => ({
    passwordRules: [
      v => !!v || 'Password is required',
      v => (v &amp;&amp; v.length <= 20) || 'Password must be less than 20 characters',
    ]
  }),
  props: {
    form: {
      required: true,
    },
    buttonTitle: {
      required: true
    }
  },
  mounted () {
  this.form.valid = false
    },
  methods: {
    validate () {
      if (this.$refs.form.validate()) {
        this.form.finish = true
        this.$emit('update:form', this.form)
      }
    }
  }
}
</script>

 

Then implement the form in a new vue which can be placed in pages/login/reset folder.

// pages/login/reset/confirmation/index.vue

<template>
  <div class="mt-12">
    <v-container>
      <v-row justify="center" class="mt-12">
        <h3>Change your password</h3>
      </v-row>
      <v-row justify="center" class="mt-12">
        <change-password-form button-title="Change password" :form.sync="form"></change-password-form>
      </v-row>
    </v-container>
    <snack-bar :snackbar-message="snackbarMessage"></snack-bar>
  </div>
</template>

<script>
import ChangePasswordForm from '@/forms/changePasswordForm'
import SnackBar from '@/components/snackBar'

export default {
  name: 'index',
  components: {
    SnackBar,
    ChangePasswordForm
  },
  data: () => ({
    form: { finish: false },
    token: '',
    snackbarMessage: ''
  }),
  computed: {
    finish () {
      return this.form.finish
    }
  },
  watch: {
    finish (newVal) {
      if (newVal) {
        this.changePassword()
        this.form.finish = false
      }
    }
  },
  mounted () {
    this.token = this.$route.query.token
  },
  methods: {
    async changePassword () {
      const token = this.token
      try {
        const verification = await this.$axios.post('/api/auth/password/change', {
          token,
          password: this.form.password
        })
        this.snackbarMessage = verification.data.message
      } catch (err) {
        this.snackbarMessage = err.response.data.message
      }
    }
  }
}
</script>

 

That's all. You have now a working authentication system in Nuxt.js based on JWT. The next part will be focused on ACL.

 

Recent posts

BlowStack 2021
Portfolio Cheat
sheets