DEV Community

Cover image for Authentication in Nuxt 3
Rafael Magalhaes
Rafael Magalhaes

Posted on • Updated on • Originally published at blog.rrrm.co.uk

Authentication in Nuxt 3

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>
Enter fullscreen mode Exit fullscreen mode

pages/about.vue

<template>
  <div>About Page</div>
</template>
<script lang="ts" setup></script>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 the layouts/ 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>
Enter fullscreen mode Exit fullscreen mode

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')
})
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 
})
Enter fullscreen mode Exit fullscreen mode

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
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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');
  }
});
Enter fullscreen mode Exit fullscreen mode

Preview

Repo: github

Top comments (14)

Collapse
 
alexbanicaululab profile image
Alex Banica

Thank you, i will try in my next project.

Collapse
 
sashakrav4 profile image
Sasha K

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?

Collapse
 
rafaelmagalhaes profile image
Rafael Magalhaes

You able to show me the code to have a look

Collapse
 
sashakrav4 profile image
Sasha K

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

Collapse
 
rafalolszewski94 profile image
Rafał Olszewski • Edited

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 set authenticated.value = true - thus you'll appear as non authenticated user.

Simple solution is to add middleware/authentication.global.ts with contents

import { useAuthStore } from "~/store/auth";

export default defineNuxtRouteMiddleware((to, from) => {
  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
  }
});
Enter fullscreen mode Exit fullscreen mode

And technically, you could remove this piece of code from middleware/auth.ts - but haven't tested this yet.

 if (token.value) {
    // check if value exists
    authenticated.value = true; // update the state to authenticated
  }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
sashakrav4 profile image
Sasha K

I have global middleware. It doesn't work in a production mode with SSR true. If I set SSR: false, it works

Collapse
 
flaghrbs1004 profile image
HoGyun Lim

I also have this phenomenon. I successfully logged in through the API and received it normally, but the login screen appears again.

Collapse
 
armandh profile image
Armand

I'm facing the same issue, have you found a way around this?

Thanks!

Collapse
 
freetoplay profile image
freetoplay

I have a question for Nuxt 3. I am using a similar setup to yours, but when I use v-ifto 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 using ClientOnly?

Collapse
 
willsilvano profile image
Willian • Edited

Thanks! This is incredibly simple!

Collapse
 
kellskamuzu profile image
Kelvin Chidothi

How about storing the users data without using vuex?

Collapse
 
rafaelmagalhaes profile image
Rafael Magalhaes

You could set up a custom composable

Collapse
 
ymir profile image
Amir H. Moayeri

Well Done 👌

Collapse
 
flaghrbs1004 profile image
HoGyun Lim

Thanks to you, I completed it well. thank you