Authentication & Login Flow
This pattern demonstrates how to implement secure, production-grade user authentication in a Nuxt 4 application using nuxt-auth-utils, an imperative $api plugin, and route middleware guards.
Key Architectural Concepts
- Browser-Egress Authentication:
- The browser POSTs credentials directly to the backend API (
/login). - Avoids passing credentials through intermediate server functions unnecessarily.
- The browser POSTs credentials directly to the backend API (
- Encrypted HTTP-Only Session Cookie:
- On successful backend login (
{ token, user }), the client sends the session payload to local Nitro endpoint/api/auth/sessionto seal an encrypted HTTP-only cookie viauseUserSession().
- On successful backend login (
- Route Guard Middleware:
logged-in.tsmiddleware verifiessession.valuebefore granting access to protected routes like/dashboard.
- Auto-Attaching Bearer Interceptor:
$apiplugin injectsAuthorization: Bearer <token>when requests set{ auth: true }and automatically logs out on401status responses.
1. Login Form Implementation (app/pages/login.vue)
<script setup lang="ts">
definePageMeta({ layout: false });
const { loggedIn, fetch: fetchSession, clear: clearSession } = useUserSession();
const { $api } = useNuxtApp();
if (loggedIn.value) await navigateTo("/dashboard");
const form = reactive({ email: "", password: "" });
const pending = ref(false);
const errorMsg = ref("");
async function onSubmit() {
pending.value = true;
errorMsg.value = "";
try {
// 1. Post credentials directly to backend
const res = await $api<{ status_code: number; data?: { token: string; user: any } }>("/login", {
method: "POST",
body: { ...form }
});
if (res.status_code !== 200) {
errorMsg.value = "Login failed.";
await clearSession();
return;
}
const { token, user } = res.data ?? {};
// 2. Write session into encrypted HTTP-only cookie
await $api("/auth/session", {
baseURL: "/api/",
method: "POST",
body: { user, token, loggedInAt: Date.now() }
});
// 3. Refresh session state & redirect to dashboard
await fetchSession();
await navigateTo("/dashboard");
} catch (e) {
errorMsg.value = "Something went wrong.";
await clearSession();
} finally {
pending.value = false;
}
}
</script>
2. Route Guard Middleware (app/middleware/logged-in.ts)
export default defineNuxtRouteMiddleware(async () => {
const { loggedIn, session, fetch: fetchSession } = useUserSession();
if (!session.value) await fetchSession();
if (!loggedIn.value) return navigateTo("/login");
});
To guard any protected page, add definePageMeta({ middleware: "logged-in" }) at the top of the Vue component.
3. Imperative $api Plugin (app/plugins/api.ts)
export default defineNuxtPlugin((nuxtApp) => {
const { session, fetch: fetchSession, clear: clearSession } = useUserSession();
const config = useRuntimeConfig();
const baseURL = config.public.enableProxy
? "/api/proxy"
: (config.public.apiBase as string);
const api = $fetch.create({
baseURL,
headers: { Accept: "application/json" },
async onRequest({ options }) {
const { auth } = options as { auth?: boolean };
if (!auth) return;
if (!session.value) {
await nuxtApp.runWithContext(() => fetchSession());
}
if (session.value?.token) {
options.headers.set("Authorization", `Bearer ${session.value.token}`);
}
},
async onResponseError({ response }) {
if (response.status === 401) {
await nuxtApp.runWithContext(async () => {
await clearSession();
await navigateTo("/");
});
}
}
});
return { provide: { api } };
});
Try It
The working pages are in examples/nuxt (app/pages/login.vue, app/pages/dashboard.vue).
Run pnpm dev:nuxt from the repo root, then open /login.