In this tutorial, we are going to learn how to create a fully secure system for our Nuxt application.
Here, we are going to consider that your Nuxt App is SSR, and we are going to use
nuxt-auth-utils for developing the secure system. The general workflow will be, instead of the browser talking with our backend server, it will communicate with the Nitro server(SSR), and the Nitro server will communicate with our backend server for fetching data.
How It Works
- The client submits the login credentials to the Nitro Server. Nitro forwards this payload to the backend server. Which then receives the JWT token or any other credentials to manage the Authentication. The
nuxt-auth-utilwill encrypt it and store it in the session under the hood. - We will not expose the token in the browser and secured fully in the Nitro server.
- Now, when a user requests the secured endpoint, the Nitro server will get the session token and add it to the header to send to the backend server for data fetching.
Install nuxt-auth-utils
Add
nuxt-auth-utils in our Nuxt app.bash
npx nuxi@latest module add auth-utilsThe above command will add the following things:
Inside
nuxt.config.tsts
modules: [
.....
'nuxt-auth-utils'
],Inside the
.env filebash
NUXT_SESSION_PASSWORD=password-with-at-least-32-charactersAdd types for user session
We can define the type of the user session by creating a type declaration file. Under
/shared/types/auth.d.ts ts
declare module '#auth-utils' {
interface UserSession {
user: {
authenticatedAt: string
},
secure: {
token: string
}
}
}Here, the user definition must be mandatory for a logged-in user. Here we are using the
authenticatedAt field; you can use your user info fields like email, profiles, etc.The
secure block is the most important part here. The fields or data inside the secure block will be available and accessible only inside the Nitro server and not in the browser. Add Session Expiry
To add the expiry of our token or user session and default password, add the below config in our Nuxt config file
nuxt.config.ts.ts
runtimeConfig: {
session: {
maxAge: 60 * 60 * 24 * 7, // 1 week
password: process.env.NUXT_SESSION_PASSWORD
},
},Client Login Component
Let's create a browser/client login. For this, create a file
login.vue vue
<script setup lang="ts">
const auth = useAuthStore()
const toast = useToast()
const loading = ref(false)
const state = reactive({
username: '',
password: ''
})
async function onSubmit() {
loading.value = true
const success = await auth.login(state)
if (success) {
toast.add({
title: 'Access Granted',
description: 'Redirecting to CSBYTE command center...',
color: 'primary'
})
navigateTo('/admin')
} else {
toast.add({
title: 'Authentication Failed',
description: 'Please check your credentials and try again.',
color: 'danger'
})
}
loading.value = false
}
</script>
<template>
<div class="min-h-screen flex items-center justify-center bg-[#050505] relative overflow-hidden">
<div class="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-primary-500/10 blur-[120px] rounded-full" />
<div class="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-blue-500/10 blur-[120px] rounded-full" />
<UContainer class="z-10 w-full max-w-md">
<div class="mb-8 text-center">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-neutral-800 to-neutral-900 border border-neutral-700 shadow-2xl mb-4 group hover:border-primary-500/50 transition-colors">
<span class="text-2xl font-black bg-clip-text text-transparent bg-gradient-to-r from-primary-400 to-blue-400">CB</span>
</div>
<h1 class="text-2xl font-bold tracking-tight text-white uppercase italic">
CsByte Admin
</h1>
<p class="text-neutral-500 text-sm mt-1">Terminal Authentication Required</p>
</div>
<UCard
:ui="{
base: 'bg-neutral-900/50 backdrop-blur-xl border-neutral-800 shadow-[0_0_50px_-12px_rgba(0,0,0,0.5)]',
body: 'p-8'
}"
>
<UForm :state="state" @submit="onSubmit" class="space-y-6">
<UFormField label="Operator Username" name="username" :ui="{ label: 'text-neutral-400 text-xs font-bold uppercase tracking-widest' }">
<UInput
v-model="state.username"
placeholder="e.g. admin"
icon="i-lucide-terminal"
color="neutral"
variant="subtle"
size="xl"
class="w-full"
/>
</UFormField>
<UFormField label="Security Key" name="password" :ui="{ label: 'text-neutral-400 text-xs font-bold uppercase tracking-widest' }">
<UInput
v-model="state.password"
type="password"
icon="i-lucide-shield-check"
color="neutral"
variant="subtle"
size="xl"
class="w-full"
/>
</UFormField>
<UButton
type="submit"
block
size="xl"
color="primary"
:loading="loading"
class="font-bold uppercase tracking-widest py-4 transition-all hover:shadow-[0_0_20px_-5px_var(--color-primary-500)]"
>
Authorize Access
</UButton>
</UForm>
</UCard>
<div class="mt-8 text-center">
<NuxtLink to="/" class="text-neutral-600 hover:text-neutral-400 text-xs flex items-center justify-center gap-2 transition-colors">
<UIcon name="i-lucide-arrow-left" class="w-3 h-3" />
Back to Public Feed
</NuxtLink>
</div>
</UContainer>
</div>
</template>
<style scoped>
/* Optional: Adding a metallic texture overlay */
.bg-neutral-900\/50 {
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3ExternalForce%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
background-blend-mode: overlay;
opacity: 0.98;
}
</style>This is the sample login page for authentication.
Creating AuthStore
Let's create a composible
useAuthStore.ts ts
export const useAuthStore = defineStore('auth', () => {
const { user, loggedIn:isAuthenticated, clear, fetch: fetchSession } = useUserSession()
async function login(credentials: { username: string; password: any }) {
try {
await $fetch('/api/auth/login', {
method: 'POST',
body: credentials
})
await fetchSession()
return true
} catch (error) {
console.error('Login failed:', error)
return false
}
}
async function logout() {
await clear()
navigateTo('/login')
}
return { isAuthenticated, login, logout }
})Here
useUserSession is the composible from nuxt-auth-utils, which gives all the session user info like user, loggedIn or not, clear for logout, fetch.fetch: fetchSession will force the client to sync the server changes after login./api/auth/login will hit the Nitro server endpoint for login. Authentication Middleware
The main function of the middleware is to protect specific pages from viewing by authenticated users.
If someone tries to access a page that uses this middleware, it checks if the user is login user or not. If they aren’t logged in, it will force them to the login page.
Create the middleware file under
/app/middleware/admin.tsts
export default defineNuxtRouteMiddleware(() => {
const auth = useAuthStore()
if (!auth.isAuthenticated) {
return navigateTo('/login')
}
})Use this middleware for auth pages.
ts
definePageMeta({
layout: 'admin',
middleware: 'admin'
})Nitro Authentication Handler
Centralized Backend API Utility
Let's first create the centralized utility class
/server/utils/api.ts where we are going to inject the login auth-token and other security headers. Creating a centralized backend utitly have benifits that if some changes are required, we don't have to make changes in all the nitro api endpoint. ts
import type { H3Event } from 'h3'
import type { NitroFetchOptions, NitroFetchRequest } from 'nitropack'
export const api = async <T = any>(
event: H3Event,
path: NitroFetchRequest,
options: NitroFetchOptions<NitroFetchRequest> = {}
): Promise<T> => {
const config = useRuntimeConfig()
const session = await getUserSession(event)
// Inject headers directly into the incoming options object before running $fetch
const headers = Array.isArray(options.headers)
? Object.fromEntries(options.headers)
: { ...options.headers }
headers['X-Forwarded-For-Nitro'] = 'true'
if (session.secure?.token) {
headers['Authorization'] = `Bearer ${session.secure.token}`
}
// Execute using the global single-instance $fetch
return $fetch<T>(path, {
baseURL: config.baseUrl,
...options,
headers
})
}X-Forwarded-For-Nitro is a Nitro security header server-to-server validation fingerprint that is sent to the backend server to verify the request is from the Nitro server.Bearer ${session.secure.token} this is where we are injecting our login token to send to the backend server.Nitro login endpoint
Let's create our api endpoint for login in the Nitro server.
ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const config = useRuntimeConfig()
try {
const accessToken = await api(event,'/api/auth/login', {
baseURL: config.baseUrl,
method: 'POST',
body
})
await setUserSession(event, {
secure: {
token: accessToken // This stays server-side and will NEVER be sent to the browser
},
user: {
authenticatedAt: new Date().toISOString()
}
})
return { success: true }
} catch (error: any) {
throw createError({
status: error.response?.status || 401,
statusText: 'Invalid credentials'
})
}
})setUserSession we are injecting the user session using setUserSession. We are calling the centralized fetch client utility class:
ts
const accessToken = await api(event,'/api/auth/login', {
baseURL: config.baseUrl,
method: 'POST',
body
})This will hit the request to the backend, get the JWT token, and set to the user session.
For your endpoint that required login or Nitro Server security header, you need to call the centralized api utility class as above.
This is how we can set up the secure system for the Nuxt Application. Here, we didn't expose our token to the browser to make it more secure. If you want to add a different Oauth Provider, reference this nuxt-auth-utils.
