Skip to content

Authenticated Search

In order to use authenticated search follow these steps.

Complete Sample

Full tutorial sample.

// --8<-- [start:usings]
using Bizzkit.Sdk.EcommerceSearch;
using Bizzkit.Sdk.Search;
using Bizzkit.Sdk.Search.Authenticated;
using Bizzkit.Sdk.Search.Authenticated.Services;
using Microsoft.Extensions.DependencyInjection;
// --8<-- [end:usings]

const string segmentId = "b2c-dk-da";
const string scopeId = "full-search";
const string searchApiBaseUrl = "https://search.bizzk.it/";
const string searchAdminBaseUrl = "https://ecommercesearch.bizzk.it/";
const string authBaseUrl = "https://auth.bzk.hdk";
const string myWebShopBaseUrl = "https://mywebshop.example.com/";
const string clientId = "my-shop";
const string clientSecret = "my-secret";
const string priceGroupId = "member";

IServiceProvider provider;

SetupServiceProvider();
await CreateSigningKeyAsync();

var token = await GetTokenAsync();
await PerformSearchAsync(token);

void SetupServiceProvider()
{
    // --8<-- [start:dependencyInjection]
    var ecommerceSearchOptions = new SearchAdministrationConnectionOptions
    {
        Authority = authBaseUrl,
        BaseUrl = searchAdminBaseUrl,
        ClientId = clientId,
        ClientSecret = clientSecret
    };

    var searchOptions = new SearchConnectionOptions
    {
        BaseUrl = searchApiBaseUrl
    };

    var services = new ServiceCollection();
    services.AddLogging();

    services.AddSingleton<ISearchAdministrationClientFactory>(
        new SearchAdministrationClientFactory(ecommerceSearchOptions));
    services.AddSingleton<ISearchClientFactory>(new SearchClientFactory(searchOptions, new HttpClient()));

    services.AddBizzkitAuthenticatedSearch();
    // --8<-- [end:dependencyInjection]

    provider = services.BuildServiceProvider();
}

async Task CreateSigningKeyAsync()
{
    // --8<-- [start:signingKey]
    var administrationClientFactory = provider.GetRequiredService<ISearchAdministrationClientFactory>();
    var administrationClient = await administrationClientFactory.CreateAuthenticatedClientAsync();
    var signingKeySettings = new SearchSigningKeySettingsModel
    {
        AudienceUrl = new Uri(searchApiBaseUrl),
        IssuerUrl = new Uri(myWebShopBaseUrl)
    };
    await administrationClient.UpsertSigningKeySettingsAsync(segmentId, signingKeySettings);
    // --8<-- [end:signingKey]
}

async Task<AuthenticatedSearchToken> GetTokenAsync()
{
    // --8<-- [start:authSearchModel]
    var authSearchModel = new AuthenticatedSearchModel(segmentId)
    {
        Filters = new Dictionary<string, Bizzkit.Sdk.Search.FilterModel>
        {
            { "PriceGroup", new Bizzkit.Sdk.Search.FilterModel{ Values = new List<string> { priceGroupId} } }
        }
    };
    // --8<-- [end:authSearchModel]

    // --8<-- [start:token]
    var tokenOpts = new AuthenticatedSearchOptions { TokenTtl = TimeSpan.FromHours(24) };

    var signingService = provider.GetRequiredService<IAuthenticatedSearchSigningService>();

    //Token should be cached and reused when not expired.
    //var expired = cachedToken.TokenExpiresUtc <= DateTimeOffset.UtcNow
    var searchToken = await signingService.CreateTokenAsync(authSearchModel, tokenOpts);
    // --8<-- [end:token]

    return searchToken;
}

async Task PerformSearchAsync(AuthenticatedSearchToken searchToken)
{
    // --8<-- [start:search]
    var searchClientFactory = provider.GetRequiredService<ISearchClientFactory>();
    var searchClient = await searchClientFactory.CreateUnauthenticatedClientAsync();

    var authSearchRequest = new UnifiedSearchRequestModel
    {
        SegmentId = segmentId,
        ScopeId = scopeId,
        AuthenticationToken = searchToken.Token,
        Phrase = string.Empty
    };
    var authSearchResult = await searchClient.SearchAsync(authSearchRequest);
    // --8<-- [end:search]

    var authPriceResults = authSearchResult.Facets.First(x => x.Key == "Price");
    Console.WriteLine("Authenticated search gives {0} products and total price range between {1} and {2} ",
        authSearchResult.TotalProducts, authPriceResults.Min, authPriceResults.Max);
}

Setup dependency injection

The classes needed to create AuthenticationTokens is located in the Bizzkit.Sdk.Search.Authenticated NuGet package.

In the code below is an example on how to setup dependency injection for authenticated search:

var ecommerceSearchOptions = new SearchAdministrationConnectionOptions
{
    Authority = authBaseUrl,
    BaseUrl = searchAdminBaseUrl,
    ClientId = clientId,
    ClientSecret = clientSecret
};

var searchOptions = new SearchConnectionOptions
{
    BaseUrl = searchApiBaseUrl
};

var services = new ServiceCollection();
services.AddLogging();

services.AddSingleton<ISearchAdministrationClientFactory>(
    new SearchAdministrationClientFactory(ecommerceSearchOptions));
services.AddSingleton<ISearchClientFactory>(new SearchClientFactory(searchOptions, new HttpClient()));

services.AddBizzkitAuthenticatedSearch();

Create Signing Key Settings

The signing key settings are used to sign the AuthenticationToken. The settings are set with UpdateSingingKeySettingsAsync method. There are two settings:

  • SearchApiBaseUrl, Is the search api base address.
  • MyWebShopBaseUrl, Is the url of the calling shop.
1
2
3
4
5
6
7
8
var administrationClientFactory = provider.GetRequiredService<ISearchAdministrationClientFactory>();
var administrationClient = await administrationClientFactory.CreateAuthenticatedClientAsync();
var signingKeySettings = new SearchSigningKeySettingsModel
{
    AudienceUrl = new Uri(searchApiBaseUrl),
    IssuerUrl = new Uri(myWebShopBaseUrl)
};
await administrationClient.UpsertSigningKeySettingsAsync(segmentId, signingKeySettings);

Create the AuthenticatedSearchModel

The AuthenticatedSearchModel contains the filters that can be set on the AuthenticationToken. In this example the price group is set. This allows to only search and return prices from the selected price group.

1
2
3
4
5
6
7
var authSearchModel = new AuthenticatedSearchModel(segmentId)
{
    Filters = new Dictionary<string, Bizzkit.Sdk.Search.FilterModel>
    {
        { "PriceGroup", new Bizzkit.Sdk.Search.FilterModel{ Values = new List<string> { priceGroupId} } }
    }
};

Create the AuthenticationToken

The AuthenticationToken is created by the IAuthenticatedSearchSigningService. The token has a lifetime that can be set to a maximum of 24 hours. Before using the token it should be checked if it is expired. Also keep in mind that the token should be cached as signing a token is a heavy process.

1
2
3
4
5
6
7
var tokenOpts = new AuthenticatedSearchOptions { TokenTtl = TimeSpan.FromHours(24) };

var signingService = provider.GetRequiredService<IAuthenticatedSearchSigningService>();

//Token should be cached and reused when not expired.
//var expired = cachedToken.TokenExpiresUtc <= DateTimeOffset.UtcNow
var searchToken = await signingService.CreateTokenAsync(authSearchModel, tokenOpts);

The last step is to set the AuthenticationToken on the search request and then perform the search.

var searchClientFactory = provider.GetRequiredService<ISearchClientFactory>();
var searchClient = await searchClientFactory.CreateUnauthenticatedClientAsync();

var authSearchRequest = new UnifiedSearchRequestModel
{
    SegmentId = segmentId,
    ScopeId = scopeId,
    AuthenticationToken = searchToken.Token,
    Phrase = string.Empty
};
var authSearchResult = await searchClient.SearchAsync(authSearchRequest);

Notes about the code above

For all the code above to work the following usings are needed:

1
2
3
4
5
using Bizzkit.Sdk.EcommerceSearch;
using Bizzkit.Sdk.Search;
using Bizzkit.Sdk.Search.Authenticated;
using Bizzkit.Sdk.Search.Authenticated.Services;
using Microsoft.Extensions.DependencyInjection;

and the following NuGet packages must be added to the project.

  • Bizzkit.Sdk.Search.Authenticated
  • Microsoft.Extensions.Logging

JWT Model

It is also possible to create your own token using the SearchSigningKey endpoint in the admin api, and your programming language of choice.

The model of the claims is very simple. It just contains the filters and scopes. As mentioned before, including the filters and scopes in the JWT allows the request to use filters that are not a part of allowed filters, as well as authenticated scopes.

The claims model in JavaScript could look like this, depending on the filters and scopes you need.

{
    filters: {
        categoryName: {
            from: null,
            to: null,
            match: null,
            values: ['value1', 'value2']
        },
        price: {
            from: 0,
            to: 1000,
            match: null,
            values: null
        }
    },
    scope_ids: ['scopeid1', 'scopeid2']
}

The token is signed using algorithm HS256, and type JWT. The rest of the required information is provided by the SearchSigningKey endpoint in the Admin API. An implementation using the jose Javascript library could look like this:

export const signKey = async (
    token: SearchSigningKeyModel,
    filters: Record<string, FilterModel>,
    scopeId: string,
) => {
    const secret = new TextEncoder().encode(token.signingSecret)

    const jwt = await new jose.SignJWT({
        filters,
        scope_ids: [scopeId],
    })
        .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
        .setIssuedAt()
        .setIssuer(token.issuer)
        .setAudience(token.audience)
        .setExpirationTime('24h')
        .setNotBefore(new Date())
        .sign(secret)

    return jwt
}