Some of the most common activities in SAP CC projects are to handle the calls to external APIs consumed by the platform.
Luckily SAP CC has ready-to-use APIs that could help configuring/using these integrations with API
Registry Module.
The goal of this article is to show you how you could leverage this module to use Standard APIs and avoid developing custom code that could be costly and heavy to maintain.
API Registry Module
The APIRegistry module provides a range of features related to managing event configurations, endpoint configurations, and credentials, as well as exposing events and destinations to a target system.
You can connect SAP Commerce to an external system to allow SAP Commerce to consume the services that the external system provides.
To achieve this connection, you can configure one of the flowing type of credentials:
-
- Basic Credential
-
- Consumed Certificate Credential
-
- Consumed OAuth Credential
-
- Exposed OAuth Credential
Check the link below for more details about this module.
OAuth 2.0 Authorisation
OAuth 2.0 specification defines 4 types of authorisation flows:
-
- Authorisation Code
-
- Resource Owner Password Credentials
-
- Implicit
-
- Client Credentials
Based on the SAP Commerce APIs (DefaultRestTemplateFactory, DefaultOAuthCredentialsRestTemplateProvider ), Only the flow Client Credentials flow is supported in API Registry Module in SAP CC for Consumed OAuth Credential and Exposed OAuth Credential
It is a critical before to start any implementation of API integrations to double check if the external APIs support OAuth2 client_credentials flow, and if it is the case make sure the partner uses this method to limit the customisation in SAP CC side.
If OAuth2 client_credentials flow is not supported, you can check if you can configure the integration with
-
- Certificate Credentials
-
- Basic Credentials
-
- Or using an API gateway (https://www.nginx.com/learn/api-gateway/) which supports OAuth2 client_credentials flow
If none of these methods is available then you will need to do custom code in SAP CC.
To know more about these flows, you can refer to the following links.
Configuring APIs in SAP CC
This section will show you how you can configure Consumed APIs using OAuth2 Client Credential flow.
Let’s assume you have an API that use OAuth2 Client Credential flows with Two endpoints.
-
- 1st endpoint to generate the token.
-
- 2nd endpoint requesting the resource using the token.
Step 1: Authentication (Token
generation)
API
In this API call, scope is used to generate the token, but SAP CC does not allow you to configure it.
-
- BODY:
-
- client_id :<CLIENT_ID>
-
- client_secret :<CLIENT_SECRET>
-
- grant_type : client_credentials
-
- scope : <SCOPE> (Optional)
-
- BODY:
Configuration in SAP Commerce Cloud
ConsumedOAuthCredential
Attribute | Value |
ID | <OAUTH2_CREDENTIALS_ID> |
OAuth URL | https://<AUTHENTICATION_URL_TO_GENERATE_TOKEN>; |
Client ID | <OAUTH2_CLIENT_ID> |
Client Secret | <CLIENT_SECRET> |
Step 2: API call requiring authentication
API
In this API call, subscription is used in the header, but Standard SAP CC does not allow you to configure it.
-
- Header :
-
- Subscription-Key: <SUBSCRIPTION_KEY>
-
- Authorization : Bearer <GENERATED_TOKEN>
-
- Header :
Configuration in SAP Commerce Cloud
Endpoint
Attribute | Value |
ID | <ENDPOINT_ID> |
NAME | <ENDPOINT_NAME> |
Version | <VERSION> |
Specification URL | https://<GET_OR_POST_REQUEST_REQUIRING_AUTHENTICATION>; |
ConsumedDestination
See Configuring Consumed Destinations
Attribute | Value | Documentation |
ID | <ENDPOINT_ID> | |
URL | Same as Specification URL configured in the Endpoint | |
Version | <VERSION> | |
Destination Target |
Destination Target configured to host all Destination (with Template : false) |
Configuring Destination Targets |
Endpoint | Endpoint configured previously | Configuring Endpoints |
Credential | Credential entry configured previously | Configuring Credentials |
Tests
-
- Run the server with the Debug Mode
-
- Test the connectivity with Ping Destination Action
-
- Error due to the scope missing (scope cannot be configured/initialised in the Standard API) in the first call (Step 1)Expand source
Caused by: org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException: Access token denied. […] Caused by: org.springframework.security.oauth2.common.exceptions.InvalidRequestException: AADSTS90014: The required field ‘scope’ is missing from the credential. Ensure that you have all the necessary parameters for the login request. Trace ID: 70094699-a056-4d5e-a65f-54014bfc0c00 Correlation ID: 37ad00cb-89f7-4e1f-9f60-2768c515a18d Timestamp: 2023-10-24 13:02:34Z at org.springframework.security.oauth2.common.exceptions.OAuth2ExceptionJackson2Deserializer.deserialize(OAuth2ExceptionJackson2Deserializer.java:104) ~[spring-security-oauth2-2.5.0.RELEASE.jar:?]
- Error due to the scope missing (scope cannot be configured/initialised in the Standard API) in the first call (Step 1)Expand source
-
- Forcing the scope in the debugger by adding it to the scope list of the BaseOAuth2ProtectedResourceDetails object (resourceDetails.setScope(List.of(“<SCOPE>”))) solved the issue.
-
- The first call successfully generated the token using the debugger
-
- Then the second call was executed with the token generated by the first call
-
- But we had a second error when executing the second call (Step 2) which is due to missing subscription key in the headerExpand source
-
- ERROR [hybrisHTTP23] [TestDestinationUrlAction] Remote system ([https://<GET_OR_POST_REQUEST_REQUIRING_AUTHENTICATION>]): ping unsuccessful. Returned status code was [401 UNAUTHORIZED]. For more information, see the server log. de.hybris.platform.apiregistryservices.exceptions.DestinationNotFoundException: 401 Access Denied: “{ “statusCode”: 401, “message”: “Access denied due to missing subscription key. Make sure to include subscription key when making requests to an API.” }”
-
- After forcing the missing subscription in the header, the call was successful.
In summary, the Standard API Registry allows you to make a call to protected resource n 2 steps:
-
- 1st call to generate the token with client Id, client secret and in ‘client_credentials’ mode (OAuth2)
-
- 2nd call to the target source using previously generated token.
If you have any additional parameters such as scope for the OAuth2 call or header parameters, you will need to add them to the Standard API (The next section explain how you can do it).
Integrating scope & subscription
This section shows how you can adapt the standard to inject the scope and the header parameters such as subscription.
-
- Create a CustomDestinationService service that inherits from the OOTB DefaultDestinationService then override testDestinationUrl() method to include support for header subscription and scope
public class CustomDestinationService extends DefaultDestinationService { private static final Logger LOG = Logger.getLogger(FBDestinationServiceImpl.class); […] final static private String WS_SCOPE=”scope”; final static private String WS_HEADER_SUBSCRIPTION_KEY=”subscription-key”; final static private String WS_HEADER_SUBSCRIPTION_VALUE=”subscription-value”; final static private String CUSTOM_WS_URL_PREFIX=”<CUSTOM_WS_URL>”; @Override public void testDestinationUrl(final AbstractDestinationModel destinationModel) throws DestinationNotFoundException { try { // Setup WS Header final HttpHeaders headers = new HttpHeaders(); headers.setAccept(Arrays.asList(MediaType.ALL)); headers.setContentType(MediaType.APPLICATION_JSON); // Add subscription’s key and value retrieved from destination.additionalProperties if(destinationModel.getUrl().contains(CUSTOM_WS_URL_PREFIX)) { headers.add(destinationModel.getAdditionalProperties().get(WS_HEADER_SUBSCRIPTION_KEY), destinationModel.getAdditionalProperties().get(WS_HEADER_SUBSCRIPTION_VALUE)); } final HttpEntity<Object> httpEntity = new HttpEntity<>(headers); //Retrieve the Rest template to be executed from Consumer Destination configured on Backoffice final RestTemplate restTemplate = getRestTemplate(destinationModel); // Enrich rest template with the scope retrieved from destination.additionalProperties enrichRestTemplateWithScope(restTemplate,destinationModel); // Call the WS with RestTemplate and return the WS Response final ResponseEntity<String> response = restTemplate.exchange(validateExposedDestinationUrl(destinationModel.getUrl()), HttpMethod.GET, httpEntity, String.class); // Process errors if(response.getStatusCode().series() != HttpStatus.Series.SUCCESSFUL) { […] } //Here we could process the WS response LOG.info(“WS Response : “+ response.toString()); } // Process errors catch (final HttpClientErrorException | HttpServerErrorException e){[…]} catch (final ResourceAccessException e){[…]} catch (final Exception e){[…]} } private void enrichRestTemplateWithScope(RestTemplate restTemplate, AbstractDestinationModel destinationModel){ //Inject the scope into the oauth2 token call if (restTemplate instanceof OAuth2RestTemplate && ((OAuth2RestTemplate)restTemplate).getResource() instanceof BaseOAuth2ProtectedResourceDetails && destinationModel.getUrl().contains(CUSTOM_WS_URL_PREFIX)){ ((BaseOAuth2ProtectedResourceDetails)((OAuth2RestTemplate)restTemplate).getResource()) .setScope(List.of(destinationModel.getAdditionalProperties().get(WS_SCOPE))); } } }
- Create a CustomDestinationService service that inherits from the OOTB DefaultDestinationService then override testDestinationUrl() method to include support for header subscription and scope
-
- Register the bean in customcore/resources/customcore-spring.xml
<alias alias=”destinationService” name=”customDestinationService”/> <bean id=”customDestinationService” class=”com.sap.custom.apiregistryservices.services.impl.CustomDestinationService” > <property name=”destinationDao” ref=”destinationDao”/> </bean>
- Register the bean in customcore/resources/customcore-spring.xml
Expand source
-
- Configure the consumed destination (Call to the consumed web-service) with scope, subscription-key and subscription-value as entries to ConsumedDestination.additonalProperties Map
final static private String WEBSERVICES_SCOPE=”scope”; final static private String WEBSERVICES_HEADER_SUBSCRIPTION_KEY=”subscription-key”; final static private String WEBSERVICES_HEADER_SUBSCRIPTION_VALUE=”subscription-value”;
- Configure the consumed destination (Call to the consumed web-service) with scope, subscription-key and subscription-value as entries to ConsumedDestination.additonalProperties Map
In addition to the values configured previously for CustomerDestination
Attribute | Value |
ID | <ENDPOINT_ID> |
URL | Same as specification URL configured in the Endpoint |
Version | <VERSION> |
Destination Target | Destination Target configured previously |
Endpoint | Endpoint configured previously |
Credential | Credential entry configured previously |
You will need to configure ConsumedDestination.additionalProperties as follow.
Key | Value |
scope | <SCOPE> |
subscription-key | <SUBSCRIPTION_KEY> |
subscription-value | <SUBSCRIPTION_VALUE> |
-
- Now you can test the connectivity with Ping Destination Action highlighted above in the screenshot.
-
- You can also check the logs on the Console.