Choosing the Right Authentication Pattern

The correct auth architecture for a JavaScript application depends on one key question: does your app have a server? The answer determines where tokens live and therefore how secure your auth implementation can be.

App Type Recommended Pattern Token Storage PKCE
Next.js (App Router) Server-side session via NextAuth.js v5 HTTP-only cookie (server-managed) ✅ Always
React SPA BFF recommended; or PKCE + in-memory tokens Memory (access token) + HTTP-only cookie (refresh token) ✅ Always
Vue 3 SPA Same as React SPA Memory (access token) + HTTP-only cookie (refresh token) ✅ Always

Next.js App Router Authentication

Next.js 14+ App Router with React Server Components is the most secure JavaScript architecture for auth in 2026. Tokens never touch the browser — the Next.js server holds the session and injects only safe, non-sensitive session data into server components.

1. Install NextAuth.js v5

npm install next-auth@5

2. Configure the Ailacs Identity OIDC Provider

// auth.ts
import NextAuth from "next-auth";
import type { NextAuthConfig } from "next-auth";

export const config: NextAuthConfig = {
    providers: [
        {
            id:       "ailacs",
            name:     "Ailacs Identity",
            type:     "oidc",
            issuer:   "https://auth.ailacs.com",
            clientId: process.env.AUTH_AILACS_CLIENT_ID,
            clientSecret: process.env.AUTH_AILACS_CLIENT_SECRET,
            checks: ["pkce", "state", "nonce"],
            authorization: {
                params: { scope: "openid profile email", response_type: "code" }
            }
        }
    ],
    callbacks: {
        async jwt({ token, account }) {
            if (account) token.accessToken = account.access_token;
            return token;
        },
        async session({ session, token }) {
            session.accessToken = token.accessToken as string;
            return session;
        }
    }
};

export const { handlers, auth, signIn, signOut } = NextAuth(config);

3. Protect Routes with middleware.ts

// middleware.ts (project root — runs on the Edge runtime)
export { auth as middleware } from "./auth";

export const config = {
    matcher: [
        "/dashboard/:path*",
        "/account/:path*",
        "/api/protected/:path*"
    ]
};

4. Access Session in Server Components

// app/dashboard/page.tsx
import { auth }     from "@/auth";
import { redirect } from "next/navigation";

export default async function DashboardPage() {
    const session = await auth();

    if (!session) redirect("/api/auth/signin");

    return (
        <div>
            <h1>Welcome, {session.user?.name}</h1>
            <p>Signed in as: {session.user?.email}</p>
        </div>
    );
}

Server components call auth() directly — no client-side hooks or context providers needed. Tokens are never serialised into client bundles.

React SPA Authentication

For a pure React SPA with no server, use react-oidc-context which wraps oidc-client-ts and implements the authorisation code flow with PKCE entirely in the browser. For high-security apps, pair it with a BFF proxy to keep tokens off the client entirely.

1. Install the Library

npm install oidc-client-ts react-oidc-context

2. Wrap Your App with AuthProvider

// main.tsx
import { AuthProvider } from "react-oidc-context";

const oidcConfig = {
    authority:    "https://auth.ailacs.com",
    client_id:    "your-react-spa-client-id",
    redirect_uri: window.location.origin + "/callback",
    response_type: "code",
    scope:        "openid profile email",
    automaticSilentRenew: true
};

ReactDOM.createRoot(document.getElementById("root")!).render(
    <AuthProvider {...oidcConfig}>
        <App />
    </AuthProvider>
);

3. Create a Protected Route Component

// components/ProtectedRoute.tsx
import { useAuth } from "react-oidc-context";

export function ProtectedRoute({ children }: { children: React.ReactNode }) {
    const auth = useAuth();

    if (auth.isLoading)       return <div>Loading...</div>;
    if (auth.error)           return <div>Auth error: {auth.error.message}</div>;
    if (!auth.isAuthenticated) { auth.signinRedirect(); return null; }

    return <>{children}</>;
}

4. Use in Your Router

<Route path="/dashboard" element={
    <ProtectedRoute>
        <Dashboard />
    </ProtectedRoute>
} />

Vue 3 Authentication

Vue 3 auth is best implemented with oidc-client-ts combined with a Pinia store for reactive auth state and vue-router navigation guards to protect routes.

1. Install oidc-client-ts and Pinia

npm install oidc-client-ts pinia

2. Create the Pinia Auth Store

// stores/auth.ts
import { defineStore } from "pinia";
import { UserManager, WebStorageStateStore } from "oidc-client-ts";

const userManager = new UserManager({
    authority:    "https://auth.ailacs.com",
    client_id:    "your-vue-client-id",
    redirect_uri: window.location.origin + "/callback",
    response_type: "code",
    scope:        "openid profile email",
    userStore:    new WebStorageStateStore({ store: window.sessionStorage })
});

export const useAuthStore = defineStore("auth", {
    state: () => ({
        user: null as Awaited<ReturnType<typeof userManager.getUser>>
    }),
    getters: {
        isAuthenticated: (state) => !!state.user && !state.user.expired,
        accessToken:     (state) => state.user?.access_token
    },
    actions: {
        async init()           { this.user = await userManager.getUser(); },
        async login()          { await userManager.signinRedirect(); },
        async handleCallback() { this.user = await userManager.signinRedirectCallback(); },
        async logout()         { await userManager.signoutRedirect(); }
    }
});

3. Add Navigation Guards

// router/index.ts
import { createRouter, createWebHistory } from "vue-router";
import { useAuthStore } from "@/stores/auth";

const router = createRouter({
    history: createWebHistory(),
    routes: [
        { path: "/",          component: () => import("../views/Home.vue") },
        { path: "/dashboard", component: () => import("../views/Dashboard.vue"),
          meta: { requiresAuth: true } },
        { path: "/callback",  component: () => import("../views/Callback.vue") }
    ]
});

router.beforeEach(async (to) => {
    const authStore = useAuthStore();
    await authStore.init();

    if (to.meta.requiresAuth && !authStore.isAuthenticated) {
        await authStore.login();
        return false;
    }
});

The BFF Pattern for High-Security SPAs

The Backend for Frontend (BFF) pattern is the most secure architecture for SPAs. A thin server-side proxy handles all OAuth flows and stores tokens server-side. The browser only holds an HTTP-only session cookie — tokens never touch client-side JavaScript.

❌ Without BFF
  1. SPA performs PKCE flow in the browser
  2. Tokens stored in browser memory or sessionStorage
  3. XSS can potentially exfiltrate tokens from memory
  4. Silent renewal required on every page reload
✅ With BFF
  1. BFF performs PKCE flow on the server
  2. Tokens stored in server-side session (Redis or in-memory)
  3. Browser only holds an HTTP-only, Secure, SameSite=Strict cookie
  4. XSS cannot read tokens — they never reach the browser

For .NET, use Duende.BFF or the built-in ASP.NET Core OIDC + cookie authentication middleware as your BFF. Your React or Vue frontend then calls BFF API endpoints directly — the browser's session cookie is automatically included in every same-origin request.

Token Security Rules for JavaScript Apps

  • NeverStore access or refresh tokens in localStorage — XSS-accessible to all scripts on the page
  • NeverStore tokens in non-HTTP-only cookies — JavaScript-readable via document.cookie
  • AlwaysUse PKCE (code_challenge_method=S256) — required for all public clients; eliminates the need for a client secret
  • AlwaysValidate state and nonce parameters to prevent CSRF and token replay attacks
  • RecommendedUse short-lived access tokens (5–15 minutes) with automatic silent renewal via a hidden iframe or refresh token rotation
  • RecommendedImplement the BFF pattern for applications that handle sensitive data, payments, or have high security requirements
  • RecommendedEnforce a strict Content Security Policy (CSP) — the most effective defence against XSS in JavaScript apps