4: Session management
4: Session management
So far, your single-click app runs locally and supports installation and launch from a BigCommerce store control panel. However, the app currently doesn’t perform any authentication of the user for individual actions and page requests.
You can see this in action if you watch the Network tab in your browser dev tools while navigating within the app and paste one of the store-scoped URLs into a private browser window. The app will load, looking up the store’s API token without any verification.
In this exercise, you’ll implement your app’s own session cookie and two-tier authorization check to ensure every request comes from a verified session within the store control panel iframe.
Remember this checklist to ensure your app is ready to run in a single-click context:
- The dev server must be running.
- A remote tunnel must be serving your local port over a public HTTPS URL.
- If using VS Code or GitHub Codespace, the forwarded port must be set to “Public” visibility.
- The current remote tunnel URL must be set in
APP_ORIGINin.env.localand in all callback URLs in the Developer Portal.
Set a session secret
Set the following in .env.local (and .env.example) to prepare for this exercise, then restart the dev server.
Generate a value for SESSION_SECRET with the following command:
This secret is separate from your app’s client secret. Only your app knows this secret, and it will use it to sign and verify its own session tracking Json Web Token (JWT).
Implement session types and JWT signing
Define the session payload and cookie config
Replace the contents of src/lib/session/types.ts.
This app mints its own session token: a short-lived, stateless JWT stored in an httpOnly, SameSite=None; Secure; Partitioned cookie. Those three attributes on the cookie aren’t optional — they’re required for the cookie to work at all inside the control panel’s cross-origin iframe. The payload itself is { userId, authenticatedStores, issuedAt }. Notice that authenticatedStores is a list, not a single store hash — that’s what lets one admin be launched into multiple stores concurrently under a single cookie.
Implement JWT signing and verification
Replace the contents of src/lib/session/session-jwt.ts.
jose is used for JWT handling. This logic handles initially signing a session JWT and verifying it on subsequent requests.
The TTL is deliberately short, because a stateless JWT can’t be revoked before it expires — there’s no server-side record to delete.
Implement the session cookie
Implement the cookie read/write functions
This step will establish utility functions for working with the session cookie itself.
Replace the contents of src/lib/session/session-cookie.ts.
These basic functions handle reading, setting, and removing the session cookie, relying on the previously created utility functions for handling the JWT value.
Note that upsertSessionStore handles setting the issuedAt value conditionally depending on whether a session cookie already exists. This value
is important for the logic that ultimately expires the session.
clearSession deletes the cookie outright, ending the session for every store it covered — unlike removeSessionStore, which drops a single stale
store claim. This is what the boilerplate’s control panel syncing calls when BigCommerce reports that the admin logged out of the control panel.
Set the session cookie during install
The install API endpoint now needs to be updated to initially set the session cookie.
Update src/lib/bc-auth/install-store.ts.
Set the session cookie during load
Update src/lib/bc-auth/load-store.ts.
Now both installing or navigating to the app in the store control panel will result in a session cookie identifying the user and their authenticated stores. The same user accessing the app from multiple stores will result in the session cookie being updated to expand the authenticated stores list.
Implement the secondary authorization check
A user’s session is now being tracked, and you can have confidence that BigCommerce initially authenticated that user via the install or load callback. But the app isn’t yet performing any access check using this session.
You’ll be implementing a two-tier auth check. The first you’ll set up is the most thorough: This check verifies that a user session exists and includes the current store in its authenticated list, as well as confirming the store/user relationship in the credentials storage.
This full check ensures that, if a user’s access is revoked, this is reflected immediately rather than waiting for the session cookie to expire.
Implement the authorization check
A central utility function will handle the check.
Update src/lib/session/is-authorized-for-store.ts as shown.
The check first verifies the information in the session cookie, then performs a DB lookup to verify a store/user record and valid API token.
Note the empty catch for removeSessionStore. Ideally, the app should proactively remove the session cookie if authorization failed, but
Next.js won’t allow this action when the check is performed from a simple page render.
Protect the list page
The AuthorizedPage component now simply needs to be used to wrap the gift certificate list page.
Update src/app/store/[storeHash]/gift-certs/page.tsx.
It’s worth noting why the authorization wrapper is added to individual page routes rather than a common layout file. Layout file components are not re-rendered on client-side navigation, so a check placed there wouldn’t actually perform authentication on each page load.
Implement the primary authorization gate
The authorization check you’ve implemented is exhaustive, but the app benefits from having a faster, more optimistic check in the layer designed for capturing requests before they are resolved to routes: the Next.js proxy.
The proxy intercepts every request matching a given pattern and can short-circuit that request before further routing. The proxy is meant to run at the edge, where resources like the credentials store would not be available. But it’s the perfect place for a lightweight check of the session cookie, which will catch the majority of unauthorized requests.
Implement the proxy authorization gate
Replace the contents of src/proxy.ts.
The proxy matches any /store/<store-hash>/... URL, verifies the session JWT, and confirms that the store hash is included in the user’s authenticated
list.
On success, proxy.ts also re-signs the cookie with a fresh TTL. This is what makes the session’s effective lifetime “since last request” rather than “since login” — necessary because BigCommerce can only mint a fresh session by calling /load, and this app has no way to trigger /load from inside its own iframe. The refresh preserves the session’s original issuedAt unchanged, so SESSION_MAX_AGE_SECONDS (defined earlier in this exercise) still bounds the total session lifetime no matter how continuously the session is used.
Together, the two tiers you’ve built mean a request has to clear a cheap signature-and-claim check first, and then a database-backed confirmation second, before any protected content renders or any server action runs.
Reinstall or relaunch the app in the store control panel, and confirm pages still render normally for an authorized user.
If every page comes back unauthorized, the most common cause is the session cookie not surviving the iframe. Double-check that your tunnel is serving HTTPS and that APP_ORIGIN in .env.local matches it exactly — the cookie’s SameSite=None; Secure; Partitioned attributes will silently fail to persist otherwise.
Nothing else has changed about the app. If you previously identified the unauthenticated behavior by copying a URL from the iframe into a private browser window, you can try this action again to confirm the app now prevents unauthorized access.