Broadleaf Microservices
  • v1.0.0-latest-prod

Client Credentials

In this documentation, we’ll be discussing how to use client credentials to acquire an access token to use for machine-to-machine (M2M) applications, as described in this Auth0 article.

This is useful when creating a new service that will interact with Broadleaf’s APIs.

Creating a service client

The first step is to create a service client for the service that will be calling the Broadleaf API. We’ll set up a service client with the name myserviceclient with the READ_PRODUCT permission.

To create the service client, we’ll first need credentials, which can be generated with the following code:

package com.broadleafcommerce.auth.user.autoconfigure;

public class GenerateEncryptedPassword {

    public static void main(String[] args) {
        String password = "my_secret";
        System.out.println(new org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder().encode(password));
    }
}

Which outputs something like: $2a$10$W1Y5qik6wr8wyUonnOlvteYeuQe8whwO/sFxq3OMe9l.SU1Z3.pqq

Then, we’ll insert the client, permission, and scope into the auth database. Below are examples for both AuthenticationServices 1.x and 2.0.

1.x Compatible
INSERT INTO auth.blc_client (id, application_id, attributes, client_id, client_secret, friendly_name, is_admin, auth_server_id, token_timeout_seconds, default_redirect_uri, refresh_token_rot_intrvl_scnds, refresh_token_timeout_seconds) VALUES ('myserviceclient', null, '{}', 'myserviceclient', '$2a$10$W1Y5qik6wr8wyUonnOlvteYeuQe8whwO/sFxq3OMe9l.SU1Z3.pqq', 'My Service Client', 'N', '2', 300, null, 60, 7200);

INSERT INTO auth.blc_client_grant_types (id, grant_type) VALUES ('myserviceclient', 'client_credentials');

INSERT INTO auth.blc_client_permissions (id, permission) VALUES ('myserviceclient', 'READ_PRODUCT');

INSERT INTO auth.blc_client_scopes (id, scope) VALUES ('myserviceclient', 'PRODUCT');
2.x Compatible
INSERT INTO auth.blc_client (id, application_id, attributes, client_id, client_secret, friendly_name, is_admin, auth_server_id, token_timeout_seconds, default_redirect_uri, refresh_token_timeout_seconds, client_authentication_methods) VALUES ('myserviceclient', null, '{}', 'myserviceclient', '$2a$10$W1Y5qik6wr8wyUonnOlvteYeuQe8whwO/sFxq3OMe9l.SU1Z3.pqq', 'My Service Client', 'N', '2', 300, null, 7200, '["client_secret_basic"]');

INSERT INTO auth.blc_client_grant_types (id, grant_type) VALUES ('myserviceclient', 'client_credentials');

INSERT INTO auth.blc_client_permissions (id, permission) VALUES ('myserviceclient', 'READ_PRODUCT');

INSERT INTO auth.blc_client_scopes (id, scope) VALUES ('myserviceclient', 'PRODUCT');

Using Spring WebClient to call Broadleaf APIs

The following example shows how to create an "external provider" that can call the Broadleaf Catalog API from a new microservice.

Maven Dependencies

First, we’ll include the relevant dependencies:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
  <groupId>com.broadleafcommerce.microservices</groupId>
  <artifactId>broadleaf-resource-security</artifactId>
</dependency>

Application Properties

Next, we’ll configure the following properties. In this example, we’ll assume we’re running locally. The token-uri and client-secret values should be different in QA and production environments.

spring.security.oauth2.client.registration.myserviceclient.authorization-grant-type=client_credentials
spring.security.oauth2.client.registration.myserviceclient.client-id=myserviceclient
spring.security.oauth2.client.registration.myserviceclient.client-secret=my_secret

spring.security.oauth2.client.provider.myserviceclient.token-uri=https://localhost:8443/oauth/token

Application Code

Our goal is to create a WebClient in our new service that is configured to call the Catalog API with OAuth2 Client Credentials. There is a fair bit of boilerplate needed to configure the WebClient. This boilerplate code is included in the configuration class below.

Tip
These classes can be added directly to a flexpackage @SpringBootApplication class for quick prototyping.

External Catalog Provider

We’ll configure an "external provider" to call the Broadleaf APIs. We’ll call the read product by ID endpoint, which has a local URL of https://localhost:8447/catalog/products/{id}.

// Required import for retrieving client credentials
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId;

import com.broadleafcommerce.data.tracking.core.context.ContextInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;


/**
 * Provide access to the Broadleaf Catalog API. This simple implementation only has one method.
 * Add additional methods for each API endpoint your service needs.
 */
@RequiredArgsConstructor
class ExternalCatalogProvider {

    private static final String PRODUCT_BY_ID_ENDPOINT = "https://localhost:8446/api/catalog/products/{id}";

    private final WebClient webClient;
    private final ObjectMapper objectMapper;

    /**
     * Read a product by ID from the Broadleaf Catalog API.
     *
     * @param id The ID of the product.
     * @param contextInfo The context of the request.
     * @return The {@link Product} that corresponds to the given ID.
     */
    public Product readProductById(String id, @Nullable ContextInfo contextInfo) {
        return webClient.get()
                // the API endpoint URL
                .uri(PRODUCT_BY_ID_ENDPOINT, id)

                // context headers and Accept type
                .headers(headers -> headers.putAll(getHeaders(contextInfo)))
                .accept(MediaType.APPLICATION_JSON)

                // indicate which authorized client to use
                .attributes(clientRegistrationId("myserviceclient"))

                // call the endpoint
                .retrieve()
                .onStatus(HttpStatusCode::isError,
                        ClientResponse::createException)

                // convert response to Product
                .bodyToMono(Product.class)
                .block();
    }

    /**
     * Add "X-Context-Request" headers to the request.
     */
    private HttpHeaders getHeaders(@Nullable final ContextInfo contextInfo) {
        final HttpHeaders headers = new HttpHeaders();

        if (contextInfo == null) {
            return headers;
        }

        // Add Broadleaf ContextInfo to headers

        headers.setAcceptLanguageAsLocales(Collections.singletonList(contextInfo.getLocale()));
        headers.add(IGNORE_TRANSLATION_HEADER, String.valueOf(contextInfo.isIgnoreTranslation()));

        if (contextInfo.getPreviewToken() != null) {
            headers.add(DefaultPreviewTokenWebRequestResolver.DEFAULT_HEADER_NAME,
                    contextInfo.getPreviewToken().getToken());
        }
        if (contextInfo.getPreviewDate() != null) {
            headers.add(DefaultPreviewDateWebRequestResolver.DEFAULT_HEADER_NAME,
                    contextInfo.getPreviewDate().toString());
        }

        try {
            headers.add(CONTEXT_REQUEST_HEADER,
                    objectMapper.writeValueAsString(contextInfo.getContextRequest()));
        } catch (JsonProcessingException e) {
            throw new IllegalStateException("Unable to convert to JSON", e);
        }

        return headers;
    }
}
Note
To use this code in a Release Train 1.x compatible service, change the import org.springframework.http.HttpStatusCode to org.springframework.http.HttpStatus and line .onStatus(HttpStatusCode::isError, accordingly.

Controller

A simple controller to exercise our new external catalog provider.

/**
 * A simple controller to exercise the external catalog provider.
 */
@RestController
class AppController {

    /*
    Call this controller with product id and X-Context-Request header like this:
    curl --location 'https://localhost:8447/external-product/product1' \
    --header 'X-Context-Request: {"applicationId":"2","tenantId":"5DF1363059675161A85F576D"}'
     */

    @Autowired
    ExternalCatalogProvider externalCatalogProvider;

    @GetMapping("/external-product/{id}")
    public Object getExternalProduct(@PathVariable String id, @ContextOperation ContextInfo contextInfo) {
        return externalCatalogProvider.readProductById(id, contextInfo);
    }
}

Configuration

This class creates the beans needed by the ExternalCatalogProvider above. There is a fair bit of boilerplate needed to configure the WebClient to use client credentials and to work locally. This also includes an example of a SecurityEnhancer to allow unauthenticated requests to our new endpoint.

/**
 * The configuration and beans for the External Catalog Provider example.
 */
@Configuration
class ExternalCatalogProviderConfiguration {

    /**
     * This component encapsulates the catalog API that this service is using.
     */
    @Bean
    ExternalCatalogProvider externalCatalogProvider(WebClient externalCatalogWebClient, ObjectMapper objectMapper) {
        return new ExternalCatalogProvider(externalCatalogWebClient, objectMapper);
    }

    /**
     * Updates the security configuration for our custom endpoint.
     * For testing, this allows all requests to "/external-product/**".
     * A real implementation would still use the SecurityEnhancer, but probably not permitAll.
     */
    @Bean
    SecurityEnhancer externalCatalogSecurityEnhancer() {
        return http -> http.authorizeHttpRequests(authorize ->
                authorize.requestMatchers("/external-product/**").permitAll());
    }

    /**
     * Builds a WebClient that your custom service can use to communicate with the Broadleaf-based services.
     * This WebClient is configured to use OAuth2 Client Credential Authorization for all requests.
     * This means the WebClient will get an access token from the authentication server before calling the resource server.
     */
    @Bean
    WebClient externalCatalogWebClient(
            @Qualifier("oAuth2FilterFunctionSupplier") Supplier<ServletOAuth2AuthorizedClientExchangeFilterFunction> oauth2FilterSupplier,
            ObjectMapper objectMapper,
            @Qualifier("externalCatalogHttpConnector") Optional<ClientHttpConnector> externalCatalogHttpConnector) {
        // Add our own object mapper and increase the default buffer size
        ExchangeStrategies strategies = ExchangeStrategies
                .builder()
                .codecs(clientDefaultCodecsConfigurer -> {
                    clientDefaultCodecsConfigurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024);
                    clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonEncoder(
                            new Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON));
                    clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonDecoder(
                            new Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON));
                }).build();

        // disable ssl
        WebClient.Builder webClientBuilder = WebClient.builder();
        externalCatalogHttpConnector.ifPresent(webClientBuilder::clientConnector);

        // disable URI encoding
        DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory();
        uriBuilderFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE);

        return webClientBuilder
                .uriBuilderFactory(uriBuilderFactory)
                .exchangeStrategies(strategies)
                // apply the OAuth2 Client Credentials filter
                .apply(oauth2FilterSupplier.get().oauth2Configuration())
                .build();
    }

    /**
     * Configures the WebClient to use OAuth2 Authorization for requests. Note that the following configuration can
     * be re-used by multiple web clients.
     */
    @Bean
    Supplier<ServletOAuth2AuthorizedClientExchangeFilterFunction> oauth2FilterFunctionSupplier(
            ClientRegistrationRepository clientRegistrations,
            @Qualifier("externalCatalogHttpConnector") Optional<ClientHttpConnector> externalCatalogHttpConnector) {

        SynchronizedDelegatingOAuth2AuthorizedClientManager authorizedClientManager =
                new SynchronizedDelegatingOAuth2AuthorizedClientManager(clientRegistrations);

        WebClient.Builder webClientBuilder = WebClient.builder();
        externalCatalogHttpConnector.ifPresent(webClientBuilder::clientConnector);
        WebClient webClient = webClientBuilder.build();

        authorizedClientManager.setAuthorizedClientProvider(
                OAuth2AuthorizedClientProviderBuilder
                        .builder()
                        .clientCredentials(clientCredentialsGrantBuilder -> clientCredentialsGrantBuilder
                                .accessTokenResponseClient(
                                        new OAuth2ClientCredentialsAccessTokenResponseClient(webClient)))
                        .build());

        return () -> new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    }

    /**
     * Disables SSL verification in the HttpConnector for a WebClient when running locally.
     */
    @Bean
    ClientHttpConnector externalCatalogHttpConnector(
            @Value("${broadleaf.common.ssl-verification.disabled:true}") boolean sslDisabled) throws SSLException {
        if (sslDisabled) {
            SslContext sslContext = SslContextBuilder
                    .forClient()
                    .trustManager(InsecureTrustManagerFactory.INSTANCE)
                    .build();
            HttpClient httpClient = HttpClient
                    .create()
                    .secure(sslContextSpec -> sslContextSpec.sslContext(sslContext));
            return new ReactorClientHttpConnector(httpClient);
        }

        return null; // WebClient builder will initialize the default ClientHttpConnector
    }
}
Note
To use this code in a Release Train 1.x compatible service, change the line authorize.requestMatchers("/external-product/**").permitAll()); to authorize.requestMatchers(new AntPathRequestMatcher("/external-product/**")).permitAll());.

Acquiring an access token via client credentials API

Note
In order to proceed with the steps below for acquiring the access token, please complete the steps in the first section of this document (Creating a service client).

The OAuth2 Token Endpoint (/oauth/token) can be called directly with client credentials (client id and client secret) to get an access token. The endpoint requires a POST request with a grant_type parameter set to client_credentials, and optionally, a scope parameter. For example, https://localhost:8443/oauth/token?grant_type=client_credentials&scope=PRODUCT.

Also include an Authorization header with the base64-encoded client id and client secret, like Authorization: Basic Auth base64(myserviceclient:my_secret).

cURL example

curl --location --request POST 'https://localhost:8443/oauth/token?grant_type=client_credentials&scope=PRODUCT' \
--header 'Authorization: Basic bXlzZXJ2aWNlY2xpZW50Om15X3NlY3JldA=='

Postman Example

In Postman, set the client ID and client secret on the Authorization tab as "Basic Authentication". Include the grant_type=client_credentials parameter. The scope parameter is optional.

Postman screenshot

Response:

{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiYnJvYWRsZWFmLWF1dGhlbnRpY2F0aW9uIiwib2F1dGgyLXJlc291cmNlIl0sIm1heCI6MTYzODk1MzY3NSwic2NvcGUiOlsiUFJPRFVDVCJdLCJpc3MiOiJicm9hZGxlYWYtYXV0aGVudGljYXRpb24iLCJleHAiOjE2Mzg5MTA3NzUsImF1dGhvcml0aWVzIjpbIlJFQURfUFJPRFVDVCIsIlBST0RVQ1QiXSwianRpIjoiNmU4MDYwZGItY2NhZi00Y2RjLWFjMjItYjBhMDk3YThkMjBhIiwiY2xpZW50X2lkIjoiY2FydG9wc2NsaWVudCJ9.iJ2s_zkcYfqw4JQyDjCzkqUqttHWBQFFltwq6-Vd67C8-51B_W6s1gCMU7I0vg3pjGDQbrsqOZvbC-ng-zLGQVPjBJOGgqD5FDRcWQCpDQzJB-6R9z92dcp9Mb3CoSxw2xEm_a4kLxWK6RnZ949mW_t9EhWTUdsQK1CSWnERv5u7JqtRo8ScJsURaoFRi1zDESk9XimEmH8dDK9BYmq0BnZTT_vxUr9AHFHrwza5F2yBajxJcE2t7LDLeypSgG9yNBcb9Pn8Jcs_x_bSSmnggukDCj3x85feDMviLx73VjZ46YVDkLFfMIWhE61Fwv1dKSKmBdNweP08eVGGWRVgCQ",
    "token_type": "bearer",
    "expires_in": 299,
    "scope": "PRODUCT",
    "iss": "broadleaf-authentication",
    "aud": [
        "broadleaf-authentication",
        "oauth2-resource"
    ],
    "max": 1638953675,
    "jti": "6e8060db-ccaf-4cdc-ac22-b0a097a8d20a"
}

Java Application example

Here is an example of retrieving an access token using the client credentials flow in a plain Java application. The OAuth2 Token endpoint is directly called with the client credentials in a header.

<!--Add these dependencies to pom.xml-->
<dependency>
  <groupId>com.googlecode.json-simple</groupId>
  <artifactId>json-simple</artifactId>
  <version>1.1.1</version>
</dependency>
<dependency>
  <groupId>com.google.oauth-client</groupId>
  <artifactId>google-oauth-client</artifactId>
  <version>1.34.0</version>
</dependency>
//Create a new class RetrieveClientCredentials.java containing the following code
import com.google.api.client.auth.oauth2.ClientCredentialsTokenRequest;
import com.google.api.client.auth.oauth2.TokenResponse;
import com.google.api.client.auth.oauth2.TokenResponseException;
import com.google.api.client.http.BasicAuthentication;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import java.io.IOException;
import java.security.GeneralSecurityException;

public class RetrieveClientCredentials {

    public static void main(String[] args) {
        try {
            requestAccessToken();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static void requestAccessToken() throws IOException {
        try {
            GenericUrl tokenServerUrl = new GenericUrl("https://localhost:8443/oauth/token?grant_type=client_credentials&scope=PRODUCT");
            BasicAuthentication clientAuthentication = new BasicAuthentication("myserviceclient", "my_secret");
            HttpTransport httpTransport = new NetHttpTransport.Builder().doNotValidateCertificate().build();
            ClientCredentialsTokenRequest request = new ClientCredentialsTokenRequest(httpTransport, new GsonFactory(),
                    tokenServerUrl).setClientAuthentication(clientAuthentication);
            TokenResponse response = request.execute();
            System.out.println("Access token: " + response.getAccessToken());

        } catch (TokenResponseException e) {
            if (e.getDetails() != null) {
                System.err.println("Error: " + e.getDetails().getError());
                if (e.getDetails().getErrorDescription() != null) {
                    System.err.println(e.getDetails().getErrorDescription());
                }
                if (e.getDetails().getErrorUri() != null) {
                    System.err.println(e.getDetails().getErrorUri());
                }
            } else {
                System.err.println(e.getMessage());
            }
        } catch (GeneralSecurityException e) {
            throw new RuntimeException(e);
        }
    }

}

The output of the code would be something like this:

Access token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiYnJvYWRsZWFmLWF1dGhlbnRpY2F0aW9uIiwib2F1dGgyLXJlc291cmNlIl0sIm1heCI6MTY1NDc0MjYxOSwic2NvcGUiOlsiUFJPRFVDVCJdLCJpc3MiOiJicm9hZGxlYWYtYXV0aGVudGljYXRpb24iLCJleHAiOjE2NTQ2OTk3MTksImF1dGhvcml0aWVzIjpbIlJFQURfUFJPRFVDVCIsIlBST0RVQ1QiXSwianRpIjoiMjY4ODlkZmMtZDhhYi00OTA3LTk1OTgtZTUxZjk5YmFhNTIwIiwiY2xpZW50X2lkIjoibXlzZXJ2aWNlY2xpZW50In0.hu2P9_G4TjmdHVJCewh4pqmZJILzUz0YOGbMIueVRMRZtuRywhTp-HQBqtW_t7G-V9Fy0iub4p08SyneQSYHgic-5JJ8z73zsJLxuL9C7cxVvImjK8hEj8kRagMuEhkVCDSsCOwSisXD7Seia4yO5Y_ZzbHGeiEqFoV7BEafgKN5e46rdcJ7QgU7RXtIZwTBWz2KVVghwQNjJ3oeCnI7ygQKWPMjD0_Q_BZ8a10BgjXFHdAXBulTTiKizFmbNksvrnBqrdWtEBCFOFnkyhHuuj7EWFMWoIKBlE9qIUkOg3q8d4LsfkHMuu66uxF5AzV3jJGLHWTtP2aZbm8F0w9wTQ

This access token could then be used to access resources.