The Intershop Knowledge Portal uses only technically necessary cookies. We do not track visitors or have visitors tracked by 3rd parties.
Please find further information on privacy in the Intershop Privacy Policy and Legal Notice.
Development Documents
Concepts
24-Nov-2025
Concept - Integration of Progressive Web App and Responsive Starter Store
Document Properties
Kbid
4788V2
Added to KB
24-Nov-2025
Status
online
Product
  • Intershop Progressive Web App
  • ICM 13
Last Modified
24-Nov-2025
Public Access
everyone
Doc Type
Concepts
Product
  • ICM 12
  • ICM 14
Document Link
https://knowledge.intershop.com/kb/4788V2

Introduction

The API token login feature enables customers to log in to both the Progressive Web App (PWA) or any other REST-based client and the Responsive Starter Store. 

This feature can be useful if certain elements of the PWA (e.g., product listing), but also elements of the Responsive Starter Store (for example, checkout) are to be used together in a project. 

References

Glossary

Term

Description

ICM

The abbreviation for Intershop Commerce Management

PWA

The abbreviation for Progressive Web App

Activation

The API token login can be enabled generally or application-specifically (see Cookbook - Configuration). The key of the property is intershop.apitoken.cookie.enabled.

For example:

intershop.apitoken.cookie.enabled=true

Furthermore, an activation on application type level is possible using the property intershop.apitoken.cookie.applicationTypes, for instance:

intershop.apitoken.cookie.applicationTypes=intershop.B2CResponsive,intershop.SMBResponsive

Implementation

The PWA must have cookies enabled. If so, a cookie is written when the ICM application server processes a request (page is not cached by the web adapter).

The cookie named apiToken contains a JSON object with the API token.

The attribute isAnonymous indicates the authentication state:

  • true for anonymous users

  • false for authenticated users

When the ICM starts handling a request and the cookie is present, the ICM ensures that the user is logged in or an anonymous basket is retrieved.
For technical reasons, ICM overwrites the PWA cookie with its own cookie. The difference is that another JSON attribute is added, which is called creator='icm'.

If the user is logged in to ICM, but no cookie is available when the ICM takes over, the user will be logged out.

Note

This feature is based on the assumption that PWA and ICM can read and write each other's cookies. That means that both cookies must have the same domain and the same path. Therefore, this feature only works if PWA and ICM are running in the same domain. Otherwise, the property intershop.apitoken.cookie.samesite needs to be set to lax or none (see paragraph configuration ).

Depending on the property intershop.apitoken.cookie.pathCalculationMode, the path set inside the cookie is calculated.
If intershop.apitoken.cookie.pathCalculationMode=headless, PWA and ICM URLs need to contain the same prefix. For details, see Cookie Path Calculation and Configuration. On ICM side, such URLs can, for instance, be achieved using URL rewriting.

Depending on the property intershop.apitoken.cookie.nameCalculationMode, the name used for the cookie is calculated.
If intershop.apitoken.cookie.nameCalculationMode=siteDependent, the PWA also needs to use the same cookie name calculation. For details, see Cookie Name Calculation and Configuration.

Activation on Application Type Level

The property intershop.apitoken.cookie.applicationTypes controls for which application types API token cookies are used. This is important because these cookies are about 1 kB in size. If multiple cookies exist, the HTTP header space can become exhausted. Normally the space is limited to 8 kB, but all web components (e.g., Tomcat, Apache HTTPD, nginx) have different limitations. Therefore, it is highly recommended to set the property intershop.apitoken.cookie.applicationTypes to contain only the relevant application types (normally the storefront application types).

Note

The default value of intershop.apitoken.cookie.applicationTypes is empty to remain compatible with previous releases (empty means active for all application types).

Cookie Path Calculation

In ICM

Depending on the configuration property intershop.apitoken.cookie.pathCalculationMode, the cookie’s path is calculated as follows:

If the property’s value is:

  • root: The path is always /.

  • headless: The path is calculated using the application preference ExternalApplicationBaseURL of the PWA application (having preference HeadlessApplication=true, also see Concept - Headless Application Type - intershop.REST) in the same site as the request to the responsive application.

In the PWA

By default, the PWA writes its apiToken cookie on the domain root level. For the combination with the intershop.apitoken.cookie.pathCalculationMode=headless configuration, the cookie needs to be written on the baseHref path. This can be achieved by adapting the PWA's source code in the api-token.service.ts where the path: '/', needs to be removed when putting or removing the apiToken cookie.

    if (!SSR) {
      ...
      // save internal calculated apiToken as cookie whenever apiToken, basket, user or order information changes
      this.calculateApiTokenCookieValueOnChanges$().subscribe(apiToken => {
        const cookieContent = apiToken?.apiToken ? JSON.stringify(apiToken) : undefined;
        if (cookieContent) {
          this.cookiesService.put('apiToken', cookieContent, {
            expires: this.cookieOptions?.expires ?? new Date(Date.now() + DEFAULT_EXPIRY_TIME),
            secure: this.cookieOptions?.secure,
          });
        }
      });

      // remove apiToken cookie on logout
      // path: '/' is added in order to remove cookie within a multi-site configuration (e.g. configured /en, /fr, /de routes)
      this.logoutUser$().subscribe(() => this.cookiesService.remove('apiToken'));
      ...
    }

In addition to writing the apiToken cookie at the baseHref path level, you need to adapt the ICM request proxying based on something other than just /INTERSHOP, since the Responsive Starter Store requests now must also have the same path (achieved by URL rewriting) as the PWA requests, so that the apiToken cookie can still be shared. You need to adapt this in the server.ts and the multi-channel.conf.tmpl. You also need to adapt the default-url-mapping-table.ts in a Hybrid Approach scenario.

Cookie Name Calculation

In ICM

Depending on the configuration property intershop.apitoken.cookie.nameCalculationMode, the cookie’s name is calculated as follows:

If the property’s value is:

  • fixed: The name is taken from the property intershop.apitoken.cookie.name as is.

  • siteDependent: The name is calculated following the pattern ${intershop.apitoken.cookie.name}-<siteName>.

In the PWA

The current Intershop PWA implementation reads and writes the API token cookie with the hard coded name apiToken, which is the default name used by ICM if not configured otherwise. To use a different cookie name with the default fixed name calculation mode, according customization in the PWA is required (e.g., search for the string apiToken and replace it with the intended cookie name where it is actually used with the methods of the cookieService).

The Intershop PWA does not provide the functionality to read and write siteDependent cookies out of the box. This needs to be customized if the PWA needs to be used with such a requirement. The following diff illustrates the necessary changes based on an Intershop PWA 9.0.0.

diff --git a/src/app/core/guards/auth.guard.ts b/src/app/core/guards/auth.guard.ts
index 2c9353239..b0b088436 100644
--- a/src/app/core/guards/auth.guard.ts
+++ b/src/app/core/guards/auth.guard.ts
@@ -4,7 +4,9 @@ import { Store, select } from '@ngrx/store';
 import { iif, of, race, timer } from 'rxjs';
 import { map, take } from 'rxjs/operators';
 
+import { getICMChannel } from 'ish-core/store/core/configuration';
 import { getUserAuthorized } from 'ish-core/store/customer/user';
+import { apiTokenCookieName } from 'ish-core/utils/api-token/api-token.service';
 import { CookiesService } from 'ish-core/utils/cookies/cookies.service';
 import { whenTruthy } from 'ish-core/utils/operators';
 
@@ -33,7 +35,17 @@ export function authGuard(snapshot: ActivatedRouteSnapshot, state: RouterStateSn
       store.pipe(select(getUserAuthorized), whenTruthy(), take(1)),
       // send to login after timeout
       // send right away if no user can be re-hydrated
-      timer(!router.navigated && cookieService.get('apiToken') ? 4000 : 0).pipe(map(() => defaultRedirect))
+      store.pipe(
+        select(getICMChannel),
+        whenTruthy(),
+        take(1),
+        map(channel => {
+          const hasApiToken = cookieService.get(apiTokenCookieName(channel));
+          return !router.navigated && hasApiToken ? 4000 : 0;
+        }),
+        map(delay => timer(delay)),
+        map(() => defaultRedirect)
+      )
     )
   );
 }
diff --git a/src/app/core/store/core/configuration/configuration.selectors.ts b/src/app/core/store/core/configuration/configuration.selectors.ts
index 8253d22c5..fea9efc86 100644
--- a/src/app/core/store/core/configuration/configuration.selectors.ts
+++ b/src/app/core/store/core/configuration/configuration.selectors.ts
@@ -9,6 +9,8 @@ import { ConfigurationState } from './configuration.reducer';
 
 export const getConfigurationState = createSelector(getCoreState, state => state.configuration);
 
+export const getICMChannel = createSelector(getConfigurationState, state => state.channel);
+
 const getICMApplication = createSelector(getConfigurationState, state => state.application || '-');
 
 export const getResponsiveStarterStoreApplication = createSelector(
diff --git a/src/app/core/store/customer/user/user.effects.ts b/src/app/core/store/customer/user/user.effects.ts
index 6474cbb1d..060814a2f 100644
--- a/src/app/core/store/customer/user/user.effects.ts
+++ b/src/app/core/store/customer/user/user.effects.ts
@@ -10,6 +10,7 @@ import { CustomerRegistrationType } from 'ish-core/models/customer/customer.mode
 import { PaymentService } from 'ish-core/services/payment/payment.service';
 import { TokenService } from 'ish-core/services/token/token.service';
 import { UserService } from 'ish-core/services/user/user.service';
+import { getICMChannel } from 'ish-core/store/core/configuration';
 import { displaySuccessMessage } from 'ish-core/store/core/messages';
 import { selectQueryParam, selectUrl } from 'ish-core/store/core/router';
 import { getServerConfigParameter } from 'ish-core/store/core/server-config';
@@ -289,7 +290,8 @@ export class UserEffects {
   determinePersonalizationStatus$ = createEffect(() =>
     this.store.pipe(
       select(getPGID),
-      map(pgid => !this.apiTokenService.hasUserApiTokenCookie() || pgid),
+      concatLatestFrom(() => this.store.pipe(select(getICMChannel))),
+      map(([pgid, channel]) => !this.apiTokenService.hasUserApiTokenCookie(channel) || pgid),
       whenTruthy(),
       delay(100),
       map(() => personalizationStatusDetermined())
diff --git a/src/app/core/utils/api-token/api-token.service.ts b/src/app/core/utils/api-token/api-token.service.ts
index feac6a125..ffafe2484 100644
--- a/src/app/core/utils/api-token/api-token.service.ts
+++ b/src/app/core/utils/api-token/api-token.service.ts
@@ -1,5 +1,6 @@
 import { HttpErrorResponse, HttpEvent, HttpHandler, HttpParams, HttpRequest, HttpResponse } from '@angular/common/http';
 import { ApplicationRef, Injectable } from '@angular/core';
+import { concatLatestFrom } from '@ngrx/effects';
 import { Store, select } from '@ngrx/store';
 import { CookieOptions } from 'express';
 import { isEqual } from 'lodash-es';
@@ -37,6 +38,7 @@ import {
 import { BasketView } from 'ish-core/models/basket/basket.model';
 import { User } from 'ish-core/models/user/user.model';
 import { ApiService } from 'ish-core/services/api/api.service';
+import { getICMChannel } from 'ish-core/store/core/configuration';
 import { getCurrentBasket, getCurrentBasketId, loadBasketByAPIToken } from 'ish-core/store/customer/basket';
 import { getOrder, getSelectedOrderId, loadOrderByAPIToken } from 'ish-core/store/customer/orders';
 import {
@@ -61,6 +63,11 @@ export interface ApiTokenCookie {
 // If no expiry date is supplied by the token endpoint, this value (in ms) is used
 const DEFAULT_EXPIRY_TIME = 3600000;
 
+// construct the apiToken cookie name based on the given channel name
+export function apiTokenCookieName(channel?: string): string {
+  return `apiToken${channel ? `-${channel}` : ''}`;
+}
+
 @Injectable({ providedIn: 'root' })
 export class ApiTokenService {
   /**
@@ -90,30 +97,42 @@ export class ApiTokenService {
   private cookieChangeEvent$: Observable<[ApiTokenCookie, ApiTokenCookie]>;
 
   constructor(private cookiesService: CookiesService, private store: Store, private appRef: ApplicationRef) {
-    // setup initial values
-    const initialCookie = this.parseCookie();
-    this.initialCookie$ = new BehaviorSubject<ApiTokenCookie>(!SSR ? initialCookie : undefined);
-    this.apiToken$ = new BehaviorSubject<string>(initialCookie?.apiToken);
+    // setup initial values - initialize with undefined first, will be set with proper channel info below
+    this.initialCookie$ = new BehaviorSubject<ApiTokenCookie>(undefined);
+    this.apiToken$ = new BehaviorSubject<string>(undefined);
+
+    // Initialize with channel-aware cookie parsing
+    if (!SSR) {
+      this.store.pipe(select(getICMChannel), whenTruthy(), take(1)).subscribe(channel => {
+        const initialCookie = this.parseCookie(channel);
+        this.initialCookie$.next(initialCookie);
+        this.apiToken$.next(initialCookie?.apiToken);
+      });
+    }
 
     if (!SSR) {
       // multicast apiTokenCookieChange$ to avoid multiple listeners
       this.cookieChangeEvent$ = this.apiTokenCookieChange$().pipe(shareReplay(1));
 
       // save internal calculated apiToken as cookie whenever apiToken, basket, user or order information changes
-      this.calculateApiTokenCookieValueOnChanges$().subscribe(apiToken => {
-        const cookieContent = apiToken?.apiToken ? JSON.stringify(apiToken) : undefined;
-        if (cookieContent) {
-          this.cookiesService.put('apiToken', cookieContent, {
-            expires: this.cookieOptions?.expires ?? new Date(Date.now() + DEFAULT_EXPIRY_TIME),
-            secure: this.cookieOptions?.secure,
-            path: '/',
-          });
-        }
-      });
+      this.calculateApiTokenCookieValueOnChanges$()
+        .pipe(concatLatestFrom(() => this.store.pipe(select(getICMChannel))))
+        .subscribe(([apiToken, channel]) => {
+          const cookieContent = apiToken?.apiToken ? JSON.stringify(apiToken) : undefined;
+          if (cookieContent) {
+            this.cookiesService.put(apiTokenCookieName(channel), cookieContent, {
+              expires: this.cookieOptions?.expires ?? new Date(Date.now() + DEFAULT_EXPIRY_TIME),
+              secure: this.cookieOptions?.secure,
+              path: '/',
+            });
+          }
+        });
 
       // remove apiToken cookie on logout
       // path: '/' is added in order to remove cookie within a multi-site configuration (e.g. configured /en, /fr, /de routes)
-      this.logoutUser$().subscribe(() => this.cookiesService.remove('apiToken', { path: '/' }));
+      this.logoutUser$()
+        .pipe(concatLatestFrom(() => this.store.pipe(select(getICMChannel))))
+        .subscribe(([, channel]) => this.cookiesService.remove(apiTokenCookieName(channel), { path: '/' }));
 
       // unset apiToken when cookie vanishes/ has been removed unexpectedly and notify public event stream
       this.tokenVanish$().subscribe(type => {
@@ -147,8 +166,8 @@ export class ApiTokenService {
     this.apiToken$.next(apiToken);
   }
 
-  hasUserApiTokenCookie() {
-    const apiTokenCookie = this.parseCookie();
+  hasUserApiTokenCookie(channel?: string) {
+    const apiTokenCookie = this.parseCookie(channel);
     return apiTokenCookie?.type === 'user' && !apiTokenCookie?.isAnonymous;
   }
 
@@ -159,9 +178,9 @@ export class ApiTokenService {
     if (SSR) {
       return of(true);
     }
-    return this.initialCookie$.pipe(
-      first(),
-      switchMap(cookie => {
+    return this.store.pipe(select(getICMChannel), whenTruthy(), take(1)).pipe(
+      switchMap(channel => {
+        const cookie = this.parseCookie(channel);
         if (types.includes(cookie?.type)) {
           switch (cookie?.type) {
             case 'user': {
@@ -216,7 +235,8 @@ export class ApiTokenService {
       this.anonymousUserTokenMechanism(),
       of(true)
     ).pipe(
-      switchMap(() =>
+      concatLatestFrom(() => this.store.pipe(select(getICMChannel))),
+      switchMap(([, channel]) =>
         this.appendAuthenticationHeader(req).pipe(
           concatMap(request =>
             next.handle(request).pipe(
@@ -239,7 +259,7 @@ export class ApiTokenService {
               }),
               catchError(err => {
                 if (this.isAuthTokenError(err)) {
-                  this.invalidateApiToken();
+                  this.invalidateApiToken(channel);
 
                   // retry request without auth token
                   const retryRequest = request.clone({
@@ -260,8 +280,8 @@ export class ApiTokenService {
   /**
    * will remove apiToken and inform cookieVanishes$ listeners that cookie in not working as expected
    */
-  private invalidateApiToken() {
-    const cookie = this.parseCookie();
+  private invalidateApiToken(channel?: string) {
+    const cookie = this.parseCookie(channel);
 
     this.removeApiToken();
 
@@ -315,7 +335,8 @@ export class ApiTokenService {
       first(),
       mergeMap(() =>
         interval(1000).pipe(
-          map(() => this.parseCookie()),
+          concatLatestFrom(() => this.store.pipe(select(getICMChannel))),
+          map(([, channel]) => this.parseCookie(channel)),
           pairwise(),
           distinctUntilChanged((prev, curr) => isEqual(prev, curr))
         )
@@ -410,7 +431,8 @@ export class ApiTokenService {
   private mapToApiTokenCookie(): OperatorFunction<[User, BasketView, string, string], ApiTokenCookie> {
     return (source$: Observable<[User, BasketView, string, string]>) =>
       source$.pipe(
-        map(([user, basket, orderId, apiToken]): ApiTokenCookie => {
+        concatLatestFrom(() => this.store.pipe(select(getICMChannel))),
+        map(([[user, basket, orderId, apiToken], channel]): ApiTokenCookie => {
           if (user) {
             return { apiToken, type: 'user', isAnonymous: false, creator: 'pwa' };
           } else if (basket) {
@@ -419,7 +441,7 @@ export class ApiTokenService {
             return { apiToken, type: 'order', orderId, creator: 'pwa' };
           }
 
-          const apiTokenCookieString = this.cookiesService.get('apiToken');
+          const apiTokenCookieString = this.cookiesService.get(apiTokenCookieName(channel));
           const apiTokenCookie: ApiTokenCookie = apiTokenCookieString ? JSON.parse(apiTokenCookieString) : undefined;
           if (apiToken && apiTokenCookie) {
             return { ...apiTokenCookie, apiToken }; // overwrite existing cookie information with new apiToken
@@ -428,8 +450,8 @@ export class ApiTokenService {
       );
   }
 
-  private parseCookie(): ApiTokenCookie {
-    const cookieContent = this.cookiesService.get('apiToken');
+  private parseCookie(channel?: string): ApiTokenCookie {
+    const cookieContent = this.cookiesService.get(apiTokenCookieName(channel));
     if (cookieContent) {
       try {
         return JSON.parse(cookieContent);

ICM Cookie Handling

ICM writes the cookie when the following conditions are met:

  • The ICM application server processes a request.

  • The feature is active/enabled.

  • In the same site as the request to the responsive application, there is a PWA application (with preference HeadlessApplication=true).

ICM deletes the cookie when the feature is active and the token inside the cookie is invalid.

The detailed workflow is as follows:

Configuration

Key

Description

Type

Mandatory/Optional

Default value

intershop.apitoken.cookie.enabled

If true, the feature is enabled.

boolean

optional

false

intershop.apitoken.cookie.name

The name of the cookie to be used.

string

optional

apiToken

intershop.apitoken.cookie.maxage

The maximum age of the cookie in minutes.

integer

optional

60 (same as session timeout)

intershop.apitoken.cookie.sslmode

If true, the secure attribute of the cookie is set. The cookie will only be submitted if the transport is secure (SSL/TLS).

boolean

optional

true

intershop.apitoken.cookie.samesite

Defines the SameSite attribute of the cookie. If the Responsive Starter Store is hosted using a different domain than the PWA, this property needs to be set to none to allow both applications to access the cookie.

enum (one of {strict,lax,none})

optional

strict

intershop.apitoken.cookie.applicationTypes

Limits the application types for which the API token cookie is used. If empty, all application types use the cookie.

comma separated list of application type names

optional

<empty>

intershop.apitoken.cookie.pathCalculationMode

Defines how the path of the cookie is calculated, see Cookie Path Calculation.

enum (one of {root,headless})

optional

root

intershop.apitoken.cookie.nameCalculationMode

Defines how the name of the cookie is calculated, see Cookie Name Calculation.

enum (one of {fixed,siteDependent})

optional

fixed

Hint for Customization

Pipeline Calls for Logging in and Out

To actually log in the token user (see diagram in ICM Cookie Handling), the pipeline UserLogin-LoginUser is called. For the logout, the pipeline UserLogin-Logout is called.
Both are implemented for the login and logout process in the platform. In icm-as-business, the pipeline is overwritten to delegate to the pipelines for ProcessUser-Login and ProcessUser-LogoutUser respectively.

If there are additional tasks in customer projects when a user is logged in/out, further overwriting may be necessary.

Disclaimer

The information provided in the Knowledge Base may not be applicable to all systems and situations. Intershop Communications will not be liable to any party for any direct or indirect damages resulting from the use of the Customer Support section of the Intershop Corporate Website, including, without limitation, any lost profits, business interruption, loss of programs or other data on your information handling system.

Home
Knowledge Base
User Manuals
Product Releases
Log on to continue
This Knowledge Base document is reserved for registered customers.
Log on with your Intershop Entra ID to continue.
Write an email to supportadmin@intershop.de if you experience login issues,
or if you want to register as customer.