Numerous tutorials exist on setting up authentication in Nuxt using the auth module, but I haven't found any that cover both backend and frontend aspects comprehensively. This post aims to address this gap by covering all facets of Nuxt authentication, allowing you to implement it step by step and secure your app effectively.
This first part focuses on basic authentication, while subsequent sections will delve into email verification, password reset, and implementing granular access control using scope.
Implementing auth module
This step is thoroughly explained on the official Nuxt.js auth module website. However, for those who don't have the time to watch a video that's almost 45 minutes long, I provide a condensed version here for quick implementation.
Installation and configuration
First install the official authentication module for Nuxt.
npm i @nuxtjs/auth
After installing, add the auth module to the Nuxt config. Note that this module requires Axios to function, so if you haven't installed it yet, do so now and include it in the modules section as well
//nuxt.config.js
...
modules: [
'@nuxtjs/axios',
'@nuxtjs/auth'
],
...
Login and register user functions
Next, within your app's 'pages' folder, create 'login' and 'register' directories and place Vue files containing forms in them. While the official auth module website provides examples of these pages, I will demonstrate their Vuetify versions here.
If you're not using Vuetify, you can adapt the basic version from the official site, as it won't affect the authentication process. The key aspect is to create forms for user email and password input, and then use Axios through the auth module to send this data to specific endpoints, which I will discuss later
For logging use:
let user = await this.$auth.loginWith('local', {
data: {
email: this.email,
password: this.password
},
})
And for registering use:
await this.$axios.post('/api/auth/register', {
email: this.email,
password: this.password
});
let user = await this.$auth.loginWith('local', {
data: {
email: this.email,
password: this.password
},
});
Vuetify version
Create a universal form, suitable for both login and registration, since they require the same input fields. You could name this file 'authenticationForm.vue' and place it in a 'forms' folder, which you'll need to create as it's not a default in Vue/Nuxt. In this file, include a basic form capable of collecting and validating a user's email and password.
Utilize props to pass two key variables: 'form' for gathering all the data, and 'buttonTitle' for setting the correct label on the submit button. By combining props with the $emit function, you can efficiently transmit data back to the parent component.
// form/authenticationForm.vue
<template>
<v-form
ref="form"
v-model="form.valid"
lazy-validation
>
<v-text-field
v-model="form.email"
:rules="emailRules"
label="E-mail"
required
></v-text-field>
<v-text-field
v-model="form.password"
:counter="20"
:rules="passwordRules"
: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: 'authenticationForm',
data: () => ({
emailRules: [
v => !!v || 'E-mail is required',
v => /.+@.+\..+/.test(v) || 'E-mail must be valid',
],
passwordRules: [
v => !!v || 'Password is required',
v => (v && 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>
Next, create a login page utilizing the form you just created. Place it in a new folder named 'login' within the 'pages' directory. Import the 'authenticationForm' component and populate its props with form objects and an appropriate title.
Monitor changes to the form; once a user enters valid data, the login method should send this data to the relevant endpoint (which I will cover soon). Additionally, create a notification component (SnackBar) to provide user feedback (details of which you will find below)
// pages/login/index.vue
<template>
<v-container class="text-center">
<v-row :align="'center'"
:justify="'center'" class="mt-12">
<v-col cols="12" md="6" lg="3">
<authentication-form button-title="Logging" :form.sync="form"></authentication-form>
</v-col>
</v-row>
<snack-bar :snackbar-message.sync="snackbarMessage"></snack-bar>
</v-container>
</template>
<script>
import AuthenticationForm from '@/forms/authenticationForm'
import SnackBar from '@/components/snackBar'
export default {
components: { AuthenticationForm, SnackBar },
data: () => ({
form: {
valid: false,
email: '',
password: '',
finish: false
},
snackbarMessage: '',
}),
computed: {
finish () {
return this.form.finish
}
},
watch: {
finish (newVal) {
if (newVal) {
this.login()
this.form.finish = false
}
}
},
methods: {
async login () {
try {
const response = await this.$auth.loginWith('local', {
data: {
email: this.form.email,
password: this.form.password
}
})
this.snackbarMessage = response.data.message
} catch (error) {
this.snackbar = true
this.snackbarMessage = error.response.data.message
}
}
}
}
</script>
Regarding the Snackbar code: The Snackbar will be displayed whenever the 'snackbarMessage' passed to it changes, provided the message is not an empty string. An empty string represents the initial state, where no message is intended to be shown.
// components/snackBar.vue
<template>
<v-snackbar
v-model="snackbar"
:top="true"
:color="'error'"
:timeout="5000"
>
{{ snackbarMessage }}
<v-btn
dark
text
@click="snackbar = false"
title="close"
>
Close
</v-btn>
</v-snackbar>
</template>
<script>
export default {
name: 'snackBar',
data: () => ({
snackbar: false
}),
props: {
snackbarMessage: {
required: true
}
},
watch: {
snackbarMessage (val) {
if (val.length > 1) {
this.snackbar = true
}
},
snackbar (newVal) {
if (newVal === false) {
this.$emit('update:snackbarMessage', '')
}
}
}
}
</script>
Registration is quite similar. Utilize the same form and Snackbar component, but replace the login method with a register method. The distinctions between the two can be seen in the code provided below.
// pages/register/index.vue
<template>
<v-container class="text-center">
<v-row :align="'center'"
:justify="'center'" class="mt-12">
<v-col cols="12" md="6" lg="3">
<authentication-form button-title="Register" form.sync="form"></authentication-form>
</v-col>
</v-row>
<snack-bar :snackbar-message.sync="snackbarMessage"></snack-bar>
</v-container>
</template>
<script>
import AuthenticationForm from '@/forms/authenticationForm'
import SnackBar from '@/components/snackBar'
export default {
components: { AuthenticationForm, SnackBar },
data: () => ({
form: {
valid: false,
email: '',
password: '',
finish: false
},
snackbarMessage: ''
}),
computed: {
finish () {
return this.form.finish
}
},
watch: {
finish (newVal) {
if (newVal) {
this.register()
this.form.finish = false
}
}
},
methods: {
async register () {
try {
await this.$axios.post('/api/auth/register', {
email: this.form.email,
password: this.form.password
})
const user = await this.$auth.loginWith('local', {
data: {
email: this.form.email,
password: this.form.password
}
})
if (user) {
await this.$router.push('/admin')
}
} catch (error) {
this.snackbarMessage = error.response.data.message
}
}
}
}
</script>
It's time to configure the auth module, which is done in the Nuxt config file. The most crucial aspect is setting up the endpoints' URLs, which you will establish in the backend soon. Copy and paste the code from below, modifying the URLs as necessary.
//nuxt.config.js
...
auth: {
localStorage: true,
strategies: {
local: {
endpoints: {
login: {
url: '/api/auth/login',
method: 'post',
propertyName: 'token'
},
logout: false,
user: {
url: '/api/auth/user',
method: 'get',
propertyName: false
},
},
}
},
redirect: {
logout: '/',
callback: '/login',
home: '/'
},
},
...
The final step on the frontend is to select the pages that will be protected by the auth module. To secure a page, add 'auth' as middleware to it. If you have several pages requiring protection, a more efficient approach would be to create a new layout and apply the middleware to this layout.
<script>
export default {
components: { },
middleware: ['auth']
}
</script
You're now ready to move on to the backend, where the actual authentication process occurs. The auth module we just discussed doesn't handle this by itself; it merely assists in managing tokens, facilitating internal API calls to backend endpoints, and restricting access to certain pages. However, the generation and validation of tokens, registration, and other such functionalities must be managed by the backend. Let's get started on that!
Create endpoints
Start with the two endpoints declared in the Nuxt config for login and user registration.
// api/routes/authentication.js
...
router.post('/auth/login', async (req, res) => {
})
router.get('/auth/user', async (req, res) => {
res.send({ ok: 'ok' })
})
One important endpoint is missing, namely the 'register' endpoint, which, although not declared in the Nuxt config, is used in the register Vue component. Let's address this issue.
// api/routes/authentication.js
...
router.post('/auth/register', async (req, res) => {
})
You will soon populate the endpoints with logic, but for now, let's create a User model and a controller to help make your code more consistent and clear.
Create a User model and a authentication controller
I prefer using Mongoose, but if you use a different ODM or simply a plain MongoDB connector, feel free to adjust this file to suit your preferences.
// api/models/user.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const UserSchema = new Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true }
})
export default mongoose.model('User', UserSchema)
In the controller, initially, there are only two functions: creating a new user and retrieving a user. That's all we need for now.
// api/controllers/authentication.controller.js
import User from '../models/user'
async function CreateUser (email, password) {
return await User.create({ email, password })
.then((data) => {
return data
}).catch((error: Error) => {
throw error
})
}
async function GetUser (email) {
return await User.findOne({ email })
.then((data) => {
return data
}).catch((error: Error) => {
throw error
})
}
export default {
CreateUser,
GetUser
}
Encrypt users passwords
During registration, you will send a password to the register endpoint. It is good practice to encrypt the password before storing it in the database. I strongly recommend using Bcrypt, as it offers significantly better security compared to SHA256, SHA512, or MD5. First, you should install Bcrypt for Node.js.
npm i bcrypt
Then, import it in the authentication controller and create a new method based on it, which will be responsible for hashing passwords.
// api/controllers/authentication.controller.js
...
const bcrypt = require('bcrypt')
...
async function generatePasswordHash (plainPassword) {
return await bcrypt.hash(plainPassword, 12)
}
...
export default {
...
generatePasswordHash,
...
}
Finally, add the missing logic to the register endpoint to enable the registration of new users.
// api/routes/authentication.js
import AuthenticationController from '../controllers/authentication.controller'
const { Router } = require('express')
const router = Router()
router.post('/auth/register', async (req, res) => {
const password = req.body.password
const email = req.body.email
const hashedPassword = await AuthenticationController.generatePasswordHash(password)
await AuthenticationController.CreateUser(email, hashedPassword)
.then(() => {
res.send({ message: 'An account has been created!' })
}).catch((err) => {
throw err
})
})
That concludes the registration process. Test your registration form and check the database for the newly created user. If it's properly inserted, you can proceed to set up the login endpoint.
Logging using JWT
First, let's discuss how the auth module with JWT (JSON Web Token) works in Nuxt. The auth module facilitates the storage of a token received from the backend. When it encounters 'auth' middleware in a Vue file, it triggers a call to the user endpoint using Axios. This process involves extracting password and other user data from the JWT token and comparing them with the data stored in the database.
Currently, the logic for the user endpoint is not implemented. The idea is to use the backend to verify if the token stored in the auth module is valid. JWT is convenient for this purpose as it can securely encapsulate multiple pieces of user data. The user endpoint plays a crucial role; it validates the token's correctness when a user accesses restricted pages. Incorrect implementation of this endpoint could allow unauthorized access, as the Nuxt auth module only expects the login endpoint to return a token—any token—and does not validate its authenticity. The validation responsibility lies with the user endpoint, which is invoked each time the auth middleware is encountered.
Now that we understand how authentication works in Nuxt, we can proceed with implementing the login and user endpoints using JWT. Although this process is not overly complex, I recommend using well-maintained tools like Passport.js. Let's install and integrate it to work effectively with the Nuxt auth module.
npm install passport passport-jwt passport-local jsonwebtoken
n the authentication controller, add a local strategy pattern for logging in. Additionally, implement a new method capable of comparing the user-inputted plain password with the hashed one stored in the database. Also, include another method that converts user data into a JWT, and define a constant holding a long string as a secret key for generating the JWT string (consider using environment variables for this purpose).
// api/controllers/authentication.controller.js
...
const passport = require('passport')
const LocalStrategy = require('passport-local').Strategy
const authUserSecret = process.env.AUTH_USER_SECRET // an arbitrary long string, you can ommit env of course
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) {
return done(null, user)
} else {
return done(null, false, { message: 'Authentication failed' })
}
}).catch((err) => {
return done(err)
})
}
)
)
async function comparePasswords (plainPassword, hashedPassword) {
return await bcrypt.compare(plainPassword, hashedPassword)
}
function signUserToken (user) {
return jwt.sign({
id: user.id,
email: user.email
}, authUserSecret)
}
Then implement the local strategy in your login route.
// api/routes/authentication.js
...
const passport = require('passport')
...
router.post('/auth/login', (req, res) => {
passport.authenticate('local', { session: false }, (err, user, message) => {
if (err) {
// you should log it
return res.status(500).send(err)
} else if (!user) {
// you should log it
return res.status(403).send(message)
} else {
const token = AuthenticationController.signUserToken(user)
return res.send({ token })
}
})(req, res)
})
You also need to initialize Passport globally. To do this, go to the index.js file and add the necessary configuration there.
// api/index.js
const passport = require('passport');
app.use(passport.initialize())
It's advisable to verify if the token is being passed to Nuxt. Attempt to log in with a previously registered account, and then check if the 'auth._token.local' variable in your cookies contains a bearer token string.
Create the user endpoint
To complete the authentication process, let's implement the user endpoint. First, we need to develop a JWT strategy and integrate it into the authentication controller. Additionally, we must create a custom extractor to handle the token passed from the auth module.
// api/controllers/authentication.controller.js
...
const JwtStrategy = require('passport-jwt').Strategy
...
const tokenExtractor = function (req) {
let token = null
if (req.req && req.req.cookies && req.req.cookies['auth._token.local']) {
const rawToken = req.req.cookies['auth._token.local'].toString()
token = rawToken.slice(rawToken.indexOf(' ') + 1, rawToken.length)
}
return token
}
passport.use(new JwtStrategy({
jwtFromRequest: tokenExtractor,
secretOrKey: authUserSecret
},
function (jwtPayload, done) {
return GetUser(jwtPayload.email)
.then((user) => {
if (user) {
return done(null, {
email: user.email,
})
} else {
return done(null, false, 'Failed')
}
})
.catch((err) => {
return done(err)
})
}
))}
))
Observe what is returned when a user is found; currently, it's only the user's email. In Nuxt, when using the $auth.user
variable, you can access this email. However, if you want to pass more information, you should integrate the JWT strategy into the user endpoint.
// api/routes/authentication.js
router.get('/auth/user', async (req, res) => {
// console.log(req.cookies['auth._token.local'])
passport.authenticate('jwt', { session: false }, (err, user, message) => {
if (err) {
// you should log it
return res.status(400).send(err)
} else if (!user) {
// you should log it
return res.status(403).send({ message })
} else {
return res.send({ user })
}
})(res, req)
})
Now, conduct a few tests to determine if the newly implemented authentication system is functioning properly.
Block endpoints
You have restricted unauthenticated users from accessing certain Vue pages, but you haven't yet blocked them from accessing specific endpoints. You can verify this yourself by using tools like Postman, which can easily access any data from your database. Fortunately, securing an endpoint can be efficiently achieved by adding Passport.js with the JWT strategy as middleware. For instance, to block all URLs starting with the ‘admin’ prefix, you can implement the following code.
// api/routes.js
const adminPages = require('./routes/admin')
app.use('/admin', passport.authenticate('jwt', { session: false }), adminPages)
Unfortunately, you'll need to make a slight modification to the tokenExtractor in the authentication controller. In Express.js, the structure of the req
object differs from that in Nuxt.js. Therefore, the following changes are necessary.
// api/controllers/authentication.js
const tokenExtractor = function (req) {
let token = null
if (req.req && req.req.cookies && req.req.cookies['auth._token.local']) {
const rawToken = req.req.cookies['auth._token.local'].toString()
token = rawToken.slice(rawToken.indexOf(' ') + 1, rawToken.length)
} else if (req &&; req.cookies && req.cookies['auth._token.local']) {
const rawToken = req.cookies['auth._token.local'].toString()
token = rawToken.slice(rawToken.indexOf(' ') + 1, rawToken.length)
}
return token
}
Finally you can use Postman to test your auth module implementation.
What's next
There are still more aspects to consider, such as email verification, password resetting, authorization for granular resource access, and limiting the number of failed attempts to prevent brute force attacks. I hope to cover these topics in the upcoming parts. Stay tuned for those!