Next.js / Intermediate: authority, freshness and route experience / Lesson 8
Sessions, server-only data access and object policy
Resolve current authority on the server and protect every path to private data or mutations.
On this page
Lesson goals
- Separate cookie transport, verified session and current principal.
- Keep data eligibility in a shared server-only layer rather than a navigation gate.
An incoming cookie is a credential claim to verify
A cookie value is caller input until the authentication protocol verifies it and establishes current authority. A server-only module marker helps prevent accidental client imports, but does not authenticate requests or decide object access. Use a reviewed authentication/session solution with explicit expiry, revocation and rotation behavior. Avoid storing secrets in props, URLs or ordinary logs. A cached role can become stale after privileges change, so define how sensitive operations obtain current policy evidence. Server rendering and a hidden navigation item should never be treated as sufficient protection for a direct action or Route Handler.
Await cookies and verify the credential behind a server boundary
The fragment uses the asynchronous cookies API, reads one cookie name and delegates verification. It is a server-only illustration, not a session implementation. verifyCredential must apply the actual protocol, current validity and safe failure behavior; a missing cookie is not automatically a privileged guest. A data-access function then needs tenant and object policy in addition to this principal. Keep authentication mechanisms replaceable through a narrow capability rather than importing a configured database everywhere. No cookie, credential, database or request context is accessed during this course.
import "server-only";
import {cookies} from "next/headers";
export async function currentPrincipal() {
const jar=await cookies();
const credential=jar.get("session")?.value;
return verifyCredential(credential);
}
Credential verification is an assumed reviewed dependency. The async cookie read and server-only import were not executed.
Protect lists, lookups and mutation results consistently
The same course can appear through a page, a Route Handler, a Server Action result or a related-object list. Centralize eligibility so a protected primary lookup is not bypassed through another representation. Tenant IDs from params and hidden form fields identify requested scope, not membership. Decide whether denied and absent objects share a public not-found response, while retaining internal diagnosis safely. Return a narrow DTO only after access is permitted. Serialization exclusions can hide fields but cannot make it safe to return a forbidden object’s existence or relationships.
Test tenant, state and expiry as separate prerequisites
The local predicate allows a synthetic published course read only for the same tenant and before the principal’s expiry. At the exact expiry instant it denies. These are deliberately small domain rules, not cryptographic verification or a complete authorization engine. Real applications can add enrollment, roles and revocation checks. Test negative cases for each prerequisite with the same object so one successful condition cannot hide another failed one. The model’s time values are supplied explicitly and no real session store, request cookie or server clock is consulted.
function mayRead(principal,course,now) {
return principal.tenantId === course.tenantId && course.state === "PUBLISHED" && now < principal.expiresAt;
}
const course={id:"c1",tenantId:"lab-a",state:"PUBLISHED"};
console.assert(mayRead({tenantId:"lab-a",expiresAt:1000},course,999));
console.assert(!mayRead({tenantId:"lab-b",expiresAt:1000},course,999));
console.assert(!mayRead({tenantId:"lab-a",expiresAt:1000},course,1000));
Only the eligible nonexpired principal passes the tested cases. Credential authenticity and revocation infrastructure are not tested.
Align caches and client state with identity changes
Do not place a current-user variable in shared mutable module state. Memoization for one render and persistent cache storage have different lifetimes; ensure personalized data is not shared under an insufficient key. Logout must retire server authority and client-visible private state according to the session design, not only hide a button. Late responses from an old principal must not repopulate the next user’s view. Verify direct actions, handlers, layouts and account switching in an authorized isolated integration. This course executes only the eligibility predicate and does not validate authentication, cookies, memoization, logout or live authorization.
Common errors and fixes
A server-only marker is called authentication
- Why it happens
- Build-time import protection was confused with verified caller authority.
- How to fix it
- Verify credentials and apply shared object policy at every server entry.
- Prevent it next time
- Test direct actions and handlers with invalid and expired credentials.
A tenant param grants membership
- Why it happens
- Requested scope was accepted as permission.
- How to fix it
- Compare requested scope with the verified principal and domain rules.
- Prevent it next time
- Use the same object ID across allowed and denied tenants.
Logout still shows the previous account’s card
- Why it happens
- Server authority and client/private cache lifetimes were not coordinated.
- How to fix it
- Retire session authority and reject incompatible pending results.
- Prevent it next time
- Test account switches while requests remain pending.
Check your understanding
1. Does server-only stop a caller from invoking a Server Action?
Show worked solution
No. It is an import-boundary safeguard. An action is still a server entry point and must verify current credentials, validate input and enforce the requested object transition. Keep that policy in the shared use case so other adapters cannot bypass it, and test direct invocation rather than only the protected page.
2. Why test expiry at equality and a different tenant separately?
Show worked solution
They are independent reasons to deny. A correct expiry comparison cannot compensate for missing tenant isolation, and a matching tenant cannot keep an expired principal valid. The explicit cases make both preconditions observable. A real authentication integration must separately establish the trusted values supplied to that decision.
Key takeaways
- Server execution and import markers do not authorize data.
- Current authority and private state require coordinated lifetimes.