Skip to main content

Use Destinations To Connect To Other Systems and Services

The SAP Cloud SDK offers some basic functionality that helps with connecting to other systems and services like SAP S/4HANA Cloud. The SAP Cloud SDK introduces the general concept of a Destination which holds basic information about how to connect to such a system. That could for instance be a url, a username, and password for basic authentication or some custom headers.

This concept is integrated with the Destination Service that is available on the SAP Business Technology Platform. If the application has a service binding to this service in place the SAP Cloud SDK will provide access to these destinations.

Accessing Destinations

In general destinations are accessed through the DestinationAccessor:

DestinationAccessor.getDestination("my-destination");

This will look up the destination in the destination service if the application is running on SAP Business Technology Platform. Other sources like the environment variables are also considered.

Authentication Flows

Authentication flows like OAuth 2.0 Client Credentials and others are usually performed by the Destination Service. They are executed when querying the destination information and the resulting authorization information will be included in the response.

See the note on supported authentication flows.

Destinations are cached for 5 minutes

To minimize the runtime overhead of finding destinations, the SAP Cloud SDK uses caching to internally maintain a list of already retrieved destinations. Therefore, subsequent calls to DestinationAccessor.getDestination("my-destination"); will be served from that cache instead of performing the look up over and over again. By default, cache entries will automatically be invalidated 5 minutes after their initial creation. The caching behavior can be customized. Please refer to our section about configuring the cache configuration for more details.

Supported Authentication Flows

The SAP Cloud SDK does not implement most of the authentication flows itself. Instead, it relies on the Destination Service to perform the flows and complements it with additional steps, if necessary.

The SAP Cloud SDK recognizes the following authentication types of the SAP BTP Destination Service:

Authentication TypeParameter Name
NoAuthenticationAuthenticationType.NO_AUTHENTICATION
BasicAuthenticationAuthenticationType.BASIC_AUTHENTICATION
PrincipalPropogationAuthenticationType.PRINCIPAL_PROPAGATION
ClientCertificateAuthenticationAuthenticationType.CLIENT_CERTIFICATE_AUTHENTICATION
AppToAppSSOAuthenticationType.APP_TO_APP_SSO
OAuth2ClientCredentialsAuthenticationType.OAUTH2_CLIENT_CREDENTIALS
OAuth2SAMLBearerAssertionAuthenticationType.OAUTH2_SAML_BEARER_ASSERTION
OAuth2UserTokenExchangeAuthenticationType.OAUTH2_USER_TOKEN_EXCHANGE
OAuth2JWTBearerAuthenticationType.OAUTH2_JWT_BEARER
SAPAssertionSSOAuthenticationType.SAP_ASSERTION_SSO
InternalSystemAuthenticationAuthenticationType.INTERNAL_SYSTEM_AUTHENTICATION
OAuth2PasswordAuthenticationType.OAUTH2_PASSWORD
SAMLAssertionAuthenticationType.SAML_ASSERTION
Other Destination Sources

In some situations you may want to obtain a destination not from the Destination Service, but e.g. from the environment variables.

In that case, only NoAuthentication and BasicAuthentication are supported by default. Additionally, OAuth2ClientCredentials as well as OAuth2UserTokenExchange are available through a dedicated API.

For more details visit the sections on how to Configure HTTP Destinations for Local Deployment and Skip Destination Creation Step for Certain Authentication Types on Cloud Foundry.

Integrated Multi-Tenancy

By default, the DestinationAccessor is tenant aware. If a tenant is available, it will be used to access the destination service on behalf of that tenant. If no destination is found in the tenant-specific destination service, the SAP Cloud SDK will try to get it using the service binding of the application.

This default retrieval strategy can be overridden by passing options to the destination loader as follows:

DestinationAccessor.getLoader().tryGetDestination(destinationName, options);

See the section on destination options below.

Using the SAMLAssertion Authentication Type

Using the SAMLAssertion authentication types requires a different caching strategy for HttpClients. This is because upon authentication at the target system, a session cookie is returned, which is stored within the used HttpClient instance. For subsequent calls, the service then expects authenticated clients to include their session cookie.

By default, the SAP Cloud SDK uses the RequestScopedHttpClientCache to store HttpClient instances for the duration of a specific (incoming) request. Using this default strategy results into deleting the HttpClient (together with the session cookie) that was used to authorize at the target system, once the initial request has been handled. To avoid authorizing over and over again, the SAP Cloud SDK offers the TimeScopedHttpClientCache, which keeps existing HttpClients for a certain period of time. These instances are then automatically reused for consecutive (incoming) requests, so that the session is retained.

Switching the cache can be done as follows:

// run this code once in a setup step, afterwards HttpClient instances are kept for 5 minutes
HttpClientAccessor.setHttpClientCache(new TimeScopedHttpClientCache(5, TimeUnit.MINUTES));
Hint

The cached HttpClient instances are also isolated based on the tenant and principal.

Supported Proxy Types

Proxy TypeParameter Name
InternetProxyType.INTERNET
OnPremiseProxyType.ON_PREMISE
PrivateLink*ProxyType.PRIVATE_LINK

* Further reading: SAP BTP Private Link Announcement Blog Post

Decorating Destinations

Depending on the use case, one needs to wrap the accessed destination before issuing a request to a system. This is to make sure all required destination properties are correctly set before invoking the actual request.

HTTP Destinations

In case of HTTP connections one needs to wrap the retrieved destination as HttpDestination using asHttp():

DestinationAccessor.getDestination("my-destination").asHttp();

This method ensures that the required destination properties are all set to make the HTTP connection. With the resulting destination instance depending on the use case one can run HTTP queries for OData or REST.

BAPI Destinations

Similarly, for BAPI endpoints you need to use asRfc():

DestinationAccessor.getDestination("my-destination").asRfc();

Connect To SAP S/4HANA On-Premise

If your destination is exposing an SAP S/4HANA On-premise service via a Cloud Connector you need to decorate the destination with DefaultErpHttpDestination:

final HttpDestination httpDestination =
destination.asHttp().decorate(DefaultErpHttpDestination::new);
  • This ensures the mapping of all SAP S/4HANA properties like sap-client and sap-locale as HTTP request headers.
  • In case Principal Propagation is specified as an authentication type for the destination the SAP Cloud SDK will automatically apply it.

Registering Destinations at Runtime

The SAP Cloud SDK offers convenient ways to create a destination at runtime and register it so that it will be available via DestinationAccessor.getDestination(). This is especially useful when working in a local environment. Destinations configured in the destination service are not available in a local setting and have to be mocked.

Mocking Destinations for Testing

The SAP Cloud SDK offers a MockUtil class that is capable of injecting destinations into the DestinationAccessor:

final MockUtil mockUtil = new MockUtil();

MockDestination destination = MockDestination
.builder("my-destination", URI.create("http://localhost:8080"))
.build();

mockUtil.mockDestination(destination);

// This will now return the mocked destination
DestinationAccessor.getDestination("my-destination").asHttp();

This helps with keeping production and test code nicely separated. There are more overloads of the mocking on the MockUtil class that you can use. Refer to these tutorial steps on how to mock destinations for local development and testing.

Using Custom Destination Loaders

The above is useful for testing but not suited for productive code. If you want to register custom destinations in your productive code you can use the following API:

customHttpDestination = DefaultHttpDestination.builder("http://url")
.name("custom-destination")
.build();

customLoader = new DefaultDestinationLoader()
.registerDestination(customHttpDestination);

DestinationAccessor.appendDestinationLoader(loader);

// This will now return the custom destination
DestinationAccessor.getDestination("custom-destination").asHttp();

By default, the DestinationAccessor is using a DestinationLoaderChain that comprises multiple loaders. These are e.g. a loader to get destinations from the destination service and a loader that reads destinations from environment variables.

The above appendDestinationLoader() will add the provided loader at the end of such a chain. That means the new loader can operate as a fallback. Use prependDestinationLoader() to add it at the beginning if you would like it to take precedence.

Lastly use setLoader() to replace all existing loaders. This is useful if you want to supply a single destination to a piece of test code.

Provide Headers for Destinations

How It Works

You can add headers that are sent for any request over destinations. You can implement custom logic that defines which headers are sent under which circumstances.

Example Use-case

Let's imagine, you want to send an API token via a request header with any calls made over any destination in your whole application.

The SAP Cloud SDK offers the interface DestinationHeaderProvider which defines the method List<Header> getHeaders(DestinationRequestContext). At runtime, while preparing an outbound request, the SAP Cloud SDK will use all implementations of that interface which have been registered via the Java ServiceLoader mechanism (see below). For each registered implementation it invokes the method getHeaders(DestinationRequestContext) and appends the returned headers to the currently prepared request.

You can also add headers to a specific destination using properties so they will be always included when calling the target system. See the relevant section below.

Implement Own Header Provider

Implement the Interface DestinationHeaderProvider

To implement your custom destination header provider, you need to implement the interface DestinationHeaderProvider. The method getHeaders provides access to the request specific context. The method argument of type DestinationRequestContext lets you access the general properties over which the currently prepared request will be issued. Depending on your logic to determine headers, you can read the following data types:

  • DestinationProperties getDestination() (e.g. the general destination name).
  • URI getRequestUri() (e.g. the request specific query parameters)

Here is one simple example implementation which returns the same header for each request:

@Nonnull
@Override
public List<Header> getHeaders( @Nonnull final DestinationRequestContext requestContext )
{
final Header header = new Header("API-Token", obtainApiToken());

return Collections.singletonList(header);
}

Make Interface Implementation Accessible

The SAP Cloud SDK uses the Service Loader mechanism of Java to look up the DestinationHeaderProvider implementations at runtime. To let your custom implementation be found, you need to provide a configuration file as resource in the folder META-INF/services. Name this resource file as the interface com.sap.cloud.sdk.cloudplatform.connectivity.DestinationHeaderProvider and mention the fully-qualified class name of your custom implementation. You can refer to several implementations by putting their fully qualified class names on separate lines.

Place the new folder + file in your resources folder. In a Maven project the resource folder is typically src/main/resources/. For example, if your custom implementation is named ApiTokenHeaderProvider in the package com.example, the configuration should be as follows:

  • File path & name:
    src/main/resources/META-INF/services/com.sap.cloud.sdk.cloudplatform.connectivity.DestinationHeaderProvider
  • File content:
    com.example.ApiTokenHeaderProvider

Add Headers via Destination Properties

Here is one example implementation which adds the ("key1", "value1") header to every call to the destination:

destination =
DefaultHttpDestination
.builder(server.baseUrl())
.property("URL.headers.key1", "value1")
.build();

To include headers into every call to a target system you can define them as properties on the destination. It can be a destination object that you defined in the Cloud Foundry Cockpit, or one you built in code via a destination builder. For that, call the .property() method on the destination. It expects a key-value pair with a key formatted as URL.headers.<key> and an arbitrary value string.

Provide Query Parameters for Destinations

How It Works

You can add query parameters to a specific destination using properties. They will always be included in the query string when calling the target system.

Add Query Parameters via Destination Properties

Here is one simple example implementation which adds "key1=value1" to the query string when calling the destination:

destination =
DefaultHttpDestination
.builder(server.baseUrl())
.property("URL.queries.key1", "value1")
.build();

To include query parameters in the query string when calling to a target system you can define them as properties on the destination. For that, call the .property() method on the destination. It expects a key-value pair with a key formatted as URL.headers.<key> and an arbitrary value string.

Retrieving Destinations

Get All Destinations From the Destination Service on Cloud Foundry

To fetch all destinations from the Destination Service, you need to make a call to tryGetAllDestinations. The method queries the Destination Service API and retrieves all the destinations available at the service instance and sub-account level. In case there is a destination available on both the levels with the same name, then this method prioritizes the destination at the service instance level.

Below is the sample call to tryGetAllDestinations:

Try<Iterable<ScpCfDestination>> destinations = destinationLoader.tryGetAllDestinations(options);

In the above call destinationLoader needs to be an instance of ScpCfDestinationLoader.

Configuring Timeout Durations When Querying the Destination Service on Cloud Foundry

By default, the TimeLimiter resilience pattern aborts requests to the Destination Service on Cloud Foundry after 6 seconds. If your app requires more time when fetching destinations, you may create your own ScpCfDestinationLoader instance by:

// Allow requests to take up to 10 seconds instead of just 6
DestinationLoader destinationLoader = ScpCfDestinationLoader.builder()
.withTimeLimiterConfiguration(ResilienceConfiguration.TimeLimiterConfiguration.of(Duration.ofSeconds(10)))
.build();

To disable the timeout entirely, use TimeLimiterConfiguration.disabled().

Hint

You may register your destinationLoader within the DestinationAccessor by:

DestinationAccessor.setLoader(destinationLoader);

That way, all future requests to the Destination Service will use the increased timeout duration.

Configuring Caching When Querying the Destination Service on Cloud Foundry

The SAP Cloud SDK, by default, caches up to 1,000 destinations retrieved from the Destination Service on Cloud Foundry for a maximum of 5 minutes.

This behavior has two implications:

  1. Application performance can be (drastically) improved by serving already known destinations from the cache instead of querying the Destination Service over and over again.
  2. Changes to destinations, for example, via the SAP BTP Cockpit, may take a while until they are reflected in the application.

Depending on the use-case, the default cache configuration might be suboptimal. Therefore, the SAP Cloud SDK offers an API for adjusting the caching behavior at application runtime.

The examaple below changes the cache to store more entries for a longer period of time.

ScpCfDestinationLoader.Cache.setSizeLimit(10_000L);
ScpCfDestinationLoader.Cache.setExpiration(Duration.ofHours(2L), CacheExpirationStrategy.WHEN_CREATED);

In rare circumstances, it might make sense to disable either specific aspects of the caching behavior, or even the entire cache. For those scenarios, following APIs are available:

ScpCfDestinationLoader.Cache.disableSizeLimit(); // cache can store an infinite amount of entries
ScpCfDestinationLoader.Cache.disableExpiration(); // cache entries never expire

ScpCfDestinationLoader.Cache.disable(); // don't cache destinations
Shared Cache Size

The size limit is applied across all tenants and principals. That means that one tenant (with a lot of principals) might fill up the entire cache.

Example

If a thousand principals of the same tenant all query a single destination, the cache will be entirely filled up with destinations belonging to the same tenant. As a consequence, existing cache entries - also those of different tenants - must be removed (evicted) from the cache to make space for the new entries.

Hint

Changing the cache configuration at runtime will lead to a recreation of the underlying cache. Hereby, all existing cache entries will be lost.

Therefore, we recommend adjusting the cache once at application startup.

Building Destination Options

You need to build a DestinationOptions object and pass it as a parameter. It defines how Destinations Service is queried. In a simple application without provider/subscriber setup, your initial configuration is as simple as:

DestinationOptions options = DestinationOptions.builder().build();
Retrieval Strategy Options

For a provider/subscriber setup, a retrieval strategy must be chosen according to your particular use case from:

  • CURRENT_TENANT (default, same behavior as the deprecated ALWAYS_SUBSCRIBER)
  • ALWAYS_PROVIDER
  • ONLY_SUBSCRIBER
  • CURRENT_TENANT_THEN_PROVIDER

Here is an example for CURRENT_TENANT_THEN_PROVIDER option:

DestinationOptions options =
DestinationOptions
.builder()
.augmentBuilder(
ScpCfDestinationOptionsAugmenter
.augmenter()
.retrievalStrategy(ScpCfDestinationRetrievalStrategy.CURRENT_TENANT_THEN_PROVIDER))
.build();
Token Exchange Options

Besides the retrieval strategy, the SAP Cloud SDK also offers a token exchange strategy that lets you change the behavior when authentication requires a user token exchange. There are three different options available:

  • LOOKUP_THEN_EXCHANGE (default)
  • LOOKUP_ONLY
  • EXCHANGE_ONLY

By default, the SAP Cloud SDK first looks up the destination on the destination service and evaluates the authentication type. If the authentication type requires a user token exchange the SAP Cloud SDK immediately performs that exchange.

If you know that the destination you are about to retrieve requires a user token exchange you can improve performance by skipping the initial "look up" request like so:

DestinationOptions options =
DestinationOptions
.builder()
.augmentBuilder(
ScpCfDestinationOptionsAugmenter
.augmenter()
.tokenExchangeStrategy(ScpCfDestinationTokenExchangeStrategy.EXCHANGE_ONLY))
.build();

Try<Destination> destination = DestinationAccessor.getLoader().tryGetDestination("MyDestination", options);

Configure HTTP Destinations for Local Development Environment

When testing your app on you local development machine instead of SAP BTP, you require some configuration steps to make the destination retrieval work.

Relevant only for HTTP destinations

Check our BAPI and RFC documentation if you want to configure an RFC destination for local development.

To better understand the configuration steps, we will shortly outline how destination retrieval works when your app runs on SAP BTP.

Background: Destination Retrieval on SAP Business Technology Platform

By default, the SAP Cloud SDK uses a set of destination loaders in the following order when searching for a destination via the DestinationAccessor API

  1. Environment variable
  2. SAP BTP Destination service

Example API call looks like this:


DestinationAccessor.tryGetDestination("MyDestination");

Details and example values of a destination retrieval
  1. Lookup in the environment variable

    The first destination loader attempts to find the requested destination in the environment variable destinations. This works the same in the local development environment. For example:

[
{
"type": "HTTP",
"name": "MyDestination",
"proxyType": "Internet",
"description": "This destination rocks!",
"authentication": "BasicAuthentication",
"url": "https://URL",
"user": "USER",
"password": "PASSWORD"
}
]

You can add as many destinations as you need this way.

Authorization Token Forwarding

By adding "forwardAuthToken": "true" to your destination, you will forward the value of the Authorization header from the current request directly to the target system.

  1. Lookup via SAP BTP Destination service

    If the previous lookup was not successful, the next destination loader examines the environment variable VCAP_SERVICES to see if the SAP BTP Destination service is bound. The SAP Cloud SDK tries to fetch the destination from the SAP BTP Destination service using the given destination name.

Reaching On Premise Systems via the SAP BTP Connectivity Service In case the destination is of proxy type OnPremise, the SAP Cloud SDK performs an additional query to the SAP BTP Connectivity service. Further proxy information and an authorization token are obtained.

Automatically Inject Service bindings via Cloud Application Programming Development Kit

If you already have a SAP BTP account with all the service binding and want to leverage them in you local environment - try CAP Development Kit. The SAP Cloud SDK will do the rest of the destination retrieval job for you as described above. You will need Node.js to install CAP Development Kit by running:


npm i -g @sap/cds-dk

Now, you can start any Java app with service binding injected into your local environment by using cds bind command. The command will create a VCAP_SERVICES variable containing service keys from a Cloud Foundry space or Kubernetes cluster without storing credentials on a disk. Start your Java application similarly to the code snippet below:


cds bind --exec mvn spring-boot:run

For more details look at the official CAP documentation

Configure Destinations Manually

Destinations With Proxy Type Internet

Create the destination configuration and pass it via the environment variable destinations. Check the example below:

[
{
"type": "HTTP",
"name": "MyDestination",
"proxyType": "Internet",
"description": "This destination rocks!",
"authentication": "BasicAuthentication",
"url": "https://URL",
"user": "USER",
"password": "PASSWORD"
}
]

You can add as many destinations as you need by adding them to the JSON array.

Authorization Token Forwarding

By adding "forwardAuthToken": "true" to your destination, you will forward the value of the Authorization header from the current request directly to the target system.

Destinations With Proxy Type OnPremise

For this destination types we need to access the Destination and Connectivity services on SAP BTP. To achieve that you have to bind them and inject via CAP Development Kit. Or manually provide your VCAP_SERVIES variable.

When your Client and On-Premise Service are in the Same Network

If your local development environment and an On-Premise service are in the same network, you can use a simple HTTP destination by providing it via destinations environment variable instead of relying on the SAP BTP Connectivity service.

Create and Provide VCAP_SERVICES Variable

How to create the VCAP_SERVICES representation depends on whether the Cloud Application Programming (CAP) Model is used.

Create the file default-env.json in your project's root and supply the VCAP_SERVICES value you copied beforehand so that the file looks similar to the following shortened example:

{
"VCAP_SERVICES": {
"connectivity": [
{
"label": "connectivity",
"plan": "lite",
"name": "my-connectivity",
"instance_name": "my-connectivity",
"credentials": {
...
}
}
],
"xsuaa": [
{
"label": "xsuaa",
"plan": "space",
"name": "my-xsuaa",
"instance_name": "my-xsuaa",
"credentials": {
...
}
}
],
"destination": [
{
"label": "destination",
"plan": "lite",
"name": "my-destination",
"instance_name": "my-destination",
"credentials": {
...
}
}
]
}
}


Furthermore, add the dependency com.sap.cds:cds-integration-cloud-sdk to your POM file:

<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-integration-cloud-sdk</artifactId>
</dependency>

Reach On-Premise Service from the SAP Business Application Studio

If you are developing an application in the Business Application Studio (BAS), and want to reach On-Premise systems, perform the following steps:

  1. Make sure your BAS instance is part of the same CF space as your Cloud Connector.
  2. Add properties WebIDEEnabled and HTML5.DynamicDestination to your On-Premise destination with the value true.
  3. In BAS, configure your destinations environment variable as follows:
[
{
"type": "HTTP",
"name": "<DESTINATION-NAME>",
"url": "http://<DESTINATION-NAME>.dest",
"ProxyHost": "localhost",
"ProxyPort": 8887
}
]

Please refer to the BAS connectivity guide created for the SAP Cloud SDK for JavaScript to get more information and a detailed description of the technical background. The information there also applies to the SAP Cloud SDK for Java.

Skip Destination Creation Step for Certain Authentication Types on Cloud Foundry

For certain authentication types i.e. NoAuthentication and OAuth2ClientCredentials there is an API that eliminates the need to manually create a destination in the CF account for connecting to services.

Example:

final Map<String, String> mapping =
ScpCfServiceDestinationLoader.buildClientCredentialsMapping(
"credentials.endpoints.service_rest_url",
"credentials.uaa.clientid",
"credentials.uaa.clientsecret",
"credentials.endpoints.token_url");

The ScpCfServiceDestinationLoader.buildClientCredentialsMapping method creates a mapping for loading a destination and provides JSON paths for all required destination properties.

final HttpDestination destination =
ScpCfServiceDestinationLoader.getDestinationForService(
"label",
"serviceBindingName",
AuthenticationType.OAUTH2_CLIENT_CREDENTIALS,
mapping);

For certain services (only SAP BTP Workflow service for now), we provide further convenience that skips the need to provide the mapping explicitly. Example:

final HttpDestination destination =
ScpCfServiceDestinationLoader.getDestinationForService(
ScpCfServiceDestinationLoader.CfServices.WORKFLOW,
"my-workflow");

Enable Access to SAP Business Technology Platform Connectivity Service

The SAP BTP connectivity service builds the connection between SAP BTP and the On-premise network. That is why it has strong built-in restrictions to allow it only to be called from within SCP. If you call the connectivity service from your local machine, you will encounter a connection timeout. We'll therefore apply port forwarding via SSH to simulate that your localhost plays the cloud app.

  • Deploy your app to the SAP BTP once.
  • Enable SSH access to your app container with the cf CLI:
cf enable-ssh app-name
cf restart app-name
  • Inspect the value of the entry connectivity of your VCAP_SERVICES and take note of the values of the fields
    • credentials.onpremise_proxy_port
    • credentials.onpremise_proxy_host
note

We'll refer to these values as proxy-port and proxy-host hereafter.

  • Create an SSH session to your app container with the following command and let the session opened:
cf ssh app-name -L proxy-port:proxy-host:proxy-port
  • Replace the value of the field VCAP_SERVICES.connectivity.credentials.onpremise_proxy_host in your default-env.json with localhost.
Ready

Now your local setup is ready to consume an On-premise HTTP destination.