Sessions and Authentication
Introduction
In this recipe we'll be setting up authentication in a full-stack Nuxt app using Nuxt Auth Utils which provides convenient utilities for managing client-side and server-side session data.
The module uses secured & sealed cookies to store session data, so you don't need to setup a database to store session data.
Install nuxt-auth-utils
Install the nuxt-auth-utils
module using the nuxi
CLI.
npx nuxi@latest module add auth-utils
nuxt-auth-utils
as dependency and push it in the modules
section of our nuxt.config.ts
Cookie Encryption Key
As nuxt-auth-utils
uses sealed cookies to store session data, session cookies are encrypted using a secret key from the NUXT_SESSION_PASSWORD
environment variable.
.env
automatically when running in development mode.NUXT_SESSION_PASSWORD=a-random-password-with-at-least-32-characters
Login API Route
For this recipe, we'll create a simple API route to sign-in a user based on static data.
Let's create a /api/login
API route that will accept a POST request with the email and password in the request body.
import { z } from 'zod'
const bodySchema = z.object({
email: z.string().email(),
password: z.string().min(8)
})
export default defineEventHandler(async (event) => {
const { email, password } = await readValidatedBody(event, bodySchema.parse)
if (email === 'admin@admin.com' && password === 'iamtheadmin') {
// set the user session in the cookie
// this server util is auto-imported by the auth-utils module
await setUserSession(event, {
user: {
name: 'John Doe'
}
})
return {}
}
throw createError({
statusCode: 401,
message: 'Bad credentials'
})
})
zod
dependency in your project (npm i zod
).Login Page
The module exposes a Vue composable to know if a user is authenticated in our application:
<script setup>
const { loggedIn, session, user, clear, fetch } = useUserSession()
</script>
Let's create a login page with a form to submit the login data to our /api/login
route.
<script setup lang="ts">
const { loggedIn, user, fetch: refreshSession } = useUserSession()
const credentials = reactive({
email: '',
password: '',
})
async function login() {
$fetch('/api/login', {
method: 'POST',
body: credentials
})
.then(async () => {
// Refresh the session on client-side and redirect to the home page
await refreshSession()
await navigateTo('/')
})
.catch(() => alert('Bad credentials'))
}
</script>
<template>
<form @submit.prevent="login">
<input v-model="credentials.email" type="email" placeholder="Email" />
<input v-model="credentials.password" type="password" placeholder="Password" />
<button type="submit">Login</button>
</form>
</template>
Protect API Routes
Protecting server routes is key to making sure your data is safe. Client-side middleware is helpful for the user, but without server-side protection your data can still be accessed. It is critical to protect any routes with sensitive data, we should return a 401 error if the user is not logged in on those.
The auth-utils
module provides the requireUserSession
utility function to help make sure that users are logged in and have an active session.
Let's create an example of a /api/user/stats
route that only authenticated users can access.
export default defineEventHandler(async (event) => {
// make sure the user is logged in
// This will throw a 401 error if the request doesn't come from a valid user session
const { user } = await requireUserSession(event)
// TODO: Fetch some stats based on the user
return {}
});
Protect App Routes
Our data is safe with the server-side route in place, but without doing anything else, unauthenticated users would probably get some odd data when trying to access the /users
page. We should create a client-side middleware to protect the route on the client side and redirect users to the login page.
nuxt-auth-utils
provides a convenient useUserSession
composable which we'll use to check if the user is logged in, and redirect them if they are not.
We'll create a middleware in the /middleware
directory. Unlike on the server, client-side middleware is not automatically applied to all endpoints, and we'll need to specify where we want it applied.
export default defineNuxtRouteMiddleware(() => {
const { loggedIn } = useUserSession()
// redirect the user to the login screen if they're not authenticated
if (!loggedIn.value) {
return navigateTo('/login')
}
})
Home Page
Now that we have our app middleware to protect our routes, we can use it on our home page that display our authenticated user informations. If the user is not authenticated, they will be redirected to the login page.
We'll use definePageMeta
to apply the middleware to the route that we want to protect.
<script setup lang="ts">
definePageMeta({
middleware: ['authenticated'],
})
const { user, clear: clearSession } = useUserSession()
async function logout() {
await clearSession()
await navigateTo('/login')
}
</script>
<template>
<div>
<h1>Welcome {{ user.name }}</h1>
<button @click="logout">Logout</button>
</div>
</template>
We also added a logout button to clear the session and redirect the user to the login page.
Conclusion
We've successfully set up a very basic user authentication and session management in our Nuxt app. We've also protected sensitive routes on the server and client side to ensure that only authenticated users can access them.
As next steps, you can:
- Add authentication using the 20+ supported OAuth providers
- Add a database to store users, see Nitro SQL Database or NuxtHub SQL Database
- Let user signup with email & password using password hashing
- Add support for WebAuthn / Passkeys
Checkout the open source atidone repository for a full example of a Nuxt app with OAuth authentication, database and CRUD operations.