Authentication & tokens
Evolve uses a federated token system (built on @labdigital/federated-token)
to manage authentication across the GraphQL gateway and backend
services. The gateway is the single public entry point: it reads
tokens from cookies, propagates them to downstream services via HTTP
headers, and writes updated tokens back to cookies on the response.
Token types
The system uses three distinct tokens, each with a different purpose and visibility:
| Token | Format | Storage | Lifetime | Readable by frontend |
|---|---|---|---|---|
| Access token | JWE (encrypted JWT) | userToken or guestToken cookie (httpOnly) | 2 days | No |
| Refresh token | JWE (encrypted JWT) | refreshToken cookie (httpOnly, path-restricted) | 200 days (CT inactivity timeout) | No |
| Data token | JWT (signed, not encrypted) | userData or guestData cookie | 90 days | Yes |
Access token: holds the commercetools API access token, its
expiry, the subject (customer_id:<id> or anonymous_id:<id>), and
whether the session is authenticated. Encrypted with AES-256-GCM so
the client cannot read or tamper with it.
Refresh token: holds the commercetools refresh token. Stored in a
separate cookie with a restricted path (/auth/graphql) so it is only
sent on refresh requests, not on every API call.
Data token: holds non-sensitive metadata the frontend needs without a server round-trip. The customer's first name, last name, and B2B business context. Signed (HS256) so the frontend can read it but cannot modify it.
The frontend also receives non-httpOnly indicator cookies
(userRefreshTokenExists / guestRefreshTokenExists) so JavaScript
can detect whether a refresh token exists without being able to read
the token itself.
Token flow
The gateway serializes the FederatedToken into a base64-encoded
X-Access-Token header for downstream services. Refresh tokens are
forwarded via X-Refresh-Token. Store context (store key, locale,
currency) travels in separate X-StoreContext-* headers.
When a downstream service modifies the token (e.g., during login), it
returns the updated token in X-Access-Token and X-Refresh-Token
response headers. The gateway merges these changes and writes new
cookies.
The @requiresSession directive
The @requiresSession GraphQL directive ensures a valid session
exists before a field resolves. It supports two modes:
@requiresSession: requires an existing session. Throws aREQUIRES_SESSIONerror if no token is present.@requiresSession(create: true): creates an anonymous session on the fly if no token exists. Used for operations that should work without prior login, such as adding items to a cart.
When create: true fires and no session exists, the gateway calls the
account service's /api/create-session REST endpoint, which creates
an anonymous commercetools token and returns it. The gateway then
hydrates the FederatedToken and continues processing the GraphQL
operation.
Fields using @requiresSession(create: true):
cartLineItemsAdd,shoppingListCreate,shoppingListLineItemsAdd,createGuestSession
Fields using @requiresSession (existing session required):
cartUpdate,cartLineItemsUpdate,cartLineItemsRemove,cartDiscountCodeAdd,cartDiscountCodeRemove,checkoutComplete,createPayment,shoppingList,shoppingLists, and other account/order mutations
Guest vs authenticated flows
Guest flow
- A visitor performs an action that requires a session (e.g., adds an
item to the cart via
cartLineItemsAdd) - The
@requiresSession(create: true)directive fires - The gateway calls
POST /api/create-sessionon the account service - The account service requests an anonymous token from commercetools
OAuth (
grant_type=client_credentialson the anonymous endpoint) - The gateway creates
guestTokenandguestDatacookies - The original GraphQL operation proceeds with the new anonymous token
Login
- The customer calls the
customerLoginmutation with email and password - If the customer has an existing anonymous session, the account
service calls commercetools'
me().login()endpoint, which automatically merges any anonymous cart into the customer's cart - A new customer token is obtained via the password grant
- The gateway replaces
guestToken→userTokenandguestData→userDatacookies
Cart merging is handled transparently by commercetools when logging in with an active anonymous session.
Token refresh
Access tokens expire after 2 days. The frontend detects expiry through the Next.js middleware:
- On each request, the middleware checks if
userTokenis present and not within 5 minutes of expiry - If the token is expired but
userRefreshTokenExistsis set, the user is redirected to/account/refresh-session - The refresh page calls the
refreshTokenGraphQL mutation via/auth/graphql(the refresh token cookie's restricted path) - The account service exchanges the refresh token for a new access
token via commercetools OAuth (
grant_type=refresh_token) - On success, the gateway writes a new
userTokencookie - On failure (expired or revoked refresh token), all cookies are cleared and the user is redirected to the login page
The refresh page uses exponential backoff with up to 5 retries before giving up.
Session REST endpoints
The account service exposes three REST endpoints for session management:
POST /api/create-session: creates an anonymous commercetools
session. Returns a FederatedToken-wrapped response with
accessToken and refreshToken (base64-encoded). Called by the
gateway's @requiresSession(create: true) directive.
POST /token/anonymous: creates an anonymous session and returns
raw TokenInfo (subject, accessToken, refreshToken,
expiresAt). Used by server-side consumers that need direct API
access (e.g., the MCP server).
POST /api/client-context: accepts an X-Access-Token header
and returns customer metadata (id, givenName, familyName,
email, customerGroupId, businessContext). Results are cached in
Redis for 300 seconds. Used by services that need customer information
without a full GraphQL query.