All labs

Hands-on

Next.js authorization boundary

Move the authorization check inside the Server Action, separate link-only pages from truly private content, and prove the difference with a direct call instead of trusting the UI.

Difficulty: IntermediateTime: ~60 min

Objective

Show that a hidden button is not access control: call the action and the route directly as a low-privilege user and confirm the server, not the client, decides.

Prerequisites

  • A Next.js App Router app you own with at least two roles.
  • Two sessions: a low-privilege and a higher-privilege account.
  • Willingness to treat every Server Action as a public endpoint.

Environment

  • A Server Action that mutates data, reachable from a UI that hides it for some roles.
  • A page linked only in navigation vs a page that must truly be private.
  • Network tab or an HTTP client able to send the action request manually.

Steps

  1. 1

    Call the action without the UI

    Replay the Server Action request from the low-privilege session. If it succeeds, the UI hiding was cosmetic; the check belongs inside the action.

  2. 2

    Move the check inside

    Perform the authorization check as the first thing the action does, against the server-side session, before any data access. Return a denial the client cannot forge.

  3. 3

    Separate link-only from private

    For a page that is merely unlisted, robotic noindex is enough; for content that must be private, enforce it in middleware/server logic with an HttpOnly session, and re-test the direct request.

  4. 4

    Re-test both roles

    Repeat the direct calls. The low-privilege session must be denied at the server even when it crafts the request by hand.

How to verify

  • The action denies the low-privilege session when called directly, not only when hidden.
  • No privileged data is read before the authorization check runs.
  • The private route returns a denial for a request with no valid server session.

Backing articles