Authenticate via search token

This article explains how to employ Coveo search token authentication in an SAP Commerce Cloud project. Using this authentication method provides better security, as it doesn’t require exposing the API key in the front-end code.

Prerequisites

Make sure your configuration meets the following criteria:

Back-end configuration

Step 1: Add an OCC extension

To use search token authentication, you must add a new API to the OCC extension of your project. Whereas you can do so by building a new extension from scratch, we recommend using the prebuilt extension published on Coveo GitHub: https://github.com/coveo/coveo-sap-commerce-connector

  1. Go to your SAP project directory.

  2. Navigate to the hybris/bin/custom directory.

  3. Clone or download the extension repository. This would create a new directory named coveocc.

  4. In the project directory, open the hybris/config/localextensions.xml file and add the coveocc extension:

    <extension name='coveocc' />
  5. Save the file.

Step 2: Build and run the server

  1. From the root of your project directory, run the following command:

    ant clean all
  2. After the command execution, navigate to the hybris/bin/platform directory.

  3. Run the hybrisserver.sh script:

    ./hybrisserver.sh

Step 3: Specify credentials

The Coveo credentials should be set through the graphical interface of the Backoffice Administration Cockpit.

  1. In the Administration Cockpit, go to the WCMS → Website page.

  2. In the list of sites, double-click the required website.

  3. Switch to the Administration tab.

  4. Fill in the following fields:

Local testing

To test retrieving a search token, you can use a Swagger UI that’s available when you’re running the hybrisserver.sh script. By default, your local server would use ports 9001 (http) and 9002 (https). The Swagger UI is available at https://localhost:9002/occ/v2/swagger-ui.html, where you can find and test the /coveo/token/ endpoint.

This endpoint returns a JSON object with the token property that contains the search token. Depending on the user type, the endpoint returns a JWT token for a logged-in user or an anonymous user.

Note

Your implementation may use price groups to manage user-specific pricing. Price groups can be attached to a user group or directly to the user.

Either way, JWT tokens will contain all the price group IDs that are associated with a logged-in user.

See more about user groups in the SAP documentation.

Front-end configuration

Step 1: Add a new file

  1. Navigate to the root of your angular application (for example, src/app directory).

  2. Create a new service for token management, coveo/services/search-token-service/search-token.service.ts:

    import { CommerceEngine } from '@coveo/headless/commerce';
    import { loadConfigurationActions } from '@coveo/headless';
    import { Injectable, EventEmitter } from '@angular/core';
    
    import { EventService, LoginEvent, LogoutEvent, AuthStorageService } from '@spartacus/core';
    import { environment } from '../../../../environments/environment';
    
    @Injectable()
    export class SearchTokenService {
      public newTokenGenerated: EventEmitter<string> = new EventEmitter<string>();
      private TOKEN_PREFIX: string = `coveo-jwt`;
    
      constructor(
        private events: EventService,
        private authStorageService: AuthStorageService) {
    
        this.events
          .get(LogoutEvent)
          .subscribe( () => {
            removeAccessToken(this.TOKEN_PREFIX);
            this.emitEvent();
          });
    
        this.events
          .get(LoginEvent)
          .subscribe(() => {
            this.emitEvent();
          });
      }
    
      getToken() {
        return getAccessToken({
          prefix: this.TOKEN_PREFIX,
          hybrisToken: this.authStorageService.getItem('access_token'),
        });
      }
    
      refreshToken() {
        return getAccessToken({
          prefix: this.TOKEN_PREFIX,
          refresh: true,
          hybrisToken: this.authStorageService.getItem('access_token'),
        });
      }
    
      reloadSearchEngineWithToken(engine: CommerceEngine, token: string) { 1
        if (engine) {
          const {updateBasicConfiguration} = loadConfigurationActions(engine);
          const action = updateBasicConfiguration({accessToken: token})
          engine.dispatch(action);
        }
      }
    
      private async emitEvent() {
        this.newTokenGenerated.emit(await getAccessToken({ 2
          prefix: this.TOKEN_PREFIX,
          hybrisToken: this.authStorageService.getItem('access_token')
        }));
      }
    
    function removeAccessToken(prefix: string) {
      for (let i = 0; i < localStorage.length; i++) {
        const key = localStorage.key(i);
        if (key && key.startsWith(`${prefix}`) && !key.startsWith(`${prefix}-guest`)) {
          localStorage.removeItem(key)
        }
      }
    }
    
    interface GetAccessTokenOptions {
      prefix: string;
      refresh?: boolean;
      hybrisToken: string;
    }
    
    async function getAccessToken(options: GetAccessTokenOptions) {
      const { prefix, refresh = false, hybrisToken } = options;
    
      const usertype = hybrisToken ? 'user' : 'guest'; 3
      const cache = createTokenCache(prefix, usertype);
    
      const cachedToken = cache.get();
    
      if (cachedToken && !refresh) return cachedToken;
    
      const token = await generateAccessToken(hybrisToken);
      if (token) cache.set(token);
    
      return token;
    }
    
    function createTokenCache(prefix: string, usertype: string) { 4
      const key = `${prefix}-${usertype}`;
      const get = () => localStorage.getItem(key);
      const set = (token: string) => localStorage.setItem(key, token);
      return { get, set };
    }
    
    async function generateAccessToken(hybrisToken?: string | null) { 5
      const url = getCoveoAccessTokenUrl();
      const headers: Record<string, string> = {};
      if (hybrisToken) {
        headers['Authorization'] = `Bearer ${hybrisToken}`;
      }
    
      const requestOptions: RequestInit = {
        method: 'GET',
        headers: headers,
      };
    
      try {
        const response = await fetch(url, requestOptions);
        const data = await response.json();
        return data.token || '';
      } catch (error) {
        console.error('Request failed:', error);
        return '';
      }
    }
    
    function getCoveoAccessTokenUrl() {
      const base = environment.occBaseUrl;
      const baseSiteId = '<YOUR_SITE_ID>'; 6
      return `${base}occ/v2/${baseSiteId}/coveo/token`;
    }
1 reloadSearchEngineWithToken updates the engine configuration with the new JWT token when a user changes its authentication status.

Once a user logs in or logs out, the emitEvent method is called to emit a new token.

2 The service uses the emitEvent method to inform the app about the generation of a new token and retrieve it.
3 getAccessToken checks if checks if there’s a token in the localStorage. If not, it check whether it’s a logged-in user or an anonymous user and creates a cache token based on the user type. Finally, it calls generateAccessToken to create and retrieve a new token.
4 createTokenCache creates a cache object that exposes get and set methods to retrieve and store the token in the localStorage.
5 generateAccessToken calls the getCoveoAccessTokenUrl function which returns the URL of OCC extension that retrieves the token.
6 Saves the ID of your site into a baseSiteId variable. The site ID is the one that you configured in the Backoffice Administration Cockpit.

Step 2: Update app.component.ts

Update the src/app/app.component.ts file to use the getAccessToken method:

import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { SearchTokenService } from './coveo/services/search-token-service/search-token.service';
import {
  CommerceEngine,
  StandaloneSearchBox,
  buildCommerceEngine,
  buildSearch,
  buildStandaloneSearchBox
} from '@coveo/headless/commerce';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  providers: [
    SearchTokenService
  ]
})
export class AppComponent implements AfterViewInit {
  @ViewChild('searchinterface')
  title = 'powertoolsstore';
  commerceEngine!: CommerceEngine;
  state: any;
  constructor(
    private searchTokenService: SearchTokenService
  ) {
    this.searchTokenService.newTokenGenerated.subscribe((value) => { 1
      const engine = this.commerceEngine;
      if (engine) {
        this.searchTokenService.reloadSearchEngineWithToken(engine, value);
      } else {
        this.initializeCommerceEngine(value);
      }
    });
  }

  async ngAfterViewInit() {
    const accessToken = await this.searchTokenService.getToken();
    this.missingAccessToken = !accessToken;

    if (this.missingAccessToken) {
      return;
    }

    this.initializeCommerceEngine(<YOUR_ACCESS_TOKEN>);

    // ... 2
  }

  private initializeCommerceEngine(accessToken: string) {
    this.commerceEngine = buildCommerceEngine({
      configuration: {
        organizationId: '<YOUR_ORG_ID>', 3
        accessToken,
        renewAccessToken: () => this.searchTokenService.refreshToken(),
      },
    });
  }

  private initializeCommerceEngine(accessToken: string) {
    this.commerceEngine = buildCommerceEngine({
      configuration: {
        organizationId: '<YOUR_ORG_ID>',
        accessToken: accessToken,
        renewAccessToken: () => this.searchTokenService.refreshToken(),
        analytics: {
          trackingId: 'sports'
        },
        context: {
          currency: 'USD',
          country: 'US',
          language: 'en',
          view: {
            url: 'example.com'
          },
        }
      },
    });

    if (this.commerceEngine) {
      // ... 4
      const search = buildSearch(this.commerceEngine);
      search.executeFirstSearch();
    }
  }
}
1 Subscribe to the newTokenGenerated event to reload the engine with the new JWT token.
2 Once the Commerce engine is initialized, you can use it to create subcontrollers.
Example

Declare a StandaloneSearchBox controller as a property of the AppComponent class.

export class AppComponent implements AfterViewInit {
  @ViewChild('searchinterface')
  controller!: StandaloneSearchBox;
  // ...
}

Then, in the ngAfterViewInit method, initialize the controller and subscribe to its state changes.

this.controller = buildStandaloneSearchBox(this.commerceEngine, {
  options: {
    redirectionUrl: '/search',
    numberOfSuggestions: 5
  },
});

this.controller.subscribe(() => {
  this.state = this.controller.state;
});

if (this.controller.state.value) {
  this.controller.clear();
}

To reflect the changes in the UI and handle user input, you can add the following event handler:

onTextChange(event: Event): void {
  const target = event.target as HTMLInputElement;
  this.controller.updateText(target.value);
}
3 Pass the following parameters:
  • ID of your Coveo organization

  • Access token

  • renewAccessToken method that renews the token if it’s expired.

    See Initialize the Commerce engine for more details.

4 You can further specify the engine configuration before executing the first search. See ConfigurationActions (Commerce Engine).

From now on, your storefront will use the search token to authenticate the search requests.