Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@
import com.sap.cloud.security.config.ClientIdentity;
import com.sap.cloud.security.token.Token;
import com.sap.cloud.security.xsuaa.client.DefaultOAuth2TokenService;
import com.sap.cloud.security.xsuaa.client.OAuth2ServiceException;
import com.sap.cloud.security.xsuaa.client.OAuth2TokenResponse;
import com.sap.cloud.security.xsuaa.client.OAuth2TokenService;

import io.vavr.CheckedFunction0;
import io.vavr.control.Try;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -50,6 +52,7 @@
* This interface handles the communication with an OAuth2 service.
*/
@RequiredArgsConstructor( access = AccessLevel.PACKAGE )
@AllArgsConstructor( access = AccessLevel.PRIVATE )
@Slf4j
class OAuth2Service
{
Expand Down Expand Up @@ -88,6 +91,8 @@ class OAuth2Service
@Nonnull
@Getter( AccessLevel.PACKAGE )
private final ResilienceConfiguration resilienceConfiguration;
@Nullable
private ServiceIdentifier serviceIdentifier;
Comment thread
Jonas-Isr marked this conversation as resolved.
Outdated

// package-private for testing
@Nonnull
Expand Down Expand Up @@ -190,7 +195,30 @@ private OAuth2TokenResponse executeClientCredentialsFlow( @Nullable final Tenant
tenantSubdomain,
additionalParameters,
false))
.getOrElseThrow(e -> new TokenRequestFailedException("Failed to resolve access token.", e));
.getOrElseThrow(e -> buildException(e, tenant));
}

private TokenRequestFailedException buildException( @Nonnull final Throwable e, @Nullable final Tenant tenant )
{
String message = "Failed to resolve access token.";
// In case where tenant is not the provider tenant, and we get 401 error, add hint to error message.
if( e instanceof OAuth2ServiceException
&& ((OAuth2ServiceException) e).getHttpStatusCode().equals(401)
&& tenant != null ) {
final String extension;
if( serviceIdentifier != null ) {
extension =
" In case you are accessing a multi-tenant BTP service on behalf of a subscriber tenant, ensure that the service instance (here, of the "
+ serviceIdentifier
+ " service) is declared as dependency to SaaS Provisioning Service or Subscription Manager (SMS) and subscribed for the current tenant.";
} else {
extension =
" In case you are accessing a multi-tenant BTP service on behalf of a subscriber tenant, ensure that the service instance"
+ " is declared as dependency to SaaS Provisioning Service or Subscription Manager (SMS) and subscribed for the current tenant.";
}
message += extension;
}
return new TokenRequestFailedException(message, e);
}

private void setAppTidInCaseOfIAS( @Nullable final String tenantId )
Expand Down Expand Up @@ -320,6 +348,7 @@ static class Builder
private TenantPropagationStrategy tenantPropagationStrategy = TenantPropagationStrategy.ZID_HEADER;
private final Map<String, String> additionalParameters = new HashMap<>();
private ResilienceConfiguration.TimeLimiterConfiguration timeLimiter = OAuth2Options.DEFAULT_TIMEOUT;
private ServiceIdentifier serviceIdentifier;

@Nonnull
Builder withTokenUri( @Nonnull final String tokenUri )
Expand Down Expand Up @@ -365,6 +394,7 @@ Builder withTenantPropagationStrategy( @Nonnull final TenantPropagationStrategy
@Nonnull
Builder withTenantPropagationStrategyFrom( @Nullable final ServiceIdentifier serviceIdentifier )
{
this.serviceIdentifier = serviceIdentifier;
Comment thread
Jonas-Isr marked this conversation as resolved.
Outdated
final TenantPropagationStrategy tenantPropagationStrategy;
if( ServiceIdentifier.IDENTITY_AUTHENTICATION.equals(serviceIdentifier) ) {
tenantPropagationStrategy = TenantPropagationStrategy.TENANT_SUBDOMAIN;
Expand Down Expand Up @@ -425,7 +455,8 @@ OAuth2Service build()
onBehalfOf,
tenantPropagationStrategy,
additionalParameters,
resilienceConfig);
resilienceConfig,
serviceIdentifier);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.unauthorized;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
import static com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingTestUtility.bindingWithCredentials;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;

import java.io.IOException;
import java.net.URI;
Expand All @@ -33,11 +35,13 @@
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
import com.sap.cloud.environment.servicebinding.api.ServiceIdentifier;
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.HttpClientInstantiationException;
import com.sap.cloud.sdk.cloudplatform.tenant.DefaultTenant;
import com.sap.cloud.sdk.cloudplatform.tenant.TenantAccessor;
import com.sap.cloud.security.client.HttpClientFactory;
import com.sap.cloud.security.config.ClientIdentity;
import com.sap.cloud.security.xsuaa.client.OAuth2ServiceException;

import io.vavr.control.Try;

Expand Down Expand Up @@ -152,6 +156,48 @@ void testIasTokenFlow()
}
}

@Test
void testExtended401ErrorMessage()
{
final ServiceBinding binding =
bindingWithCredentials(
ServiceIdentifier.IDENTITY_AUTHENTICATION,
Comment thread
Jonas-Isr marked this conversation as resolved.
Outdated
entry("credential-type", "binding-secret"),
entry("clientid", "myClientId2"),
entry("clientsecret", "myClientSecret2"),
entry("url", "http://provider.ias.domain"),
entry("app_tid", "provider"));
final ServiceBindingDestinationOptions options = ServiceBindingDestinationOptions.forService(binding).build();

final Try<HttpDestination> maybeDestination =
new OAuth2ServiceBindingDestinationLoader().tryGetDestination(options);
assertThat(maybeDestination.isSuccess()).isTrue();
final HttpDestination destination = maybeDestination.get();

{
// provider case - no tenant:
// Here, the short error message is returned.
stubFor(post("/oauth2/token").withHost(equalTo("provider.ias.domain")).willReturn(unauthorized()));
assertThatCode(destination::getHeaders)
.isInstanceOf(DestinationAccessException.class)
.hasMessageEndingWith("Failed to resolve access token.")
.hasRootCauseInstanceOf(OAuth2ServiceException.class);
}
{
// subscriber tenant:
// Here, the error message contains a note about the SaaS registry.
stubFor(post("/oauth2/token").withHost(equalTo("subscriber.ias.domain")).willReturn(unauthorized()));

TenantAccessor.executeWithTenant(new DefaultTenant("subscriber", "subscriber"), () -> {
assertThatCode(destination::getHeaders)
.isInstanceOf(DestinationAccessException.class)
.hasMessageContaining("identity service")
.hasMessageEndingWith("subscribed for the current tenant.")
.hasRootCauseInstanceOf(OAuth2ServiceException.class);
});
}
}

@Test
@DisplayName( "The subdomain should be replaced for subscriber tenants when using IAS and ZTIS" )
void testIasFlowWithZeroTrustAndSubscriberTenant()
Expand Down
Loading