Basic Authentication for Nuxt.js (JSON Web Token + Local Storage)
When developing a web application, ensuring security is essential. In this guide, we’ll explore how to set up a secure authentication flow using Prisma ORM and JSON Web Tokens (JWT) in NUXT 3 projects...
We'll implement a login mechanism that verifies user credentials and generates a JWT token. This token will be saved in localStorage, allowing the user to stay logged in across sessions. There will be two layouts in our application: a login layout for users who are not authenticated and a default layout for authenticated users.
Middleware will check for the presence of a valid token in localStorage on each request, ensuring users without a token are redirected to the login page. Logging out will be straightforward simply clearing the token from localStorage will log the user out and redirect them to the login page.
By following this approach, you’ll implement a basic bearer authentication system in your web application with server & client side guide. If you already have an API endpoint that provides a token upon successful login, you can skip the server-side setup and focus directly on the front-end operations.
Let’s start!
Server-Side setups
Assuming you’ve already set up Prisma and connected your database (or with any ORM tool), it’s time to refine your user model,
model Users {
Id Int @id @default(autoincrement())
Username String @unique
Password String
CreatedAt DateTime @default(now())
}
run the necessary migrations, create an user. The folder architecture will be like;
| components/
| layouts/
| middleware/
| pages/
| prisma/
| |- schema.prisma/
| server/
| |- api/
| | |- auth/
| | | |- login.post.js
|- app.vue
|- nuxt.config.ts
|- package.json
Let’s install required packages that “bcrypt” “jsonwebtoken” “jwt-decode”
npm install bcrypt jsonwebtoken jwt-decode
or
yarn add bcrypt jsonwebtoken jwt-decode
You can create your own JWT token with a help of JWT token generator. The content of token is totally up to you.
/server/api/auth/login.post.js
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
const prisma = new PrismaClient();
const JWT_SECRET = 'your_token'; // Replace with your secret key
export default defineEventHandler(async (event) => {
const { username, password } = await readBody(event);
// Find the user in the database
const user = await prisma.users.findUnique({
where: { Username: username }
});
if (!user) {
return { statusCode: 401, body: { success: false, message: 'Invalid username or password' } };
}
// Check if the password is correct
const validPassword = await bcrypt.compare(password, user.Password);
if (!validPassword) {
return { statusCode: 401, body: { success: false, message: 'Invalid username or password' } };
}
// Create a JWT token
const token = jwt.sign(
{ id: user.Id, username: user.Username },
JWT_SECRET,
{ expiresIn: '12h' } // Token expires in 12 hour
);
// Return the token to the frontend
return {
statusCode: 200,
body: {
success: true,
message: 'Login successful',
token
}
};
});
This handler performs several key tasks: it first extracts the username and password from the request body, then queries the database to locate the user with the specified username. It verifies the provided password against the hashed password stored in the database. Upon successful validation, it generates a JSON Web Token (JWT) with a 48-hour expiration and returns this token to the frontend for subsequent authentication. If the credentials are invalid, it responds with a 401 status code and an appropriate error message.
Let’s fix nuxt.config.ts and check the API if it works.
export default defineNuxtConfig({
...
server: {
proxy: {
'/api': 'http://localhost:3000' // Adjust the port if your backend server is running on a different port
},
router: {
base: '/api'
},
},
router: {
middleware: ['auth']
},
...
});
And the Postman response be like,
It works well!
Client-Side Setups:
We can start by revisiting the file structure. With Prisma and server files already set up, the next steps involve defining layouts, forms, pages, and middleware. Let’s begin with setting up the two layout files.
| components/
| |- LoginForm.vue
| layouts/
| |- default.vue
| |- login.vue
| middleware/
| |- auth.js
| pages/
| |- index.vue
| |- login.vue
| prisma/
| server/
|- app.vue
|- nuxt.config.ts
|- package.json
/layouts/default.vue
<template>
<div v-if="isAuthenticated">
<!-- You can add header footer sidebars etc. -->
<NuxtPage />
<DialogWrapper />
</div>
<div v-else>
<p>Loading...</p>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
import { DialogWrapper } from "vue3-promise-dialog";
const router = useRouter();
const isAuthenticated = ref(null); // Use null to distinguish between loading and not authenticated
onMounted(() => {
const token = localStorage.getItem("authToken");
if (token) {
isAuthenticated.value = true;
} else {
isAuthenticated.value = false;
router.push("/login");
}
});
</script>
*/layouts/login.vue*
You can hide all the side bars, panels, pages, header and footer if user is not authenticated.
<template>
<div>
<NuxtPage />
<DialogWrapper />
</div>
</template>
<script setup>
import { useRouter } from "vue-router";
import { DialogWrapper } from "vue3-promise-dialog";
const router = useRouter();
const navigateToRoute = (route) => {
router.push(route);
};
</script>
*/app.vue*
<script setup>
import { useRouter } from "vue-router";
import { DialogWrapper } from "vue3-promise-dialog";
const router = useRouter();
const navigateToRoute = (route) => {
router.push(route);
};
</script>
*/pages/index.vue*
<template>
<section>
<div class="block">
<h1>
Welcome to your Web App!
</h1>
<button
@click="logout">Logout
</button>
</div>
</section>
</template>
<script>
export default {
methods: {
logout() {
// Remove the auth token from localStorage
localStorage.removeItem("authToken");
// Redirect the user to the login page
this.$router.push("/login");
},
},
</script>
/pages/login.vue
<template>
<section>
<div >
<LoginForm />
</div>
</section>
</template>
<script>
definePageMeta({
layout: "login",
});
export default {};
</script>
Page setups are completed! We can now proceed with implementing the login functionality. A crucial step is to extract the token from the login response and store it in local storage. This ensures that the token is available for authenticating future requests.
/components/LoginForm.vue
<template>
<div>
<form @submit.prevent="signIn">
<div>
<label for="username">Username</label
>
<input
type="text"
id="username"
v-model="Username"
required
class="w-full"
/>
</div>
<div>
<label for="password">Password</label
>
<input
type="password"
id="password"
v-model="Password"
required
class="w-full"
/>
</div>
<button
type="submit"
class="w-full"
>
Sign in
</button>
</form>
</div>
</template>
<script>
export default {
data() {
return {
Username: "",
Password: "",
};
},
methods: {
async signIn() {
try {
const response = await fetch("/api/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: this.Username,
password: this.Password,
}),
});
const data = await response.json();
if (data.statusCode === 200) {
// Set the token in local storage
localStorage.setItem("authToken", data.token);
alert("Login is successful");
this.$router.push("/");
} else {
console.log("Login failed:", data.message);
alert("Username or password is invalid");
}
} catch (error) {
console.error("Error during login:", error);
alert("Error during login. Please try again.");
}
},
},
};
</script>
Time for middleware/auth.js file. Essentially, it ensures that only users with a valid token can access to some pages of the application.
/middleware/auth.js
//middleware/auth.js
import * as jwtDecode from 'jwt-decode';
export default defineNuxtRouteMiddleware((to, from) => {
const token = localStorage.getItem('authToken');
if (token) {
try {
const decodedToken = jwtDecode(token);
const currentTime = Date.now() / 1000;
if (decodedToken.exp < currentTime) {
localStorage.removeItem('authToken');
return navigateTo('/login');
}
} catch (error) {
// Handle token decoding error
localStorage.removeItem('authToken');
return navigateTo('/login');
}
} else if (to.name !== 'login') {
return navigateTo('/login');
}
});
This middleware function handles JWT authentication by:
Retrieving the JWT token from localStorage.
Decoding and verifying the token’s expiration.
If expired, it removes the token and redirects to the login page.
If decoding fails, it also removes the token and redirects to the login page.
Redirecting unauthenticated users to the login page if no valid token is found.
In essence, it ensures only users with a valid, non-expired token can access protected routes.
However, it’s important to note that this setup only addresses front-end authentication. If your application has server-side endpoints for managing user sessions or access control, you’ll need to implement additional server-side middleware to handle API safety.
See you in next article!
Top comments (0)