Skip to main content

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:

TokenFormatStorageLifetimeReadable by frontend
Access tokenJWE (encrypted JWT)userToken or guestToken cookie (httpOnly)2 daysNo
Refresh tokenJWE (encrypted JWT)refreshToken cookie (httpOnly, path-restricted)200 days (CT inactivity timeout)No
Data tokenJWT (signed, not encrypted)userData or guestData cookie90 daysYes

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 a REQUIRES_SESSION error 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

  1. A visitor performs an action that requires a session (e.g., adds an item to the cart via cartLineItemsAdd)
  2. The @requiresSession(create: true) directive fires
  3. The gateway calls POST /api/create-session on the account service
  4. The account service requests an anonymous token from commercetools OAuth (grant_type=client_credentials on the anonymous endpoint)
  5. The gateway creates guestToken and guestData cookies
  6. The original GraphQL operation proceeds with the new anonymous token

Login

  1. The customer calls the customerLogin mutation with email and password
  2. 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
  3. A new customer token is obtained via the password grant
  4. The gateway replaces guestTokenuserToken and guestDatauserData cookies

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:

  1. On each request, the middleware checks if userToken is present and not within 5 minutes of expiry
  2. If the token is expired but userRefreshTokenExists is set, the user is redirected to /account/refresh-session
  3. The refresh page calls the refreshToken GraphQL mutation via /auth/graphql (the refresh token cookie's restricted path)
  4. The account service exchanges the refresh token for a new access token via commercetools OAuth (grant_type=refresh_token)
  5. On success, the gateway writes a new userToken cookie
  6. 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.