Storefront Next Authentication
Authentication is server-only. One middleware (src/middlewares/auth.server.ts, registered in the middleware array of src/root.tsx) runs on every request: it reads cookies, validates or refreshes the SLAS access token, performs a guest login when nothing usable exists, and writes Set-Cookie. Tokens never reach the browser. Components only see a small non-sensitive slice through useAuth().
Read docs/README-AUTH.md in your project for the full reference; this skill is the task-oriented summary. Route modules use server loader/action only, never clientLoader/clientAction.
Request flow
- Valid access token (JWT
expin the future): use it. - Expired access token with a refresh token: refresh, rewrite
cc-at. - No tokens: guest login.
- Registered shopper whose refresh fails: 307 to
/login?returnUrl=<path>&error=session_expired(site/locale prefix included) and a fresh guest session is created in parallel. Guests are silently re-issued a guest session. - A SCAPI call returning 401 throws
AuthTokenInvalidError; the middleware clears state, re-runs login/refresh and 307-redirects to the same URL. The short-livedcc-auth-recovercookie (30 s) prevents loops.
User type comes from the access-token JWT (registered tokens carry an rcid claim), not from which cookie exists. customerId is not a cookie; it is derived per request from the JWT.
Read auth on the server
import { getAuth } from '@/middlewares/auth.server';
import type { LoaderFunctionArgs } from 'react-router';
export async function loader({ context }: LoaderFunctionArgs) {
const auth = getAuth(context); // full SessionData, server only
const isRegistered = auth.userType === 'registered';
return { isRegistered }; // never return auth.accessToken / refreshToken
}
Read auth in components
import { useAuth } from '@/providers/auth';
function AccountBadge() {
const auth = useAuth(); // PublicSessionData | undefined
return auth?.userType === 'registered' ? <span>Welcome back</span> : <SignInLink />;
}
PublicSessionData is exactly customerId, userType, usid, encUserId, trackingConsent. The root loader derives it with getPublicSessionData (from @/middlewares/auth.utils) and passes it to AuthProvider; there is no client getAuth.
Protect a route
Branch inside the server loader. Prefer the existing account layout (src/routes/_app.account.tsx) for anything under /account instead of adding per-route checks.
import { redirect } from 'react-router';
import { getAuth } from '@/middlewares/auth.server';
import { buildUrlFromContext } from '@/lib/url.server';
import { routes } from '@/route-paths';
export async function loader({ context }: LoaderFunctionArgs) {
if (getAuth(context).userType !== 'registered') {
throw redirect(buildUrlFromContext(routes.login, context)); // keeps site/locale prefix
}
// ...
}
Log in and out
Never write auth cookies yourself. Use the helpers exported from @/middlewares/auth.server:
import { loginRegisteredUser, updateAuth } from '@/middlewares/auth.server';
export async function action({ request, context }: ActionFunctionArgs) {
const form = await request.formData();
const tokens = await loginRegisteredUser(context, String(form.get('email')), String(form.get('password')));
updateAuth(context, tokens); // re-derives userType/customerId/usid, swaps cc-nx-g for cc-nx
return redirect('/account');
}
updateAuth accepts a SLAS token response or an updater function (updateAuth(context, (cur) => ({ ...cur, codeVerifier }))). Logout: destroyAuth(context) clears every auth cookie on the response; the shipped src/routes/_empty.logout.ts also calls clients.auth.logout and destroyBasket. Prefer src/lib/api/auth/standard-login.server.ts and social-login.server.ts over hand-rolled flows: they already handle the merge below.
Other exported helpers: loginGuestUser, refreshAccessToken, authorizePasswordless, getPasswordLessAccessToken, getPasswordResetToken, resetPasswordWithToken, requestOtp, verifyOtp, flashAuth(context, message) (clear session plus an error message), clearInvalidSessionAndRestoreGuest(context) (deleted customer or corrupted session).
Guest to registered merge
customerId changes from the guest gcid to the registered rcid at the token swap, and SCAPI rejects the guest id under the registered token. So: snapshot guest-owned data (wishlist) before the swap, then mergeBasket and mergeWishlist after. src/lib/api/auth/social-login.server.ts is the reference implementation. dw_dnt tracking consent is preserved across the swap.
Common SCAPI calls for account data
These are real operations: clients.shopperCustomers.getCustomerOrders, getCustomerProductLists (wishlists), getCustomer. Use the existing helpers (src/lib/api/order.server.ts, wishlist.server.ts, customer.server.ts) rather than calling clients directly. Client setup and the @/lib/api-clients.server import are covered in storefront-next:sfnext-scapi.
Configuration
| Setting | Where |
|---|---|
| Refresh-token lifetime | PUBLIC_COMMERCE_API_GUEST_REFRESH_TOKEN_EXPIRY_SECONDS (max 30 days), PUBLIC_COMMERCE_API_REGISTERED_REFRESH_TOKEN_EXPIRY_SECONDS (max 90 days) |
| Feature flags | features.passwordlessLogin, features.passkey, features.socialLogin, features.otpRequest, auth.otpLength in config.server.ts |
| Private SLAS client | commerce.api.privateKeyEnabled (required for passwordless / OTP) |
| Cookie domain | app.cookies.domain (see storefront-next:sfnext-security) |
secure on cookies is only set on deployed (HTTPS) environments; local pnpm dev over http://localhost writes non-secure cookies so Safari keeps them.
Pitfalls
- Reading tokens with
document.cookie: impossible by design (httpOnly). UseuseAuth()or loader data. - Returning
getAuth(context)from a loader leaks tokens into the page. Return only derived values. - Refreshing tokens manually in routes: the middleware already does it.
- Logging
accessTokenorrefreshToken: never. - Changing the cookie domain on a live site leaves old host-only cookies shadowing new ones; roll out in a quiet window.
- A loop of redirects to
/login?error=session_expiredusually means refresh tokens are being rejected (SLAS client config, or cookie domain mismatch between hosts).
References
- COOKIES.md: full cookie inventory, namespacing, expiry, hints.
- LOGIN-FLOWS.md: passwordless, social, OTP, passkeys, password reset, guest order lookup, email cartridge prerequisites.
- In your project:
docs/README-AUTH.md,docs/README-EMAIL-VERIFICATION.md,docs/README-GUEST-ORDER-LOOKUP.md,docs/README-EMAIL-CARTRIDGE.md.
Related Skills
storefront-next:sfnext-security- Turnstile bot protection, CSP for social providers, cookie domainstorefront-next:sfnext-scapi- SCAPI clients and custom APIsstorefront-next:sfnext-hybrid-storefronts- shareddwsidsession bridge with SFRAstorefront-next:sfnext-analytics-consent-dw_dnttracking consentstorefront-next:sfnext-configuration-config.server.tsandPUBLIC__overridesb2c-cli:b2c-slas- manage SLAS clients and scopes