--- title: Authenticate via search token slug: ladf0004 canonical_url: https://docs.coveo.com/en/ladf0004/ collection: coveo-for-commerce source_format: adoc --- # Authenticate via search token This article explains how to employ [Coveo search token authentication](https://docs.coveo.com/en/56/) 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: * The [Apache Ant](https://ant.apache.org/) utility is installed on your machine. * You added an [Omni Commerce Connect (OCC)](https://help.sap.com/docs/SAP_COMMERCE_CLOUD_PUBLIC_CLOUD/3476714bba0b4cb9b3eb58c270e44439/8c19ab00866910148f87bf32d4a60d38.html) extension to your project. * You have a Coveo-powered search UI embedded in your SAP Commerce Cloud storefront. See [Use Coveo Search in SAP Commerce Cloud Composable Storefront](https://docs.coveo.com/en/ladf0003/). ## 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, you should use the prebuilt extension published on Coveo GitHub: https://github.com/coveo/coveo-sap-commerce-connector . Go to your SAP project directory. . Navigate to the `hybris/bin/custom` directory. . Clone or download [the extension repository](https://github.com/coveo/coveo-sap-commerce-connector/tree/v4/coveo/coveocc). This would create a new directory named `coveocc`. . In the project directory, open the `hybris/config/localextensions.xml` file and add the `coveocc` extension: ```xml ``` . Save the file. ### Step 2: Build and run the server . From the root of your project directory, run the following command: ```shell ant clean all ``` . After the command execution, navigate to the `hybris/bin/platform` directory. . Run the `hybrisserver.sh` script: ```shell ./hybrisserver.sh ``` ### Step 3: Specify credentials The Coveo credentials should be set through the graphical interface of the [Backoffice Administration Cockpit](https://help.sap.com/docs/SAP_COMMERCE/5c9ea0c629214e42b727bf08800d8dfa/8c16979286691014abe6f41434c7614a.html). . In the Administration Cockpit, go to the **WCMS → Website** page. . In the list of sites, double-click the required website. . Switch to the **Administration** tab. . Fill in the following fields: ** **Coveo Platform URL**. For production, it must be `https://platform.cloud.coveo.com/`. ** **Coveo** [**API Key**](https://docs.coveo.com/en/1718/). [Create an authenticated search API key](https://docs.coveo.com/en/1718#create-an-api-key), and then paste the key in the field. ![steps to set the Coveo credentials in the Backoffice Administration Cockpit](https://docs.coveo.com/en/assets/images/coveo-for-commerce/images/sap-cockpit-website-settings.png) ### 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](https://help.sap.com/docs/SAP_COMMERCE_CLOUD_PUBLIC_CLOUD/aa417173fe4a4ba5a473c93eb730a417/8c797311866910149debcebff02be567.html). ## Front-end configuration ### Step 1: Add a new file . Navigate to the root of your angular application (for example, `src/app` directory). . Create a new service for token management, `coveo/services/search-token-service/search-token.service.ts`: ```tsx 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 = new EventEmitter(); 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 = {}; 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 = ''; <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: ```tsx 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(); // ... <2> } private initializeCommerceEngine(accessToken: string) { this.commerceEngine = buildCommerceEngine({ configuration: { organizationId: '', <3> accessToken, renewAccessToken: () => this.searchTokenService.refreshToken(), }, }); } private initializeCommerceEngine(accessToken: string) { this.commerceEngine = buildCommerceEngine({ configuration: { organizationId: '', 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. ```tsx export class AppComponent implements AfterViewInit { @ViewChild('searchinterface') controller!: StandaloneSearchBox; // ... } ``` Then, in the `ngAfterViewInit` method, initialize the controller and subscribe to its state changes. ```tsx 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: ```tsx 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](https://docs.coveo.com/en/o6r70022#initialize-the-headless-commerce-engine) for more details. <4> You can further specify the engine configuration before executing the first search. See [ConfigurationActions (Commerce Engine)](https://docs.coveo.com/en/headless/latest/reference/interfaces/Commerce.ConfigurationActionCreators.html). From now on, your storefront will use the search token to authenticate the search requests.