Files
Miranda Limonczenko a4106b01f5 docs(auth): correct what getClaims verifies, and fix the Express env setup (#50288)
## Problem

These findings came from a technical audit and verification of the
claims in the doc.

I found two accuracy problems:

- **The guide said `getClaims()` is safe to trust** because it
"validates the JWT signature against the project's published public keys
every time". That only describes projects on asymmetric signing keys.
With a symmetric secret it calls the Auth server instead, which the
page's own partial already said. The advanced guide then read as a flat
contradiction: `getUser()` was "the only way" to know a session is
valid. The real distinction is revocation, not verification.

- **Running the Express sample verbatim doesn't work.** In the docs
sandbox, it printed `SUPABASE_URL = undefined`, so `createServerClient`
received undefined for both the URL and the key. The env var tab
installed dotenv twice, once inline and once through the package manager
tabs, and its "And initialize it" lead-in was followed by the second
install rather than any initialization. The route sample then required
dotenv without calling `config()`.

## Solution

- Say what `getClaims()` verifies against in each signing key mode.
- Reframe the advanced guide's `getUser()` answer around session
revocation, so the two pages stop contradicting each other.
- Switch the advanced guide's two middleware snippets from `getUser()`
to `getClaims()`, matching the guide.
- Rename its `Next.js middleware` heading and CloudFront bullet, which
the proxy rename missed.
- Load dotenv on the first line of the Express entry point, and drop the
duplicate install.
- Tag both Express fences `js`. They are CommonJS, not TypeScript.
- Update the stale "middleware refreshing user sessions" comment in the
rendered Next.js `server.ts` sample.

## Manual testing

1. Open the [SSR client
guide](https://docs-git-docs-ssr-client-accuracy-supabase.vercel.app/docs/guides/auth/server-side/creating-a-client)
on the deploy preview, then the Express tab. dotenv is installed once,
followed by `require('dotenv').config()`.
2. Open the [advanced
guide](https://docs-git-docs-ssr-client-accuracy-supabase.vercel.app/docs/guides/auth/server-side/advanced-guide).
The Next.js heading reads `Next.js proxy` and both snippets call
`getClaims()`.

Part of DOCS-1313.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Documentation**
- Clarified the difference between token validation and detecting
revoked server-side sessions.
  - Updated Next.js guidance and examples to use “proxy” terminology.
  - Refined CloudFront caching guidance for authenticated routes.
- Improved Express setup instructions, including dotenv loading and
JavaScript examples.
  - Expanded explanations of signing-key verification.
  - Updated Astro and Nuxt examples to forward cache headers correctly.
- Updated session-refresh guidance in the Next.js example to reference
the proxy.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-16 12:31:11 -07:00

124 lines
9.7 KiB
Plaintext

---
title: 'Advanced guide'
subtitle: 'Details about SSR Auth flows and implementation for advanced users.'
---
When a user authenticates with Supabase Auth, two pieces of information are issued by the server:
1. **Access token** in the form of a JWT.
2. **Refresh token** which is a randomly generated string.
The default behavior if you're not using SSR is to store this information in local storage. Local storage isn't accessible by the server, so for SSR, the tokens instead need to be stored in a secure cookie. The cookie can then be passed back and forth between your app code in the client and your app code in the server.
If you're not using SSR, you might also be using the [implicit flow](/docs/guides/auth/sessions/implicit-flow) to get the access and refresh tokens. The server can't access the tokens in this flow, so for SSR, you should change to the [PKCE flow](/docs/guides/auth/sessions/pkce-flow). You can change the flow type when initiating your Supabase client if your client library provides this option.
<Admonition type="note">
In the `@supabase/ssr` package, Supabase clients are initiated to use the PKCE flow by default. They are also automatically configured to handle the saving and retrieval of session information in cookies.
</Admonition>
## How it works
In the PKCE flow, a redirect is made to your app, with an Auth Code contained in the URL. When you exchange this code using `exchangeCodeForSession`, you receive the session information, which contains the access and refresh tokens.
To maintain the session, these tokens must be stored in a storage medium securely shared between client and server, which is traditionally cookies. Whenever the session is refreshed, the auth and refresh tokens in the shared storage medium must be updated. Supabase client libraries provide a customizable `storage` option when a client is initiated, allowing you to change where tokens are stored.
## Frequently asked questions
{/* supa-mdx-lint-disable Rule004ExcludeWords */}
### No session on the server side with Next.js route prefetching?
When you use route prefetching in Next.js using `<Link href="/...">` components or the `Router.push()` APIs can send server-side requests before the browser processes the access and refresh tokens. This means that those requests may not have any cookies set and your server code will render unauthenticated content.
To improve experience for your users, we recommend redirecting users to one specific page after sign-in that does not include any route prefetching from Next.js. Once the Supabase client library running in the browser has obtained the access and refresh tokens from the URL fragment, you can send users to any pages that use prefetching.
### How do I make the cookies `HttpOnly`?
This is not necessary. Both the access token and refresh token are designed to be passed around to different components in your application. The browser-based side of your application needs access to the refresh token to properly maintain a browser session anyway.
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
### My server is getting invalid refresh token errors. What's going on?
It is likely that the refresh token sent from the browser to your server is stale. Make sure the `onAuthStateChange` listener callback is free of bugs and is registered relatively early in your application's lifetime
When you receive this error on the server-side, try to defer rendering to the browser where the client library can access an up-to-date refresh token and present the user with a better experience.
A common cause is calling `supabase.auth.signOut()` without a `scope`. It defaults to `scope: 'global'`, which revokes the refresh token for every session that user has. That includes other tabs, devices, and any SSR context still holding the old token. See [Sign out and scopes](/docs/guides/auth/signout#sign-out-and-scopes). Pass `{ scope: 'local' }` if you only want to end the current session.
### Should I set a shorter `Max-Age` parameter on the cookies?
The `Max-Age` or `Expires` cookie parameters only control whether the browser sends the value to the server. Since a refresh token represents the long-lived authentication session of the user on that browser, setting a short `Max-Age` or `Expires` parameter on the cookies only results in a degraded user experience.
The only way to detect that a session ended server-side, for example because the user signed out on another device, is to fetch the user with `getUser()`. `getClaims()` verifies the token's signature and expiry, which is what authorizes a request, but an unexpired token stays valid even when the session behind it was revoked. Call `getUser()` where that gap matters.
### What should I use for the `SameSite` property?
Make sure you [understand the behavior of the property in different situations](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) as some properties can degrade the user experience.
A good default is to use `Lax` which sends cookies when users are navigating to your site. Cookies typically require the `Secure` attribute, which only sends them over HTTPS. However, this can be a problem when developing on `localhost`.
### Can I use server-side rendering with a CDN or cache?
Yes, but there are two specific scenarios that can cause users to receive another user's session. Both are related to caching of HTTP responses that contain `Set-Cookie` headers.
#### ISR (incremental static regeneration)
If you use ISR on pages that trigger a Supabase session refresh, the cached response will include the `Set-Cookie` header containing the refreshed JWT. When that cached response is served to a subsequent user, their browser stores the token and they are signed in as the wrong person.
Do not enable ISR on any route where authentication is handled or where a session refresh can occur. In Nuxt, avoid setting `isr` on authenticated routes. In Next.js, use `export const dynamic = 'force-dynamic'` on pages that require authentication.
#### CDN and reverse proxy caching
When `@supabase/ssr` refreshes a session token server-side, it writes the updated JWT to the HTTP response via a `Set-Cookie` header. If your CDN (e.g. Vercel Edge, Cloudflare) caches that response and serves it to a different user, that user's browser will store the cached token and be signed in as the wrong person.
As of `@supabase/ssr` v0.10.0, the library automatically passes the necessary cache headers (`Cache-Control`, `Expires`, `Pragma`) to your `setAll` callback as a second argument whenever a token refresh occurs. If your `setAll` implementation applies those headers to the response (as shown in the examples in [Creating a Supabase client for SSR](/docs/guides/auth/server-side/creating-a-client)), no additional manual configuration is needed for most CDNs.
If you are on an older version or need to set headers manually, add `Cache-Control: private, no-store` to responses from any route that handles authentication:
#### Next.js proxy
```ts
const response = NextResponse.next()
// ... supabase client setup and getClaims() call
response.headers.set('Cache-Control', 'private, no-store')
return response
```
#### Nuxt server middleware
```ts
// ... supabase client setup and getClaims() call
setHeader(event, 'Cache-Control', 'private, no-store')
```
**CloudFront**
CloudFront's behavior depends on its cache policy configuration and is not solely controlled by the `Cache-Control` response header. Even with `Cache-Control: private, no-store`, CloudFront can still cache the response and the `Set-Cookie` header if its cache policy has a Minimum TTL greater than 0, or if cookies and the `Set-Cookie` header are not forwarded to the origin.
To protect against session leakage on CloudFront, use one or more of the following steps:
- **Set Minimum TTL to 0** in your CloudFront cache policy. This allows `Cache-Control: no-store` to take effect as intended.
- **Use `Cache-Control: no-cache="Set-Cookie"`** to instruct CloudFront not to cache the `Set-Cookie` header specifically, while still allowing other parts of the response to be cached.
- **Disable caching entirely** for authenticated routes such as your proxy path, by associating a cache policy with TTL set to 0, or by using the managed `CachingDisabled` policy for those behaviors.
<Admonition type="note">
On other managed CDN platforms (for example AWS Amplify), cache policies are configured at the platform level and may similarly not fully respect `Cache-Control` headers set in your application. Always verify your CDN's caching behavior for routes that set cookies.
</Admonition>
If you need to cache SSR pages for performance, apply caching only to routes that do not write `Set-Cookie` headers, and always include the refresh token cookie value in the cache key for any routes that serve user-specific content.
#### Vercel Fluid compute (in-memory client sharing)
Vercel's Fluid compute model can keep server instances warm and reuse them across requests. In some cases this means a Supabase client initialized in module scope — or stored in a shared variable — may be reused across requests from different users, causing one user's session to leak into another user's request.
Always initialize the Supabase client inside the request handler, not at module level. Do not store the client or any user-specific state in a variable that persists between requests.
### Which authentication flows have PKCE support?
At present, PKCE is supported on the Magic Link, OAuth, Sign Up, and Password Recovery routes. These correspond to the `signInWithOtp`, `signInWithOAuth`, `signUp`, and `resetPasswordForEmail` methods on the Supabase client library. When using PKCE with Phone and Email OTPs, there is no behavior change with respect to the implicit flow - an access token will be returned in the body when a request is successful.