---
title: Authentication
description: Built-in customer authentication with Hydrogen and Shopify Customer Account API OIDC.
type: guide
---

# Authentication



The template includes opt-in customer authentication using the framework-neutral session and OAuth helpers from `@shopify/hydrogen/customer-account`. Customers can sign in through Shopify, view their profile, order history, and address book, refresh expired access tokens, and sign out of both the storefront and Shopify identity provider.

Authentication is disabled by default. The canonical default lives in `lib/config.ts` as `auth.isEnabled`. When disabled, the storefront works as a guest-only experience and no auth UI is rendered.

## Configuration

Enable the feature in `lib/config.ts`:

```ts
export const shopConfig = {
  // ...
  auth: {
    isEnabled: true,
  },
  // ...
};
```

Then set the Customer Account API client ID, session secret, and public base URL:

```bash
CUSTOMER_ACCOUNT_SESSION_SECRET="[redacted]"
SHOPIFY_CUSTOMER_ACCOUNT_API_CLIENT_ID="your-customer-account-client-id"
NEXT_PUBLIC_BASE_URL="your-domain.com"
```

Shopify provides `SHOPIFY_CUSTOMER_ACCOUNT_API_CLIENT_ID`. It does not provide `CUSTOMER_ACCOUNT_SESSION_SECRET` — that one is app-owned, so generate it yourself:

```bash
openssl rand -base64 32
```

The template derives its cookie encryption key from that secret. Keep it server-only and identical across every instance of a deployment, and treat rotation as deliberate: it invalidates every existing customer session. Enabling auth without both values fails the build.

Customer Account OAuth requires a public HTTPS origin. Use a tunnel for local OAuth testing and set `NEXT_PUBLIC_BASE_URL` to that tunnel's bare domain (no protocol).

To enable it in Shopify Admin:

1. Enable customer accounts: **Settings → Customer accounts → Edit**, choose **Customer accounts**, and Save
2. Install the **Headless** sales channel from the Shopify App Store
3. Go to **Sales channels → Headless → (your storefront) → Customer Account API**
4. Use a public Customer Account API client; Hydrogen performs authorization code + PKCE without a client secret
5. Set the callback URI to `{YOUR_DOMAIN}/account/authorize` and the logout URI to `{YOUR_DOMAIN}/`
6. Copy the client ID to `SHOPIFY_CUSTOMER_ACCOUNT_API_CLIENT_ID`, and confirm the store domain matches `NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN`

Shopify does not support wildcard callback or logout URIs — register each production, preview, or tunnel origin that needs authentication. See [Environment Variables](/docs/reference/env-vars) for the full variable reference.

## How it works

**No client-side auth SDK.** Sign-in is a normal GET navigation to `/account/login`; sign-out is a same-origin POST form to `/account/logout`. The `shopConfig.auth.isEnabled` flag (`lib/config.ts`) gates every auth surface and is safe to read from server and client code alike. When it's false: the account icon disappears from the nav, the Hydrogen auth routes and protected account pages return `404`, and the shared session/access-token helpers in `lib/auth/server.ts` fail before initializing Hydrogen — no auth-related code runs at request time.

**The session lives in an encrypted cookie, not a server store.** `/account/login` asks Hydrogen to generate state, nonce, and PKCE values, which the app encrypts into chunked AES-256-GCM HttpOnly cookies. Shopify redirects to `/account/authorize`, where Hydrogen validates state, nonce, issuer, audience, and ID-token expiry before exchanging the authorization code — access, refresh, and ID tokens then replace the pending-login state in that same cookie. Server Components only perform read-only login and token checks; they never mutate cookies.

**Token refresh happens out-of-band, not inline.** `isCustomerLoggedIn()` treats a refreshable session as logged in, so the nav doesn't flicker to signed-out when the access token expires. `requireCustomerAccessToken()` redirects through `/account/refresh` when only a refresh token remains, where Hydrogen rotates tokens and commits the updated cookie before returning to the account page. Logout mirrors this: the sign-out button POSTs to `/account/logout`, which clears the local session and redirects through Shopify's logout endpoint with `id_token_hint`, ending both the storefront session and the Shopify SSO session.

```ts
import {
  isCustomerLoggedIn,
  requireCustomerAccessToken,
  requireCustomerSession,
} from "@/lib/auth/server";

const loggedIn = await isCustomerLoggedIn();
await requireCustomerSession();
const accessToken = await requireCustomerAccessToken("/account/orders");
```

Use `isCustomerLoggedIn()` for read-only UI state, `requireCustomerSession()` for account-page gates, and `requireCustomerAccessToken()` immediately before private Customer Account API work.

> **Guardrails.**
>
> * Never expose access, refresh, or ID tokens to Client Components or browser-readable storage.
> * Keep Server Components on the read-only session path. Token refresh belongs in `/account/refresh`.
> * Keep login as GET and logout as same-origin POST, and register the callback and logout URIs in Shopify exactly.
> * Rotate `CUSTOMER_ACCOUNT_SESSION_SECRET` deliberately — rotation invalidates every existing session.
> * `auth.isEnabled` lives in `lib/config.ts` (not an env var), so the server and client feature gates read one shared value and stay in agreement under cache components.

## Out of the box

* **Auth routes** — `/account/login` (GET, starts PKCE and redirects to Shopify), `/account/authorize` (GET, validates the OAuth callback), `/account/refresh` (GET, rotates an expired access token), and `/account/logout` (POST, clears the session). All four are installed via Hydrogen's standard handlers through `handleShopifyRoutes` in the [proxy](/docs/anatomy/proxy), which intercepts the customer-account OAuth paths before routing.
* **Account pages** — `/account` (redirects to profile), `/account/profile` (edit name, read-only email), `/account/orders` (paginated order history), `/account/orders/[id]` (order detail), and `/account/addresses` (address book with create, edit, delete, and default selection). The `(authenticated)` route group gates these without claiming the auth-handler paths.
* **Customer Account API data** — profile, orders, and addresses come from the Customer Account API, which has its own GraphQL endpoint and schema, separate from the Storefront API. Every call resolves a usable access token first, routing through the refresh step when only a refresh token remains, so no operation runs with an expired token. Customer responses are personalized and never publicly cached.
* **Rendering and mutations** — order and profile pages are Server Components wrapped in Suspense. Address and profile editing use client forms backed by server actions that validate input, surface Shopify `userErrors`, and revalidate the affected pages.

## Common customizations

* **Additional account data** — store credit, subscriptions, draft orders, or richer order fields all follow the same path as the shipped account data: a Customer Account API operation, a transform to a domain type, and a server action for anything that mutates. Validate new fields against the live Customer Account API schema, since it differs from the Storefront API.
* **Account page composition** — the shipped profile, orders, and addresses pages are a starting point. Reordering them, adding a landing dashboard, or splitting order history into its own section is presentation work that leaves the auth flow untouched.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)