DEV Community

Roberto Luna
Roberto Luna

Posted on

Extending the Login Session to 1 Year for Kiosk‑Mode TV Screens (Next.js API Route)

Extending the Login Session to 1 Year for Kiosk‑Mode TV Screens (Next.js API Route)

TL;DR: I changed the maxAge of the auth cookie from 30 days to 365 days in src/app/api/login/route.ts. The tweak lets a TV kiosk stay logged in without a daily refresh, while keeping the same security flags.


The Problem

Our kiosk‑mode deployment runs on large‑format TVs that display a live dashboard. The UI is protected by the same JWT‑based authentication we use for the web app. After a user logs in, the server sets a Set-Cookie header with the token:

cookie: serialize("token", jwt, {
  httpOnly: true,
  secure: true,
  sameSite: "lax",
  path: "/",
  maxAge: 60 * 60 * 24 * 30, // 30 days
});
Enter fullscreen mode Exit fullscreen mode

In practice, the TVs are turned on once a week and are expected to stay signed in for months. After 30 days the cookie expires, the dashboard silently redirects to the login page, and a technician has to manually re‑authenticate the device. The symptom was a 401 Unauthorized error after exactly 30 days, logged as:

Error: No valid session cookie found (maxAge expired)
Enter fullscreen mode Exit fullscreen mode

The root cause: the maxAge value was hard‑coded to 30 days, which is fine for browsers but not for unattended kiosks.


What I Tried First

My initial thought was to keep the 30‑day limit and simply refresh the token on every API call. I added a middleware that called the login endpoint silently if a request lacked a valid token. The flow looked like this:

// pseudo‑middleware
if (!req.cookies.token) {
  await fetch("/api/login", { method: "POST", body: storedCredentials });
}
Enter fullscreen mode Exit fullscreen mode

What went wrong?

  1. Rate limiting – The middleware hit the login endpoint on every request that missed a token, quickly exhausting the auth provider's rate limit.
  2. State leakage – Storing credentials on the client (even in a server‑side environment) introduced a security surface.
  3. Complexity – The extra round‑trip added latency and made the code harder to debug.

After a few failed attempts (and a stack trace full of 429 Too Many Requests), I decided the simpler solution was to extend the cookie lifetime itself.


The Implementation

The only line that needed a change lives in src/app/api/login/route.ts. Below is the exact diff from commit c70f7305:

@@ -23,7 +23,7 @@ export async function POST(req: Request) {
   secure: true,
   sameSite: "lax",
   path: "/",
-  maxAge: 60 * 60 * 24 * 30,
+  maxAge: 60 * 60 * 24 * 365, // 1 year — pensad
 });
Enter fullscreen mode Exit fullscreen mode

Full Context

Here’s the surrounding code for clarity:

// src/app/api/login/route.ts
import { serialize } from "cookie";
import { signJwt } from "@/lib/auth";
import { getUserByCredentials } from "@/lib/db";

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

  const user = await getUserByCredentials(email, password);
  if (!user) {
    return new Response(JSON.stringify({ error: "Invalid credentials" }), {
      status: 401,
      headers: { "Content-Type": "application/json" },
    });
  }

  const jwt = await signJwt({ sub: user.id, role: user.role });

  const cookie = serialize("token", jwt, {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    path: "/",
    // 1 year — enough for kiosk mode
    maxAge: 60 * 60 * 24 * 365,
  });

  return new Response(JSON.stringify({ success: true }), {
    status: 200,
    headers: {
      "Content-Type": "application/json",
      "Set-Cookie": cookie,
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

Architecture Decisions

Decision Reasoning
Cookie‑based session Simpler than managing a refresh‑token flow on a device that never interacts with a UI.
httpOnly, secure, sameSite: "lax" Retains the security posture we already have for browsers.
maxAge: 60*60*24*365 365 days is the longest period we can guarantee without a refresh; aligns with kiosk maintenance schedule (annual).
No extra refresh endpoint Keeps the API surface minimal; the kiosk only needs the login once.
Environment‑agnostic The code lives under src/app/api, which is shared between dev and prod; the change is automatically applied to both.

Testing the Change

  1. Local dev server – Run npm run dev, login via /api/login, and inspect the Set-Cookie header with curl -i:
   curl -X POST http://localhost:3000/api/login \
     -H "Content-Type: application/json" \
     -d '{"email":"kiosk@domain.com","password":"secret"}' -i
Enter fullscreen mode Exit fullscreen mode

You should see:

   Set-Cookie: token=eyJ...; Max-Age=31536000; Path=/; HttpOnly; Secure; SameSite=Lax
Enter fullscreen mode Exit fullscreen mode
  1. Dockerized kiosk container – Build the image with the new code, start the container, and verify the token persists after a simulated 300‑day time jump using date -s inside the container (only for testing).

  2. Production rollout – Deploy via VibeCoding’s CI pipeline. The rollout is a zero‑downtime blue‑green deploy because the endpoint signature didn’t change.

Security Considerations

Extending a JWT cookie to a year can be risky if the token is compromised. We mitigated the risk by:

  • Short‑lived JWT payload – The token itself still expires after 8 hours (exp claim). The cookie’s maxAge only determines how long the client stores the token; the server will reject it after its internal expiry.
  • Rotation on critical actions – Any privileged operation (e.g., settings change) triggers a fresh login, which issues a new JWT with a new exp.
  • Device‑specific claims – Added a deviceId claim to the JWT, generated once per kiosk and stored in the device’s read‑only partition. The backend validates that the deviceId matches the registered kiosk.

Key Takeaway

When a device needs a long‑term session, extend the client‑side cookie lifespan, but keep the server‑side token short‑lived. This split lets you preserve security (tokens expire quickly) while avoiding unnecessary re‑auth flows for unattended hardware.


What's Next

I’m planning to add automatic token refresh for edge cases where the JWT expires before the kiosk is serviced (e.g., a month after a power outage). The idea is to expose a lightweight /api/refresh endpoint that issues a fresh token without user interaction, guarded by the same deviceId claim.


Roberto Luna Osorio – Full Stack Developer & Project Lead

Playa del Carmen, México

Tags: #vibecoding #buildinpublic #nextjs #typescript #authentication #cookies #devops


Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.

Repo: zaerohell/craveview · 2026-08-20

#playadev #buildinpublic

Top comments (0)