9. JWT Cookies & Dual Auth - A Detailed Guide (Nova Shop)
Explains why Nova has two auth systems (NextAuth + NestJS JWT), how authFetch / refresh works, and compares it against best practice.
On this page
- Table of Contents
- 1. What Is Dual Auth?
- 2. The Overall Diagram
- 3. Cookies - Names and Roles
- 4. setAuthCookies / clearAuthCookies
- 5. ensureValidAccessToken & refreshTokens
- 6. authFetch - the Fetch Wrapper
- 7. Middleware + authFetch - Two Layers of Checking
- 8. The NestJS API Contract
- 9. Comparing Other Patterns
- 10. Best Practice vs Nova & Improvements
- 11. FAQ & Implementing From Scratch
9. JWT Cookies & Dual Auth - A Detailed Guide (Nova Shop)
Explains why Nova has two auth systems (NextAuth + NestJS JWT), how authFetch / refresh work, and compares it against best practice.
Read after: 7. Authentication, 8. NextAuth v5.
Table of Contents
- What is dual auth?
- The overall diagram
- Cookies - names and roles
setAuthCookies/clearAuthCookiesensureValidAccessToken&refreshTokensauthFetch- the fetch wrapper- Middleware +
authFetch- two layers of checking - The NestJS API contract
- Comparing other patterns
- Best practice vs Nova & improvements
- FAQ & implementing from scratch
1. What Is Dual Auth?
| System | Stored where | Used for |
|---|---|---|
| NextAuth session | A JWT session cookie (Auth.js) | useSession, middleware auth?.user, login UX |
| Backend JWT | HttpOnly access_token, refresh_token | authFetch → NestJS Authorization: Bearer |
They don't overlap - both exist simultaneously after a successful login.
User login succeeds
→ NextAuth: "the user has signed in" (middleware knows this)
→ Cookies: "API calls carry a Bearer token" (authFetch knows this)
2. The Overall Diagram
3. Cookies - Names and Roles
File: app/lib/auth-constants.ts
| Cookie | HttpOnly? | Purpose |
|---|---|---|
access_token | ✅ | Bearer token sent to NestJS |
refresh_token | ✅ | Gets a new access token via /token |
access_expires_at | ✅ | Unix timestamp for access expiry |
user_id | ❌ (in Nova) | Used to query the cart - should be derived from the backend JWT instead |
4. setAuthCookies / clearAuthCookies
After a successful login
// auth-tokens.ts
await setAuthCookies({
accessToken: response.accessToken,
refreshToken: response.refreshToken,
userId: response.userId,
});
Called from:
Credentials.authorize- The Google
signIncallback
Security flags
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: ACCESS_TOKEN_MAX_AGE,
Logout
events: {
async signOut() {
await logout(); // POST /logout + refresh token in the body
await clearAuthCookies();
},
}
Clear the cookies even if the backend call fails - to avoid a "zombie logged in" UI state.
5. ensureValidAccessToken & refreshTokens
The Problem
The access token expires midway through a session - the user still has a refresh_token.
Nova's Solution
export async function ensureValidAccessToken(): Promise<boolean> {
const access = cookies().get(ACCESS_TOKEN_COOKIE)?.value;
const expiresAt = cookies().get(ACCESS_EXPIRES_COOKIE)?.value;
if (access && !isAccessTokenExpired(expiresAt)) return true;
return refreshTokens();
}
export async function refreshTokens(): Promise<boolean> {
const refresh = cookies().get(REFRESH_TOKEN_COOKIE)?.value;
if (!refresh) return false;
const res = await fetch(`${API}/token`, {
method: "POST",
body: JSON.stringify({ refreshToken: refresh }),
});
if (!res.ok) return false;
const data = await res.json();
await setAuthCookies(data);
return true;
}
6. authFetch - the Fetch Wrapper
File: app/lib/api-client.ts
export async function authFetch(url: string, init?: RequestInit) {
await ensureValidAccessToken();
let response = await fetch(url, {
...init,
headers: await getAuthHeaders(extra),
});
if (response.status === 401) {
if (await refreshTokens()) {
response = await fetch(url, { ...init, headers: await getAuthHeaders(extra) });
}
}
return response;
}
| Step | What happens |
|---|---|
| 1 | Proactively refreshes if the access token is about to (or already) expired |
| 2 | Attaches Authorization: Bearer + forwards cookies |
| 3 | On 401 → refreshes once → retries |
Services (cart.ts, products.ts) always use authFetch for any API that requires login.
7. Middleware + authFetch - Two Layers of Checking
| Layer | Question | On failure |
|---|---|---|
| Middleware | Is there a session + valid cookies to enter the page? | Redirect to /login |
authFetch | Is the token still good enough to call the API? | unauthorized() / empty result |
Scenario: middleware passes (refresh token still exists) but the access token has expired → ensureValidAccessToken refreshes before the fetch.
Scenario: the refresh token has also expired → getProducts → 401 → unauthorized().
8. The NestJS API Contract
| Method | Path | Body |
|---|---|---|
| POST | /login | { email, password } |
| POST | /google | { idToken } |
| POST | /token | { refreshToken } |
| POST | /logout | { refreshToken } + Bearer |
| GET | /products | Bearer |
| GET | /cart?userId= | Bearer - should drop the query param and derive it from the JWT instead |
9. Comparing Other Patterns
| Pattern | Description | Nova |
|---|---|---|
| NextAuth only | API proxy inside Next, session acts as auth | ❌ Nova has a separate API |
| localStorage JWT only | A pure SPA | ❌ |
| Dual auth (BFF) | NextAuth UX + a backend JWT | ✅ |
| Server-only session | A single session cookie, the API trusts Next | Close to this if everything were proxied |
10. Best Practice vs Nova & Improvements
| Best practice | Nova | Assessment |
|---|---|---|
| HttpOnly cookies | ✅ | ✅ |
| Short-lived access + refresh | ✅ | ✅ |
| Refresh before the request | ensureValidAccessToken | ✅ Good |
| Retry once on 401 | ✅ | ✅ |
| Identity derived from the JWT server-side | userId query param | ⚠️ Needs a backend fix |
Navbar's client-side getCart | Present | ⚠️ Prefetch it in a server layout instead |
| Logout revokes the refresh token | POST /logout | ✅ |
11. FAQ & Implementing From Scratch
FAQ
Q: Can I just use the Bearer token and drop NextAuth?
You'd lose useSession and Auth.js's middleware - you'd have to write a lot more yourself.
Q: Can authFetch run on the client?
Only through a Server Action / Server Component - cookies are server-only.
Q: Is dual auth worth it?
Yes, when the NestJS API already has its own JWT; a small greenfield project could simplify this.
Order of Implementation From Scratch
- Backend:
/login,/token,/logout setAuthCookies/clearAuthCookiesauthFetch+refreshTokens- NextAuth Credentials + calling
setAuthCookiesinsideauthorize - Middleware
authorizedchecking the cookies - Google provider +
POST /google - Test: log in → call the API → delete the access cookie → reload → confirm it auto-refreshes
