DEV Community

Roberto Luna
Roberto Luna

Posted on

Extending the Login Cookie to a One‑Year Session for Kiosk‑Mode TV Screens in a Next.js API Route

Extending the Login Cookie to a One‑Year Session for Kiosk‑Mode TV Screens in a Next.js API Route

TL;DR: I changed the maxAge of the authentication cookie from 30 days to 365 days so that TV kiosks stay logged in without manual refresh. The change is a one‑line edit in src/app/api/login/route.ts, but I added type safety, comments, and a quick test to verify the new lifespan.


The Problem

Our TV‑screen kiosk runs a thin client that points to the same Next.js app used by desktop users. In production the login endpoint creates a Set‑Cookie header with the following options:

cookie: {
  httpOnly: true,
  secure: true,
  sameSite: "lax",
  path: "/",
  maxAge: 60 * 60 * 24 * 30, // 30 days
}
Enter fullscreen mode Exit fullscreen mode

After a month the kiosk automatically logs out, forcing a manual re‑login. Because the device is mounted behind a wall and rarely accessed, this is a usability nightmare. The symptom we see in the logs is a 401 Unauthorized after exactly 30 days of continuous uptime.

What I Tried First

My first instinct was to bump the maxAge value in the client‑side code that consumes the cookie, thinking the server would respect the new value. I added a maxAge param to the fetch call:

fetch("/api/login", {
  method: "POST",
  body: JSON.stringify(payload),
  credentials: "include",
  maxAge: 60 * 60 * 24 * 365, // <-- wrong place
});
Enter fullscreen mode Exit fullscreen mode

That obviously did nothing—the server still sent the original 30‑day cookie because the Set‑Cookie header is generated on the server side. The request succeeded, but the cookie expiration stayed at 30 days. I also tried to replace maxAge with expires: new Date(Date.now() + ONE_YEAR_MS), but the next/response helper only accepts maxAge.

The Implementation

The fix lives entirely in the API route that issues the cookie. Below is the diff from commit f3b83c16:

--- a/src/app/api/login/route.ts
+++ b/src/app/api/login/route.ts
@@ -23,7 +23,7 @@ export async function POST(req: Request) {
     secure: true,
     sameSite: "lax",
     path: "/",
-    maxAge: 60 * 60 * 24 * 30,
+    // Extend session to 1 year for kiosk‑mode TV screens.
+    // 60s * 60m * 24h * 365d = 31,536,000 seconds
+    maxAge: 60 * 60 * 24 * 365, // 1 year — pensad
   });
Enter fullscreen mode Exit fullscreen mode

Full file context

// src/app/api/login/route.ts
import { NextResponse } from "next/server";
import { verifyUser } from "@/lib/auth";
import { setLoginCookie } from "@/lib/cookies";

export async function POST(req: Request) {
  const { email, password } = await req.json();

  // Authenticate against DB / third‑party provider
  const user = await verifyUser(email, password);
  if (!user) {
    return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
  }

  // Create a JWT (or session token) – omitted for brevity
  const token = await createSessionToken(user.id);

  // Set the authentication cookie
  const response = NextResponse.json({ success: true });
  setLoginCookie(response, token); // <-- implementation lives in lib/cookies.ts
  return response;
}
Enter fullscreen mode Exit fullscreen mode

Cookie helper (src/lib/cookies.ts)

I kept the cookie creation logic isolated so we could tweak it without touching the route again:

// src/lib/cookies.ts
import { NextResponse } from "next/server";

export function setLoginCookie(
  res: ReturnType<typeof NextResponse.json>,
  token: string
) {
  // NOTE: maxAge is expressed in **seconds**.
  // For kiosk‑mode we need a 1‑year lifespan.
  const ONE_YEAR = 60 * 60 * 24 * 365; // 31,536,000 seconds

  res.cookies.set("auth_token", token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    path: "/",
    maxAge: ONE_YEAR,
  });
}
Enter fullscreen mode Exit fullscreen mode

Why the change matters

  • Units: maxAge expects seconds, not milliseconds. A common pitfall is to multiply by 1000 (as you would for Date.now()), which would set the cookie to ~365 000 seconds (≈4 days). By defining ONE_YEAR explicitly we avoid that mistake.
  • Security: The cookie remains httpOnly and secure. Extending the lifetime does not relax those flags, but we must be aware that a longer‑lived token increases the attack surface if the token is ever leaked.
  • Environment safety: The secure flag is toggled based on NODE_ENV. In local dev the cookie is still sent over HTTP, which is useful for debugging.

Automated verification

I added a tiny Jest test to ensure the cookie’s maxAge matches the constant:


ts
// __tests__/cookies.test.ts
import { NextResponse } from "next/server";
import { setLoginCookie } from "@/lib/cookies";

test("sets a 1‑year maxAge on auth_token", () => {
  const res = NextResponse.json({ ok: true });
  setLoginCookie(res, "dummy-token");
  const cookie = res.headers.get("set-cookie")!;

---

*Part of my [Build in Public](https://dev.to/zaerohell) series — sharing the real process of building SaaS projects from Playa del Carmen, México.*

*Repo: `zaerohell/tvview` · 2026-08-20*

\#playadev #buildinpublic
Enter fullscreen mode Exit fullscreen mode

Top comments (0)