Authentication is the process through which a website or app verifies the identity of its users. As the gatekeeper to an app, authentication needs to be secure and reliable. After all, it deals with user information, potentially the most critical data in an app.
Implementing a secure authentication mechanism from scratch could be difficult and can quickly become cumbersome. Where do you save the user data? Should you roll out a database and model the user content type? What about registration, login, and password reset? It doesn't end there. How do you deal with passwords securely?
That's a lot to think about when you just want to dive into the core functionality of the app you're developing. You could build your own authentication mechanism, but do you really need or want to? It may be better to rely on something that's already been built and thoroughly tested.
An Introduction to Strapi
Strapi, the leading open-source Headless CMS, speeds up the process and gives you the freedom to use your favorite frameworks. Strapi is the leading open-source headless CMS made by developers for deverlopers.. Strapi provides authentication right out of the box; it comes with built-in role-based access control and customizable REST API to perform the main authentication operations.
Goals
This is a step-by-step guide on how to implement Strapi-based authentication in a Nuxt.js app. On the frontend, we'll be using Nuxt.js and relying on Nuxt's Auth Module, which is the official zero-boilerplate authentication support for Nuxt.js.
In this article, we'll build two projects:
- A Strapi application to store and manage users, and
- A Nuxt application that will use Strapi's API for authentication purposes.
Here's a preview of what we want to achieve:
Prerequisites
To follow this tutorial, make sure you have:
- Node.js, we recommend you have the v14 version installed.
- Yarn, a modern package manager.
- A text editor, we recommend VS Code with the Vetur extension, a Vue tooling for VS Code, powered by vls. It comes with multiple features like syntax highlighting, formatting, IntelliSense, debugging, and more.
- A terminal, we recommend using VS Code's integrated terminal.
Let's get started!
Step 1: Set up Strapi
Begin by creating a Strapi project. Copy and paste the following command line into your terminal to create your first Strapi project.
yarn create strapi-app backend --quickstartUsing the --quickstart flag at the end of the command to directly create the project in quickstart mode, which uses the default database (SQLite).
Once the installation is complete, your browser automatically opens a new tab.
Complete the form to create your new administrator account.
Now that you've created your Strapi application, you are ready to start a new Nuxt.js project.
Step 2: Set up Nuxt.js
In this step, we will use create-nuxt-app to create a new Nuxt project.
Open a terminal or, from Visual Studio Code, open an integrated terminal and use the following command to create a new starter project:
yarn create nuxt-app frontendAfter running the command above, you’ll have to answer some questions. Once all questions are answered, it will install all the dependencies.
? Project name: frontend
? Programming language: JavaScript
? Package manager: Yarn
? UI framework: Windi CSS
? Nuxt.js modules: None
? Linting tools: ESLint, Prettier
? Testing framework: None
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Server (Node.js hosting)
? Development tools: jsconfig.json (Recommended for VS Code if you're not using typescript)
? Continuous integration: None
? Version control system: GitOnce the project is created, follow the instructions to install dependencies and start the dev server:
cd frontend
yarn
yarn devNow that the dev server is running, open http://localhost:3000/ in your browser.
Good job, you successfully set up both Nuxt and Strapi projects! 🎉
Step 3: Set up Auth Module
**nuxt auth** authenticates users using a configurable authentication scheme or by using one of the directly supported providers. It provides an API for triggering authentication and accessing the resulting user information.
Let's install the packages we need. Open a new terminal:
# Ctrl + C to close process js
cd frontend yarn add --exact @nuxtjs/auth-next
yarn add @nuxtjs/axiosThen, add the following to the modules section of nuxt.config.js:
// frontend/nuxt.config.js
export default {
// Modules: https://go.nuxtjs.dev/config-modules
modules: [
'@nuxtjs/axios',
'@nuxtjs/auth-next'
],
}The auth-module relies on Vuex, a state management pattern + library for Vue.js applications. You can activate the store by creating a new ./store/index.js file
// frontend/store/index.js
export const getters = {
isAuthenticated(state) {
return state.auth.loggedIn
},
loggedInUser(state) {
return state.auth.user
},
}You can now start configuring your local scheme.
Step 4: Set up Local Scheme
Schemes define authentication logic, see IANA list of authentication schemes. local is the default credentials based scheme for flows like JWT (JSON Web Token), which is the authentication process provided by Strapi roles & permissions plugin.
In this guide we will see how to validate a JWT with Strapi. Let’s map the Strapi authentication endpoints to the local strategy. Add a new axios and auth sections to your nuxt.config.js:
// frontend/nuxt.config.js
export default {
axios: {
baseURL: process.env.STRAPI_URL || 'http://localhost:1337/api'
},
auth: {
// Options
strategies: {
local: {
token: {
property: 'jwt',
},
user: {
property: false,
},
endpoints: {
login: {
url: 'auth/local',
method: 'post',
},
user: {
url: 'users/me',
method: 'get',
},
logout: false,
},
},
},
}
}Here, you set the base URL that axios will use when making requests. In our case, we are referencing the Strapi API endpoint we set up earlier.
Then, you define the authentication endpoints for the local strategy corresponding to those on your API:
login: authenticates the user. On successful authentication, the JWT token will be available in thejwtproperty of the response object.user: retrieves the authenticated user's info. If the user is authenticated, the JWT token will be added to the request, allowing Strapi to identify the user.- We've also disabled the
logoutendpoint, since logging out a user is only done locally and doesn't require any request to Strapi's API. The token is simply removed from the local storage when the user logs out.
Step 5: Create a Navbar component
Create a new ./components/Navbar.vue file, and copy/paste the following code in it:
// frontend/components/Navbar.vue
<template>
<div class="bg-gray-800 py-4 px-4">
<NuxtLink class="text-white p-2 hover:bg-gray-700" to="/">Home</NuxtLink>
<NuxtLink
v-if="!isAuthenticated"
class="text-white p-2 hover:bg-gray-700"
to="/user/login"
>Sign In</NuxtLink
>
<NuxtLink
v-if="!isAuthenticated"
class="text-white p-2 hover:bg-gray-700"
to="/user/register"
>Sign Up</NuxtLink
>
<NuxtLink
v-if="isAuthenticated"
class="text-white p-2 hover:bg-gray-700"
to="/user/me"
>Your profile</NuxtLink
>
<a
v-if="isAuthenticated"
class="text-white p-2 hover:bg-gray-700"
href="/logout"
@click.prevent="userLogout"
>Logout</a
>
</div>
</template> <script>
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters(['isAuthenticated']),
},
methods: {
async userLogout() {
await this.$auth.logout()
},
},
}
</script>In the code above, we have created a navbar component using Tailwind CSS. Apart from that, we've defined the computed property isAuthenticated and userLogout method used in the component's template.
Add your navbar component to all your pages by extending the main layout. Add a new layouts/default.vue file, and copy/paste the following code in it:
// frontend/layouts/default.vue
<template>
<div>
<Navbar />
<nuxt />
</div>
</template>Great! After completing the navbar:
- Run
yarn devto start the development server. - Visit
http://localhost:3000to view your application.
Step 6: Create a Login Page
Create a new ./pages/user/Login.vue file, and copy/paste the following code in it:
// frontend/pages/user/Login.vue
<template>
<div class="max-w-md w-full mx-auto mt-8">
<h1 class="text-3xl font-extrabold mb-4">Sign in</h1>
<form @submit.prevent="userLogin">
<div
v-if="err"
class="
p-4
mb-4
text-sm text-red-700
bg-red-100
rounded-lg
dark:bg-red-200 dark:text-red-800
"
role="alert"
>
{{ err }}
</div>
<div class="mb-6">
<label
for="email"
class="
block
mb-2
text-sm
font-medium
text-gray-900
dark:text-gray-300
"
>Your email</label
>
<input
v-model="email"
type="email"
class="
shadow-sm
bg-gray-50
border border-gray-300
text-gray-900 text-sm
rounded-lg
focus:ring-blue-500 focus:border-blue-500
block
w-full
p-2.5
dark:bg-gray-700
dark:border-gray-600
dark:placeholder-gray-400
dark:text-white
dark:focus:ring-blue-500
dark:focus:border-blue-500
dark:shadow-sm-light
"
placeholder="name@strapi.io"
required
/>
</div>
<div class="mb-6">
<label
for="password"
class="
block
mb-2
text-sm
font-medium
text-gray-900
dark:text-gray-300
"
>Your password</label
>
<input
v-model="password"
type="password"
class="
shadow-sm
bg-gray-50
border border-gray-300
text-gray-900 text-sm
rounded-lg
focus:ring-blue-500 focus:border-blue-500
block
w-full
p-2.5
dark:bg-gray-700
dark:border-gray-600
dark:placeholder-gray-400
dark:text-white
dark:focus:ring-blue-500
dark:focus:border-blue-500
dark:shadow-sm-light
"
required
/>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
<NuxtLink
class="font-medium text-blue-600 hover:underline dark:text-blue-500"
to="/user/forgot"
>Reset password</NuxtLink
>?
</p>
</div>
<button
type="submit"
class="
text-white
bg-blue-700
hover:bg-blue-800
focus:ring-4 focus:outline-none focus:ring-blue-300
font-medium
rounded-lg
text-sm
px-5
py-2.5
text-center
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
"
>
Sign in
</button>
</form>
</div>
</template> <script>
export default {
auth: 'guest',
data() {
return {
err: null,
email: '',
password: '',
}
},
methods: {
async userLogin() {
try {
await this.$auth.loginWith('local', {
data: { identifier: this.email, password: this.password },
})
} catch (e) {
if (e.response) this.err = e.response.data.error.message
}
},
},
}
</script>In the code above, we've defined three properties err, email and password used in the component's template and userLogin method which log in the user after sending the data to the Strapi application. An error notification will be displayed if login attempt is not successful, If not the user will be redirected to the homepage.
The auth: 'guest``' middleware help redirected the user to the homepage, if already logged in.
Step 7: Create a Registration Page
Create a new ./pages/user/Register.vue file, and copy/paste the following code in it:
// frontend/pages/user/Register.vue
<template>
<div class="max-w-md w-full mx-auto mt-8">
<h1 class="text-3xl font-extrabold mb-4">Sign up</h1>
<form @submit.prevent="userRegister">
<div
v-if="err"
class="
p-4
mb-4
text-sm text-red-700
bg-red-100
rounded-lg
dark:bg-red-200 dark:text-red-800
"
role="alert"
>
{{ err }}
</div>
<div
v-if="success"
class="
p-4
mb-4
text-sm text-green-700
bg-green-100
rounded-lg
dark:bg-green-200 dark:text-green-800
"
role="alert"
>
Your account has been created successfully you can now
<NuxtLink class="font-medium" to="/user/login">Login</NuxtLink>
</div>
<div class="mb-6">
<label
for="username"
class="
block
mb-2
text-sm
font-medium
text-gray-900
dark:text-gray-300
"
>Your username</label
>
<input
v-model="username"
type="text"
class="
shadow-sm
bg-gray-50
border border-gray-300
text-gray-900 text-sm
rounded-lg
focus:ring-blue-500 focus:border-blue-500
block
w-full
p-2.5
dark:bg-gray-700
dark:border-gray-600
dark:placeholder-gray-400
dark:text-white
dark:focus:ring-blue-500
dark:focus:border-blue-500
dark:shadow-sm-light
"
placeholder="name"
required
/>
</div>
<div class="mb-6">
<label
for="email"
class="
block
mb-2
text-sm
font-medium
text-gray-900
dark:text-gray-300
"
>Your email</label
>
<input
v-model="email"
type="email"
class="
shadow-sm
bg-gray-50
border border-gray-300
text-gray-900 text-sm
rounded-lg
focus:ring-blue-500 focus:border-blue-500
block
w-full
p-2.5
dark:bg-gray-700
dark:border-gray-600
dark:placeholder-gray-400
dark:text-white
dark:focus:ring-blue-500
dark:focus:border-blue-500
dark:shadow-sm-light
"
placeholder="name@strapi.io"
required
/>
</div>
<div class="mb-6">
<label
for="password"
class="
block
mb-2
text-sm
font-medium
text-gray-900
dark:text-gray-300
"
>Your password</label
>
<input
v-model="password"
type="password"
class="
shadow-sm
bg-gray-50
border border-gray-300
text-gray-900 text-sm
rounded-lg
focus:ring-blue-500 focus:border-blue-500
block
w-full
p-2.5
dark:bg-gray-700
dark:border-gray-600
dark:placeholder-gray-400
dark:text-white
dark:focus:ring-blue-500
dark:focus:border-blue-500
dark:shadow-sm-light
"
required
/>
</div>
<button
type="submit"
class="
text-white
bg-blue-700
hover:bg-blue-800
focus:ring-4 focus:outline-none focus:ring-blue-300
font-medium
rounded-lg
text-sm
px-5
py-2.5
text-center
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
"
>
Sign up
</button>
</form>
</div>
</template> <script>
export default {
auth: 'guest',
data() {
return {
success: false,
err: null,
username: '',
email: '',
password: '',
}
},
methods: {
async userRegister() {
try {
this.$axios.setToken(false)
await this.$axios.post('auth/local/register', {
username: this.username,
email: this.email,
password: this.password,
})
this.success = true
} catch (e) {
if (e.response) this.err = e.response.data.error.message
}
},
},
}
</script>In the code above, we've defined two more properties success and username used to display a success message after a successful registration.
Time for the user profile page.
Step 9: Create a Profile Page
Create a new ./pages/user/Me.vue file, and copy/paste the following code in it:
// frontend/pages/user/Me.vue
<template>
<div class="max-w-md w-full mx-auto mt-8">
<h1 class="text-3xl font-extrabold mb-4">Your profile</h1>
<form @submit.prevent="userLogin">
<div class="mb-6">
<label
for="email"
class="
block
mb-2
text-sm
font-medium
text-gray-900
dark:text-gray-300
"
>Your email</label
>
<input
type="email"
class="
mb-6
bg-gray-100
border border-gray-300
text-gray-900 text-sm
rounded-lg
focus:ring-blue-500 focus:border-blue-500
block
w-full
p-2.5
cursor-not-allowed
dark:bg-gray-700
dark:border-gray-600
dark:placeholder-gray-500
dark:text-gray-500
dark:focus:ring-blue-500
dark:focus:border-blue-500
"
:value="loggedInUser.email"
disabled
/>
</div>
<div class="mb-6">
<label
for="username"
class="
block
mb-2
text-sm
font-medium
text-gray-900
dark:text-gray-300
"
>Your username</label
>
<input
type="text"
class="
mb-6
bg-gray-100
border border-gray-300
text-gray-900 text-sm
rounded-lg
focus:ring-blue-500 focus:border-blue-500
block
w-full
p-2.5
cursor-not-allowed
dark:bg-gray-700
dark:border-gray-600
dark:placeholder-gray-500
dark:text-gray-500
dark:focus:ring-blue-500
dark:focus:border-blue-500
"
:value="loggedInUser.username"
disabled
/>
</div>
</form>
</div>
</template> <script>
import { mapGetters } from 'vuex'
export default {
middleware: 'auth',
computed: {
...mapGetters(['loggedInUser']),
},
}
</script>The auth middleware guarantees that only logged in users can access this page.
Let's now implement a password reset mechanism.
Step 10: Set-up Password Reset
This will be achieved with the following workflow:
- In the forgot password page, a user submits an email.
- If the email is in Strapi's user database, it receives a unique reset password link.
- When the user click on the link, it open a reset password page.
- The new specified password is sent to Strapi, along with the reset code.
- The password is updated and the user can now use it to log in.
Before implementing the password reset feature. We need to enable the Public Role permissions for the auth/reset-password and auth/forgot-password endpoints.
To do so, open your Strapi application at http://localhost:1337/admin, from the left sidebar of the admin dashboard, click Settings, Roles, Edit Public, open Users-permissions, check both forgotPassword and resetPassword, then click Save.
Step 11: Create a New Password page
Create a new ./pages/user/Forgot.vue file, and copy/paste the following code in it:
// frontend/pages/user/Forgot.vue
<template>
<div class="max-w-md w-full mx-auto mt-8">
<h1 class="text-3xl font-extrabold mb-4">New password</h1>
<form @submit.prevent="userPassword">
<div
v-if="err"
class="
p-4
mb-4
text-sm text-red-700
bg-red-100
rounded-lg
dark:bg-red-200 dark:text-red-800
"
role="alert"
>
{{ err }}
</div>
<div class="mb-6">
<label
for="email"
class="
block
mb-2
text-sm
font-medium
text-gray-900
dark:text-gray-300
"
>Your email</label
>
<input
v-model="email"
type="email"
class="
shadow-sm
bg-gray-50
border border-gray-300
text-gray-900 text-sm
rounded-lg
focus:ring-blue-500 focus:border-blue-500
block
w-full
p-2.5
dark:bg-gray-700
dark:border-gray-600
dark:placeholder-gray-400
dark:text-white
dark:focus:ring-blue-500
dark:focus:border-blue-500
dark:shadow-sm-light
"
placeholder="name@strapi.io"
required
/>
</div>
<button
type="submit"
class="
text-white
bg-blue-700
hover:bg-blue-800
focus:ring-4 focus:outline-none focus:ring-blue-300
font-medium
rounded-lg
text-sm
px-5
py-2.5
text-center
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
"
>
New password
</button>
</form>
</div>
</template> <script>
export default {
auth: 'guest',
data() {
return {
err: null,
email: '',
}
},
methods: {
async userPassword() {
try {
await this.$axios.post('auth/forgot-password', {
email: this.email,
})
} catch (e) {
if (e.response) this.err = e.response.data.error.message
}
},
},
}
</script>In the code above, we've defined a new userPassword method that sends a request to Strapi's auth/forgot-password endpoint. If the email address exists in Strapi's user database, an email is sent with a link to a reset password page in the frontend app.
This link contains an empty URL with code param which is required to reset user password. To specify a URL, there’s some simple configuration you need to do inside the admin panel.
From the left sidebar of the admin dashboard, click Settings, Advanced settings, paste http://localhost:3000/user/reset in the “Reset password page" field, then click Save.
Let's now create the reset page that will allow the user to define a new password.
Step 12: Complete the New Password page
Create a new file ./pages/user/Reset.vue and paste the following code into it:
// frontend/pages/user/Rest.vue
<template>
<div class="max-w-md w-full mx-auto mt-8">
<h1 class="text-3xl font-extrabold mb-4">New password</h1>
<form @submit.prevent="userPassword">
<div
v-if="err"
class="
p-4
mb-4
text-sm text-red-700
bg-red-100
rounded-lg
dark:bg-red-200 dark:text-red-800
"
role="alert"
>
{{ err }}
</div>
<div
v-if="success"
class="
p-4
mb-4
text-sm text-green-700
bg-green-100
rounded-lg
dark:bg-green-200 dark:text-green-800
"
role="alert"
>
Your password has been updated successfully you can now
<NuxtLink class="font-medium" to="/user/login">Login</NuxtLink>
</div>
<div class="mb-6">
<label
for="password"
class="
block
mb-2
text-sm
font-medium
text-gray-900
dark:text-gray-300
"
>New password</label
>
<input
v-model="password"
type="password"
class="
shadow-sm
bg-gray-50
border border-gray-300
text-gray-900 text-sm
rounded-lg
focus:ring-blue-500 focus:border-blue-500
block
w-full
p-2.5
dark:bg-gray-700
dark:border-gray-600
dark:placeholder-gray-400
dark:text-white
dark:focus:ring-blue-500
dark:focus:border-blue-500
dark:shadow-sm-light
"
required
/>
</div>
<div class="mb-6">
<label
for="password-confirmation"
class="
block
mb-2
text-sm
font-medium
text-gray-900
dark:text-gray-300
"
>Confirm new password</label
>
<input
v-model="passwordConfirmation"
type="password"
class="
shadow-sm
bg-gray-50
border border-gray-300
text-gray-900 text-sm
rounded-lg
focus:ring-blue-500 focus:border-blue-500
block
w-full
p-2.5
dark:bg-gray-700
dark:border-gray-600
dark:placeholder-gray-400
dark:text-white
dark:focus:ring-blue-500
dark:focus:border-blue-500
dark:shadow-sm-light
"
required
/>
</div>
<button
type="submit"
class="
text-white
bg-blue-700
hover:bg-blue-800
focus:ring-4 focus:outline-none focus:ring-blue-300
font-medium
rounded-lg
text-sm
px-5
py-2.5
text-center
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
"
>
New password
</button>
</form>
</div>
</template> <script>
export default {
auth: 'guest',
data() {
return {
success: false,
err: null,
password: '',
passwordConfirmation: '',
}
},
methods: {
async userPassword() {
try {
await this.$axios.post('auth/reset-password', {
code: this.$route.query.code,
password: this.password,
passwordConfirmation: this.passwordConfirmation,
})
this.success = true
} catch (e) {
if (e.response) this.err = e.response.data.error.message
}
},
},
}
</script>In the code above, we've defined three propreties.
password, which will be our new passwordpasswordConfirmation, to make sure that the user didn’t make any mistakecode, the code contained in the reset link sent to the user via email
To test the new functionality, run npm run dev to start the development server.
Then, visit http://localhost:3000/forgot in your browser, go through the forgot password process and then try the reset, starting with the reset link sent to your email address.
Step 13: Token bonus
Awesome! We've done a lot, but there's still something missing before we wrap up.
If the JWT token expires, subsequent requests to Strapi will return a 401 Unauthorized error.
To deal with this, we'll intercept error responses in axios and check if the status code is 401. If it is, we redirect the user to the login page.
Create a new ./plugins/axios.js file, and paste the following:
export default function ({ $axios, redirect }) {
$axios.onError((error) => {
const code = parseInt(error.response && error.response.status)
if (code === 401) redirect('/user/login')
})
}Now, let’s import the new plugin in nuxt.config.js, add the following to your plugins section:
plugins: ['~plugins/axios'],Now, when the JWT expires the user will be gracefully redirected to the login page.
Conclusion
Hopefully, you've found this tutorial helpful for implementing Strapi authentication in your Nuxt app! You should now have a fully-built, ready to be deployed in production authentication flow, through which users can register, login, and reset their password.
Resources
A Front-end developer, Acquia Certified and React.js expert.