Skip to main content

Connecting To Bound Services

The SAP Cloud SDK supports connecting to arbitrary services that are bound to an application via a service binding. Service Bindings provide an application with the necessary credentials to connect to a service when deployed on the SAP Business Technology Platform.

Connecting to a Service

You can obtain a Destination to connect to a service by using the ServiceBindingDestinationLoader API:

var options = ServiceBindingDestinationOptions
.forService(ServiceIdentifier.<MY_SERVICE>)
.build();

HttpDestination destination = ServiceBindingDestinationLoader.defaultLoaderChain()
.getDestination(options);

Choose from the available ServiceIdentifier constants to specify the service you want to connect to.

For example, for connecting to the Workflow REST API use:

var options = ServiceBindingDestinationOptions
.forService(ServiceIdentifier.WORKFLOW)
.withOption(BtpServiceOptions.WorkflowOptions.REST_API)
.build();

HttpDestination destination = ServiceBindingDestinationLoader.defaultLoaderChain()
.getDestination(options);

Note the additional option BtpServiceOptions.WorkflowOptions.REST_API that is specific to the Workflow API (REST). This is an example where the service offers multiple API endpoints and you need to define which endpoint the resulting HttpDestination should point to.

Explore the BtpServiceOptions class to find the options relevant for your service and your use-case.

List of Supported Services

The SAP Cloud SDK supports a variety of services out of the box. For example, the AI Core service or the Business Rules service. You'll find a full list below.

Full List of Supported Services

The following services are supported out of the box:

Your service is not in the list?

In case the service you want to connect to is not yet recognized by the SAP Cloud SDK by default, you can still use the API by providing some additional information.

For services backed by the SAP Identity and Authentication Service (IAS), please see the section on IAS below. For other services, please refer to the section on adding support for more services.

In case you are a service provider and want to make your service compatible with the SAP Cloud SDK: Please consider contributing!

Working with Multiple Service Bindings

In case your application is bound to multiple instances of the same service, passing the ServiceIdentifier is not sufficient. Instead, you need to provide the ServiceBinding instance directly.

var bindingList = DefaultServiceBindingAccessor.getInstance()
.getServiceBindings();

// select the desired binding from the list of all available bindings
ServiceBinding binding = bindingList.get(0);

ServiceBindingDestinationOptions
.forService(binding)
.build()

Multitenancy and Principal Propagation

Destinations created via the ServiceBindingDestinationLoader API are tenant-aware by default. That means authorization to the target system will by default be performed on behalf of the current tenant at the time of request execution.

This can be customized by passing a different value for OnBehalfOf.

For example, to propagate the current user to the target system use:

ServiceBindingDestinationOptions
.forService(ServiceIdentifier.WORKFLOW)
.withOption(BtpServiceOptions.WorkflowOptions.REST_API)
.onBehalfOf(OnBehalfOf.NAMED_USER_CURRENT_TENANT)
.build();

Using the Identity and Authentication Service (IAS)

Beta API

The API for connecting to services secured by the SAP Identity and Authentication Service (IAS) is currently in beta and subject to change.

In case your application is bound to an instance of the SAP Identity and Authentication Service (IAS) you can use the SAP Cloud SDK to connect to other applications and services that are secured using IAS. Effectively, the SAP Cloud SDK implements the OAuth flows described here.

Supported Credential Types

The SAP Cloud SDK supports the credential types binding-secret, X509_GENERATED and X509_ATTESTED for IAS service bindings.

If you want to use the X509_ATTESTED credential type, you need to add the connectivity-ztis dependency to your project. Read more about how to configure your app for this credential type on the documentation for using certificates from the Zero Trust Identity Service (ZTIS).

The type X509_PROVIDED is currently not supported.

Connecting to Services

If your service is secured using IAS and is using the dedicated service binding format supported by the SAP Cloud SDK, you can obtain a destination by passing the service label as the ServiceIdentifier:

var options = ServiceBindingDestinationOptions
.forService(ServiceIdentifier.of("your-service-label"))
.build();

In case your service is not using the default format you can still use the IasOptions to provide the necessary information:

var options = ServiceBindingDestinationOptions
.forService(ServiceIdentifier.IDENTITY_AUTHENTICATION)
.withOption(BtpServiceOptions.IasOptions.withTargetUri("https://foo.com"))
.build();

Obtain the URL from the service binding of the service you want to connect to.

Mutual TLS Only Authentication

In case the service does not require a JWT token (e.g. the Event Broker service) you can pass an additional option to skip the JWT token flow:

var options = ServiceBindingDestinationOptions
.forService(ServiceIdentifier.IDENTITY_AUTHENTICATION)
.withOption(BtpServiceOptions.IasOptions.withTargetUri("https://foo.com"))
.withOption(BtpServiceOptions.IasOptions.withoutTokenForTechnicalProviderUser())
.onBehalfOf(OnBehalfOf.TECHNICAL_USER_PROVIDER)
.build();

This is a moderate performance improvement, but not substantial, since tokens are of course cached.

Please note that this only applies when authenticating on behalf of the provider tenant using an X.509 Certificate. For authenticating on behalf of a subscriber tenant or a when propagating user information, a JWT is always required to carry the additional tenant/user information.

Connecting to Applications

In case you want to connect to a system that is registered as an application within IAS use the IasOptions.withApplicationName to provide the application name:

var options = ServiceBindingDestinationOptions
.forService(ServiceIdentifier.IDENTITY_AUTHENTICATION)
.withOption(BtpServiceOptions.IasOptions.withApplicationName("application-name"))
.withOption(BtpServiceOptions.IasOptions.withTargetUri("https://foo.com"))
.build();

Calling Back Applications

If you received an incoming request from an application using IAS you can use the following options to create a destination for calling back the application:

var options = ServiceBindingDestinationOptions
.forService(ServiceIdentifier.IDENTITY_AUTHENTICATION)
.withOption(BtpServiceOptions.IasOptions.withConsumerClient("client-id", "tenant-id"))
.withOption(BtpServiceOptions.IasOptions.withTargetUri("https://foo.com"))
.build();

Use the client and tenant IDs you received in the JWT of the incoming request.

Service Binding Format

The SAP Cloud SDK expects services backed by IAS to have the following service binding format:

  • The credentials section must contain an object authentication-service with at least one field service-label set to identity.
  • The credentials section must contain an object endpoints which must contain at least one entry.
    • Each entry under endpoints must be an object containing a uri field set to a string.
  • Further fields under authentication-service and endpoints are optional as shown below
  • Aside from these requirements the service binding may contain any other fields as needed.

For example, the following binding would be recognized:

{
"label": "eventmesh-sap2sap-internal",
"credentials": {
"authentication-service": {
"service-label": "identity"
},
"endpoints": {
"eventing-endpoint": {
"uri": "https://http-gateway.eu11.beb.em.services.cloud.sap",
"always-requires-token": false
}
}
}
}
Full format with optional fields
{
// the usual binding metadata fields here
// ...
"credentials": {
"authentication-service": {
"service-label": "identity",
"app-name": "app" // optional
},
"endpoints": {
"my-endpoint": {
"uri": "https://cert.service.com",
// the following fields are optional and shown here with their assumed default values
"protocol": "http",
"always-requires-token": true,
"requires-mtls": true
},
// multiple endpoints can be defined in addition
"my-other-endpoint": {
"uri": "https://service.com",
"requires-mtls": false
},
"my-ftp-endpoint": {
"uri": "ftp://service.com",
"protocol": "ftp"
}
}
}
}
Multiple Endpoints

Currently, the SAP Cloud SDK only supports service bindings with a single entry under endpoints.

Adding Support for More Services

In case you want to connect to a service that is not supported out of the box, you can still use the ServiceBindingDestinationLoader API by providing some additional information.

In most cases it's sufficient to provide an implementation of OAuth2PropertySupplier that can extract the required information from the specific service binding. Services backed by XSUAA typically have rather similar service binding formats that differ only slightly. For services backed by IAS please refer to the sections above instead.

In most cases overriding the DefaultOAuth2PropertySupplier and implementing the getServiceUri() method is already sufficient.

For example, assume the following binding structure:

{
"label": "my-service",
"credentials": {
"some": {
"json": {
"path": "https://my-service.com/"
}
},
"clientid": "my-client-id"
// ...
}
}

This could be handled by the following implementation:

class MyServicePropertySupplier extends DefaultOAuth2PropertySupplier
{
MyServicePropertySupplier( @Nonnull final ServiceBindingDestinationOptions options )
{
super(options, Collections.emptyList());
}

@Nonnull
@Override
public URI getServiceUri()
{
return getCredentialOrThrow(URI.class, "some", "json", "path");
}
}

OAuth2ServiceBindingDestinationLoader.registerPropertySupplier(
ServiceIdentifier.of("my-service"), MyServicePropertySupplier::new);

Note that:

  • Line 5 indicates that the OAuth credentials are stored directly in the credentials object.
  • Line 12 extracts the service URI from the credentials.some.json.path field.
  • Line 17 registers the implementation for any binding with the label my-service.

For more details refer to the more extensive guidance given in the advanced section below. Also consider the existing implementations and tests.

Local Development

When developing locally you have various options to provide the necessary service binding information:

  • When using CAP you can use the Hybrid Testing feature.
  • You can manually copy the VCAP_SERVICES environment variable and set it locally (e.g. in your terminal or IDE or using an IDE plugin that loads a .env file)
  • You can define a binding programmatically in your application (e.g. in a test setup or a local development profile)

Programmatically Defining a Service Binding

You can create a ServiceBinding instance programmatically and use it to create a destination:

var binding = new DefaultServiceBindingBuilder()
.withServiceIdentifier(ZTIS_IDENTIFIER)
.withCredentials(Map.of(
"clientid", "id",
"url", "https://foo.com"))
.build();

You can pass the binding explicitly via the options as described in the section on working with multiple bindings.

However, in some cases the SAP Cloud SDK or other frameworks may implicitly access service bindings. For such cases one can modify the global ServiceBindingAccessor.

The SAP Cloud SDK will use the default instance whenever looking for service bindings. You can override this instance:

DefaultServiceBindingAccessor.setInstance(() -> List.of(binding));

In case you want to keep all existing bindings and just add your own you can use the following code:

final ServiceBindingAccessor accessor = DefaultServiceBindingAccessor.getInstance();
var newAccessor = new ServiceBindingMerger(List.of(accessor, () -> List.of(binding)), ServiceBindingMerger.KEEP_EVERYTHING);
DefaultServiceBindingAccessor.setInstance(newAccessor);

(Advanced) Background Information

In general, the ServiceBindingDestinationLoader API can be used to convert ServiceBinding instances into HttpDestination instances. It comes as part of the cloudplatform-connectivity artifact.

The API is designed for easy extensibility and customization. Hereby, each ServiceBindingDestinationLoader implementation is supposed to provide the transformation logic for one specific "type" of ServiceBinding. The SAP Cloud SDK provides, for example, an implementation for service bindings that contain OAuth2 credentials.

As an input, the API requires the ServiceBindingDestinationOptions instance. This class is designed to hold all information that is required to transform a ServiceBinding into an HttpDestination - this, first and foremost, includes the ServiceBinding itself.

Furthermore, there is also a chain implementation that can be used to combine multiple ServiceBindingDestinationLoader implementations into one. The chain will try to load a destination using the given ServiceBindingDestinationLoader delegates until the first of them succeeds - much like a fallback mechanism. This concept is also reflected in the example from above, where the defaultLoaderChain is used to load a destination.

About the Options

The ServiceBindingDestinationOptions is an extensible and type-safe collection of parameters that can be used to configure the ServiceBinding to HttpDestination transformation process. Hereby, the least information that is needed is the ServiceBinding itself, which can be provided using the forService(ServiceBinding) method.

Alternatively, you can also use forService(ServiceIdentifier). This method will try to find a ServiceBinding for the given ServiceIdentifier using the DefaultServiceBindingAccessor instance.
Please note: If there are multiple or no ServiceBindings for the given ServiceIdentifier, an exception will be thrown.

Besides the ServiceBinding, there is a second parameter that is shared between all ServiceBindingDestinationLoader implementations: The OnBehalfOf, for which the destination should be loaded. This can be set using the onBehalfOf(OnBehalfOf) method.
If not explicitly specified, OnBehalfOf.TECHNICAL_USER_CURRENT_TENANT is used.

Custom Parameters

Adding custom parameters to the ServiceBindingDestinationOptions can be done using the withOption(OptionsEnhancer) method. Hereby, an OptionsEnhancer is a type-safe representation of arbitrary parameters.

Example: Custom Service Binding Transformation
Advanced Usage

The following example demonstrates rather advanced usage of our ServiceBindingDestinationLoader API.

A vast majority of use cases do not require a custom ServiceBindingDestinationLoader implementation.
Instead, customizing the existing OAuth2ServiceBindingDestinationLoader as described below is most probably sufficient.

Lets assume we are providing a custom ServiceBindingDestinationLoader implementation that requires some additional information to be able to transform a ServiceBinding into an HttpDestination.

In this example, we assume that there are two different APIs for our service. Depending on which API we want to create our HttpDestination for, we need to slightly alter the transformation logic. Additionally, we want to be able to supply an additional String parameter that also affects the created HttpDestination.

To achieve this behavior, we would need to implement two OptionsEnhancers like so:

MyApiChoice.java
enum MyApiChoice implements ServiceBindingDestinationOptions.OptionsEnhancer<MyApiChoice> {
API1,
API2;

@Nonnull
@Override
public MyApiChoice getValue() {
return this;
}
}
MyStringParameter.java
class MyStringParameter implements ServiceBindingDestinationOptions.OptionsEnhancer<String> {
private final String value;

public static MyStringParameter of(String value) {
return new MyStringParameter(value);
}

private MyStringParameter(String value) {
this.value = value;
}

@Nonnull
@Override
public String getValue() {
return value;
}
}

The first implementation uses an enum to implement a fixed set of choices option. It is noteworthy that the actual parameter type is the MyApiChoice enum itself. That way, we will be able to retrieve the set value later on.

In contrast to that, the second implementation provides a String parameter.

Setting the options can then be done like so:

ServiceBinding someServiceBinding;

ServiceBindingDestinationOptions
.forService(someServiceBinding)
.withOption(MyApiChoice.API1)
.withOption(MyStringParameter.of("some-value"))
.build();

Finally, we can implement our custom ServiceBindingDestinationLoader that uses our new options like so:

MyCustomServiceBindingLoader.java
class MyCustomServiceBindingLoader implements ServiceBindingDestinationLoader
{
@Nonnull
@Override
public Try<HttpDestination> tryGetDestination( @Nonnull ServiceBindingDestinationOptions options )
{
Option<MyApiChoice> maybeOption = options.getOption(MyApiChoice.class);
if( maybeOption.isEmpty() ) {
return Try.failure(new DestinationNotFoundException("No API choice was specified."));
}

Option<String> maybeStringValue = options.getOption(MyStringParameter.class);

MyApiChoice option = maybeOption.get();
switch( option ) {
case API1:
// do something
break;
case API2:
// do something else
break;
}
}
}

About the OAuth2Loader

The OAuth2ServiceBindingDestinationLoader comes as part of the connectivity-oauth artifact and provides a customizable implementation for converting ServiceBindings that use OAuth2 authentication into HttpDestination instances.
More precisely, it supports the OAuth2 Client Credentials and OAuth2 JWT Bearer grant types.

Internally, the implementation performs the following steps:

  1. Check whether the given ServiceBindingDestinationOptions can be transformed (e.g. whether the ServiceBinding has a clientid).
  2. Extract the required information from the ServiceBinding (i.e. the service URL, the token URL, and the client identity).
  3. Assemble an HttpDestination that uses the extracted information for the target authentication (i.e. HTTP header Authorization).

Dealing With Arbitrary Service Bindings

Even though service bindings are a fundamental concept in the SAP Business Technology Platform, the way they are used and structured can vary greatly from service to service. As a consequence, it is not possible to provide a uniform way of dealing with arbitrary ServiceBindings.
Therefore, the OAuth2ServiceBindingDestinationLoader leverages the OAuth2PropertySupplier interface to extract the required information from a given ServiceBindingDestinationLoaderOptions instance.

The OAuth2ServiceBindingDestinationLoader stores a static list of OAuth2PropertySupplier instances. Whenever the OAuth2ServiceBindingDestinationLoader is asked to load a destination, it will iterate over this list (in order) to see whether

  1. the given ServiceBindingDestinationOptions can be transformed into an HttpDestination using the current OAuth2PropertySupplier instance, and, if so
  2. extract the required information from the given ServiceBindingDestinationOptions using the current OAuth2PropertySupplier.

By default, there are OAuth2PropertySupplier for the services mentioned above in place.

Customization

In case the default configuration of the OAuth2ServiceBindingDestinationLoader does not fit your needs, you can customize it by adding your own OAuth2PropertySupplier instances to the static list of the OAuth2ServiceBindingDestinationLoader:

OAuth2ServiceBindingDestinationLoader.registerPropertySupplier(this::canBeConverted, this::propertySupplierFactory);

boolean canBeConverted(ServiceBindingDestinationOptions options);
OAuth2PropertySupplier propertySupplierFactory(ServiceBindingDestinationOptions options);

As shown above, the registerPropertySupplier method takes two methods as parameters. This might be unintuitive, but is needed to allow for efficient implementations.

What is happening internally when this method is used, is that the given parameters are transformed into an internal representation, which is then prepended to the list of available OAuth2PropertySupplier instances. In other words: Using the registerPropertySupplier method will make your OAuth2PropertySupplier instance be considered before any previously registered OAuth2PropertySupplier instance, including the default ones.

Full Example

To make things a bit more concrete, let's assume we wanted to implement support for an other service, which uses its own ServiceBinding structure.

Our hypothetical service binding could look like this:

{
"service": "my-service",
"credentials": {
"service": {
"read": "https://my-service.com/api/v1/read",
"write": "https://my-service.com/api/v2/write"
},
"oauth": {
"uri": "https://my-service.com/oauth/token",
"id": "my-client-id",
"secret": "my-client-secret"
}
}
}

The service binding above contains two different API endpoints (read and write), which we would like to be able to access using the OAuth2ServiceBindingDestinationLoader. To support that, we need to implement an OAuth2PropertySupplier (to implement the property extraction logic) and an OptionsEnhancer (to allow users to define which endpoint they would like to use):

MyApiEndpoint.java
enum MyApiEndpoint implements ServiceBindingDestinationOptions.OptionsEnhancer<MyApiEndpoint> {
READ,
WRITE;

@Nonnull
@Override
public MyApiEndpoint getValue() {
return this;
}
}
MyServiceOAuth2PropertySupplier.java
class MyServiceOAuth2PropertySupplier extends DefaultOAuth2PropertySupplier
{
public MyServiceOAuth2PropertySupplier(ServiceBindingDestinationOptions options) {
super(options, Collections.singletonList("oauth"));
}

@Override
public boolean isOAuth2Binding() {
return getOAuthCredential(String.class, "id").isDefined();
}

@Nonnull
@Override
public URI getServiceUri() {
MyApiEndpoint endpoint = options.getOption(MyApiEndpoint.class).getOrElse(MyApiEndpoint.READ);
return switch (endpoint) {
case READ -> getCredentialOrThrow(URI.class, "service", "read");
case WRITE -> getCredentialOrThrow(URI.class, "service", "write");
};
}

@Nonnull
@Override
public URI getTokenUri() {
return getOAuthCredentialOrThrow(URI.class, "uri");
}

@Nonnull
@Override
public ClientIdentity getClientIdentity() {
var clientId = getOAuthCredentialOrThrow(String.class, "id");
var clientSecret = getOAuthCredentialOrThrow(String.class, "secret");
return new ClientCredentials(clientId, clientSecret);
}
}

Lets examine the implementation above:

Right in the first line, you can see that our implementation extends the DefaultOAuth2PropertySupplier class, instead of implementing the OAuth2PropertySupplier interface directly.
This is because the DefaultOAuth2PropertySupplier class provides a lot of useful functionality, which we can reuse in our implementation.
We will see that in a second.

Next thing we should pay attention to is the constructor of our class.
It takes an instance of ServiceBindingDestinationOptions as a parameter. This instance is then passed to the super constructor (line 4) along with a list of Strings.

The super class (DefaultOAuth2PropertySupplier) uses these parameters to

  1. cache the credentials portion of the ServiceBinding (given in the ServiceBindingDestinationOptions) as a TypedMapView instance and
  2. set the default oauth2 properties path within the credentials portion of the ServiceBinding.

Converting the credentials portion of the ServiceBinding into a TypedMapView is helpful for parsing the nested structure.
Therefore, it is a good idea to do the conversion (i.e. TypeMapView.ofCredentials(ServiceBinding)) just once as it performs an expensive deep-copy. Hereby, the DefaultOAuth2PropertySupplier takes care of doing the conversion upon initialization and caching the result for later use.

Further down in our code (i.e. within the overridden methods), we can then use the getCredential/OrThrow and getOAuthCredential/OrThrow methods to extract the required information from the cached credentials.
The main difference between getCredential and getOAuthCredential is that the latter will prepend the default oauth2 properties path to the one given in the method invocation before trying to extract the information.
In our example (line 9), this means that getOAuthCredential(String.class, "id") will try to extract a String value from credentials.get("oauth").get("id") (pseudo code).
In contrast to that, using just getCredential(String.class, "id") would try to extract a String value from credentials.get("id") (pseudo code).
getOAuthCredential is therefore just a neat shortcut that can help to reduce repetitive code.

Please note how we are handling our MyApiEndpoint option in the getServiceUri method (lines 14 - 20).
We are reading the option by providing the MyApiEndpoint.class as the parameter to the getOption method. That way, we can decide which service URI to use, depending on the user input.

Lastly, we need to register our OAuth2PropertySupplier implementation with the OAuth2ServiceBindingDestinationLoader:

MyServiceOAuth2PropertySupplier.java
class MyServiceOAuth2PropertySupplier extends DefaultOAuth2PropertySupplier {
private static final ServiceIdentifier MY_SERVICE_IDENTIFIER = ServiceIdentifier.of("my-service");

public static boolean matches(ServiceBindingDestinationOptions options) {
var serviceIdentifier = options.getServiceBinding().getServiceIdentifier().orElse(null);
return MY_SERVICE_IDENTIFIER.equals(serviceIdentifier);
}

// skipped for brevity
}

OAuth2ServiceBindingDestinationLoader.registerPropertySupplier(MyServiceOAuth2PropertySupplier::matches, MyServiceOAuth2PropertySupplier::new);

In the above example, we added a static matches method (lines 4 - 7) to our OAuth2PropertySupplier implementation, which we can use to check whether the given ServiceBindingDestinationOptions can be transformed into an HttpDestination using our implementation. We are then using method references to register our implementation with the OAuth2ServiceBindingDestinationLoader.

About the Chain Implementation

As mentioned above, the ServiceBindingDestinationLoader comes with a private chain implementation.

The default instance of that chain (ServiceBindingDestinationLoader.defaultLoaderChain) is created using the Service Locator Pattern to find all available ServiceBindingDestinationLoader implementations on the classpath. Instances of these implementations are then used as delegate loaders. When transforming a ServiceBindingDestinationOptions into an HttpDestination, the chain will invoke the delegates in order until the first one either succeeds or throws an exception other than DestinationNotFoundException.

info

The Service Locator Pattern does not provide any guarantees about the order in which implementations on the classpath are found. Therefore, it is crucial that ServiceBindingDestinationLoader implementations do not handle the same ServiceBindingDestinationOptions as otherwise the default chain's behavior would be unpredictable.

Implementations provided by the SAP Cloud SDK follow this rule.

Custom Chain Instances

If the default chain implementation does not meet your needs, you can create your own chain instance:

ServiceBindingDestinationLoader myChain = ServiceBindingDestinationLoader.newLoaderChain(
Arrays.asList(
new MyFirstLoader(),
new MySecondLoader()
));

The code above initializes a new instance of the private chain implementation, which uses the given ServiceBindingDestinationLoader instances in the specified order as its delegates. Therefore, deterministic behavior is guaranteed.