How to add authentication in nuxt 3
I've seen a few tutorials on this subject but most of them cover authentication with Supabase, Amplify or Firebase, most of these services have a nuxt component which makes it easier to add authentication to your website.
If you're like me and use the middleware to handle the authentication state of your application by calling an endpoint which provides a token. I will show you how to do this in nuxt 3.
I will be using DummyJSON fake API to help me do this.
What is DummyJSON?
With DummyJSON, what you get is different types of REST Endpoints filled with JSON data which you can use in developing the frontend with your favourite framework and library without worrying about writing a backend.
Essentially it's a mock API with few endpoints, most importantly it provides a rest login endpoint which returns a fake token.
Creating the project
first lets start by creating a project
npx nuxi init nuxt3-auth
Create the following folders/files in the root of the project
-
pages
- index.vue
- login.vue
- about.vue
-
layouts
- default.vue
delete app.vue
Creating required pages
pages/index.vue
<template>
<div>Hello Home Page</div>
</template>
<script lang="ts" setup></script>
pages/about.vue
<template>
<div>About Page</div>
</template>
<script lang="ts" setup></script>
The login page is going to be a very simple just username and password fields with a login button
pages/login.vue
<template>
<div>
<div class="title">
<h2>Login</h2>
</div>
<div class="container form">
<label for="uname"><b>Username</b></label>
<input
v-model="user.username"
type="text"
class="input"
placeholder="Enter Username"
name="uname"
required
/>
<label for="psw"><b>Password</b></label>
<input
v-model="user.password"
type="password"
class="input"
placeholder="Enter Password"
name="psw"
required
/>
<button @click.prevent="login" class="button">Login</button>
</div>
</div>
</template>
<script lang="ts" setup>
const user = ref({
username: '',
password: '',
});
const login = async () => {
// TODO send user Data to the login endpoint and redirect if successful
};
</script>
Creating our default layout
Nuxt provides a customizable layouts framework you can use throughout your application, ideal for extracting common UI or code patterns into reusable layout components.
Layouts are placed in thelayouts/
directory and will be automatically loaded via asynchronous import when used.
Our layout is going to consist of a navbar with Home, About, and Login links, and a footer at the bottom with our content in the middle the <slot/>
will automatically be replaced with our code in the pages
layouts/default.vue
<template>
<div>
<header>
<ul>
<li><nuxt-link to="/">Home</nuxt-link></li>
<li><nuxt-link to="/about">About</nuxt-link></li>
<li v-if="!authenticated" class="loginBtn" style="float: right">
<nuxt-link to="/login">Login</nuxt-link>
</li>
</ul>
</header>
<div class="mainContent">
<slot />
</div>
<footer>
<h1>Footer</h1>
</footer>
</div>
</template>
Middlewares
Nuxt provides a customizable route middleware framework you can use throughout your application, ideal for extracting code that you want to run before navigating to a particular route.
Route middleware is navigation guards that receive the current route and the next route as arguments.
We are going to create a named route middleware. which is placed in the middleware/
directory and will be automatically loaded via asynchronous import when used on a page.
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
console.log('From auth middleware')
})
add a console log to verify this is working on our homepage we can now add this middleware
pages/index.vue
<template>
<div>Hello Home Page</div>
</template>
<script lang="ts" setup>
definePageMeta({
middleware: 'auth' // this should match the name of the file inside the middleware directory
})
</script>
run the code and check and verify if you can see the console.log
in this case, I want to protect every route so how do I make this middleware global? we just need to add the .global
suffix to our file like so auth.global.ts
and it will automatically run on every route change.
we can now remove this piece of code from the homepage
definePageMeta({
middleware: 'auth' // this should match the name of the file inside the middleware directory
})
Store with Pinia
I'm going to create an auth store to handle login and the authenticated state. I've already covered an article on how to set up Pinia in nuxt 3. Pinia and Nuxt 3
// store/auth.ts
import { defineStore } from 'pinia';
interface UserPayloadInterface {
username: string;
password: string;
}
export const useAuthStore = defineStore('auth', {
state: () => ({
authenticated: false,
loading: false,
}),
actions: {
async authenticateUser({ username, password }: UserPayloadInterface) {
// useFetch from nuxt 3
const { data, pending }: any = await useFetch('https://dummyjson.com/auth/login', {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: {
username,
password,
},
});
this.loading = pending;
if (data.value) {
const token = useCookie('token'); // useCookie new hook in nuxt 3
token.value = data?.value?.token; // set token to cookie
this.authenticated = true; // set authenticated state value to true
}
},
logUserOut() {
const token = useCookie('token'); // useCookie new hook in nuxt 3
this.authenticated = false; // set authenticated state value to false
token.value = null; // clear the token cookie
},
},
});
We have two actions authenticateUser
and logUserOut
authenticateUser
function receives a payload of username and password, then we make a post request using the useFetch
hook to /auth/login
endpoint from dummyjson, we pass username and password in the body.
we should receive a response like so
{
"id": 15,
"username": "kminchelle",
"email": "kminchelle@qq.com",
"firstName": "Jeanne",
"lastName": "Halvorson",
"gender": "female",
"image": "https://robohash.org/autquiaut.png?size=50x50&set=set1",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTUsInVzZXJuYW1lIjoia21pbmNoZWxsZSIsImVtYWlsIjoia21pbmNoZWxsZUBxcS5jb20iLCJmaXJzdE5hbWUiOiJKZWFubmUiLCJsYXN0TmFtZSI6IkhhbHZvcnNvbiIsImdlbmRlciI6ImZlbWFsZSIsImltYWdlIjoiaHR0cHM6Ly9yb2JvaGFzaC5vcmcvYXV0cXVpYXV0LnBuZz9zaXplPTUweDUwJnNldD1zZXQxIiwiaWF0IjoxNjM1NzczOTYyLCJleHAiOjE2MzU3Nzc1NjJ9.n9PQX8w8ocKo0dMCw3g8bKhjB8Wo7f7IONFBDqfxKhs"
}
if we have data we save the token to the cookies
logUserOut
this function simply removes the token from the cookies
Finalising
now we need to modify the middleware, login and layouts
Login page
now we can import our auth store and finish the login function
pages/login.vue
<script lang="ts" setup>
import { storeToRefs } from 'pinia'; // import storeToRefs helper hook from pinia
import { useAuthStore } from '~/store/auth'; // import the auth store we just created
const { authenticateUser } = useAuthStore(); // use authenticateUser action from auth store
const { authenticated } = storeToRefs(useAuthStore()); // make authenticated state reactive with storeToRefs
const user = ref({
username: 'kminchelle',
password: '0lelplR',
});
const router = useRouter();
const login = async () => {
await authenticateUser(user.value); // call authenticateUser and pass the user object
// redirect to homepage if user is authenticated
if (authenticated) {
router.push('/');
}
};
</script>
layouts
in the default layout we now display a login/logout button according to the state of our app and handle the logout event
adjust the navbar to show and or hide the buttons based on the authenticated
state
<li v-if="!authenticated" class="loginBtn" style="float: right">
<nuxt-link to="/login">Login</nuxt-link>
</li>
<li v-if="authenticated" class="loginBtn" style="float: right">
<nuxt-link @click="logout">Logout</nuxt-link>
</li>
add the following to the script in layouts/default.vue
<script lang="ts" setup>
import { storeToRefs } from 'pinia'; // import storeToRefs helper hook from pinia
import { useAuthStore } from '~/store/auth'; // import the auth store we just created
const router = useRouter();
const { logUserOut } = useAuthStore(); // use authenticateUser action from auth store
const { authenticated } = storeToRefs(useAuthStore()); // make authenticated state reactive with storeToRefs
const logout = () => {
logUserOut();
router.push('/login');
};
</script>
middleware
Now in middleware, we can handle the authentication based on the value of the token in the cookies.
export default defineNuxtRouteMiddleware((to) => {
const { authenticated } = storeToRefs(useAuthStore()); // make authenticated state reactive
const token = useCookie('token'); // get token from cookies
if (token.value) {
// check if value exists
authenticated.value = true; // update the state to authenticated
}
// if token exists and url is /login redirect to homepage
if (token.value && to?.name === 'login') {
return navigateTo('/');
}
// if token doesn't exist redirect to log in
if (!token.value && to?.name !== 'login') {
abortNavigation();
return navigateTo('/login');
}
});
Preview
Repo: github
Top comments (18)
Thank you, i will try in my next project.
That saves my day Thanks Alot
In a production mode(SSR: true):
After successful authorization I go to "About page", then reload the page in a browser and I stay authorized, but it shows me "Login page" on a short time, than push me to "Home page". I guess middleware doesn't have access to storage on the reloading time. Is there a way to change this behavior?
You able to show me the code to have a look
Basicly, it is a copy of Your code:
github.com/SashaKrav4/storetest.git
npm run dev - works fine, but if I do:
'npm run generate' and 'npx serve .output/public -l 5173'
then I see the issue
Had the same issue.
That is because
middleware/auth.ts
is running per page, so if you go to different page which doesn't have this middleware - it won't setauthenticated.value = true
- thus you'll appear as non authenticated user.Simple solution is to add
middleware/authentication.global.ts
with contentsAnd technically, you could remove this piece of code from
middleware/auth.ts
- but haven't tested this yet.I have global middleware. It doesn't work in a production mode with SSR true. If I set SSR: false, it works
I also have this phenomenon. I successfully logged in through the API and received it normally, but the login screen appears again.
I'm facing the same issue, have you found a way around this?
Thanks!
Storing tokens in regular cookies is not recommended because they are sent with every request to the server, which increases security risks. Using a Set-Cookie header in an API response to store tokens is safer, as it allows setting HttpOnly and Secure flags. This way, the token is less exposed to JavaScript, which reduces the risk of XSS attacks, and can only be transmitted over HTTPS.
Thank you, but i am missing something, i am following the tutorial. First i get this when i try to login
auth.ts:16 [nuxt] [useFetch] Component is already mounted, please use $fetch instead. See nuxt.com/docs/getting-started/data...
Then i change to $fetch but then when i try to login i get a bunch or errors.
Can somebody help please?
I have a question for Nuxt 3. I am using a similar setup to yours, but when I use
v-if
to check if user is auth, I run into rehydration issues because the client and server sees different data. The server see the login user, but the client does not. The big difference I see here and my setup is that you set the cookie in the store vs getting it set from the API. Do you know if there is a way to solve this without usingClientOnly
?Well Done 👌
Thanks to you, I completed it well. thank you
Thanks! This is incredibly simple!
How about storing the users data without using vuex?
You could set up a custom composable