diff --git a/.github/workflows/maven-build-2.x.yml b/.github/workflows/maven-build-2.x.yml new file mode 100644 index 0000000000..06a277a141 --- /dev/null +++ b/.github/workflows/maven-build-2.x.yml @@ -0,0 +1,62 @@ +# SPDX-FileCopyrightText: 2018-2023 SAP SE or an SAP affiliate company and Cloud Security Client Java contributors +# SPDX-License-Identifier: Apache-2.0 +--- +<<<<<<<< HEAD:.github/workflows/maven-build.yml +name: Maven Build main + +env: + NVD_API_KEY: ${{ secrets.NVD_API_KEY }} +======== +name: Maven Build main-2.x +>>>>>>>> fix-flaky:.github/workflows/maven-build-2.x.yml + +on: + push: + branches: [ main-2.x ] + pull_request: + branches: [ main-2.x ] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + java-version: [ 17 ] + name: Build with Java ${{ matrix.java-version }} + + steps: + - uses: actions/checkout@v2 + - name: mvn cache + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-owasp-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-owasp- + - name: Set up JDK ${{ matrix.java-version }} + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: ${{ matrix.java-version }} + - name: Build with Maven + run: mvn -B install --file pom.xml + - name: Run java-security integration tests + run: cd java-security-it; mvn -B package --file pom.xml + - name: Run spring-xsuaa integration tests + run: cd spring-xsuaa-it; mvn -B package --file pom.xml + - name: Build spring-security-basic-auth + run: cd samples/spring-security-basic-auth; mvn -B package --file pom.xml + - name: Build spring-security-xsuaa-usage + run: cd samples/spring-security-xsuaa-usage; mvn -B package --file pom.xml + - name: Build spring-webflux-security-xsuaa-usage + run: cd samples/spring-webflux-security-xsuaa-usage; mvn -B package --file pom.xml + - name: Build java-security-usage + run: cd samples/java-security-usage; mvn -B package --file pom.xml + - name: Build sap-java-buildpack-api-usage + run: cd samples/sap-java-buildpack-api-usage; mvn -B package --file pom.xml + - name: Build java-tokenclient-usage + run: cd samples/java-tokenclient-usage; mvn -B package --file pom.xml + - name: Build java-security-usage-ias + run: cd samples/java-security-usage-ias; mvn -B package --file pom.xml + - name: Build spring-security-hybrid-usage + run: cd samples/spring-security-hybrid-usage; mvn -B package --file pom.xml diff --git a/.github/workflows/maven-build.yml b/.github/workflows/maven-build.yml index 6233798157..06a277a141 100644 --- a/.github/workflows/maven-build.yml +++ b/.github/workflows/maven-build.yml @@ -1,16 +1,20 @@ # SPDX-FileCopyrightText: 2018-2023 SAP SE or an SAP affiliate company and Cloud Security Client Java contributors # SPDX-License-Identifier: Apache-2.0 --- +<<<<<<<< HEAD:.github/workflows/maven-build.yml name: Maven Build main env: NVD_API_KEY: ${{ secrets.NVD_API_KEY }} +======== +name: Maven Build main-2.x +>>>>>>>> fix-flaky:.github/workflows/maven-build-2.x.yml on: push: - branches: [ main ] + branches: [ main-2.x ] pull_request: - branches: [ main ] + branches: [ main-2.x ] jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f43e739ab..6b54104ef7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,32 @@ # Change Log All notable changes to this project will be documented in this file. +## 3.3.4 +- [env] service plan property is no longer uppercased when building `OAuth2ServiceConfiguration` from service bindings of the environment +- [spring-security] fixes a bug in which a second XSUAA configuration of plan "broker" was ignored in spring-security auto-configuration for versions 3.3.2 and 3.3.3 + +#### Dependency upgrades +- Bump io.projectreactor:reactor-core from 3.6.1 to 3.6.2 +- Bump spring.core.version from 6.1.2 to 6.1.3 +- Bump slf4j.api.version from 2.0.10 to 2.0.11 + +## 3.3.3 +- [java-security] + - reduce `HybridTokenFactory` logging noise - in case of missing service configuration warn message will be logged just once + - upgrade jetty ee9 to jetty ee10 +- [java-security-test] + - fixes version mismatch issue when jetty BoM is used + - `JwtGenerator` ensures that claims are always in the same order +- [token-client] + - remove httpclient caching from DefaultHttpClientFactory (#1416) + +#### Dependency upgrades +- Bump spring.boot.version from 3.2.0 to 3.2.1 +- Bump spring.core.version from 6.0.14 to 6.1.2 +- Bump log4j2.version from 2.22.0 to 2.22.1 +- Bump slf4j.api.version from 2.0.9 to 2.0.10 + + ## 3.3.2 - [java-security] - add `name` property of service binding as property to OAuth2ServiceConfiguration diff --git a/README.md b/README.md index fc1bb47713..20cf123a4e 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ The SAP Cloud Security Services Integration is published to maven central: https com.sap.cloud.security java-bom - 3.3.2 + 3.3.4 import pom diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000000..0138baeaa4 --- /dev/null +++ b/api/README.md @@ -0,0 +1,10 @@ +## Configuration + +### Maven Dependencies +```xml + + com.sap.cloud.security.xsuaa + api + 2.13.7 + +``` diff --git a/api/pom.xml b/api/pom.xml new file mode 100644 index 0000000000..9b394eff5c --- /dev/null +++ b/api/pom.xml @@ -0,0 +1,43 @@ + + + + + 4.0.0 + + com.sap.cloud.security.xsuaa + api + + + com.sap.cloud.security.xsuaa + parent + 2.13.7 + + + jar + + api + http://maven.apache.org + + + UTF-8 + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.5.0 + + + attach-javadocs + + jar + + + + + + + diff --git a/bom/pom.xml b/bom/pom.xml index c5a6b0c248..5ecfff4945 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -8,7 +8,7 @@ com.sap.cloud.security java-bom - 3.3.2 + 3.3.4 pom java-bom diff --git a/env/pom.xml b/env/pom.xml index 720b891dca..5a39cb1c76 100644 --- a/env/pom.xml +++ b/env/pom.xml @@ -9,7 +9,7 @@ com.sap.cloud.security.xsuaa parent - 3.3.2 + 3.3.4 com.sap.cloud.security diff --git a/env/src/main/java/com/sap/cloud/security/config/ServiceBindingEnvironment.java b/env/src/main/java/com/sap/cloud/security/config/ServiceBindingEnvironment.java index 9699e62439..1a04a7d835 100644 --- a/env/src/main/java/com/sap/cloud/security/config/ServiceBindingEnvironment.java +++ b/env/src/main/java/com/sap/cloud/security/config/ServiceBindingEnvironment.java @@ -78,7 +78,8 @@ public ServiceBindingEnvironment withEnvironmentVariableReader(UnaryOperator orderedServicePlans = List.of(ServiceConstants.Plan.APPLICATION, ServiceConstants.Plan.BROKER, + List orderedServicePlans = List.of(ServiceConstants.Plan.APPLICATION, + ServiceConstants.Plan.BROKER, ServiceConstants.Plan.SPACE, ServiceConstants.Plan.DEFAULT); List xsuaaConfigurations = getServiceConfigurationsAsList().get(XSUAA); @@ -135,11 +136,14 @@ public Map> getServiceConfigurationsAs * Gives access to all service configurations parsed from the environment. The * service configurations are parsed on the first access, then cached. * - * Note that the result contains only one service configuration per service plan and does not contain configurations - * with a service plan other than those from {@link ServiceConstants}#Plan. - * Use {@link ServiceBindingEnvironment#getServiceConfigurationsAsList()} to get a complete list of configurations. + * Note that the result contains only one service configuration per service plan + * and does not contain configurations with a service plan other than those from + * {@link ServiceConstants}#Plan. Use + * {@link ServiceBindingEnvironment#getServiceConfigurationsAsList()} to get a + * complete list of configurations. * - * @return the service configurations grouped first by service, then by service plan. + * @return the service configurations grouped first by service, then by service + * plan. */ @Override public Map> getServiceConfigurations() { @@ -158,8 +162,7 @@ public Map> getS .collect(Collectors.toMap( config -> ServiceConstants.Plan.from(config.getProperty(SERVICE_PLAN)), Function.identity(), - (a, b) -> a - )); + (a, b) -> a)); result.put(service, planConfigurations); } @@ -194,7 +197,7 @@ private void clearServiceConfigurations() { private ServiceConstants.Plan getServicePlan(OAuth2ServiceConfiguration config) { try { return ServiceConstants.Plan.from(config.getProperty(SERVICE_PLAN)); - } catch(IllegalArgumentException e) { + } catch (IllegalArgumentException e) { return null; } } diff --git a/env/src/main/java/com/sap/cloud/security/config/ServiceBindingMapper.java b/env/src/main/java/com/sap/cloud/security/config/ServiceBindingMapper.java index ef75d58801..012f0e8601 100644 --- a/env/src/main/java/com/sap/cloud/security/config/ServiceBindingMapper.java +++ b/env/src/main/java/com/sap/cloud/security/config/ServiceBindingMapper.java @@ -42,7 +42,7 @@ public static OAuth2ServiceConfigurationBuilder mapToOAuth2ServiceConfigurationB OAuth2ServiceConfigurationBuilder builder = OAuth2ServiceConfigurationBuilder.forService(service) .withProperties(credentials.getEntries(String.class)) .withProperty(NAME, b.getName().orElse("")) - .withProperty(SERVICE_PLAN, b.getServicePlan().orElse(ServiceConstants.Plan.APPLICATION.name()).toUpperCase()); + .withProperty(SERVICE_PLAN, b.getServicePlan().orElse(ServiceConstants.Plan.APPLICATION.toString())); if (IAS.equals(service)) { parseDomains(builder, credentials); diff --git a/env/src/test/java/com/sap/cloud/security/config/ServiceBindingEnvironmentTest.java b/env/src/test/java/com/sap/cloud/security/config/ServiceBindingEnvironmentTest.java index 9ceac39b55..a2f6907368 100644 --- a/env/src/test/java/com/sap/cloud/security/config/ServiceBindingEnvironmentTest.java +++ b/env/src/test/java/com/sap/cloud/security/config/ServiceBindingEnvironmentTest.java @@ -26,7 +26,8 @@ class ServiceBindingEnvironmentTest { static void setUp() throws IOException { String singleXsuaaConfiguration = IOUtils.resourceToString("/vcapXsuaaServiceSingleBinding.json", UTF_8); String multipleXsuaaConfigurations = IOUtils.resourceToString("/vcapXsuaaServiceMultipleBindings.json", UTF_8); - String multipleXsuaaApplicationPlanConfigurations = IOUtils.resourceToString("/vcapXsuaaServiceMultipleApplicationPlanBindings.json", UTF_8); + String multipleXsuaaApplicationPlanConfigurations = IOUtils + .resourceToString("/vcapXsuaaServiceMultipleApplicationPlanBindings.json", UTF_8); String singleIasConfiguration = IOUtils.resourceToString("/vcapIasServiceSingleBinding.json", UTF_8); String unknownXsuaaPlanConfig = IOUtils.resourceToString("/vcapUnknownServicePlan.json", UTF_8); vcapXsa = IOUtils.resourceToString("/vcapXsuaaXsaSingleBinding.json", UTF_8); @@ -73,7 +74,9 @@ void getXsuaaConfigurationForTokenExchange() { assertNotSame(cutMultipleXsuaa.getXsuaaConfigurationForTokenExchange(), cutMultipleXsuaa.getXsuaaConfiguration()); - assertThat(cutMultipleApplicationPlanXsuaa.getXsuaaConfigurationForTokenExchange().getProperty(ServiceConstants.SERVICE_PLAN), + assertThat( + cutMultipleApplicationPlanXsuaa.getXsuaaConfigurationForTokenExchange() + .getProperty(ServiceConstants.SERVICE_PLAN), equalToIgnoringCase(ServiceConstants.Plan.BROKER.toString())); assertNotSame(cutMultipleApplicationPlanXsuaa.getXsuaaConfigurationForTokenExchange(), cutMultipleXsuaa.getXsuaaConfiguration()); @@ -138,7 +141,8 @@ void getServiceConfigurations() { configs = cutMultipleApplicationPlanXsuaa.getServiceConfigurations(); assertThat(configs.get(Service.XSUAA).entrySet(), hasSize(2)); assertThat(configs.get(Service.IAS).entrySet(), is(empty())); - assertThat(configs.get(Service.XSUAA).get(ServiceConstants.Plan.APPLICATION).getProperty(ServiceConstants.XSUAA.APP_ID), equalTo("na-d6a3278d-5e07-40e9-92ae-546bbfd9cdde!t8066")); + assertThat(configs.get(Service.XSUAA).get(ServiceConstants.Plan.APPLICATION) + .getProperty(ServiceConstants.XSUAA.APP_ID), equalTo("na-d6a3278d-5e07-40e9-92ae-546bbfd9cdde!t8066")); assertNotNull(configs.get(Service.XSUAA).get(ServiceConstants.Plan.BROKER)); assertNotNull(configs.get(Service.XSUAA).get(ServiceConstants.Plan.APPLICATION)); diff --git a/java-api/README.md b/java-api/README.md index 1f10c6c9f5..3ed9a43f36 100644 --- a/java-api/README.md +++ b/java-api/README.md @@ -5,6 +5,6 @@ com.sap.cloud.security java-api - 3.3.2 + 3.3.4 ``` diff --git a/java-api/pom.xml b/java-api/pom.xml index ac11d3eec7..a268cffe46 100644 --- a/java-api/pom.xml +++ b/java-api/pom.xml @@ -9,7 +9,7 @@ com.sap.cloud.security.xsuaa parent - 3.3.2 + 3.3.4 com.sap.cloud.security diff --git a/java-api/src/main/java/com/sap/cloud/security/config/Environment.java b/java-api/src/main/java/com/sap/cloud/security/config/Environment.java index eb661c169d..92d928c33f 100644 --- a/java-api/src/main/java/com/sap/cloud/security/config/Environment.java +++ b/java-api/src/main/java/com/sap/cloud/security/config/Environment.java @@ -5,21 +5,19 @@ */ package com.sap.cloud.security.config; +import javax.annotation.Nullable; import java.util.List; import java.util.Map; -import javax.annotation.Nullable; - - - /** * Central entry point to access the OAuth configuration * ({@link OAuth2ServiceConfiguration}) of a supported identity {@link Service}. */ public interface Environment { /** - * Return the primary OAuth service configuration of Xsuaa identity service instance. - * + * Return the primary OAuth service configuration of Xsuaa identity service + * instance. + * * @return the OAuth service configuration or null, in case there is no instance */ @Nullable @@ -43,8 +41,8 @@ public interface Environment { /** * In case there is only one Xsuaa identity service instance, this one gets - * returned. In case there are multiple bindings the primary one of plan "broker" gets - * returned. + * returned. In case there are multiple bindings the primary one of plan + * "broker" gets returned. * * @return the service configuration to be used for token exchange * @@ -53,9 +51,9 @@ public interface Environment { */ @Nullable OAuth2ServiceConfiguration getXsuaaConfigurationForTokenExchange(); - + /** - * Gives access to all service configurations parsed from the environment. + * Gives access to all service configurations parsed from the environment. * * @return the service configurations grouped by service */ diff --git a/java-api/src/main/java/com/sap/cloud/security/token/Token.java b/java-api/src/main/java/com/sap/cloud/security/token/Token.java index 7edf7f3621..807e3b7e1e 100644 --- a/java-api/src/main/java/com/sap/cloud/security/token/Token.java +++ b/java-api/src/main/java/com/sap/cloud/security/token/Token.java @@ -201,8 +201,9 @@ default String getZoneId() { * * @return the unique application tenant identifier. */ - default String getAppTid(){ - return hasClaim(SAP_GLOBAL_APP_TID) ? getClaimAsString(SAP_GLOBAL_APP_TID) : getClaimAsString(SAP_GLOBAL_ZONE_ID); + default String getAppTid() { + return hasClaim(SAP_GLOBAL_APP_TID) ? getClaimAsString(SAP_GLOBAL_APP_TID) + : getClaimAsString(SAP_GLOBAL_ZONE_ID); } /** diff --git a/java-api/src/main/java/com/sap/cloud/security/token/TokenClaims.java b/java-api/src/main/java/com/sap/cloud/security/token/TokenClaims.java index e9f792de2f..afef087477 100644 --- a/java-api/src/main/java/com/sap/cloud/security/token/TokenClaims.java +++ b/java-api/src/main/java/com/sap/cloud/security/token/TokenClaims.java @@ -34,7 +34,6 @@ private TokenClaims() { public static final String SAP_GLOBAL_SCIM_ID = "scim_id"; public static final String SAP_GLOBAL_USER_ID = "user_uuid"; - /** * @deprecated Use {@link TokenClaims#SAP_GLOBAL_APP_TID} instead. */ diff --git a/java-api/src/main/java/com/sap/cloud/security/token/validation/TestIssuerValidator.java b/java-api/src/main/java/com/sap/cloud/security/token/validation/TestIssuerValidator.java index f2bc805383..4ce844f4c3 100644 --- a/java-api/src/main/java/com/sap/cloud/security/token/validation/TestIssuerValidator.java +++ b/java-api/src/main/java/com/sap/cloud/security/token/validation/TestIssuerValidator.java @@ -1,8 +1,9 @@ package com.sap.cloud.security.token.validation; /** - * This interface is for INTERNAL usage only to add backward-compatibility for test credentials with trusted domain 'localhost' to the issuer validation. + * This interface is for INTERNAL usage only to add backward-compatibility for + * test credentials with trusted domain 'localhost' to the issuer validation. */ public interface TestIssuerValidator { - boolean isValidIssuer(String issuer); + boolean isValidIssuer(String issuer); } \ No newline at end of file diff --git a/java-api/src/main/java/com/sap/cloud/security/token/validation/XsuaaJkuFactory.java b/java-api/src/main/java/com/sap/cloud/security/token/validation/XsuaaJkuFactory.java index e58ab50293..307d2bcd31 100644 --- a/java-api/src/main/java/com/sap/cloud/security/token/validation/XsuaaJkuFactory.java +++ b/java-api/src/main/java/com/sap/cloud/security/token/validation/XsuaaJkuFactory.java @@ -1,8 +1,9 @@ package com.sap.cloud.security.token.validation; /** - * This interface is for INTERNAL usage only to add backward-compatibility for test credentials with uaadomain 'localhost' during JKU construction. + * This interface is for INTERNAL usage only to add backward-compatibility for + * test credentials with uaadomain 'localhost' during JKU construction. */ public interface XsuaaJkuFactory { - String create(String token); + String create(String token); } diff --git a/java-security-it/pom.xml b/java-security-it/pom.xml index 9cb98582a9..95e6f5c303 100644 --- a/java-security-it/pom.xml +++ b/java-security-it/pom.xml @@ -9,7 +9,7 @@ parent com.sap.cloud.security.xsuaa - 3.3.2 + 3.3.4 java-security-it diff --git a/java-security-test/README.md b/java-security-test/README.md index f40cbabed6..cb94d66dca 100644 --- a/java-security-test/README.md +++ b/java-security-test/README.md @@ -40,7 +40,7 @@ It is pre-configured with a security filter that only accepts valid tokens. Furt com.sap.cloud.security java-security-test - 3.3.2 + 3.3.4 test ``` diff --git a/java-security-test/pom.xml b/java-security-test/pom.xml index b76143c91e..52e857a3eb 100644 --- a/java-security-test/pom.xml +++ b/java-security-test/pom.xml @@ -9,7 +9,7 @@ com.sap.cloud.security.xsuaa parent - 3.3.2 + 3.3.4 com.sap.cloud.security @@ -38,26 +38,17 @@ org.eclipse.jetty jetty-server + ${org.eclipse.jetty.version} - org.eclipse.jetty.ee9 - jetty-ee9-servlet + org.eclipse.jetty.ee10 + jetty-ee10-servlet + ${org.eclipse.jetty.version} - org.eclipse.jetty.ee9 - jetty-ee9-webapp - - - org.eclipse.jetty.ee9 - jetty-ee9-annotations - - - org.eclipse.jetty - jetty-io - - - org.eclipse.jetty - jetty-util + org.eclipse.jetty.ee10 + jetty-ee10-webapp + ${org.eclipse.jetty.version} org.wiremock @@ -85,11 +76,6 @@ assertj-core test - - org.slf4j - slf4j-simple - test - diff --git a/java-security-test/src/main/java/com/sap/cloud/security/test/ApplicationServerOptions.java b/java-security-test/src/main/java/com/sap/cloud/security/test/ApplicationServerOptions.java index 7d057998b1..5d4fb48b44 100644 --- a/java-security-test/src/main/java/com/sap/cloud/security/test/ApplicationServerOptions.java +++ b/java-security-test/src/main/java/com/sap/cloud/security/test/ApplicationServerOptions.java @@ -76,15 +76,15 @@ public static ApplicationServerOptions forService(Service service, int jwksPort) public static ApplicationServerOptions forService(Service service) { return switch (service) { - case XSUAA -> forXsuaaService(SecurityTestRule.DEFAULT_APP_ID, SecurityTestRule.DEFAULT_CLIENT_ID); - case IAS -> new ApplicationServerOptions(new IasTokenAuthenticator() - .withServiceConfiguration(OAuth2ServiceConfigurationBuilder.forService(Service.IAS) - .withClientId(SecurityTestRule.DEFAULT_CLIENT_ID) - .withUrl("http://localhost") - .withDomains("localhost") - .build())); - default -> - throw new UnsupportedOperationException("Identity Service " + service + " is not yet supported."); + case XSUAA -> forXsuaaService(SecurityTestRule.DEFAULT_APP_ID, SecurityTestRule.DEFAULT_CLIENT_ID); + case IAS -> new ApplicationServerOptions(new IasTokenAuthenticator() + .withServiceConfiguration(OAuth2ServiceConfigurationBuilder.forService(Service.IAS) + .withClientId(SecurityTestRule.DEFAULT_CLIENT_ID) + .withUrl("http://localhost") + .withDomains("localhost") + .build())); + default -> + throw new UnsupportedOperationException("Identity Service " + service + " is not yet supported."); }; } diff --git a/java-security-test/src/main/java/com/sap/cloud/security/test/JwtGenerator.java b/java-security-test/src/main/java/com/sap/cloud/security/test/JwtGenerator.java index 758e67225e..7f880253c6 100644 --- a/java-security-test/src/main/java/com/sap/cloud/security/test/JwtGenerator.java +++ b/java-security-test/src/main/java/com/sap/cloud/security/test/JwtGenerator.java @@ -18,6 +18,7 @@ import javax.annotation.Nonnull; import java.io.IOException; +import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import java.security.*; import java.time.Instant; @@ -42,8 +43,8 @@ public class JwtGenerator { private static final String DEFAULT_JWKS_URL = "http://localhost/token_keys"; private static final char DOT = '.'; - private final JSONObject jsonHeader = new JSONObject(); - private final JSONObject jsonPayload = new JSONObject(); + private final JSONObject jsonHeader = newPredictableOrderingJSONObject(); + private final JSONObject jsonPayload = newPredictableOrderingJSONObject(); private final SignatureCalculator signatureCalculator; private final Service service; @@ -60,6 +61,25 @@ private JwtGenerator(Service service, SignatureCalculator signatureCalculator) { predefineTokenClaims(); } + /** + * Creates a new JSONObject object with LinkedHashMap with predictable iteration + * order. + * + * @return JSONObject + */ + private static JSONObject newPredictableOrderingJSONObject() { + JSONObject jsonObject = new JSONObject(); + try { + Field declaredMapField = jsonObject.getClass().getDeclaredField("map"); + declaredMapField.setAccessible(true); + declaredMapField.set(jsonObject, new LinkedHashMap<>()); + declaredMapField.setAccessible(false); + } catch (IllegalAccessException | NoSuchFieldException e) { + LOGGER.info("Couldn't create a JSONObject with a LinkedHashMap field. {}", e.getMessage()); + } + return jsonObject; + } + /** * This factory method creates an {@link JwtGenerator} instance that can be used * to create tokens for testing purposes. The tokens are prefilled with data so @@ -148,7 +168,8 @@ private void setDefaultsForNewToken(String azp) { withClaimValue(TokenClaims.XSUAA.CLIENT_ID, azp); // Client Id left for backward compatibility if (service == Service.IAS) { jsonPayload.put(TokenClaims.AUDIENCE, azp); - jsonPayload.put(TokenClaims.SAP_GLOBAL_ZONE_ID, DEFAULT_ZONE_ID); //TODO to be removed once fallback is not supported + jsonPayload.put(TokenClaims.SAP_GLOBAL_ZONE_ID, DEFAULT_ZONE_ID); // TODO to be removed once fallback is not + // supported jsonPayload.put(TokenClaims.SAP_GLOBAL_APP_TID, DEFAULT_APP_TID); jsonPayload.put(TokenClaims.SAP_GLOBAL_USER_ID, DEFAULT_USER_ID); jsonPayload.put(TokenClaims.SAP_GLOBAL_SCIM_ID, DEFAULT_USER_ID); diff --git a/java-security-test/src/main/java/com/sap/cloud/security/test/SecurityTest.java b/java-security-test/src/main/java/com/sap/cloud/security/test/SecurityTest.java index 7a9c99c5a8..e1994c7eb0 100644 --- a/java-security-test/src/main/java/com/sap/cloud/security/test/SecurityTest.java +++ b/java-security-test/src/main/java/com/sap/cloud/security/test/SecurityTest.java @@ -29,11 +29,11 @@ import jakarta.servlet.Filter; import jakarta.servlet.Servlet; import org.apache.commons.io.IOUtils; -import org.eclipse.jetty.ee9.security.ConstraintSecurityHandler; +import org.eclipse.jetty.ee10.servlet.FilterHolder; +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.eclipse.jetty.ee10.servlet.security.ConstraintSecurityHandler; +import org.eclipse.jetty.ee10.webapp.WebAppContext; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.ee9.servlet.FilterHolder; -import org.eclipse.jetty.ee9.servlet.ServletHolder; -import org.eclipse.jetty.ee9.webapp.WebAppContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -188,12 +188,13 @@ public OAuth2ServiceConfigurationBuilder getOAuth2ServiceConfigurationBuilderFro } ServiceBinding binding = serviceBindings.get(0); - OAuth2ServiceConfigurationBuilder builder = ServiceBindingMapper.mapToOAuth2ServiceConfigurationBuilder(binding); + OAuth2ServiceConfigurationBuilder builder = ServiceBindingMapper + .mapToOAuth2ServiceConfigurationBuilder(binding); if (builder != null) { // adjust domain and URL of the config to fit the mocked service instance builder = builder.withDomains(URI.create(issuerUrl).getHost()).withUrl(issuerUrl); - if(Objects.equals(Service.from(binding.getServiceName().get()), XSUAA)) { + if (Objects.equals(Service.from(binding.getServiceName().get()), XSUAA)) { builder.withProperty(ServiceConstants.XSUAA.UAA_DOMAIN, wireMockServer.baseUrl()); } } @@ -228,7 +229,7 @@ void startApplicationServer() throws Exception { WebAppContext context = new WebAppContext(); context.setContextPath("/"); - context.setResourceBase("src/main/webapp"); + context.setBaseResourceAsString("src/main/webapp"); context.setSecurityHandler(security); applicationServletsByPath @@ -278,7 +279,7 @@ public void setup() throws Exception { wireMockServer.resetAll(); } if (useApplicationServer && (applicationServer == null || !applicationServer.isStarted())) { - if (applicationServerOptions == null){ + if (applicationServerOptions == null) { this.applicationServerOptions = ApplicationServerOptions.forService(service, wireMockServer.port()); } startApplicationServer(); diff --git a/java-security-test/src/main/java/com/sap/cloud/security/test/SecurityTestRule.java b/java-security-test/src/main/java/com/sap/cloud/security/test/SecurityTestRule.java index a63b93bbc1..70ffb2827e 100644 --- a/java-security-test/src/main/java/com/sap/cloud/security/test/SecurityTestRule.java +++ b/java-security-test/src/main/java/com/sap/cloud/security/test/SecurityTestRule.java @@ -5,6 +5,11 @@ */ package com.sap.cloud.security.test; +import javax.annotation.Nullable; + +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.junit.rules.ExternalResource; + import com.github.tomakehurst.wiremock.WireMockServer; import com.sap.cloud.security.config.OAuth2ServiceConfigurationBuilder; import com.sap.cloud.security.config.Service; @@ -12,12 +17,9 @@ import com.sap.cloud.security.test.api.SecurityTestContext; import com.sap.cloud.security.test.api.ServiceMockConfiguration; import com.sap.cloud.security.token.Token; + import jakarta.servlet.Filter; import jakarta.servlet.Servlet; -import org.eclipse.jetty.ee9.servlet.ServletHolder; -import org.junit.rules.ExternalResource; - -import javax.annotation.Nullable; public class SecurityTestRule extends ExternalResource implements SecurityTestContext, ServiceMockConfiguration, ApplicationServerConfiguration { diff --git a/java-security-test/src/main/java/com/sap/cloud/security/test/api/ApplicationServerConfiguration.java b/java-security-test/src/main/java/com/sap/cloud/security/test/api/ApplicationServerConfiguration.java index 962a66f453..eb39604f64 100644 --- a/java-security-test/src/main/java/com/sap/cloud/security/test/api/ApplicationServerConfiguration.java +++ b/java-security-test/src/main/java/com/sap/cloud/security/test/api/ApplicationServerConfiguration.java @@ -5,12 +5,14 @@ */ package com.sap.cloud.security.test.api; +import org.eclipse.jetty.ee10.servlet.ServletHolder; + import com.sap.cloud.security.config.Service; import com.sap.cloud.security.test.ApplicationServerOptions; import com.sap.cloud.security.test.SecurityTestRule; + import jakarta.servlet.Filter; import jakarta.servlet.Servlet; -import org.eclipse.jetty.ee9.servlet.ServletHolder; public interface ApplicationServerConfiguration { diff --git a/java-security-test/src/main/java/com/sap/cloud/security/test/extension/SecurityTestExtension.java b/java-security-test/src/main/java/com/sap/cloud/security/test/extension/SecurityTestExtension.java index 693be82f40..639f041728 100644 --- a/java-security-test/src/main/java/com/sap/cloud/security/test/extension/SecurityTestExtension.java +++ b/java-security-test/src/main/java/com/sap/cloud/security/test/extension/SecurityTestExtension.java @@ -13,7 +13,7 @@ import com.sap.cloud.security.test.api.ServiceMockConfiguration; import jakarta.servlet.Filter; import jakarta.servlet.Servlet; -import org.eclipse.jetty.ee9.servlet.ServletHolder; +import org.eclipse.jetty.ee10.servlet.ServletHolder; import org.junit.jupiter.api.extension.*; /** diff --git a/java-security-test/src/main/java/com/sap/cloud/security/test/jetty/JettyTokenAuthenticator.java b/java-security-test/src/main/java/com/sap/cloud/security/test/jetty/JettyTokenAuthenticator.java index dd1897ec75..8b519f34d2 100644 --- a/java-security-test/src/main/java/com/sap/cloud/security/test/jetty/JettyTokenAuthenticator.java +++ b/java-security-test/src/main/java/com/sap/cloud/security/test/jetty/JettyTokenAuthenticator.java @@ -10,26 +10,30 @@ import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletResponse; -import org.eclipse.jetty.ee9.security.Authenticator; -import org.eclipse.jetty.ee9.security.UserAuthentication; -import org.eclipse.jetty.ee9.nested.Authentication; +import org.eclipse.jetty.ee10.servlet.ServletContextRequest; +import org.eclipse.jetty.ee10.servlet.ServletContextResponse; +import org.eclipse.jetty.security.AuthenticationState; +import org.eclipse.jetty.security.Authenticator; +import org.eclipse.jetty.security.Constraint.Authorization; +import org.eclipse.jetty.security.ServerAuthException; +import org.eclipse.jetty.security.authentication.LoginAuthenticator; import org.eclipse.jetty.security.internal.DefaultUserIdentity; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Session; +import org.eclipse.jetty.util.Callback; import javax.security.auth.Subject; -import java.io.IOException; import java.security.Principal; import java.util.HashSet; import java.util.Set; +import java.util.function.Function; /** * Decorates the TokenAuthenticator and adapts it to Jetty. */ public class JettyTokenAuthenticator implements Authenticator { - private static final Logger LOGGER = LoggerFactory.getLogger(JettyTokenAuthenticator.class); - private final TokenAuthenticator tokenAuthenticator; public JettyTokenAuthenticator(TokenAuthenticator tokenAuthenticator) { @@ -37,52 +41,46 @@ public JettyTokenAuthenticator(TokenAuthenticator tokenAuthenticator) { } @Override - public Authentication validateRequest(ServletRequest request, ServletResponse response, boolean mandatory) { - TokenAuthenticationResult tokenAuthenticationResult = tokenAuthenticator.validateRequest(request, response); - if (tokenAuthenticationResult.isAuthenticated()) { - return createAuthentication(tokenAuthenticationResult); - } else { - sendUnauthenticatedResponse(response, tokenAuthenticationResult.getUnauthenticatedReason()); - return Authentication.UNAUTHENTICATED; - } - } - - private void sendUnauthenticatedResponse(ServletResponse response, String unauthenticatedReason) { - if (response instanceof HttpServletResponse) { - try { - HttpServletResponse httpServletResponse = (HttpServletResponse) response; - httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, unauthenticatedReason); // 401 - } catch (IOException e) { - LOGGER.error("Failed to send error response", e); - } - } - } - - @Override - public void setConfiguration(AuthConfiguration configuration) { + public void setConfiguration(Configuration configuration) { } @Override - public String getAuthMethod() { + public String getAuthenticationType() { return "Token"; } @Override - public void prepareRequest(ServletRequest request) { + public Authorization getConstraintAuthentication(String pathInContext, Authorization existing, + Function getSession) { + return Authorization.ANY_USER; } @Override - public boolean secureResponse(ServletRequest request, ServletResponse response, boolean mandatory, - Authentication.User validatedUser) { - return true; + public AuthenticationState validateRequest(Request request, Response response, Callback callback) + throws ServerAuthException { + ServletRequest servletRequest = request instanceof ServletContextRequest scr ? scr.getServletApiRequest() + : null; + ServletResponse servletResponse = response instanceof ServletContextResponse scr ? scr.getServletApiResponse() + : null; + + TokenAuthenticationResult tokenAuthenticationResult = tokenAuthenticator.validateRequest(servletRequest, + servletResponse); + if (tokenAuthenticationResult.isAuthenticated()) { + return createAuthentication(tokenAuthenticationResult); + } else { + Response.writeError(request, response, callback, HttpServletResponse.SC_UNAUTHORIZED, + tokenAuthenticationResult.getUnauthenticatedReason()); + return AuthenticationState.SEND_FAILURE; + } } - private Authentication createAuthentication(TokenAuthenticationResult tokenAuthentication) { + private AuthenticationState createAuthentication(TokenAuthenticationResult tokenAuthentication) { Principal principal = tokenAuthentication.getPrincipal(); Set principals = new HashSet<>(); principals.add(principal); Subject subject = new Subject(true, principals, new HashSet<>(), new HashSet<>()); String[] scopes = tokenAuthentication.getScopes().toArray(new String[0]); - return new UserAuthentication(getAuthMethod(), new DefaultUserIdentity(subject, principal, scopes)); + return new LoginAuthenticator.UserAuthenticationSucceeded(getAuthenticationType(), + new DefaultUserIdentity(subject, principal, scopes)); } } diff --git a/java-security-test/src/main/java/com/sap/cloud/security/token/validation/LocalhostIssuerValidator.java b/java-security-test/src/main/java/com/sap/cloud/security/token/validation/LocalhostIssuerValidator.java index 328cd4fae6..ea7541e758 100644 --- a/java-security-test/src/main/java/com/sap/cloud/security/token/validation/LocalhostIssuerValidator.java +++ b/java-security-test/src/main/java/com/sap/cloud/security/token/validation/LocalhostIssuerValidator.java @@ -1,14 +1,18 @@ package com.sap.cloud.security.token.validation; /** - * LocalhostIssuerValidator brings backward-compatibility for test credentials in consumer applications written before 2.17.0 that are used to validate java-security-test tokens. - * This is necessary for successful validation of localhost issuers that include a port when 'localhost' is defined as trusted domain without port in the service credentials. - * This class MUST NOT be loaded outside test scope and MUST be the ONLY implementation of {@link TestIssuerValidator}. + * LocalhostIssuerValidator brings backward-compatibility for test credentials + * in consumer applications written before 2.17.0 that are used to validate + * java-security-test tokens. This is necessary for successful validation of + * localhost issuers that include a port when 'localhost' is defined as trusted + * domain without port in the service credentials. This class MUST NOT be loaded + * outside test scope and MUST be the ONLY implementation of + * {@link TestIssuerValidator}. */ public class LocalhostIssuerValidator implements TestIssuerValidator { - @Override - public boolean isValidIssuer(String issuer) { - return issuer.startsWith("http://localhost") || issuer.startsWith("https://localhost"); - } + @Override + public boolean isValidIssuer(String issuer) { + return issuer.startsWith("http://localhost") || issuer.startsWith("https://localhost"); + } } diff --git a/java-security-test/src/main/java/com/sap/cloud/security/token/validation/XsuaaLocalhostJkuFactory.java b/java-security-test/src/main/java/com/sap/cloud/security/token/validation/XsuaaLocalhostJkuFactory.java index 1e4c247578..12c2ac6d28 100644 --- a/java-security-test/src/main/java/com/sap/cloud/security/token/validation/XsuaaLocalhostJkuFactory.java +++ b/java-security-test/src/main/java/com/sap/cloud/security/token/validation/XsuaaLocalhostJkuFactory.java @@ -4,21 +4,24 @@ import com.sap.cloud.security.token.TokenHeader; /** - * XsuaaLocalhostJkuFactory brings backward-compatibility for test credentials in consumer applications written before 2.17.0 that are used to validate java-security-test tokens. - * This is necessary for successful JKU construction when 'localhost' is defined as uaadomain in the service credentials. - * This class MUST NOT be loaded outside test scope and MUST be the ONLY implementation of {@link XsuaaJkuFactory}. + * XsuaaLocalhostJkuFactory brings backward-compatibility for test credentials + * in consumer applications written before 2.17.0 that are used to validate + * java-security-test tokens. This is necessary for successful JKU construction + * when 'localhost' is defined as uaadomain in the service credentials. This + * class MUST NOT be loaded outside test scope and MUST be the ONLY + * implementation of {@link XsuaaJkuFactory}. */ public class XsuaaLocalhostJkuFactory implements XsuaaJkuFactory { - @Override - public String create(String jwt) { - Token token = Token.create(jwt); - String tokenJku = (String) token.getHeaders().get(TokenHeader.JWKS_URL); + @Override + public String create(String jwt) { + Token token = Token.create(jwt); + String tokenJku = (String) token.getHeaders().get(TokenHeader.JWKS_URL); - if (tokenJku.contains("localhost") || tokenJku.contains("127.0.0.1")) { - return tokenJku; - } + if (tokenJku.contains("localhost") || tokenJku.contains("127.0.0.1")) { + return tokenJku; + } - throw new IllegalArgumentException("JKU is not trusted because it does not target localhost."); - } + throw new IllegalArgumentException("JKU is not trusted because it does not target localhost."); + } } \ No newline at end of file diff --git a/java-security-test/src/test/java/com/sap/cloud/security/test/SecurityTestRuleTest.java b/java-security-test/src/test/java/com/sap/cloud/security/test/SecurityTestRuleTest.java index bf2a3bcfc7..9ef9776af7 100644 --- a/java-security-test/src/test/java/com/sap/cloud/security/test/SecurityTestRuleTest.java +++ b/java-security-test/src/test/java/com/sap/cloud/security/test/SecurityTestRuleTest.java @@ -21,7 +21,7 @@ import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.HttpClients; -import org.eclipse.jetty.ee9.servlet.ServletHolder; +import org.eclipse.jetty.ee10.servlet.ServletHolder; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; diff --git a/java-security/Migration_SAPJavaBuildpackProjects.md b/java-security/Migration_SAPJavaBuildpackProjects.md index 523744fe9f..7dadba2ad3 100644 --- a/java-security/Migration_SAPJavaBuildpackProjects.md +++ b/java-security/Migration_SAPJavaBuildpackProjects.md @@ -61,4 +61,4 @@ This comes with a change regarding scopes. For a business application A that wan In case you face issues to apply the migration steps check this [troubleshooting](README.md#troubleshooting) for known issues and how to file the issue. ## [OPTIONAL] Leverage new API and features -You can continue [here](Migration_SAPJavaBuildpackProjects_V2.md) to understand what needs to be done to leverage the new `java-api` that is exposed by the SAP Java Buildpack as of version `1.26.1`. +You can continue [here](Migration_SAPJavaBuildpackProjects_V2.md) to understand what needs to be done to leverage the new `java-api` that is exposed by the SAP Java Buildpack starting with version `1.26.1`. diff --git a/java-security/Migration_SAPJavaBuildpackProjects_V2.md b/java-security/Migration_SAPJavaBuildpackProjects_V2.md index 157bb9b561..3a6bdeab47 100644 --- a/java-security/Migration_SAPJavaBuildpackProjects_V2.md +++ b/java-security/Migration_SAPJavaBuildpackProjects_V2.md @@ -3,7 +3,7 @@ **This document is only applicable for J2EE web applications securing their application with SAP Java Buildpack.** -This migration document is a step-by-step guide explaining how to leverage the new api that is exposed by the SAP Java Buildpack as of version `1.26.1`. +This migration document is a step-by-step guide explaining how to leverage the new api that is exposed by the SAP Java Buildpack starting with version `1.26.1`. ## Prerequisites @@ -12,7 +12,7 @@ Please note, this Migration Guide is only intended for applications, using SAP J **Before you proceed, make sure you have completed [this guide](Migration_SAPJavaBuildpackProjects.md).** ## Adapt Maven Dependencies -To use the latest API exposed by SAP Java Buildpack version as of version `1.26.1` the dependency declared in maven `pom.xml` needs to be adapted. +To use the latest API exposed by SAP Java Buildpack version starting with version `1.26.1` the dependency declared in maven `pom.xml` needs to be adapted. First make sure you have the following dependency defined in your pom.xml: ```xml @@ -29,7 +29,7 @@ Now you are ready to **remove** the dependency to the **`api`** by deleting the api ``` -The dependency `com.sap.cloud.security:java-api` is the new api exposed by the SAP Java Buildpack as of version `1.26.1`. +The dependency `com.sap.cloud.security:java-api` is the new api exposed by the SAP Java Buildpack starting with version `1.26.1`. ## Adapt Environment Variable As the new `java-api` is incompatible with the former one, you have to tell the SAP Java Buildpack, that you want to use the latest api. This is done by setting the `ENABLE_SECURITY_JAVA_API_V2` environment variable to `true` as part of your deployment descriptor, e.g. in your `manifest.yml` file. diff --git a/java-security/Migration_SpringSecurityProjects.md b/java-security/Migration_SpringSecurityProjects.md new file mode 100644 index 0000000000..bc2ba2b50d --- /dev/null +++ b/java-security/Migration_SpringSecurityProjects.md @@ -0,0 +1,283 @@ +# Migration Guide for Applications that use Spring Security and java-container-security + +This migration guide is a step-by-step guide explaining how to replace the following SAP-internal Java Container Security Client libraries +- com.sap.xs2.security:java-container-security +- com.sap.cloud.security.xsuaa:java-container-security + +with this open-source version. + +## Prerequisite +**Please note, that as of now, this Migration Guide is NOT intended for applications using SAP Java Buildpack.** +You're using the SAP Java Buildpack, if you can find the `sap_java_buildpack` in the deployment descriptor of your application, e.g. in your `manifest.yml` file. + +This [documentation](Migration_SAPJavaBuildpackProjects.md) describes the setup when using SAP Java Buildpack. + +## :bulb: Deprecation Notice + +The Spring Security OAuth project is deprecated. The latest OAuth 2.0 support is provided by Spring Security. See the [OAuth 2.0 Migration Guide](https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide) for further details. + +The `java-container-security` as well as the `SAPOfflineTokenServicesCloud` provided as part of the current solution bases on `org.springframework.security.oauth:spring-security-oauth2` which is deprecated. In case of Spring-Boot application you may want to follow this [Migration Guide](/spring-xsuaa/Migration_JavaContainerSecurityProjects.md). + + +## Maven Dependencies +To use the new [java-security](/java-security) client library the dependencies declared in maven `pom.xml` need to be updated. + +First make sure you have the following dependencies defined in your pom.xml: + +```xml + + + + org.springframework.security.oauth + spring-security-oauth2 + 2.4.1.RELEASE + + + + + com.sap.cloud.security.xsuaa + api + 2.13.7 + + + com.sap.cloud.security + java-security + 2.13.7 + + + + + com.sap.cloud.security + java-security-test + 2.13.7 + test + +``` + +Now you are ready to **remove** the **`java-container-security`** client library by deleting the following dependencies from the pom.xml: + +groupId (deprecated) | artifactId (deprecated) +--- | --- +com.sap.xs2.security | java-container-security +com.sap.xs2.security | api +com.sap.cloud.security.xssec | api +com.sap.cloud.security.xsuaa | java-container-security-api +com.sap.cloud.security.xsuaa | java-container-security + +Make sure that you do not refer to any other sap security library with group-id `com.sap.security` or `com.sap.security.nw.sso.*`. + +## Configuration changes +After the dependencies have been changed, the spring security configuration needs some adjustments as well. + +If your security configuration was using the `SAPOfflineTokenServicesCloud` class from the `java-container-security` library, +you need to change it slightly to use the `SAPOfflineTokenServicesCloud` adapter class from the new library. + +> Note: There is no replacement for `SAPPropertyPlaceholderConfigurer` as you can always parameterize the `SAPOfflineTokenServicesCloud` bean with your `OAuth2ServiceConfiguration`. + +### Code-based + +For example see the following snippet on how to instantiate the `SAPOfflineTokenServicesCloud`. + +```java +@Bean +@Profile("cloud") +protected SAPOfflineTokenServicesCloud offlineTokenServices() { + return new SAPOfflineTokenServicesCloud( + Environments.getCurrent().getXsuaaConfiguration(), //optional + new RestTemplate()) //optional + .setLocalScopeAsAuthorities(true); //optional +} +``` +You might need to fix your java imports to get rid of the old import for the `SAPOfflineTokenServicesCloud` class. + +### XML-based +As you may have updated the + +In case of XML-based Spring (Security) configuration you need to replace your current `SAPOfflineTokenServicesCloud` bean definition with that: + +```xml + + + +``` + +With `localScopeAsAuthorities` you can perform spring scope checks without referring to the XS application name (application id), e.g. `myapp!t1234`. For example: + +Or +```java +... +.authorizeRequests() + .antMatchers(POST, "/api/v1/ads/**").access("#oauth2.hasScopeMatching('Update')") //instead of '${xs.appname}.Update' +``` + + +```xml + +``` + +### SAP_JWT_TRUST_ACL obsolete +There is no need to configure `SAP_JWT_TRUST_ACL` within your deployment descriptor such as `manifest.yml`. +Instead the Xsuaa service instance adds audiences to the issued JSON Web Token (JWT) as part of the `aud` claim. + +Whether the token is issued for your application or not is now validated by the [`JwtAudienceValidator`](/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtAudienceValidator.java). + +This comes with a change regarding scopes. For a business application A that wants to call an application B, it's now mandatory that the application B grants at least one scope to the calling business application A. You can grant scopes with the `xs-security.json` file. For additional information, refer to the [Application Security Descriptor Configuration Syntax](https://help.sap.com/viewer/65de2977205c403bbc107264b8eccf4b/Cloud/en-US/517895a9612241259d6941dbf9ad81cb.html), specifically the sections [referencing the application](https://help.sap.com/viewer/65de2977205c403bbc107264b8eccf4b/Cloud/en-US/517895a9612241259d6941dbf9ad81cb.html#loio517895a9612241259d6941dbf9ad81cb__section_fm2_wsk_pdb) and [authorities](https://help.sap.com/viewer/65de2977205c403bbc107264b8eccf4b/Cloud/en-US/517895a9612241259d6941dbf9ad81cb.html#loio517895a9612241259d6941dbf9ad81cb__section_d1m_1nq_zy). + +## Fetch basic infos from Token +You may have code parts that requests information from the access token, like the user's name, its tenant, and so on. So, look up your code to find its usage. + + +```java +import com.sap.xs2.security.container.SecurityContext; +import com.sap.xs2.security.container.UserInfo; +import com.sap.xs2.security.container.UserInfoException; + + +try { + UserInfo userInfo = SecurityContext.getUserInfo(); + String logonName = userInfo.getLogonName(); +} catch (UserInfoException e) { + // handle exception +} + +``` + +This can be easily replaced with the `Token` or `XsuaaToken` Api. + +```java +import com.sap.cloud.security.token.*; + +AccessToken token = SecurityContext.getAccessToken(); +String logonName = token.getClaimAsString(TokenClaims.USER_NAME); +boolean hasDisplayScope = token.hasLocalScope("Display"); +GrantType grantType = token.getGrantType(); +``` + +> Note, that no `XSUserInfoException` is raised, in case the token does not contain the requested claim. + +## Fetch further `XSUserInfo` infos from Token +When you're done with the first part and need further information from the token you can use `XSUserInfoAdapter` in order to access the remaining methods exposed by the [`XSUserInfo`](/api/src/main/java/com/sap/xsa/security/container/XSUserInfo.java) Interface. + +```java +try { + XSUserInfo userInfo = new XSUserInfoAdapter(token); + String dbToken = userInfo.getHdbToken(); +} catch (XSUserInfoException e) { + // handle exception +} +``` + +## Test Code Changes + +### Security configuration for tests +If you want to overwrite the service configuration of the `SAPOfflineTokenServicesCloud` for your test, you can do so by +using some test constants provided by the test library. The following snippet shows how to do that: +```java +import static com.sap.cloud.security.config.cf.CFConstants.*; + +@Configuration +public class TestSecurityConfig { + @Bean + @Primary + public SAPOfflineTokenServicesCloud sapOfflineTokenServices() { + OAuth2ServiceConfiguration configuration = OAuth2ServiceConfigurationBuilder + .forService(Service.XSUAA) + .withClientId(SecurityTestRule.DEFAULT_CLIENT_ID) + .withProperty(CFConstants.XSUAA.APP_ID, SecurityTestRule.DEFAULT_APP_ID) + .withProperty(CFConstants.XSUAA.UAA_DOMAIN, SecurityTestRule.DEFAULT_DOMAIN) + .build(); + + return new SAPOfflineTokenServicesCloud(configuration).setLocalScopeAsAuthorities(true); + } +} +``` + +### Unit testing +In your unit test you might want to generate jwt tokens and have them validated. The [java-security-test](/java-security-test) library provides it's own `JwtGenerator`. This can be embedded using the +`SecurityTestRule`. See the following snippet as example: + +```java +@ClassRule +public static SecurityTestRule securityTestRule = + SecurityTestRule.getInstance(Service.XSUAA) + .setKeys("/publicKey.txt", "/privateKey.txt"); +``` + +Using the `SecurityTestRule` you can use a preconfigured `JwtGenerator` to create JWT tokens with custom scopes for your tests. It configures the JwtGenerator in such a way that **it uses the public key from the [`publicKey.txt`](/java-security-test/src/main/resources) file to sign the token.** + +```java +static final String XSAPPNAME = SecurityTestRule.DEFAULT_APP_ID; +static final String DISPLAY_SCOPE = XSAPPNAME + ".Display"; +static final String UPDATE_SCOPE = XSAPPNAME + ".Update"; + +String jwt = securityTestRule.getPreconfiguredJwtGenerator() + .withScopes(DISPLAY_SCOPE, UPDATE_SCOPE) + .createToken() + .getTokenValue(); + +``` +Make sure, that your JUnit tests are running. + +The `java-security-test` library provides also JUnit 5 support as described [here](/java-security-test). + +## Enable local testing +For local testing you might need to provide custom `VCAP_SERVICES` before you run the application. +The new security library requires the following key value pairs in the `VCAP_SERVICES` +under `xsuaa/credentials` for jwt validation: +- `"uaadomain" : "localhost"` +- `"verificationkey" : ""` + +Before calling the service you need to provide a digitally signed JWT token to simulate that you are an authenticated user. +- Therefore simply set a breakpoint in `JWTGenerator.createToken()` and run your `JUnit` tests to fetch the value of `jwt` from there. + +Now you can test the service manually in the browser using the `Postman` chrome plugin and check whether the secured functions can be accessed when providing a valid generated Jwt Token. + + +## Things to check after migration +When your code compiles again you should first check that all your unit tests are running again. If you can test your +application locally make sure that it is still working and finally test the application in cloud foundry. + +## Troubleshoot + +### Issues with XML schema declarations +If you get errors in the aplication log similar to this one + +``` +Configuration problem: You cannot use a spring-security-4.0.xsd or spring-security-4.1.xsd schema with Spring Security 4.2. Please update your schema declarations to the 4.2 schema. Offending resource: ServletContext resource [/WEB-INF/spring-security.xml] +``` + +You can fix this by removing the Spring versions of the schema declaration in the file `/WEB-INF/spring-security.xml`. Without explicit versions set, the latest version will be used. + +``` +xsi:schemaLocation="http://www.springframework.org/schema/security/oauth2 + http://www.springframework.org/schema/security/spring-security-oauth2.xsd + http://www.springframework.org/schema/security + http://www.springframework.org/schema/security/spring-security.xsd + http://www.springframework.org/schema/beans + http://www.springframework.org/schema/beans/spring-beans.xsd +``` + +If you get an `org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException` like this: + +``` +org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException: Line XX in XML document from ServletContext resource [/WEB-INF/spring-security.xml] is invalid; +nested exception is org.xml.sax.SAXParseException; lineNumber: 51; columnNumber: 118; cvc-complex-type.2.4.c: +The matching wildcard is strict, but no declaration can be found for element 'oauth:resource-server'. +``` + +You can fix this by changing the schema location to `https` for `oauth2` as below in the `/WEB-INF/spring-security.xml`. With this change, the local jar is available and solves the issue of server trying to connect to get the jar and fails due to some restrictions. + +``` +xsi:schemaLocation="http://www.springframework.org/schema/security/oauth2 +https://www.springframework.org/schema/security/spring-security-oauth2.xsd +``` + +### HTTP 403 unauthorized errors although token contains scope + +If you use `SAPOfflineTokenServicesCloud` together with `HttpServlet` scope checks (e.g. `httpRequest.isUserInRole`), you might get 403 unauthorized errors even though the token contains the correct scope. This can happen if *automatic ROLE_prefixing* is enabled in Spring and the roles of the token do not have a `ROLE_` prefix. To fix this problem you can disable the automatic ROLE_prefixing in Spring. This is described [here](https://docs.spring.io/spring-security/site/migrate/current/3-to-4/html5/migrate-3-to-4-jc.html#m3to4-role-prefixing). Anther way to fix this problem is to manually prefix all scopes with `ROLE_`. After that your scope checks should work as expected. + + +## Issues +In case you face issues to apply the migration steps check this [troubleshoot](README.md#troubleshoot) for known issues and how to file the issue. + diff --git a/java-security/README.md b/java-security/README.md index d78ab961fc..662fe9a731 100644 --- a/java-security/README.md +++ b/java-security/README.md @@ -67,7 +67,7 @@ To be able to validate tokens it performs the following tasks: com.sap.cloud.security java-security - 3.3.2 + 3.3.4 org.apache.httpcomponents.client5 diff --git a/java-security/pom.xml b/java-security/pom.xml index 1cb168fc21..5220f24aa7 100644 --- a/java-security/pom.xml +++ b/java-security/pom.xml @@ -9,7 +9,7 @@ com.sap.cloud.security.xsuaa parent - 3.3.2 + 3.3.4 com.sap.cloud.security @@ -97,11 +97,6 @@ assertj-core test - - org.slf4j - slf4j-simple - test - com.sap.cloud.security.xsuaa spring-xsuaa @@ -125,6 +120,11 @@ spring-context test + + ch.qos.logback + logback-classic + test + diff --git a/java-security/src/main/java/com/sap/cloud/security/servlet/HybridTokenFactory.java b/java-security/src/main/java/com/sap/cloud/security/servlet/HybridTokenFactory.java index 1c7ba207f4..10e0f2edd6 100644 --- a/java-security/src/main/java/com/sap/cloud/security/servlet/HybridTokenFactory.java +++ b/java-security/src/main/java/com/sap/cloud/security/servlet/HybridTokenFactory.java @@ -18,6 +18,7 @@ import javax.annotation.Nonnull; import java.util.Objects; +import java.util.Optional; import java.util.regex.Pattern; import static com.sap.cloud.security.token.TokenClaims.XSUAA.EXTERNAL_ATTRIBUTE; @@ -31,8 +32,8 @@ public class HybridTokenFactory implements TokenFactory { private static final Logger LOGGER = LoggerFactory.getLogger(HybridTokenFactory.class); - private static String xsAppId; - private static ScopeConverter xsScopeConverter; + static Optional xsAppId; + static ScopeConverter xsScopeConverter; /** * Determines whether the JWT token is issued by XSUAA or IAS identity service, @@ -66,25 +67,28 @@ public Token create(String jwtToken) { */ static void withXsuaaAppId(@Nonnull String xsAppId) { LOGGER.debug("XSUAA app id = {}", xsAppId); - HybridTokenFactory.xsAppId = xsAppId; + HybridTokenFactory.xsAppId = Optional.of(xsAppId); getOrCreateScopeConverter(); } private static ScopeConverter getOrCreateScopeConverter() { - if (xsScopeConverter == null && getXsAppId() != null) { - xsScopeConverter = new XsuaaScopeConverter(getXsAppId()); + if (xsScopeConverter == null && getXsAppId().isPresent()) { + xsScopeConverter = new XsuaaScopeConverter(getXsAppId().get()); } return xsScopeConverter; } - private static String getXsAppId() { - if (xsAppId == null) { - OAuth2ServiceConfiguration serviceConfiguration = Environments.getCurrent().getXsuaaConfiguration(); - if (serviceConfiguration == null) { - LOGGER.warn("There is no xsuaa service configuration: no local scope check possible."); - } else { - xsAppId = serviceConfiguration.getProperty(ServiceConstants.XSUAA.APP_ID); - } + private static Optional getXsAppId() { + if (Objects.nonNull(xsAppId)) { + return xsAppId; + } + OAuth2ServiceConfiguration serviceConfiguration = Environments.getCurrent().getXsuaaConfiguration(); + if (serviceConfiguration != null) { + xsAppId = Optional.of(serviceConfiguration.getProperty(ServiceConstants.XSUAA.APP_ID)); + } else { + LOGGER.warn( + "There is no xsuaa service configuration with 'xsappname' property: no local scope check possible."); + xsAppId = Optional.empty(); } return xsAppId; } diff --git a/java-security/src/main/java/com/sap/cloud/security/token/SapIdToken.java b/java-security/src/main/java/com/sap/cloud/security/token/SapIdToken.java index ceef67d5d8..5941c51490 100644 --- a/java-security/src/main/java/com/sap/cloud/security/token/SapIdToken.java +++ b/java-security/src/main/java/com/sap/cloud/security/token/SapIdToken.java @@ -37,14 +37,17 @@ public Service getService() { } /** - * Gets the token issuer domain that is required to check trust in the issuing identity service. - * In multi-tenant or single-tenant with custom domain scenarios, claim {@link TokenClaims#IAS_ISSUER} must be - * used for trust checks instead of claim {@link TokenClaims#ISSUER}. It contains the internal domain of the identity - * service. External customer domains are not trusted. - *

- * Use {@link Token#getClaimAsString(String)} with {@link TokenClaims#ISSUER} instead to get the custom domain if one is used. + * Gets the token issuer domain that is required to check trust in the issuing + * identity service. In multi-tenant or single-tenant with custom domain + * scenarios, claim {@link TokenClaims#IAS_ISSUER} must be used for trust checks + * instead of claim {@link TokenClaims#ISSUER}. It contains the internal domain + * of the identity service. External customer domains are not trusted.
+ *
+ * Use {@link Token#getClaimAsString(String)} with {@link TokenClaims#ISSUER} + * instead to get the custom domain if one is used. * - * @return value of claim {@link TokenClaims#IAS_ISSUER} if exists, otherwise value of {@link Token#getIssuer()} + * @return value of claim {@link TokenClaims#IAS_ISSUER} if exists, otherwise + * value of {@link Token#getIssuer()} */ @Override public String getIssuer() { diff --git a/java-security/src/main/java/com/sap/cloud/security/token/XsuaaToken.java b/java-security/src/main/java/com/sap/cloud/security/token/XsuaaToken.java index 37b631e8b3..bee78226bc 100644 --- a/java-security/src/main/java/com/sap/cloud/security/token/XsuaaToken.java +++ b/java-security/src/main/java/com/sap/cloud/security/token/XsuaaToken.java @@ -112,7 +112,8 @@ public Principal getPrincipal() { GrantType grantType = getGrantType(); String principalName; - if (grantType != null && (grantType.equals(GrantType.CLIENT_CREDENTIALS) || grantType.equals(GrantType.CLIENT_X509))) { + if (grantType != null + && (grantType.equals(GrantType.CLIENT_CREDENTIALS) || grantType.equals(GrantType.CLIENT_X509))) { principalName = String.format(UNIQUE_CLIENT_NAME_FORMAT, getClientId()); } else { principalName = getUniquePrincipalName(getClaimAsString(ORIGIN), getClaimAsString(USER_NAME)); diff --git a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtIssuerValidator.java b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtIssuerValidator.java index febb8096b6..d9ecb11862 100644 --- a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtIssuerValidator.java +++ b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtIssuerValidator.java @@ -44,9 +44,13 @@ class JwtIssuerValidator implements Validator { protected static final Logger LOGGER = LoggerFactory.getLogger(JwtIssuerValidator.class); /* - * The following validator brings backward-compatibility for test credentials in consumer applications written before 2.17.0 that are used to validate java-security-test tokens. - * This is necessary for successful validation of localhost issuers that include a port when 'localhost' is defined as trusted domain without port in the service credentials. - * Implementations of this interface absolutely MUST NOT be supplied outside test scope and MUST NOT be used for any other purpose to preserve application security. + * The following validator brings backward-compatibility for test credentials in + * consumer applications written before 2.17.0 that are used to validate + * java-security-test tokens. This is necessary for successful validation of + * localhost issuers that include a port when 'localhost' is defined as trusted + * domain without port in the service credentials. Implementations of this + * interface absolutely MUST NOT be supplied outside test scope and MUST NOT be + * used for any other purpose to preserve application security. */ static TestIssuerValidator localhostIssuerValidator; static { @@ -66,7 +70,8 @@ private static void tryLoadingLocalhostIssuerValidator() { localhostIssuerValidator = v; break; } - LOGGER.debug("loaded TestIssuerValidator service providers: {}. Using first one: {}.", validators, localhostIssuerValidator); + LOGGER.debug("loaded TestIssuerValidator service providers: {}. Using first one: {}.", validators, + localhostIssuerValidator); } protected static final String HTTPS_SCHEME = "https://"; @@ -91,36 +96,47 @@ public ValidationResult validate(Token token) { try { issuer = token.getIssuer(); - } catch(JsonParsingException e) { - return createInvalid("Issuer validation can not be performed because token issuer claim was not a String value."); + } catch (JsonParsingException e) { + return createInvalid( + "Issuer validation can not be performed because token issuer claim was not a String value."); } if (issuer == null || issuer.isBlank()) { - return createInvalid("Issuer validation can not be performed because token does not contain an issuer claim."); + return createInvalid( + "Issuer validation can not be performed because token does not contain an issuer claim."); } - String issuerUrl = issuer.startsWith(HTTPS_SCHEME) || issuer.startsWith("http://localhost") ? issuer : HTTPS_SCHEME + issuer; + String issuerUrl = issuer.startsWith(HTTPS_SCHEME) || issuer.startsWith("http://localhost") ? issuer + : HTTPS_SCHEME + issuer; try { new URL(issuerUrl); } catch (MalformedURLException e) { - return createInvalid("Issuer validation can not be performed because token issuer is not a valid URL suitable for https."); + return createInvalid( + "Issuer validation can not be performed because token issuer is not a valid URL suitable for https."); } - String issuerDomain = issuerUrl.substring(issuerUrl.indexOf("://") + 3); // issuerUrl was validated above to begin either with http:// or https:// - for(String d : domains) { - // a string that ends with . and contains 1-63 letters, digits or '-' before that for the subdomain + String issuerDomain = issuerUrl.substring(issuerUrl.indexOf("://") + 3); // issuerUrl was validated above to + // begin either with http:// or + // https:// + for (String d : domains) { + // a string that ends with . and contains 1-63 letters, digits or + // '-' before that for the subdomain String validSubdomainPattern = String.format("^[a-zA-Z0-9-]{1,63}\\.%s$", Pattern.quote(d)); - if(Objects.equals(d, issuerDomain) || issuerDomain.matches(validSubdomainPattern)) { + if (Objects.equals(d, issuerDomain) || issuerDomain.matches(validSubdomainPattern)) { return createValid(); } - if("localhost".equals(d) && localhostIssuerValidator != null && localhostIssuerValidator.isValidIssuer(issuer)) { - LOGGER.debug("Accepting {} as valid issuer on trusted domain 'localhost' for backward-compatibility with java-security-test.", issuer); + if ("localhost".equals(d) && localhostIssuerValidator != null + && localhostIssuerValidator.isValidIssuer(issuer)) { + LOGGER.debug( + "Accepting {} as valid issuer on trusted domain 'localhost' for backward-compatibility with java-security-test.", + issuer); return createValid(); } } - return createInvalid("Issuer {} was not a trusted domain or a subdomain of the trusted domains {}.", issuer, domains); + return createInvalid("Issuer {} was not a trusted domain or a subdomain of the trusted domains {}.", issuer, + domains); } } diff --git a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtSignatureValidator.java b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtSignatureValidator.java index 6a915f4826..c0ff43f3d7 100644 --- a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtSignatureValidator.java +++ b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtSignatureValidator.java @@ -26,87 +26,96 @@ /** * Validates the signature of the JWT.
* - retrieves the public key used for validation via the tokenKeyService.
- * - checks whether the signature section of the JWT is a valid signature for the header and payload sections for this public key. + * - checks whether the signature section of the JWT is a valid signature for + * the header and payload sections for this public key. */ abstract class JwtSignatureValidator implements Validator { - protected final OAuth2TokenKeyServiceWithCache tokenKeyService; - protected final OidcConfigurationServiceWithCache oidcConfigurationService; - protected final OAuth2ServiceConfiguration configuration; - - JwtSignatureValidator(OAuth2ServiceConfiguration configuration, OAuth2TokenKeyServiceWithCache tokenKeyService, - OidcConfigurationServiceWithCache oidcConfigurationService) { - assertNotNull(configuration, "JwtSignatureValidator requires configuration."); - assertNotNull(tokenKeyService, "JwtSignatureValidator requires a tokenKeyService."); - assertNotNull(oidcConfigurationService, "JwtSignatureValidator requires a oidcConfigurationService."); - - this.configuration = configuration; - this.tokenKeyService = tokenKeyService; - this.oidcConfigurationService = oidcConfigurationService; - } - - @Override - public ValidationResult validate(Token token) { - if(token.getTokenValue() == null) { - return createInvalid("JWT token validation failed because token content was null."); - } - - JwtSignatureAlgorithm algorithm = JwtSignatureAlgorithm.RS256; - if (token.hasHeaderParameter(ALG_PARAMETER_NAME)) { - String algHeader = token.getHeaderParameterAsString(ALG_PARAMETER_NAME); - algorithm = JwtSignatureAlgorithm.fromValue(algHeader); - if (algorithm == null) { - return createInvalid("JWT token validation with signature algorithm '" + algHeader + "' is not supported."); - } - } - - PublicKey publicKey; - try { - publicKey = getPublicKey(token, algorithm); - } catch (OAuth2ServiceException e) { - return createInvalid("Token signature can not be validated because JWKS could not be fetched: {}", e.getMessage()); - } catch (IllegalArgumentException | InvalidKeySpecException | NoSuchAlgorithmException e) { - return createInvalid("Token signature can not be validated because: {}", e.getMessage()); - } - - if(publicKey == null) { - return createInvalid("Token signature can not be validated because JWKS was empty."); - } - - return validateSignature(token, publicKey, algorithm); - } - - /** - * Service-specific implementation for the retrieval of the public key, e.g. via URL from JKU header (XSUAA) or OIDC .well-known endpoint (IAS) - */ - protected abstract PublicKey getPublicKey(Token token, JwtSignatureAlgorithm algorithm) throws OAuth2ServiceException, InvalidKeySpecException, NoSuchAlgorithmException; - - protected ValidationResult validateSignature(Token token, PublicKey publicKey, JwtSignatureAlgorithm algorithm) { - Signature publicSignature; - try { - publicSignature = Signature.getInstance(algorithm.javaSignature()); - } catch (NoSuchAlgorithmException e) { - return createInvalid("Token signature can not be validated because implementation of algorithm could not be found: {}", e.getMessage()); - } - - String[] tokenSections = token.getTokenValue().split("\\."); - if (tokenSections.length != 3) { - return createInvalid("Jwt token does not consist of three sections: 'header'.'payload'.'signature'."); - } - - String headerAndPayload = tokenSections[0] + "." + tokenSections[1]; - String signature = tokenSections[2]; - try { - publicSignature.initVerify(publicKey); - publicSignature.update(headerAndPayload.getBytes(UTF_8)); - - byte[] decodedSignatureBytes = Base64.getUrlDecoder().decode(signature); - if (publicSignature.verify(decodedSignatureBytes)) { - return createValid(); - } - - return createInvalid("Signature of Jwt Token is not valid: the identity provided by the JSON Web Token Key can not be trusted (Signature: {}).", signature); - } catch (Exception e) { - return createInvalid("Unexpected Error occurred during Json Web Signature Validation: {}.", e.getMessage()); - } - } + protected final OAuth2TokenKeyServiceWithCache tokenKeyService; + protected final OidcConfigurationServiceWithCache oidcConfigurationService; + protected final OAuth2ServiceConfiguration configuration; + + JwtSignatureValidator(OAuth2ServiceConfiguration configuration, OAuth2TokenKeyServiceWithCache tokenKeyService, + OidcConfigurationServiceWithCache oidcConfigurationService) { + assertNotNull(configuration, "JwtSignatureValidator requires configuration."); + assertNotNull(tokenKeyService, "JwtSignatureValidator requires a tokenKeyService."); + assertNotNull(oidcConfigurationService, "JwtSignatureValidator requires a oidcConfigurationService."); + + this.configuration = configuration; + this.tokenKeyService = tokenKeyService; + this.oidcConfigurationService = oidcConfigurationService; + } + + @Override + public ValidationResult validate(Token token) { + if (token.getTokenValue() == null) { + return createInvalid("JWT token validation failed because token content was null."); + } + + JwtSignatureAlgorithm algorithm = JwtSignatureAlgorithm.RS256; + if (token.hasHeaderParameter(ALG_PARAMETER_NAME)) { + String algHeader = token.getHeaderParameterAsString(ALG_PARAMETER_NAME); + algorithm = JwtSignatureAlgorithm.fromValue(algHeader); + if (algorithm == null) { + return createInvalid( + "JWT token validation with signature algorithm '" + algHeader + "' is not supported."); + } + } + + PublicKey publicKey; + try { + publicKey = getPublicKey(token, algorithm); + } catch (OAuth2ServiceException e) { + return createInvalid("Token signature can not be validated because JWKS could not be fetched: {}", + e.getMessage()); + } catch (IllegalArgumentException | InvalidKeySpecException | NoSuchAlgorithmException e) { + return createInvalid("Token signature can not be validated because: {}", e.getMessage()); + } + + if (publicKey == null) { + return createInvalid("Token signature can not be validated because JWKS was empty."); + } + + return validateSignature(token, publicKey, algorithm); + } + + /** + * Service-specific implementation for the retrieval of the public key, e.g. via + * URL from JKU header (XSUAA) or OIDC .well-known endpoint (IAS) + */ + protected abstract PublicKey getPublicKey(Token token, JwtSignatureAlgorithm algorithm) + throws OAuth2ServiceException, InvalidKeySpecException, NoSuchAlgorithmException; + + protected ValidationResult validateSignature(Token token, PublicKey publicKey, JwtSignatureAlgorithm algorithm) { + Signature publicSignature; + try { + publicSignature = Signature.getInstance(algorithm.javaSignature()); + } catch (NoSuchAlgorithmException e) { + return createInvalid( + "Token signature can not be validated because implementation of algorithm could not be found: {}", + e.getMessage()); + } + + String[] tokenSections = token.getTokenValue().split("\\."); + if (tokenSections.length != 3) { + return createInvalid("Jwt token does not consist of three sections: 'header'.'payload'.'signature'."); + } + + String headerAndPayload = tokenSections[0] + "." + tokenSections[1]; + String signature = tokenSections[2]; + try { + publicSignature.initVerify(publicKey); + publicSignature.update(headerAndPayload.getBytes(UTF_8)); + + byte[] decodedSignatureBytes = Base64.getUrlDecoder().decode(signature); + if (publicSignature.verify(decodedSignatureBytes)) { + return createValid(); + } + + return createInvalid( + "Signature of Jwt Token is not valid: the identity provided by the JSON Web Token Key can not be trusted (Signature: {}).", + signature); + } catch (Exception e) { + return createInvalid("Unexpected Error occurred during Json Web Signature Validation: {}.", e.getMessage()); + } + } } diff --git a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtValidatorBuilder.java b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtValidatorBuilder.java index fd41dab078..184659d573 100644 --- a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtValidatorBuilder.java +++ b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtValidatorBuilder.java @@ -218,14 +218,16 @@ private List> createDefaultValidators() { OAuth2TokenKeyServiceWithCache tokenKeyServiceWithCache = getTokenKeyServiceWithCache(); Optional.ofNullable(tokenKeyCacheConfiguration).ifPresent(tokenKeyServiceWithCache::withCacheConfiguration); if (configuration.getService() == XSUAA) { - signatureValidator = new XsuaaJwtSignatureValidator(configuration, tokenKeyServiceWithCache, getOidcConfigurationServiceWithCache()); + signatureValidator = new XsuaaJwtSignatureValidator(configuration, tokenKeyServiceWithCache, + getOidcConfigurationServiceWithCache()); } else if (configuration.getService() == IAS) { - if(configuration.getDomains() != null && !configuration.getDomains().isEmpty()) { + if (configuration.getDomains() != null && !configuration.getDomains().isEmpty()) { defaultValidators.add(new JwtIssuerValidator(configuration.getDomains())); } - signatureValidator = new SapIdJwtSignatureValidator(configuration, tokenKeyServiceWithCache, getOidcConfigurationServiceWithCache()); - if(isTenantIdCheckDisabled) { + signatureValidator = new SapIdJwtSignatureValidator(configuration, tokenKeyServiceWithCache, + getOidcConfigurationServiceWithCache()); + if (isTenantIdCheckDisabled) { ((SapIdJwtSignatureValidator) signatureValidator).disableTenantIdCheck(); } } diff --git a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/OAuth2TokenKeyServiceWithCache.java b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/OAuth2TokenKeyServiceWithCache.java index bb25d7e2db..5325d6283d 100644 --- a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/OAuth2TokenKeyServiceWithCache.java +++ b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/OAuth2TokenKeyServiceWithCache.java @@ -106,7 +106,9 @@ public OAuth2TokenKeyServiceWithCache withTokenKeyService(OAuth2TokenKeyService } /** - * Returns {@link OAuth2TokenKeyServiceWithCache#getPublicKey(JwtSignatureAlgorithm, String, URI, Map)} with {@link HttpHeaders#X_APP_TID} = appTid inside params. + * Returns + * {@link OAuth2TokenKeyServiceWithCache#getPublicKey(JwtSignatureAlgorithm, String, URI, Map)} + * with {@link HttpHeaders#X_APP_TID} = appTid inside params. */ @Nullable public PublicKey getPublicKey(JwtSignatureAlgorithm keyAlgorithm, String keyId, URI keyUri, String appTid) @@ -126,7 +128,8 @@ public PublicKey getPublicKey(JwtSignatureAlgorithm keyAlgorithm, String keyId, * the Token Key Uri (jwks) of the Access Token (can be tenant * specific). * @param params - * additional parameters that are sent along with the request. Use constants from {@link HttpHeaders} for the parameter keys. + * additional parameters that are sent along with the request. Use + * constants from {@link HttpHeaders} for the parameter keys. * @return a PublicKey * @throws OAuth2ServiceException * in case the call to the jwks endpoint of the identity service @@ -137,7 +140,8 @@ public PublicKey getPublicKey(JwtSignatureAlgorithm keyAlgorithm, String keyId, * in case the algorithm of the json web key is not supported. * */ - public PublicKey getPublicKey(JwtSignatureAlgorithm keyAlgorithm, String keyId, URI keyUri, Map params) + public PublicKey getPublicKey(JwtSignatureAlgorithm keyAlgorithm, String keyId, URI keyUri, + Map params) throws OAuth2ServiceException, InvalidKeySpecException, NoSuchAlgorithmException { assertNotNull(keyAlgorithm, "keyAlgorithm must not be null."); assertHasText(keyId, "keyId must not be null."); @@ -146,9 +150,9 @@ public PublicKey getPublicKey(JwtSignatureAlgorithm keyAlgorithm, String keyId, CacheKey cacheKey = new CacheKey(keyUri, params); JsonWebKeySet jwks = getCache().getIfPresent(cacheKey.toString()); - if(jwks == null) { - jwks = retrieveTokenKeysAndUpdateCache(cacheKey); - } + if (jwks == null) { + jwks = retrieveTokenKeysAndUpdateCache(cacheKey); + } if (jwks.getAll().isEmpty()) { LOGGER.error("Retrieved no token keys from {} for the given header parameters.", keyUri); @@ -165,14 +169,14 @@ public PublicKey getPublicKey(JwtSignatureAlgorithm keyAlgorithm, String keyId, throw new IllegalArgumentException("Key with kid " + keyId + " not found in JWKS."); } - private JsonWebKeySet retrieveTokenKeysAndUpdateCache(CacheKey cacheKey) throws OAuth2ServiceException { - String jwksJson = getTokenKeyService().retrieveTokenKeys(cacheKey.keyUri(), cacheKey.params()); + private JsonWebKeySet retrieveTokenKeysAndUpdateCache(CacheKey cacheKey) throws OAuth2ServiceException { + String jwksJson = getTokenKeyService().retrieveTokenKeys(cacheKey.keyUri(), cacheKey.params()); - JsonWebKeySet keySet = JsonWebKeySetFactory.createFromJson(jwksJson); - getCache().put(cacheKey.toString(), keySet); + JsonWebKeySet keySet = JsonWebKeySetFactory.createFromJson(jwksJson); + getCache().put(cacheKey.toString(), keySet); - return keySet; - } + return keySet; + } private TokenKeyCacheConfiguration getCheckedConfiguration(CacheConfiguration cacheConfiguration) { Assertions.assertNotNull(cacheConfiguration, "CacheConfiguration must not be null!"); diff --git a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/SapIdJwtSignatureValidator.java b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/SapIdJwtSignatureValidator.java index 86193f7322..74453b5d85 100644 --- a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/SapIdJwtSignatureValidator.java +++ b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/SapIdJwtSignatureValidator.java @@ -23,79 +23,86 @@ * Jwt Signature validator for OIDC tokens issued by Identity service */ class SapIdJwtSignatureValidator extends JwtSignatureValidator { - private boolean isTenantIdCheckEnabled = true; - - SapIdJwtSignatureValidator(OAuth2ServiceConfiguration configuration, OAuth2TokenKeyServiceWithCache tokenKeyService, OidcConfigurationServiceWithCache oidcConfigurationService) { - super(configuration, tokenKeyService, oidcConfigurationService); - } - - /** - * Disables the tenant id check. In case JWT issuer (`iss` claim) differs from `url` attribute of - * {@link OAuth2ServiceConfiguration}, claim {@link TokenClaims#SAP_GLOBAL_APP_TID} needs to be - * present in token to ensure that the tenant belongs to this issuer. - *

- * Use with caution as it relaxes the validation rules! It is not recommended to - * disable this check for standard Identity service setup. - */ - protected void disableTenantIdCheck() { - this.isTenantIdCheckEnabled = false; - } - - @Override - protected PublicKey getPublicKey(Token token, JwtSignatureAlgorithm algorithm) throws OAuth2ServiceException { - String keyId = DEFAULT_KEY_ID; - if (token.hasHeaderParameter(KID_PARAMETER_NAME)) { - keyId = token.getHeaderParameterAsString(KID_PARAMETER_NAME); - } - - URI jkuUri = getJwksUri(token); - Map params = new HashMap<>(3, 1); - params.put(HttpHeaders.X_APP_TID, token.getAppTid()); - params.put(HttpHeaders.X_CLIENT_ID, configuration.getClientId()); - params.put(HttpHeaders.X_AZP, token.getClaimAsString(TokenClaims.AUTHORIZATION_PARTY)); - - try { - return tokenKeyService.getPublicKey(algorithm, keyId, jkuUri, params); - } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { - throw new IllegalArgumentException(e); - } - } - - private URI getJwksUri(Token token) throws OAuth2ServiceException { - String domain = token.getIssuer(); - if (domain == null) { - throw new IllegalArgumentException("Token does not contain mandatory " + TokenClaims.ISSUER + " header."); - } - - if (isTenantIdCheckEnabled && !domain.equals("" + configuration.getUrl()) && token.getAppTid() == null) { - throw new IllegalArgumentException("OIDC token must provide the " + TokenClaims.SAP_GLOBAL_APP_TID + " claim for tenant validation when issuer is not the same as the url from the service credentials."); - } - - - return this.getOidcJwksUri(domain); - } - - /** - * Fetches the JWKS URI from the OIDC .well-known endpoint under the given domain that must have already been validated to be trustworthy in advance, e.g. with an additional {@link JwtIssuerValidator}. - * - * @param domain a trustworthy domain that supplies an OIDC .well-known endpoint - * @return the URI to the JWKS of the OIDC service under the given domain - * @throws OAuth2ServiceException if server call fails - */ - @Nonnull - private URI getOidcJwksUri(String domain) throws OAuth2ServiceException { - URI discoveryUri = DefaultOidcConfigurationService.getDiscoveryEndpointUri(domain); - - OAuth2ServiceEndpointsProvider endpointsProvider = oidcConfigurationService.getOrRetrieveEndpoints(discoveryUri); - if (endpointsProvider == null) { - throw new OAuth2ServiceException("OIDC .well-known configuration could not be retrieved."); - } - - URI jkuUri = endpointsProvider.getJwksUri(); - if (jkuUri == null) { - throw new IllegalArgumentException("OIDC .well-known response did not contain JWKS URI."); - } - - return jkuUri; - } + private boolean isTenantIdCheckEnabled = true; + + SapIdJwtSignatureValidator(OAuth2ServiceConfiguration configuration, OAuth2TokenKeyServiceWithCache tokenKeyService, + OidcConfigurationServiceWithCache oidcConfigurationService) { + super(configuration, tokenKeyService, oidcConfigurationService); + } + + /** + * Disables the tenant id check. In case JWT issuer (`iss` claim) differs from + * `url` attribute of {@link OAuth2ServiceConfiguration}, claim + * {@link TokenClaims#SAP_GLOBAL_APP_TID} needs to be present in token to ensure + * that the tenant belongs to this issuer. + *

+ * Use with caution as it relaxes the validation rules! It is not recommended to + * disable this check for standard Identity service setup. + */ + protected void disableTenantIdCheck() { + this.isTenantIdCheckEnabled = false; + } + + @Override + protected PublicKey getPublicKey(Token token, JwtSignatureAlgorithm algorithm) throws OAuth2ServiceException { + String keyId = DEFAULT_KEY_ID; + if (token.hasHeaderParameter(KID_PARAMETER_NAME)) { + keyId = token.getHeaderParameterAsString(KID_PARAMETER_NAME); + } + + URI jkuUri = getJwksUri(token); + Map params = new HashMap<>(3, 1); + params.put(HttpHeaders.X_APP_TID, token.getAppTid()); + params.put(HttpHeaders.X_CLIENT_ID, configuration.getClientId()); + params.put(HttpHeaders.X_AZP, token.getClaimAsString(TokenClaims.AUTHORIZATION_PARTY)); + + try { + return tokenKeyService.getPublicKey(algorithm, keyId, jkuUri, params); + } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + } + + private URI getJwksUri(Token token) throws OAuth2ServiceException { + String domain = token.getIssuer(); + if (domain == null) { + throw new IllegalArgumentException("Token does not contain mandatory " + TokenClaims.ISSUER + " header."); + } + + if (isTenantIdCheckEnabled && !domain.equals("" + configuration.getUrl()) && token.getAppTid() == null) { + throw new IllegalArgumentException("OIDC token must provide the " + TokenClaims.SAP_GLOBAL_APP_TID + + " claim for tenant validation when issuer is not the same as the url from the service credentials."); + } + + return this.getOidcJwksUri(domain); + } + + /** + * Fetches the JWKS URI from the OIDC .well-known endpoint under the given + * domain that must have already been validated to be trustworthy in advance, + * e.g. with an additional {@link JwtIssuerValidator}. + * + * @param domain + * a trustworthy domain that supplies an OIDC .well-known endpoint + * @return the URI to the JWKS of the OIDC service under the given domain + * @throws OAuth2ServiceException + * if server call fails + */ + @Nonnull + private URI getOidcJwksUri(String domain) throws OAuth2ServiceException { + URI discoveryUri = DefaultOidcConfigurationService.getDiscoveryEndpointUri(domain); + + OAuth2ServiceEndpointsProvider endpointsProvider = oidcConfigurationService + .getOrRetrieveEndpoints(discoveryUri); + if (endpointsProvider == null) { + throw new OAuth2ServiceException("OIDC .well-known configuration could not be retrieved."); + } + + URI jkuUri = endpointsProvider.getJwksUri(); + if (jkuUri == null) { + throw new IllegalArgumentException("OIDC .well-known response did not contain JWKS URI."); + } + + return jkuUri; + } } diff --git a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/XsuaaJwtSignatureValidator.java b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/XsuaaJwtSignatureValidator.java index 0c27020fa7..483c2f6f93 100644 --- a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/XsuaaJwtSignatureValidator.java +++ b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/XsuaaJwtSignatureValidator.java @@ -23,82 +23,94 @@ * Jwt Signature validator for Access tokens issued by Xsuaa service */ class XsuaaJwtSignatureValidator extends JwtSignatureValidator { - public static final Logger LOGGER = LoggerFactory.getLogger(XsuaaJwtSignatureValidator.class); - - /* - * The following list of factories brings backward-compatibility for test credentials in consumer applications written before 2.17.0 that are used to validate java-security-test tokens. - * This is necessary to construct the correct JKU when 'localhost' without port is defined as uaadomain in the service credentials. - * Implementations of this interface absolutely MUST NOT be supplied outside test scope and MUST NOT be used for any other purpose to preserve application security. - */ - List jkuFactories = new ArrayList<>() { - { - try { - ServiceLoader.load(XsuaaJkuFactory.class).forEach(this::add); - LOGGER.debug("loaded XsuaaJkuFactory service providers: {}", this); - } catch (Exception | ServiceConfigurationError e) { - LOGGER.warn("Unexpected failure while loading XsuaaJkuFactory service providers: {}", e.getMessage()); - } - } - }; - - XsuaaJwtSignatureValidator(OAuth2ServiceConfiguration configuration, OAuth2TokenKeyServiceWithCache tokenKeyService, OidcConfigurationServiceWithCache oidcConfigurationService) { - super(configuration, tokenKeyService, oidcConfigurationService); - } - - @Override - protected PublicKey getPublicKey(Token token, JwtSignatureAlgorithm algorithm) throws OAuth2ServiceException, InvalidKeySpecException, NoSuchAlgorithmException { - PublicKey key = null; - - try { - key = fetchPublicKey(token, algorithm); - } catch (OAuth2ServiceException | InvalidKeySpecException | NoSuchAlgorithmException | IllegalArgumentException e) { - if (!configuration.hasProperty(ServiceConstants.XSUAA.VERIFICATION_KEY)) { - throw e; - } - } - - if (key == null && configuration.hasProperty(ServiceConstants.XSUAA.VERIFICATION_KEY)) { - String fallbackKey = configuration.getProperty(ServiceConstants.XSUAA.VERIFICATION_KEY); - try { - key = JsonWebKeyImpl.createPublicKeyFromPemEncodedPublicKey(JwtSignatureAlgorithm.RS256, fallbackKey); - } catch (NoSuchAlgorithmException | InvalidKeySpecException ex) { - throw new IllegalArgumentException("Fallback validation key supplied via " + ServiceConstants.XSUAA.VERIFICATION_KEY + " property in service credentials could not be used: {}", ex); - } - } - - return key; - } - - - private PublicKey fetchPublicKey(Token token, JwtSignatureAlgorithm algorithm) throws OAuth2ServiceException, InvalidKeySpecException, NoSuchAlgorithmException { - String keyId = configuration.isLegacyMode() ? KEY_ID_VALUE_LEGACY : token.getHeaderParameterAsString(KID_PARAMETER_NAME); - if (keyId == null) { - throw new IllegalArgumentException("Token does not contain the mandatory " + KID_PARAMETER_NAME + " header."); - } - - String zidQueryParam = composeZidQueryParameter(token); - - String jwksUri; - if (jkuFactories.isEmpty()) { - jwksUri = configuration.isLegacyMode() - ? configuration.getUrl() + "/token_keys" - : configuration.getProperty(UAA_DOMAIN) + "/token_keys" + zidQueryParam; - } else { - LOGGER.info("Loaded custom JKU factory"); - jwksUri = jkuFactories.get(0).create(token.getTokenValue()); - } - - URI uri = URI.create(jwksUri); - uri = uri.isAbsolute() ? uri : URI.create("https://" + jwksUri); - Map params = Collections.singletonMap(HttpHeaders.X_ZID, token.getAppTid()); - return tokenKeyService.getPublicKey(algorithm, keyId, uri, params); - } - - private String composeZidQueryParameter(Token token) { - String zid = token.getAppTid(); - if (zid != null && !zid.isBlank()){ - return "?zid=" + zid; - } - return ""; - } + public static final Logger LOGGER = LoggerFactory.getLogger(XsuaaJwtSignatureValidator.class); + + /* + * The following list of factories brings backward-compatibility for test + * credentials in consumer applications written before 2.17.0 that are used to + * validate java-security-test tokens. This is necessary to construct the + * correct JKU when 'localhost' without port is defined as uaadomain in the + * service credentials. Implementations of this interface absolutely MUST NOT be + * supplied outside test scope and MUST NOT be used for any other purpose to + * preserve application security. + */ + List jkuFactories = new ArrayList<>() { + { + try { + ServiceLoader.load(XsuaaJkuFactory.class).forEach(this::add); + LOGGER.debug("loaded XsuaaJkuFactory service providers: {}", this); + } catch (Exception | ServiceConfigurationError e) { + LOGGER.warn("Unexpected failure while loading XsuaaJkuFactory service providers: {}", e.getMessage()); + } + } + }; + + XsuaaJwtSignatureValidator(OAuth2ServiceConfiguration configuration, OAuth2TokenKeyServiceWithCache tokenKeyService, + OidcConfigurationServiceWithCache oidcConfigurationService) { + super(configuration, tokenKeyService, oidcConfigurationService); + } + + @Override + protected PublicKey getPublicKey(Token token, JwtSignatureAlgorithm algorithm) + throws OAuth2ServiceException, InvalidKeySpecException, NoSuchAlgorithmException { + PublicKey key = null; + + try { + key = fetchPublicKey(token, algorithm); + } catch (OAuth2ServiceException | InvalidKeySpecException | NoSuchAlgorithmException + | IllegalArgumentException e) { + if (!configuration.hasProperty(ServiceConstants.XSUAA.VERIFICATION_KEY)) { + throw e; + } + } + + if (key == null && configuration.hasProperty(ServiceConstants.XSUAA.VERIFICATION_KEY)) { + String fallbackKey = configuration.getProperty(ServiceConstants.XSUAA.VERIFICATION_KEY); + try { + key = JsonWebKeyImpl.createPublicKeyFromPemEncodedPublicKey(JwtSignatureAlgorithm.RS256, fallbackKey); + } catch (NoSuchAlgorithmException | InvalidKeySpecException ex) { + throw new IllegalArgumentException( + "Fallback validation key supplied via " + ServiceConstants.XSUAA.VERIFICATION_KEY + + " property in service credentials could not be used: {}", + ex); + } + } + + return key; + } + + private PublicKey fetchPublicKey(Token token, JwtSignatureAlgorithm algorithm) + throws OAuth2ServiceException, InvalidKeySpecException, NoSuchAlgorithmException { + String keyId = configuration.isLegacyMode() ? KEY_ID_VALUE_LEGACY + : token.getHeaderParameterAsString(KID_PARAMETER_NAME); + if (keyId == null) { + throw new IllegalArgumentException( + "Token does not contain the mandatory " + KID_PARAMETER_NAME + " header."); + } + + String zidQueryParam = composeZidQueryParameter(token); + + String jwksUri; + if (jkuFactories.isEmpty()) { + jwksUri = configuration.isLegacyMode() + ? configuration.getUrl() + "/token_keys" + : configuration.getProperty(UAA_DOMAIN) + "/token_keys" + zidQueryParam; + } else { + LOGGER.info("Loaded custom JKU factory"); + jwksUri = jkuFactories.get(0).create(token.getTokenValue()); + } + + URI uri = URI.create(jwksUri); + uri = uri.isAbsolute() ? uri : URI.create("https://" + jwksUri); + Map params = Collections.singletonMap(HttpHeaders.X_ZID, token.getAppTid()); + return tokenKeyService.getPublicKey(algorithm, keyId, uri, params); + } + + private String composeZidQueryParameter(Token token) { + String zid = token.getAppTid(); + if (zid != null && !zid.isBlank()) { + return "?zid=" + zid; + } + return ""; + } } diff --git a/java-security/src/test/java/com/sap/cloud/security/servlet/HybridTokenFactoryTest.java b/java-security/src/test/java/com/sap/cloud/security/servlet/HybridTokenFactoryTest.java new file mode 100644 index 0000000000..1da1174474 --- /dev/null +++ b/java-security/src/test/java/com/sap/cloud/security/servlet/HybridTokenFactoryTest.java @@ -0,0 +1,50 @@ +package com.sap.cloud.security.servlet; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import com.sap.cloud.security.token.XsuaaToken; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class HybridTokenFactoryTest { + + private ListAppender logWatcher; + private HybridTokenFactory cut; + + @BeforeEach + public void setup() { + cut = new HybridTokenFactory(); + cut.xsAppId = null; + cut.xsScopeConverter = null; + logWatcher = new ListAppender<>(); + logWatcher.start(); + ((Logger) LoggerFactory.getLogger(HybridTokenFactory.class)).addAppender(logWatcher); + } + + @AfterEach + void teardown() { + ((Logger) LoggerFactory.getLogger(HybridTokenFactory.class)).detachAndStopAllAppenders(); + } + + @Test + void oneWarningMessageIncaseSecurityConfigIsMissing() throws IOException { + String jwt = IOUtils.resourceToString("/xsuaaJwtBearerTokenRSA256.txt", UTF_8); + XsuaaToken token = (XsuaaToken) cut.create(jwt); + cut.create(jwt); + + assertThat(token.getIssuer()).isEqualTo("http://auth.com"); + assertThrows(IllegalArgumentException.class, () -> token.hasLocalScope("abc")); + assertThat(logWatcher.list).isNotNull().hasSize(1); + assertThat(logWatcher.list.get(0).getMessage()).contains("There is no xsuaa service configuration"); + } +} \ No newline at end of file diff --git a/java-security/src/test/java/com/sap/cloud/security/token/XsuaaTokenTest.java b/java-security/src/test/java/com/sap/cloud/security/token/XsuaaTokenTest.java index 8832a5481b..2a17b0dc94 100644 --- a/java-security/src/test/java/com/sap/cloud/security/token/XsuaaTokenTest.java +++ b/java-security/src/test/java/com/sap/cloud/security/token/XsuaaTokenTest.java @@ -34,7 +34,8 @@ public XsuaaTokenTest() throws IOException { @Test public void constructor_raiseIllegalArgumentExceptions() { - assertThatThrownBy(() -> new XsuaaToken("")).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("jwtToken must not be null / empty"); + assertThatThrownBy(() -> new XsuaaToken("")).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("jwtToken must not be null / empty"); assertThatThrownBy(() -> new XsuaaToken("abc")).isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("JWT token does not consist of 'header'.'payload'.'signature'."); diff --git a/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/IdTokenSignatureValidatorTest.java b/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/IdTokenSignatureValidatorTest.java index 392dc11451..bb4658a9a0 100644 --- a/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/IdTokenSignatureValidatorTest.java +++ b/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/IdTokenSignatureValidatorTest.java @@ -73,7 +73,7 @@ public void setup() throws IOException { tokenKeyServiceMock = Mockito.mock(OAuth2TokenKeyService.class); when(tokenKeyServiceMock.retrieveTokenKeys(JKU_URI, PARAMS)) - .thenReturn(IOUtils.resourceToString("/iasJsonWebTokenKeys.json", UTF_8)); + .thenReturn(IOUtils.resourceToString("/iasJsonWebTokenKeys.json", UTF_8)); cut = new SapIdJwtSignatureValidator( mockConfiguration, diff --git a/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/JwtIssuerValidatorTest.java b/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/JwtIssuerValidatorTest.java index 500069f108..405ab5b0c8 100644 --- a/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/JwtIssuerValidatorTest.java +++ b/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/JwtIssuerValidatorTest.java @@ -66,7 +66,8 @@ void validationSucceeds_forValidIssuers(String issuer) { } /** - * Test ensures that issuer validation also succeeds for servers running on http://localhost:, e.g. when using java-security-test module. + * Test ensures that issuer validation also succeeds for servers running on + * http://localhost:, e.g. when using java-security-test module. */ @Test void supportsHttpLocalhostIssuers() { @@ -81,7 +82,7 @@ void supportsHttpLocalhostIssuers() { @Test void validationFails_whenSubdomainHasMoreThan63Characters() { - for(String d : trustedDomains) { + for (String d : trustedDomains) { when(token.getIssuer()).thenReturn("https://a." + d); assertThat(cut.validate(token).isValid(), is(true)); diff --git a/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/OAuth2TokenKeyServiceWithCacheTest.java b/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/OAuth2TokenKeyServiceWithCacheTest.java index 2894bed269..f0caae1840 100644 --- a/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/OAuth2TokenKeyServiceWithCacheTest.java +++ b/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/OAuth2TokenKeyServiceWithCacheTest.java @@ -41,9 +41,9 @@ public class OAuth2TokenKeyServiceWithCacheTest { private static final String CLIENT_ID = "client_id"; private static final String AZP = "azp"; private static final Map PARAMS = Map.of( - HttpHeaders.X_APP_TID, APP_TID, - HttpHeaders.X_CLIENT_ID, CLIENT_ID, - HttpHeaders.X_AZP, AZP); + HttpHeaders.X_APP_TID, APP_TID, + HttpHeaders.X_CLIENT_ID, CLIENT_ID, + HttpHeaders.X_AZP, AZP); @Before public void setup() throws IOException { @@ -89,9 +89,11 @@ public void changeCacheConfiguration_tooLongDuration_leftUnchanged() { } @Test - public void retrieveTokenKeysUsesCorrectParams() throws OAuth2ServiceException, InvalidKeySpecException, NoSuchAlgorithmException { + public void retrieveTokenKeysUsesCorrectParams() + throws OAuth2ServiceException, InvalidKeySpecException, NoSuchAlgorithmException { PublicKey key1 = cut.getPublicKey(JwtSignatureAlgorithm.RS256, "key-id-0", TOKEN_KEYS_URI, PARAMS); - Map otherParams = Map.of(HttpHeaders.X_APP_TID, "otherAppTid", HttpHeaders.X_CLIENT_ID, "otherClientId"); + Map otherParams = Map.of(HttpHeaders.X_APP_TID, "otherAppTid", HttpHeaders.X_CLIENT_ID, + "otherClientId"); PublicKey key2 = cut.getPublicKey(JwtSignatureAlgorithm.RS256, "key-id-0", TOKEN_KEYS_URI, otherParams); assertThat(String.valueOf(key1.getAlgorithm())).isEqualTo("RSA"); @@ -139,7 +141,8 @@ public void retrieveTokenKeys_afterCacheWasCleared() } @Test - public void getCachedTokenKeys_noAppTid_noAzp() throws IOException, InvalidKeySpecException, NoSuchAlgorithmException { + public void getCachedTokenKeys_noAppTid_noAzp() + throws IOException, InvalidKeySpecException, NoSuchAlgorithmException { Map params = Map.of(HttpHeaders.X_CLIENT_ID, CLIENT_ID); when(tokenKeyServiceMock.retrieveTokenKeys(eq(TOKEN_KEYS_URI), eq(params))) .thenReturn(IOUtils.resourceToString("/jsonWebTokenKeys.json", StandardCharsets.UTF_8)); @@ -189,14 +192,17 @@ public void retrieveTokenKeysForNewKeyId() public void retrieveTokenKeysDoesNotCacheOnServerException() throws OAuth2ServiceException, InvalidKeySpecException, NoSuchAlgorithmException { Map invalidParams = Map.of(HttpHeaders.X_APP_TID, "invalidAppTid"); - when(tokenKeyServiceMock.retrieveTokenKeys(any(), eq(invalidParams))).thenThrow(new OAuth2ServiceException("Invalid parameters provided")); + when(tokenKeyServiceMock.retrieveTokenKeys(any(), eq(invalidParams))) + .thenThrow(new OAuth2ServiceException("Invalid parameters provided")); cut.getPublicKey(JwtSignatureAlgorithm.RS256, "key-id-0", TOKEN_KEYS_URI, PARAMS); - assertThatThrownBy(() -> cut.getPublicKey(JwtSignatureAlgorithm.RS256, "key-id-0", TOKEN_KEYS_URI, invalidParams)). - isInstanceOf(OAuth2ServiceException.class).hasMessageStartingWith("Invalid"); + assertThatThrownBy( + () -> cut.getPublicKey(JwtSignatureAlgorithm.RS256, "key-id-0", TOKEN_KEYS_URI, invalidParams)) + .isInstanceOf(OAuth2ServiceException.class).hasMessageStartingWith("Invalid"); - assertThatThrownBy(() -> cut.getPublicKey(JwtSignatureAlgorithm.RS256, "key-id-0", TOKEN_KEYS_URI, invalidParams)) - .isInstanceOf(OAuth2ServiceException.class).hasMessageStartingWith("Invalid"); + assertThatThrownBy( + () -> cut.getPublicKey(JwtSignatureAlgorithm.RS256, "key-id-0", TOKEN_KEYS_URI, invalidParams)) + .isInstanceOf(OAuth2ServiceException.class).hasMessageStartingWith("Invalid"); verify(tokenKeyServiceMock, times(1)).retrieveTokenKeys(any(), eq(PARAMS)); verify(tokenKeyServiceMock, times(2)).retrieveTokenKeys(any(), eq(invalidParams)); diff --git a/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/SapIdJwtSignatureValidatorTest.java b/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/SapIdJwtSignatureValidatorTest.java index 5d59f45566..5f335a8a16 100644 --- a/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/SapIdJwtSignatureValidatorTest.java +++ b/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/SapIdJwtSignatureValidatorTest.java @@ -59,7 +59,7 @@ public void setup() throws IOException { tokenKeyServiceMock = Mockito.mock(OAuth2TokenKeyService.class); when(tokenKeyServiceMock .retrieveTokenKeys(any(), anyMap())) - .thenReturn(IOUtils.resourceToString("/iasJsonWebTokenKeys.json", UTF_8)); + .thenReturn(IOUtils.resourceToString("/iasJsonWebTokenKeys.json", UTF_8)); endpointsProviderMock = Mockito.mock(OAuth2ServiceEndpointsProvider.class); when(endpointsProviderMock.getJwksUri()).thenReturn(DUMMY_JKU_URI); @@ -76,28 +76,30 @@ public void setup() throws IOException { @Test public void validate_throwsWhenTokenIsNull() { - Token tokenSpy = Mockito.spy(iasToken); - doReturn(null).when(tokenSpy).getTokenValue(); + Token tokenSpy = Mockito.spy(iasToken); + doReturn(null).when(tokenSpy).getTokenValue(); ValidationResult validationResult = cut.validate(tokenSpy); assertTrue(validationResult.isErroneous()); - assertThat(validationResult.getErrorDescription(), containsString("JWT token validation failed because token content was null.")); + assertThat(validationResult.getErrorDescription(), + containsString("JWT token validation failed because token content was null.")); } @Test public void validate_throwsWhenAlgorithmIsNull() { - Token tokenSpy = Mockito.spy(iasToken); - doReturn(null).when(tokenSpy).getHeaderParameterAsString(JsonWebKeyConstants.ALG_PARAMETER_NAME); + Token tokenSpy = Mockito.spy(iasToken); + doReturn(null).when(tokenSpy).getHeaderParameterAsString(JsonWebKeyConstants.ALG_PARAMETER_NAME); ValidationResult validationResult = cut.validate(tokenSpy); assertTrue(validationResult.isErroneous()); - assertThat(validationResult.getErrorDescription(), containsString("JWT token validation with signature algorithm 'null' is not supported")); + assertThat(validationResult.getErrorDescription(), + containsString("JWT token validation with signature algorithm 'null' is not supported")); } @Test public void validate_throwsWhenKeyIdIsNull() { - Token tokenSpy = Mockito.spy(iasToken); - doReturn(null).when(tokenSpy).getHeaderParameterAsString(JsonWebKeyConstants.KID_PARAMETER_NAME); + Token tokenSpy = Mockito.spy(iasToken); + doReturn(null).when(tokenSpy).getHeaderParameterAsString(JsonWebKeyConstants.KID_PARAMETER_NAME); ValidationResult validationResult = cut.validate(tokenSpy); assertTrue(validationResult.isErroneous()); @@ -118,7 +120,8 @@ public void validationFails_WhenAppTidIsNull() { ValidationResult validationResult = cut.validate(iasPaasToken); assertTrue(validationResult.isErroneous()); assertThat(validationResult.getErrorDescription(), - containsString("Token signature can not be validated because: OIDC token must provide the app_tid claim for tenant validation when issuer is not the same as the url from the service credentials.")); + containsString( + "Token signature can not be validated because: OIDC token must provide the app_tid claim for tenant validation when issuer is not the same as the url from the service credentials.")); } @Test @@ -162,35 +165,39 @@ public void validationFails_whenJwtProvidesNoSignature() { String tokenWithNoSignature = new StringBuilder(tokenHeaderPayloadSignature[0]) .append(".") .append(tokenHeaderPayloadSignature[1]).toString(); - Token tokenSpy = Mockito.spy(iasToken); - doReturn(tokenWithNoSignature).when(tokenSpy).getTokenValue(); + Token tokenSpy = Mockito.spy(iasToken); + doReturn(tokenWithNoSignature).when(tokenSpy).getTokenValue(); ValidationResult result = cut.validate(tokenSpy); assertThat(result.isErroneous(), is(true)); - assertThat(result.getErrorDescription(), containsString("Jwt token does not consist of three sections: 'header'.'payload'.'signature'.")); + assertThat(result.getErrorDescription(), + containsString("Jwt token does not consist of three sections: 'header'.'payload'.'signature'.")); } @Test public void validationFails_whenTokenAlgorithmIsNotSupported() { - Token tokenSpy = Mockito.spy(iasToken); + Token tokenSpy = Mockito.spy(iasToken); String unsupportedAlgorithm = "UnsupportedAlgorithm"; - doReturn(unsupportedAlgorithm).when(tokenSpy).getHeaderParameterAsString(JsonWebKeyConstants.ALG_PARAMETER_NAME); + doReturn(unsupportedAlgorithm).when(tokenSpy) + .getHeaderParameterAsString(JsonWebKeyConstants.ALG_PARAMETER_NAME); - ValidationResult validationResult = cut.validate(tokenSpy); + ValidationResult validationResult = cut.validate(tokenSpy); assertThat(validationResult.isErroneous(), is(true)); - assertThat(validationResult.getErrorDescription(), startsWith("JWT token validation with signature algorithm '" + unsupportedAlgorithm + "' is not supported.")); + assertThat(validationResult.getErrorDescription(), startsWith( + "JWT token validation with signature algorithm '" + unsupportedAlgorithm + "' is not supported.")); } @Test public void validationFails_whenTokenAlgorithmIsNone() { - Token tokenSpy = Mockito.spy(iasToken); - doReturn("NONE").when(tokenSpy).getHeaderParameterAsString(JsonWebKeyConstants.ALG_PARAMETER_NAME); + Token tokenSpy = Mockito.spy(iasToken); + doReturn("NONE").when(tokenSpy).getHeaderParameterAsString(JsonWebKeyConstants.ALG_PARAMETER_NAME); - ValidationResult validationResult = cut.validate(tokenSpy); + ValidationResult validationResult = cut.validate(tokenSpy); - assertThat(validationResult.isErroneous(), is(true)); - assertThat(validationResult.getErrorDescription(), startsWith("JWT token validation with signature algorithm 'NONE' is not supported.")); + assertThat(validationResult.isErroneous(), is(true)); + assertThat(validationResult.getErrorDescription(), + startsWith("JWT token validation with signature algorithm 'NONE' is not supported.")); } @Test diff --git a/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/XsaJwtSignatureValidatorTest.java b/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/XsaJwtSignatureValidatorTest.java index 19926bffcd..52bf16e101 100644 --- a/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/XsaJwtSignatureValidatorTest.java +++ b/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/XsaJwtSignatureValidatorTest.java @@ -52,7 +52,7 @@ public void setup() throws IOException { tokenKeyServiceMock = Mockito.mock(OAuth2TokenKeyService.class); when(tokenKeyServiceMock .retrieveTokenKeys(eq(JKU_URI), anyMap())) - .thenReturn(IOUtils.resourceToString("/jsonWebTokenKeys.json", UTF_8)); + .thenReturn(IOUtils.resourceToString("/jsonWebTokenKeys.json", UTF_8)); cut = new XsuaaJwtSignatureValidator( mockConfiguration, diff --git a/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/XsuaaJwtSignatureValidatorTest.java b/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/XsuaaJwtSignatureValidatorTest.java index 8eeb54832e..b50d1d4f9a 100644 --- a/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/XsuaaJwtSignatureValidatorTest.java +++ b/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/XsuaaJwtSignatureValidatorTest.java @@ -59,9 +59,10 @@ public void setup() throws IOException { tokenKeyServiceMock = Mockito.mock(OAuth2TokenKeyService.class); when(tokenKeyServiceMock - .retrieveTokenKeys(URI.create("https://authentication.stagingaws.hanavlab.ondemand.com/token_keys?zid=uaa"), + .retrieveTokenKeys( + URI.create("https://authentication.stagingaws.hanavlab.ondemand.com/token_keys?zid=uaa"), Map.of(HttpHeaders.X_ZID, "uaa"))) - .thenReturn(IOUtils.resourceToString("/jsonWebTokenKeys.json", UTF_8)); + .thenReturn(IOUtils.resourceToString("/jsonWebTokenKeys.json", UTF_8)); cut = new XsuaaJwtSignatureValidator( mockConfiguration, diff --git a/pom.xml b/pom.xml index 15b477f6bf..89233a1c0b 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ com.sap.cloud.security.xsuaa parent - 3.3.2 + 3.3.4 pom parent @@ -57,33 +57,33 @@ 17 3.2.1 - 3.2.1 - 6.1.2 + 3.2.2 + 6.1.3 6.2.1 2.5.2.RELEASE 1.1.1.RELEASE - 12.0.5 - 3.6.1 + 12.0.5 + 3.6.2 2.22.1 - 2.0.10 + 2.0.11 20231013 - 0.10.1 - 5.3 + 0.10.2 + 5.3.1 4.5.14 3.1.5 2.15.1 6.0.0 4.13.2 5.9.2 - 3.2.3 + 3.2.5 1.3 - 5.8.0 - 3.24.2 + 5.9.0 + 3.25.2 3.3.1 - 3.6.1 + 3.6.2 1.3.2 4.8.3 - 4.8.2.0 + 4.8.3.0 false ${skipTests} @@ -105,12 +105,6 @@ - - - org.springframework - spring-expression - ${spring.core.version} - com.sap.cloud.security @@ -119,20 +113,6 @@ pom import - - org.eclipse.jetty - jetty-bom - ${org.eclipse.jetty.bom.version} - pom - import - - - org.eclipse.jetty.ee9 - jetty-ee9-bom - ${org.eclipse.jetty.bom.version} - pom - import - @@ -340,7 +320,7 @@ org.apache.maven.plugins maven-pmd-plugin - 3.21.0 + 3.21.2 ${skipTests} @@ -405,7 +385,7 @@ org.owasp dependency-check-maven - 9.0.7 + 9.0.9 diff --git a/samples/java-security-usage-ias/pom.xml b/samples/java-security-usage-ias/pom.xml index 05c539a48f..37df71b8aa 100755 --- a/samples/java-security-usage-ias/pom.xml +++ b/samples/java-security-usage-ias/pom.xml @@ -6,13 +6,13 @@ 4.0.0 com.sap.cloud.security.xssec.samples java-security-usage-ias - 3.3.2 + 3.3.4 war 17 17 - 3.3.2 + 3.3.4 2.0.5 4.5.14 6.0.0 diff --git a/samples/java-security-usage/pom.xml b/samples/java-security-usage/pom.xml index 4d4a3371a2..7d9f644d80 100755 --- a/samples/java-security-usage/pom.xml +++ b/samples/java-security-usage/pom.xml @@ -6,7 +6,7 @@ 4.0.0 com.sap.cloud.security.xssec.samples java-security-usage - 3.3.2 + 3.3.4 war dependency-check-maven - 9.0.2 + 9.0.6 diff --git a/samples/spring-security-xsuaa-usage/pom.xml b/samples/spring-security-xsuaa-usage/pom.xml index 588f2f9a74..a8ab8e4230 100644 --- a/samples/spring-security-xsuaa-usage/pom.xml +++ b/samples/spring-security-xsuaa-usage/pom.xml @@ -16,7 +16,7 @@ com.sap.cloud.security.samples spring-security-xsuaa-usage - 3.3.2 + 3.3.4 spring-security-xsuaa-usage @@ -29,7 +29,7 @@ UTF-8 UTF-8 17 - 3.3.2 + 3.3.4 5.2.1 @@ -98,7 +98,7 @@ org.owasp dependency-check-maven - 9.0.2 + 9.0.6 diff --git a/samples/spring-webflux-security-xsuaa-usage/pom.xml b/samples/spring-webflux-security-xsuaa-usage/pom.xml index b640c5bef1..451c99a1f8 100644 --- a/samples/spring-webflux-security-xsuaa-usage/pom.xml +++ b/samples/spring-webflux-security-xsuaa-usage/pom.xml @@ -81,7 +81,7 @@ org.owasp dependency-check-maven - 9.0.2 + 9.0.6 diff --git a/spring-security-compatibility/pom.xml b/spring-security-compatibility/pom.xml index d704e46586..c77e4cd900 100644 --- a/spring-security-compatibility/pom.xml +++ b/spring-security-compatibility/pom.xml @@ -9,7 +9,7 @@ com.sap.cloud.security.xsuaa parent - 3.3.2 + 3.3.4 com.sap.cloud.security.xsuaa diff --git a/spring-security-compatibility/src/test/java/com/sap/cloud/security/comp/XsuaaTokenCompTest.java b/spring-security-compatibility/src/test/java/com/sap/cloud/security/comp/XsuaaTokenCompTest.java index 39b5fe218f..061971c69c 100644 --- a/spring-security-compatibility/src/test/java/com/sap/cloud/security/comp/XsuaaTokenCompTest.java +++ b/spring-security-compatibility/src/test/java/com/sap/cloud/security/comp/XsuaaTokenCompTest.java @@ -1,6 +1,6 @@ /** - * SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Cloud Security Client Java contributors - * + * SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Cloud Security Client Java contributors* + *

* SPDX-License-Identifier: Apache-2.0 */ package com.sap.cloud.security.comp; @@ -227,7 +227,7 @@ void getSubdomainFails() { @Test void getAppToken() { token = XsuaaTokenComp.createInstance(jwtGenerator.createToken()); - assertThat(token.getAppToken(), startsWith("eyJqa3UiOiJodHRwOi8vbG9jYWx")); + assertThat(token.getAppToken(), startsWith("eyJraWQiOiJkZWZhdWx0LWtpZCIs")); } @Test diff --git a/spring-security-starter/pom.xml b/spring-security-starter/pom.xml index 1d84c1cfd0..22ab6bbaeb 100644 --- a/spring-security-starter/pom.xml +++ b/spring-security-starter/pom.xml @@ -16,7 +16,7 @@ com.sap.cloud.security.xsuaa parent - 3.3.2 + 3.3.4 com.sap.cloud.security diff --git a/spring-security/Migration_SpringXsuaaProjects.md b/spring-security/Migration_SpringXsuaaProjects.md index 790979a479..9b0d7daa7b 100644 --- a/spring-security/Migration_SpringXsuaaProjects.md +++ b/spring-security/Migration_SpringXsuaaProjects.md @@ -157,7 +157,7 @@ It is provided in an extra module. This maven dependency needs to be provided ad com.sap.cloud.security.xsuaa spring-security-compatibility - 3.3.2 + 3.3.4 ``` diff --git a/spring-security/README.md b/spring-security/README.md index 0478777a69..4412f3e3b9 100644 --- a/spring-security/README.md +++ b/spring-security/README.md @@ -66,7 +66,7 @@ These (spring) dependencies need to be provided: com.sap.cloud.security resourceserver-security-spring-boot-starter - 3.3.2 + 3.3.4 ``` @@ -78,12 +78,12 @@ Depending on the service bindings in the environment, a different implementation In addition, a bean of type [XsuaaTokenFlows](../token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/XsuaaTokenFlows.java) is provided that can be used to fetch XSUAA tokens. #### Autoconfiguration classes -| Autoconfiguration class | Description | -|--------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [HybridAuthorizationAutoConfiguration](./src/main/java/com/sap/cloud/security/spring/autoconfig/HybridAuthorizationAutoConfiguration.java) | Creates a converter ([XsuaaTokenAuthorizationConverter](./src/main/java/com/sap/cloud/security/spring/token/authentication/XsuaaTokenAuthorizationConverter.java)) that removes the XSUAA application identifier from the scope names, allowing local scope checks to be performed using [Spring's common built-in expression](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#el-common-built-in) `hasAuthority | -| [HybridIdentityServicesAutoConfiguration](./src/main/java/com/sap/cloud/security/spring/autoconfig/HybridIdentityServicesAutoConfiguration.java) | Configures a `JwtDecoder` which is able to decode and validate tokens from Xsuaa and/or Identity service
Furthermore it registers `IdentityServiceConfiguration` and optionally `XsuaaServiceConfiguration`, that allow overriding the identity service configurations found in the service bindings (via `identity.*` and `xsuaa.*` properties). | -| [XsuaaTokenFlowAutoConfiguration](./src/main/java/com/sap/cloud/security/spring/autoconfig/XsuaaTokenFlowAutoConfiguration.java) | Configures an `XsuaaTokenFlows` bean to fetch the XSUAA tokens. Starting with `2.10.0` version it supports X.509 based authentication | -| [SecurityContextAutoConfiguration](./src/main/java/com/sap/cloud/security/spring/autoconfig/SecurityContextAutoConfiguration.java) | Configures [`JavaSecurityContextHolderStrategy`](./src/main/java/com/sap/cloud/security/spring/token/authentication/JavaSecurityContextHolderStrategy.java) to be used as `SecurityContextHolderStrategy` to keep the `com.sap.cloud.security.token.SecurityContext` in sync | +| Autoconfiguration class | Description | +|--------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [HybridAuthorizationAutoConfiguration](./src/main/java/com/sap/cloud/security/spring/autoconfig/HybridAuthorizationAutoConfiguration.java) | Creates a converter ([XsuaaTokenAuthorizationConverter](./src/main/java/com/sap/cloud/security/spring/token/authentication/XsuaaTokenAuthorizationConverter.java)) that removes the XSUAA application identifier from the scope names, allowing local scope checks to be performed using [Spring's common built-in expression](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#el-common-built-in) `hasAuthority`. Supports only single Xsuaa binding | +| [HybridIdentityServicesAutoConfiguration](./src/main/java/com/sap/cloud/security/spring/autoconfig/HybridIdentityServicesAutoConfiguration.java) | Configures a `JwtDecoder` which is able to decode and validate tokens from Xsuaa and/or Identity service
Furthermore it registers `IdentityServiceConfiguration` and optionally `XsuaaServiceConfiguration`, that allow overriding the identity service configurations found in the service bindings (via `identity.*` and `xsuaa.*` properties). | +| [XsuaaTokenFlowAutoConfiguration](./src/main/java/com/sap/cloud/security/spring/autoconfig/XsuaaTokenFlowAutoConfiguration.java) | Configures an `XsuaaTokenFlows` bean to fetch the XSUAA tokens. Starting with `2.10.0` version it supports X.509 based authentication | +| [SecurityContextAutoConfiguration](./src/main/java/com/sap/cloud/security/spring/autoconfig/SecurityContextAutoConfiguration.java) | Configures [`JavaSecurityContextHolderStrategy`](./src/main/java/com/sap/cloud/security/spring/token/authentication/JavaSecurityContextHolderStrategy.java) to be used as `SecurityContextHolderStrategy` to keep the `com.sap.cloud.security.token.SecurityContext` in sync | #### Autoconfiguration properties | Autoconfiguration property | Default value | Description | @@ -93,6 +93,19 @@ In addition, a bean of type [XsuaaTokenFlows](../token-client/src/main/java/com/ You can gradually replace auto-configurations as explained [here](https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-auto-configuration.html). +#### Multiple Xsuaa configurations +:warning: In case of multiple Xsuaa configurations, the [XsuaaTokenAuthorizationConverter](./src/main/java/com/sap/cloud/security/spring/token/authentication/XsuaaTokenAuthorizationConverter.java) bean is not autoconfigured. +The bean needs to be created manually based on the service configuration you want the converter to be initialized with. + +For example, to create a converter that removes the application identifier of the *first* XSUAA configuration from the scope names, you could create the following bean: + +```java +@Bean +public Converter xsuaaAuthConverter(XsuaaServiceConfigurations xsuaaConfigs) { + return new XsuaaTokenAuthorizationConverter(xsuaaConfigs.getConfigurations().get(0).getProperty(APP_ID)); +} +``` +You may want to filter the list accessible via `XsuaaServiceConfigurations#getConfigurations` based on the configuration properties to find a specific configuration from the list. ### Security Configuration This is an example how to configure your application as Spring Security OAuth 2.0 Resource Server for authentication of HTTP requests: @@ -314,14 +327,7 @@ sap.security.services: ``` #### Multiple XSUAA bindings -If you need to manually configure the application for more than one XSUAA service instances (e.g. one of plan `application` and another one of plan `broker`), you can provide them as follows: -````yaml - sap.security.services: - xsuaa[0]: - ... # credentials of XSUAA of plan 'application' - xsuaa[1]: - clientid: # clientid of XSUAA of plan 'broker' -```` +If you need to manually configure the application for more than one XSUAA service instances (e.g. one of plan `application` and another one of plan `broker`), you need to provide them as `VCAP_SERVICES` environment variable (see second point of [Local Testing](#local-testing) section). ### Local testing To run or debug your secured application locally you need to provide the mandatory Xsuaa or Identity service configuration attributes prior to launching the application. @@ -425,8 +431,10 @@ Make sure that you have defined the following mandatory attribute in the service :bulb: Example of minimal application configuration [application.yml](../samples/spring-security-hybrid-usage/src/test/resources/application.yml) for local setup. +❗Limitation❗ Multiple Xsuaa services cannot be defined as application properties in Spring application.yml or application.properties files as Spring does not recognize arrays in the property files. The service Configurations have to be defined as environment variable e.g. `VCAP_SERVICES={"xsuaa": [...]} ` + ## Samples - [Hybrid Usage](../samples/spring-security-hybrid-usage) Demonstrates how to leverage ``spring-security`` library to secure a Spring Boot web application with tokens issued by SAP Identity service or XSUAA. Furthermore, it documents how to implement Spring WebMvcTests using `java-security-test` library. - [Basic Auth Usage](../samples/spring-security-basic-auth) -Legacy example that demonstrates how to leverage ``spring-security`` library to secure a Spring Boot web application with username/password provided via Basic Auth header. Furthermore, it documents how to implement Spring WebMvcTests using `java-security-test` library. \ No newline at end of file +Legacy example that demonstrates how to leverage ``spring-security`` library to secure a Spring Boot web application with username/password provided via Basic Auth header. Furthermore, it documents how to implement Spring WebMvcTests using `java-security-test` library. diff --git a/spring-security/pom.xml b/spring-security/pom.xml index cff8f537e2..0dda8020c1 100644 --- a/spring-security/pom.xml +++ b/spring-security/pom.xml @@ -9,13 +9,13 @@ com.sap.cloud.security.xsuaa parent - 3.3.2 + 3.3.4 com.sap.cloud.security spring-security jar - 3.3.2 + 3.3.4 diff --git a/spring-security/src/main/java/com/sap/cloud/security/spring/autoconfig/HybridIdentityServicesAutoConfiguration.java b/spring-security/src/main/java/com/sap/cloud/security/spring/autoconfig/HybridIdentityServicesAutoConfiguration.java index 215e3fc541..a1ee27ad77 100644 --- a/spring-security/src/main/java/com/sap/cloud/security/spring/autoconfig/HybridIdentityServicesAutoConfiguration.java +++ b/spring-security/src/main/java/com/sap/cloud/security/spring/autoconfig/HybridIdentityServicesAutoConfiguration.java @@ -5,8 +5,8 @@ */ package com.sap.cloud.security.spring.autoconfig; -import com.sap.cloud.security.spring.config.IdentityServiceConfiguration; import com.sap.cloud.security.config.ServiceConstants; +import com.sap.cloud.security.spring.config.IdentityServiceConfiguration; import com.sap.cloud.security.spring.config.XsuaaServiceConfiguration; import com.sap.cloud.security.spring.config.XsuaaServiceConfigurations; import com.sap.cloud.security.spring.token.authentication.JwtDecoderBuilder; @@ -83,11 +83,16 @@ public JwtDecoder hybridJwtDecoder(XsuaaServiceConfiguration xsuaaConfig, public JwtDecoder hybridJwtDecoderMultiXsuaaServices(IdentityServiceConfiguration identityConfig) { LOGGER.debug("auto-configures HybridJwtDecoder when bound to multiple xsuaa service instances."); - /* Use only primary XSUAA config and up to 1 more config of type BROKER to stay backward-compatible now that XsuaaServiceConfigurations contains all XSUAA - configurations instead of only two. */ + /* + * Use only primary XSUAA config and up to 1 more config of type BROKER to stay + * backward-compatible now that XsuaaServiceConfigurations contains all XSUAA + * configurations instead of only two. + */ List allXsuaaConfigs = xsuaaConfigs.getConfigurations(); - List usedXsuaaConfigs = allXsuaaConfigs.subList(0, Math.min(2, allXsuaaConfigs.size())); - if (usedXsuaaConfigs.size() == 2 && !ServiceConstants.Plan.BROKER.toString().equals(usedXsuaaConfigs.get(1).getProperty(ServiceConstants.SERVICE_PLAN))) { + List usedXsuaaConfigs = allXsuaaConfigs.subList(0, + Math.min(2, allXsuaaConfigs.size())); + if (usedXsuaaConfigs.size() == 2 && !ServiceConstants.Plan.BROKER.toString() + .equals(usedXsuaaConfigs.get(1).getProperty(ServiceConstants.SERVICE_PLAN))) { usedXsuaaConfigs = usedXsuaaConfigs.subList(0, 1); } diff --git a/spring-security/src/main/java/com/sap/cloud/security/spring/config/IdentityServicesPropertySourceFactory.java b/spring-security/src/main/java/com/sap/cloud/security/spring/config/IdentityServicesPropertySourceFactory.java index adbb8053f3..fd3685a275 100644 --- a/spring-security/src/main/java/com/sap/cloud/security/spring/config/IdentityServicesPropertySourceFactory.java +++ b/spring-security/src/main/java/com/sap/cloud/security/spring/config/IdentityServicesPropertySourceFactory.java @@ -59,7 +59,7 @@ public class IdentityServicesPropertySourceFactory implements PropertySourceFact .asList("clientid", "clientsecret", "domains", "url", "name", "plan")); private Properties properties; - + @Override @SuppressWarnings("squid:S2259") // false positive public PropertySource createPropertySource(String name, EncodedResource resource) throws IOException { @@ -68,18 +68,19 @@ public PropertySource createPropertySource(String name, EncodedResource resou && resource.getResource().getFilename() != null && !resource.getResource().getFilename().isEmpty()) { environment = Environments.readFromInput(resource.getResource().getInputStream()); } - + this.properties = new Properties(); - + mapXsuaaProperties(environment); mapIasProperties(environment); logger.debug("Parsed {} properties from identity services. {}", this.properties.size(), this.properties.stringPropertyNames()); - + return new PropertiesPropertySource(name == null ? PROPERTIES_KEY : name, this.properties); } - private void mapXsuaaAttributesSingleInstance(final OAuth2ServiceConfiguration oAuth2ServiceConfiguration, final String prefix) { + private void mapXsuaaAttributesSingleInstance(final OAuth2ServiceConfiguration oAuth2ServiceConfiguration, + final String prefix) { for (String key : XSUAA_ATTRIBUTES) { if (oAuth2ServiceConfiguration.hasProperty(key)) { this.properties.put(prefix + key, oAuth2ServiceConfiguration.getProperty(key)); @@ -92,27 +93,30 @@ private void mapXsuaaProperties(@Nonnull Environment environment) { if (numberOfXsuaaConfigurations == 0) { return; } - + /* - * Case "single XSUAA service configuration": - * Then we do not use an array for describing the properties. + * Case "single XSUAA service configuration": Then we do not use an array for + * describing the properties. */ final OAuth2ServiceConfiguration xsuaaConfiguration = environment.getXsuaaConfiguration(); if (numberOfXsuaaConfigurations == 1) { mapXsuaaAttributesSingleInstance(xsuaaConfiguration, XSUAA_PREFIX); return; } - + /* - * Case "multiple XSUAA service configurations": - * For historic reasons, the first two items in the array have a special meaning: - * - Item 0 is exclusively used for environment.getXsuaaConfiguration() ("an arbitrary Xsuaa configuration" of plan "application"). - * - Item 1 is optionally used for environment.getXsuaaConfigurationForTokenExchange() ("an arbitrary Xsuaa configuration" of plan "broker"). + * Case "multiple XSUAA service configurations": For historic reasons, the first + * two items in the array have a special meaning: - Item 0 is exclusively used + * for environment.getXsuaaConfiguration() ("an arbitrary Xsuaa configuration" + * of plan "application"). - Item 1 is optionally used for + * environment.getXsuaaConfigurationForTokenExchange() + * ("an arbitrary Xsuaa configuration" of plan "broker"). */ mapXsuaaAttributesSingleInstance(xsuaaConfiguration, PROPERTIES_KEY + ".xsuaa[0]."); - + int position = 1; - final OAuth2ServiceConfiguration xsuaaConfigurationForTokenExchange = environment.getXsuaaConfigurationForTokenExchange(); + final OAuth2ServiceConfiguration xsuaaConfigurationForTokenExchange = environment + .getXsuaaConfigurationForTokenExchange(); if (xsuaaConfigurationForTokenExchange != null) { mapXsuaaAttributesSingleInstance(xsuaaConfigurationForTokenExchange, PROPERTIES_KEY + ".xsuaa[1]."); position = 2; @@ -122,13 +126,15 @@ private void mapXsuaaProperties(@Nonnull Environment environment) { * For all other items coming thereafter, there is no order defined anymore. * However, we must not duplicate the instances... */ - final List remainingXsuaaConfigurations = environment.getServiceConfigurationsAsList().get(Service.XSUAA) + final List remainingXsuaaConfigurations = environment + .getServiceConfigurationsAsList().get(Service.XSUAA) .stream() .filter(e -> e != xsuaaConfiguration && e != xsuaaConfigurationForTokenExchange) .toList(); - /* Usage of ".forEach" would have been preferred here, - * but Closures in JDK8 do not permit accessing non-final "position". + /* + * Usage of ".forEach" would have been preferred here, but Closures in JDK8 do + * not permit accessing non-final "position". */ for (OAuth2ServiceConfiguration config : remainingXsuaaConfigurations) { final String prefix = String.format(PROPERTIES_KEY + ".xsuaa[%d].", position++); diff --git a/spring-security/src/main/java/com/sap/cloud/security/spring/token/SpringSecurityContext.java b/spring-security/src/main/java/com/sap/cloud/security/spring/token/SpringSecurityContext.java index 0b460309b9..ca384d3c06 100644 --- a/spring-security/src/main/java/com/sap/cloud/security/spring/token/SpringSecurityContext.java +++ b/spring-security/src/main/java/com/sap/cloud/security/spring/token/SpringSecurityContext.java @@ -56,7 +56,8 @@ public static Token getToken() { return (Token) principal; } throw new AccessDeniedException( - "Access forbidden: SecurityContextHolder does not contain a principal of type 'Token'. Found instead a principal of type " + principal.getClass()); + "Access forbidden: SecurityContextHolder does not contain a principal of type 'Token'. Found instead a principal of type " + + principal.getClass()); } /** diff --git a/spring-security/src/main/java/com/sap/cloud/security/spring/token/authentication/AuthenticationToken.java b/spring-security/src/main/java/com/sap/cloud/security/spring/token/authentication/AuthenticationToken.java index 1928bf35a0..9eea873d51 100644 --- a/spring-security/src/main/java/com/sap/cloud/security/spring/token/authentication/AuthenticationToken.java +++ b/spring-security/src/main/java/com/sap/cloud/security/spring/token/authentication/AuthenticationToken.java @@ -10,6 +10,7 @@ import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.util.Assert; +import java.lang.reflect.Field; import java.io.Serial; import java.util.Collection; @@ -60,17 +61,35 @@ public String getName() { @Override public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj != null && this.getClass() != obj.getClass()) { + AuthenticationToken that = (AuthenticationToken) obj; + return compareObjects(this.token,that.token) && this.getAuthorities().equals(that.getAuthorities()); + } + + public static boolean compareObjects(Object obj1, Object obj2) { + if (obj2 == null) { return false; } - if (obj == null) { + if (!obj1.getClass().equals(obj2.getClass())) { return false; } - AuthenticationToken that = (AuthenticationToken) obj; - return this.token.equals(that.token) && this.getAuthorities().equals(that.getAuthorities()); + Field[] fields = obj1.getClass().getDeclaredFields(); + for (Field field : fields) { + field.setAccessible(true); + try { + Object value1 = field.get(obj1); + Object value2 = field.get(obj2); + if (value1 == null || value2 == null) { + if (value1 != value2) { + return false; + } + } else if (!value1.equals(value2)) { + return false; + } + } catch (IllegalAccessException e) { + // handle exception + } + } + return true; } @Override diff --git a/spring-security/src/test/java/com/sap/cloud/security/spring/config/ConfigurationAssertions.java b/spring-security/src/test/java/com/sap/cloud/security/spring/config/ConfigurationAssertions.java index b6ae430d9f..b930a4ae91 100644 --- a/spring-security/src/test/java/com/sap/cloud/security/spring/config/ConfigurationAssertions.java +++ b/spring-security/src/test/java/com/sap/cloud/security/spring/config/ConfigurationAssertions.java @@ -6,12 +6,15 @@ import static org.junit.jupiter.api.Assertions.assertEquals; public class ConfigurationAssertions { - static void assertXsuaaConfigsAreEqual(XsuaaServiceConfiguration xsuaaConfig, OAuth2ServiceConfiguration oauthConfig) { - assertEquals(oauthConfig.getClientId(), xsuaaConfig.getClientId()); - assertEquals(oauthConfig.getClientSecret(), xsuaaConfig.getClientSecret()); - assertEquals(oauthConfig.getProperty(ServiceConstants.XSUAA.UAA_DOMAIN), xsuaaConfig.getProperty(ServiceConstants.XSUAA.UAA_DOMAIN)); - assertEquals(oauthConfig.getProperty(ServiceConstants.XSUAA.APP_ID), xsuaaConfig.getProperty(ServiceConstants.XSUAA.APP_ID)); - assertEquals(oauthConfig.getProperty(ServiceConstants.NAME), xsuaaConfig.getName()); - assertEquals(oauthConfig.getProperty(ServiceConstants.SERVICE_PLAN), xsuaaConfig.getPlan()); - } + static void assertXsuaaConfigsAreEqual(XsuaaServiceConfiguration xsuaaConfig, + OAuth2ServiceConfiguration oauthConfig) { + assertEquals(oauthConfig.getClientId(), xsuaaConfig.getClientId()); + assertEquals(oauthConfig.getClientSecret(), xsuaaConfig.getClientSecret()); + assertEquals(oauthConfig.getProperty(ServiceConstants.XSUAA.UAA_DOMAIN), + xsuaaConfig.getProperty(ServiceConstants.XSUAA.UAA_DOMAIN)); + assertEquals(oauthConfig.getProperty(ServiceConstants.XSUAA.APP_ID), + xsuaaConfig.getProperty(ServiceConstants.XSUAA.APP_ID)); + assertEquals(oauthConfig.getProperty(ServiceConstants.NAME), xsuaaConfig.getName()); + assertEquals(oauthConfig.getProperty(ServiceConstants.SERVICE_PLAN), xsuaaConfig.getPlan()); + } } diff --git a/spring-security/src/test/java/com/sap/cloud/security/spring/config/IdentityServicesPropertySourceFactoryBrokerNoHoleTest.java b/spring-security/src/test/java/com/sap/cloud/security/spring/config/IdentityServicesPropertySourceFactoryBrokerNoHoleTest.java index 651eb294c6..9bd364b9cc 100644 --- a/spring-security/src/test/java/com/sap/cloud/security/spring/config/IdentityServicesPropertySourceFactoryBrokerNoHoleTest.java +++ b/spring-security/src/test/java/com/sap/cloud/security/spring/config/IdentityServicesPropertySourceFactoryBrokerNoHoleTest.java @@ -19,12 +19,15 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Tests that the {@link IdentityServicesPropertySourceFactory} puts 2 XSUAA service instances with plan 'application' into the Spring properties without creating a hole at index 1. - * For backward-compatibility, the order of the service instance must be as follows: - * Index 0: Configuration accessible via Environment#getXsuaaConfiguration (Application) - * Index 1: Configuration accessible via Environment#getXsuaaConfigurationForTokenExchange (Broker) if exists, otherwise next XSUAA configuration - * Index 2+: Remaining XSUAA configurations - * In addition, tests that the IAS service instance from the environment is correctly added as well. + * Tests that the {@link IdentityServicesPropertySourceFactory} puts 2 XSUAA + * service instances with plan 'application' into the Spring properties without + * creating a hole at index 1. For backward-compatibility, the order of the + * service instance must be as follows: Index 0: Configuration accessible via + * Environment#getXsuaaConfiguration (Application) Index 1: Configuration + * accessible via Environment#getXsuaaConfigurationForTokenExchange (Broker) if + * exists, otherwise next XSUAA configuration Index 2+: Remaining XSUAA + * configurations In addition, tests that the IAS service instance from the + * environment is correctly added as well. */ @SpringBootTest(classes = { BrokerHoleTestConfigurationFromFile.class }) class IdentityServicesPropertySourceFactoryBrokerNoHoleTest { @@ -41,7 +44,7 @@ void testInjectedPropertyValues() { assertEquals("xsuaadomain", configuration.xsuaaDomain0); assertEquals("xsappname2", configuration.xsuaaAppName0); assertEquals("xsuaaInstance0", configuration.xsuaaName0); - assertEquals("application", configuration.xsuaaPlan0.toLowerCase()); + assertEquals("application", configuration.xsuaaPlan0); assertEquals("", configuration.unknown0); /* Index 1 */ @@ -49,12 +52,12 @@ void testInjectedPropertyValues() { assertEquals("client-secret", configuration.xsuaaClientSecret1); assertEquals("xsappname", configuration.xsuaaAppName1); assertEquals("xsuaaInstance1", configuration.xsuaaName1); - assertEquals("application", configuration.xsuaaPlan1.toLowerCase()); + assertEquals("application", configuration.xsuaaPlan1); /* Index 2 */ assertEquals("none", configuration.xsuaaClientId2); assertEquals("none", configuration.xsuaaClientSecret2); - + /* IAS */ assertEquals("client-id-ias", configuration.identityClientId); assertEquals("client-secret-ias", configuration.identityClientSecret); @@ -72,7 +75,7 @@ void testInjectedPropertyValues() { class BrokerHoleTestConfigurationFromFile { /* Index 0 */ - + @Value("${sap.security.services.xsuaa[0].url:}") public String xsuaaUrl0; @@ -97,9 +100,8 @@ class BrokerHoleTestConfigurationFromFile { @Value("${sap.security.services.xsuaa[0].unknown:}") public String unknown0; - /* Index 1 */ - + @Value("${sap.security.services.xsuaa[1].clientid:none}") public String xsuaaClientId1; @@ -114,19 +116,17 @@ class BrokerHoleTestConfigurationFromFile { @Value("${sap.security.services.xsuaa[1].plan:}") public String xsuaaPlan1; - + /* Index 2 */ - + @Value("${sap.security.services.xsuaa[2].clientid:none}") public String xsuaaClientId2; @Value("${sap.security.services.xsuaa[2].clientsecret:none}") public String xsuaaClientSecret2; - - /* IAS */ - + @Value("${sap.security.services.identity.clientid:}") public String identityClientId; diff --git a/spring-security/src/test/java/com/sap/cloud/security/spring/config/IdentityServicesPropertySourceFactoryFourXsuaaOneIasTest.java b/spring-security/src/test/java/com/sap/cloud/security/spring/config/IdentityServicesPropertySourceFactoryFourXsuaaOneIasTest.java index cf13509d4a..f5e9debf8d 100644 --- a/spring-security/src/test/java/com/sap/cloud/security/spring/config/IdentityServicesPropertySourceFactoryFourXsuaaOneIasTest.java +++ b/spring-security/src/test/java/com/sap/cloud/security/spring/config/IdentityServicesPropertySourceFactoryFourXsuaaOneIasTest.java @@ -5,7 +5,6 @@ */ package com.sap.cloud.security.spring.config; -import com.sap.cloud.security.config.ServiceConstants; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -19,13 +18,15 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Tests that the {@link IdentityServicesPropertySourceFactory} puts 2 XSUAA service instances with plan 'application' and 2 with plan 'broker' into the Spring properties - * in the correct order. - * For backward-compatibility, the order of the service instance must be as follows: - * Index 0: Configuration accessible via Environment#getXsuaaConfiguration (Application) - * Index 1: Configuration accessible via Environment#getXsuaaConfigurationForTokenExchange (Broker) if exists, otherwise next XSUAA configuration - * Index 2+: Remaining XSUAA configurations - * In addition, tests that the IAS service instance from the environment is correctly added as well. + * Tests that the {@link IdentityServicesPropertySourceFactory} puts 2 XSUAA + * service instances with plan 'application' and 2 with plan 'broker' into the + * Spring properties in the correct order. For backward-compatibility, the order + * of the service instance must be as follows: Index 0: Configuration accessible + * via Environment#getXsuaaConfiguration (Application) Index 1: Configuration + * accessible via Environment#getXsuaaConfigurationForTokenExchange (Broker) if + * exists, otherwise next XSUAA configuration Index 2+: Remaining XSUAA + * configurations In addition, tests that the IAS service instance from the + * environment is correctly added as well. */ @SpringBootTest(classes = { FourXsuaaOneIasTestConfigurationFromFile.class }) class IdentityServicesPropertySourceFactoryFourXsuaaOneIasTest { @@ -42,28 +43,28 @@ void testInjectedPropertyValues() { assertEquals("xsuaadomain", configuration.xsuaaDomain0); assertEquals("xsappname2", configuration.xsuaaAppName0); assertEquals("xsuaaInstance2", configuration.xsuaaName0); - assertEquals("application", configuration.xsuaaPlan0.toLowerCase()); + assertEquals("application", configuration.xsuaaPlan0); assertEquals("", configuration.unknown0); /* Index 1 */ assertEquals("client-id-broker", configuration.xsuaaClientId1); assertEquals("client-secret-broker", configuration.xsuaaClientSecret1); assertEquals("xsuaaInstance0", configuration.xsuaaName1); - assertEquals("broker", configuration.xsuaaPlan1.toLowerCase()); + assertEquals("broker", configuration.xsuaaPlan1); /* Index 2 */ assertEquals("client-id-broker2", configuration.xsuaaClientId2); assertEquals("client-secret-broker2", configuration.xsuaaClientSecret2); assertEquals("xsuaaInstance1", configuration.xsuaaName2); - assertEquals("broker", configuration.xsuaaPlan2.toLowerCase()); + assertEquals("broker", configuration.xsuaaPlan2); /* Index 3 */ assertEquals("client-id", configuration.xsuaaClientId3); assertEquals("client-secret", configuration.xsuaaClientSecret3); assertEquals("xsappname", configuration.xsuaaAppName3); assertEquals("xsuaaInstance3", configuration.xsuaaName3); - assertEquals("application", configuration.xsuaaPlan3.toLowerCase()); - + assertEquals("application", configuration.xsuaaPlan3); + /* IAS */ assertEquals("client-id-ias", configuration.identityClientId); assertEquals("client-secret-ias", configuration.identityClientSecret); @@ -71,7 +72,7 @@ void testInjectedPropertyValues() { assertTrue(configuration.identityDomains.contains("iasdomain.com")); assertEquals(2, configuration.identityDomains.size()); assertEquals("identityInstance0", configuration.identityName0); - assertEquals(ServiceConstants.Plan.BROKER, ServiceConstants.Plan.from(configuration.identityPlan)); + assertEquals("broker", configuration.identityPlan); } } @@ -81,7 +82,7 @@ void testInjectedPropertyValues() { class FourXsuaaOneIasTestConfigurationFromFile { /* Index 0 */ - + @Value("${sap.security.services.xsuaa[0].url:}") public String xsuaaUrl0; @@ -105,9 +106,9 @@ class FourXsuaaOneIasTestConfigurationFromFile { @Value("${sap.security.services.xsuaa[0].unknown:}") public String unknown0; - + /* Index 1 */ - + @Value("${sap.security.services.xsuaa[1].clientid:}") public String xsuaaClientId1; @@ -120,9 +121,8 @@ class FourXsuaaOneIasTestConfigurationFromFile { @Value("${sap.security.services.xsuaa[1].plan:}") public String xsuaaPlan1; - /* Index 2 */ - + @Value("${sap.security.services.xsuaa[2].clientid:}") public String xsuaaClientId2; @@ -136,7 +136,7 @@ class FourXsuaaOneIasTestConfigurationFromFile { public String xsuaaPlan2; /* Index 3 */ - + @Value("${sap.security.services.xsuaa[3].clientid:}") public String xsuaaClientId3; @@ -151,9 +151,9 @@ class FourXsuaaOneIasTestConfigurationFromFile { @Value("${sap.security.services.xsuaa[3].plan:}") public String xsuaaPlan3; - + /* IAS */ - + @Value("${sap.security.services.identity.clientid:}") public String identityClientId; diff --git a/spring-security/src/test/java/com/sap/cloud/security/spring/config/IdentityServicesPropertySourceFactoryTest.java b/spring-security/src/test/java/com/sap/cloud/security/spring/config/IdentityServicesPropertySourceFactoryTest.java index 1c18f70ba5..f29a7cc682 100644 --- a/spring-security/src/test/java/com/sap/cloud/security/spring/config/IdentityServicesPropertySourceFactoryTest.java +++ b/spring-security/src/test/java/com/sap/cloud/security/spring/config/IdentityServicesPropertySourceFactoryTest.java @@ -30,7 +30,7 @@ void testInjectedPropertyValues() { assertEquals("xsuaadomain", configuration.xsuaaDomain); assertEquals("xsappname", configuration.xsuaaAppName); assertEquals("xsuaaInstance0", configuration.xsuaaName); - assertEquals("application", configuration.xsuaaPlan.toLowerCase()); + assertEquals("application", configuration.xsuaaPlan); assertEquals("", configuration.unknown); @@ -38,7 +38,7 @@ void testInjectedPropertyValues() { assertEquals("client-secret-ias", configuration.identityClientSecret); assertEquals("iasdomain", configuration.identityDomains.get(0)); assertEquals("identityInstance0", configuration.identityName); - assertEquals("broker", configuration.identityPlan.toLowerCase()); + assertEquals("broker", configuration.identityPlan); } } diff --git a/spring-security/src/test/java/com/sap/cloud/security/spring/config/XsuaaServiceConfigurationLoadingIntegrationTest.java b/spring-security/src/test/java/com/sap/cloud/security/spring/config/XsuaaServiceConfigurationLoadingIntegrationTest.java index a87289762d..0a8cd4d8c9 100644 --- a/spring-security/src/test/java/com/sap/cloud/security/spring/config/XsuaaServiceConfigurationLoadingIntegrationTest.java +++ b/spring-security/src/test/java/com/sap/cloud/security/spring/config/XsuaaServiceConfigurationLoadingIntegrationTest.java @@ -21,8 +21,11 @@ import static java.nio.charset.StandardCharsets.UTF_8; /** - * Tests the integration between XsuaaServiceConfiguration and IdentityServicesPropertySourceFactory. - * The XSUAA configuration properties of the XsuaaServiceConfiguration are asserted to be equal to those of the configuration used to populate the Spring properties via IdentityServicesPropertySourceFactory. + * Tests the integration between XsuaaServiceConfiguration and + * IdentityServicesPropertySourceFactory. The XSUAA configuration properties of + * the XsuaaServiceConfiguration are asserted to be equal to those of the + * configuration used to populate the Spring properties via + * IdentityServicesPropertySourceFactory. */ @SpringBootTest(classes = { SingleXsuaaConfigurationFromFile.class }) class XsuaaServiceConfigurationLoadingIntegrationTest { @@ -46,6 +49,8 @@ void configuresXsuaaServiceConfiguration() { } @Configuration -@PropertySource(factory = IdentityServicesPropertySourceFactory.class, value = { "classpath:singleXsuaaAndIasBinding.json" }) +@PropertySource(factory = IdentityServicesPropertySourceFactory.class, value = { + "classpath:singleXsuaaAndIasBinding.json" }) @EnableConfigurationProperties(XsuaaServiceConfiguration.class) -class SingleXsuaaConfigurationFromFile {} +class SingleXsuaaConfigurationFromFile { +} diff --git a/spring-security/src/test/java/com/sap/cloud/security/spring/config/XsuaaServiceConfigurationsLoadingIntegrationTest.java b/spring-security/src/test/java/com/sap/cloud/security/spring/config/XsuaaServiceConfigurationsLoadingIntegrationTest.java index 05450a014b..6dbd709557 100644 --- a/spring-security/src/test/java/com/sap/cloud/security/spring/config/XsuaaServiceConfigurationsLoadingIntegrationTest.java +++ b/spring-security/src/test/java/com/sap/cloud/security/spring/config/XsuaaServiceConfigurationsLoadingIntegrationTest.java @@ -25,9 +25,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; /** - * Tests the integration between XsuaaServiceConfigurations and IdentityServicesPropertySourceFactory. - * The XSUAA configuration properties of the XsuaaServiceConfigurations are asserted to be equal to those of the configuration used to populate the Spring properties via IdentityServicesPropertySourceFactory. - * In addition, to assert backward compatibility, the test makes assertions about indices 0 and 1 in the configuration list of XsuaaServiceConfigurations. + * Tests the integration between XsuaaServiceConfigurations and + * IdentityServicesPropertySourceFactory. The XSUAA configuration properties of + * the XsuaaServiceConfigurations are asserted to be equal to those of the + * configuration used to populate the Spring properties via + * IdentityServicesPropertySourceFactory. In addition, to assert backward + * compatibility, the test makes assertions about indices 0 and 1 in the + * configuration list of XsuaaServiceConfigurations. */ @SpringBootTest(classes = { MultipleXsuaaConfigurationsFromFile.class }) class XsuaaServiceConfigurationsLoadingIntegrationTest { @@ -59,6 +63,8 @@ void configuresXsuaaServiceConfigurations() { } @Configuration -@PropertySource(factory = IdentityServicesPropertySourceFactory.class, value = { "classpath:fourXsuaaBindingsAndOneIasBinding.json" }) +@PropertySource(factory = IdentityServicesPropertySourceFactory.class, value = { + "classpath:fourXsuaaBindingsAndOneIasBinding.json" }) @EnableConfigurationProperties(XsuaaServiceConfigurations.class) -class MultipleXsuaaConfigurationsFromFile {} \ No newline at end of file +class MultipleXsuaaConfigurationsFromFile { +} \ No newline at end of file diff --git a/spring-security/src/test/java/com/sap/cloud/security/spring/config/XsuaaServiceConfigurationsTest.java b/spring-security/src/test/java/com/sap/cloud/security/spring/config/XsuaaServiceConfigurationsTest.java index 4bcdb2c786..b50945b4bd 100644 --- a/spring-security/src/test/java/com/sap/cloud/security/spring/config/XsuaaServiceConfigurationsTest.java +++ b/spring-security/src/test/java/com/sap/cloud/security/spring/config/XsuaaServiceConfigurationsTest.java @@ -31,12 +31,14 @@ void configuresXsuaaServiceConfigurations() { "sap.security.services.xsuaa[1].name:xsuaaInstance1", "sap.security.services.xsuaa[1].plan:broker") .run(context -> { - XsuaaServiceConfiguration config0 = context.getBean(XsuaaServiceConfigurations.class).getConfigurations().get(0); + XsuaaServiceConfiguration config0 = context.getBean(XsuaaServiceConfigurations.class) + .getConfigurations().get(0); assertEquals("cid0", config0.getClientId()); assertEquals("xsuaaInstance0", config0.getName()); assertEquals("broker", config0.getPlan()); - XsuaaServiceConfiguration config1 = context.getBean(XsuaaServiceConfigurations.class).getConfigurations().get(1); + XsuaaServiceConfiguration config1 = context.getBean(XsuaaServiceConfigurations.class) + .getConfigurations().get(1); assertEquals("cid1", config1.getClientId()); assertEquals("xsuaaInstance1", config1.getName()); assertEquals("broker", config1.getPlan()); diff --git a/spring-xsuaa-it/pom.xml b/spring-xsuaa-it/pom.xml index 89e325943b..fb91421e0d 100644 --- a/spring-xsuaa-it/pom.xml +++ b/spring-xsuaa-it/pom.xml @@ -14,12 +14,12 @@ spring-xsuaa-it spring-xsuaa-it - 3.3.2 + 3.3.4 4.10.0 17 - 3.3.2 + 3.3.4 diff --git a/spring-xsuaa-mock/README.md b/spring-xsuaa-mock/README.md new file mode 100644 index 0000000000..d2b3d1f1e8 --- /dev/null +++ b/spring-xsuaa-mock/README.md @@ -0,0 +1,81 @@ +# XSUAA Security Xsuaa Mock Library + +## Deprecation Note +**This is in maintaince mode, don't use it for new projects!** +Instead, make use of [`java-security-test`](/java-security-test) testing library, which uses WireMock, supports JUnit 4 and JUnit 5 and can be enhanced easily. Have a look at the [spring-security-xsuaa-usage](/samples/spring-security-xsuaa-usage) as usage reference. + +## Description +This library enhances the spring-xsuaa project. This includes a `XsuaaMockWebServer` web server for the Xsuaa service that can provide *token_keys* for an offline JWT token validation. This is required only when there is no Xsuaa service (OAuth resource-server) in place, which is only the case in context of unit tests, as well as when running your Spring boot application locally. + +The default implementation offers already valid *token_keys* for JWT tokens, that are generated by the [`JwtGenerator`](/spring-xsuaa-test/src/main/java/com/sap/cloud/security/xsuaa/test/JwtGenerator.java) (`spring-xsuaa-test` library). + +## Requirements +- Java 8 +- maven 3.3.9 or later +- Spring Boot 2.1 and later + +## Configuration + +### Maven Dependency +```xml + + com.sap.cloud.security.xsuaa + spring-xsuaa-mock + 2.13.7 + + + org.springframework.boot + spring-boot-autoconfigure + +``` + +### Setup Mock Web Server +Add the following class, which makes sure, that the Xsuaa mock web server is only started in case a dedicated profile e.g. `uaamock` is active. Make sure that this profile (`uaamock`) is never active in production! + +```java +import com.sap.cloud.security.xsuaa.mock.XsuaaMockWebServer; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Profiles; + +public class XsuaaMockPostProcessor implements EnvironmentPostProcessor { + + private static final XsuaaMockWebServer mockAuthorizationServer = new XsuaaMockWebServer(); + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + if (environment.acceptsProfiles(Profiles.of("uaamock"))) { + environment.getPropertySources().addFirst(mockAuthorizationServer); + } + } +} +``` + +Then you have to register this class to `META-INF/spring.factories`: + +``` +org.springframework.boot.env.EnvironmentPostProcessor=<>.XsuaaMockPostProcessor +``` + +### XSUAA Service Configuration + +From version `1.5.0` on the [`MockXsuaaServiceConfiguration`](src/main/java/com/sap/cloud/security/xsuaa/mock/MockXsuaaServiceConfiguration.java) is auto-configured [here](src/main/java/com/sap/cloud/security/xsuaa/mock/autoconfiguration/XsuaaMockAutoConfiguration.java). This class overwrites Xsuaa url and uaadomain to point to the Xsuaa Mock Web Server. This is relevant for validating the `jku` URI that is provided as part of the JSON Web Signature (JWS). The `jku` of the Jwt token issued by the `JwtGenerator` references the public key URI of the `XsuaaMockWebServer` used for generating the signature. + +### Extendability +Note: it is possible to extend the dispatcher and pass this to the `XsuaaMockWebServer` constructor. An example `XsuaaMockPostProcessor` implementation can be found [here](src/test/java/com/sap/cloud/security/xsuaa/mock/XsuaaMockPostProcessor.java). + +### Multitenancy +From version `1.3.0` and higher you can configure the `JwtGenerator` with a dedicated **subdomain** of a subaccount, e.g. `testdomain` and the header with a **keyId**: +```java +String yourSubdomain = "testdomain"; +String yourClientId = "sb-xsapp!20"; +String jwtTokenHeaderKeyId = "legacy-token-key-" + yourSubdomain; + +String jwtToken = new JwtGenerator(yourClientId, yourSubdomain).setJwtHeaderKeyId(jwtTokenHeaderKeyId).getToken().getTokenValue(); +``` + +Then your Mock Web Server is called for example with `http://localhost:33195/testdomain/token_keys` and can be configured in such a way, that it provides different token keys for different domains. The domain `testdomain` is already handled by the default [`XsuaaRequestDispatcher`](src/main/java/com/sap/cloud/security/xsuaa/mock/XsuaaRequestDispatcher.java) implementation. + +## Samples +- [cloud-application-security-sample](https://github.com/SAP-samples/cloud-application-security-sample) diff --git a/spring-xsuaa-mock/pom.xml b/spring-xsuaa-mock/pom.xml new file mode 100644 index 0000000000..f33456a72f --- /dev/null +++ b/spring-xsuaa-mock/pom.xml @@ -0,0 +1,118 @@ + + + + + 4.0.0 + + + com.sap.cloud.security.xsuaa + parent + 2.13.7 + + + spring-xsuaa-mock + spring-xsuaa-mock + DEPRECATED in favor of com.sap.cloud.security:java-security-test + jar + + 4.9.3 + + + + + com.sap.cloud.security.xsuaa + spring-xsuaa + + + org.springframework.security + spring-security-oauth2-resource-server + provided + + + org.springframework.security + spring-security-oauth2-jose + provided + + + org.springframework.boot + spring-boot-autoconfigure + provided + + + com.squareup.okhttp3 + mockwebserver + ${mockwebserver.version} + + + org.bouncycastle + bcprov-jdk15on + + + + + commons-io + commons-io + compile + + + org.hamcrest + hamcrest-all + test + + + org.springframework.boot + spring-boot-starter-web + test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-source-plugin + ${maven.source.plugin.version} + + + attach-sources + + jar + + + + + + org.jacoco + jacoco-maven-plugin + + + org.apache.maven.plugins + maven-pmd-plugin + + + com.github.spotbugs + spotbugs-maven-plugin + + + + + diff --git a/spring-xsuaa-starter/pom.xml b/spring-xsuaa-starter/pom.xml index 6778272ac6..275de41653 100644 --- a/spring-xsuaa-starter/pom.xml +++ b/spring-xsuaa-starter/pom.xml @@ -16,7 +16,7 @@ com.sap.cloud.security.xsuaa parent - 3.3.2 + 3.3.4 xsuaa-spring-boot-starter @@ -31,20 +31,6 @@ - - - ch.qos.logback - logback-core - 1.4.14 - provided - - - - ch.qos.logback - logback-classic - 1.4.14 - provided - org.springframework.boot spring-boot-starter diff --git a/spring-xsuaa-test/README.md b/spring-xsuaa-test/README.md index f27734040b..e3f99ec469 100644 --- a/spring-xsuaa-test/README.md +++ b/spring-xsuaa-test/README.md @@ -31,7 +31,7 @@ This includes for example a `JwtGenerator` that generates JSON Web Tokens (JWT) com.sap.cloud.security.xsuaa spring-xsuaa-test - 3.3.2 + 3.3.4 test diff --git a/spring-xsuaa-test/pom.xml b/spring-xsuaa-test/pom.xml index 70b6ed041d..1e68e1489e 100644 --- a/spring-xsuaa-test/pom.xml +++ b/spring-xsuaa-test/pom.xml @@ -9,7 +9,7 @@ com.sap.cloud.security.xsuaa parent - 3.3.2 + 3.3.4 spring-xsuaa-test diff --git a/spring-xsuaa/Migration_JavaContainerSecurityProjects.md b/spring-xsuaa/Migration_JavaContainerSecurityProjects.md index d530fc4535..c033b4086c 100644 --- a/spring-xsuaa/Migration_JavaContainerSecurityProjects.md +++ b/spring-xsuaa/Migration_JavaContainerSecurityProjects.md @@ -117,7 +117,8 @@ In case of multiple bindings you need to adapt your **Spring Security Configurat ```java @PropertySource(factory = XsuaaServicePropertySourceFactory.class, value = {""}) ``` -2. Instead, provide your own implementation of `XsuaaSecurityConfiguration` interface to access the **primary Xsuaa service configuration** of your application (chose the service instance of plan `application` here), which are exposed in the `VCAP_SERVICES` system environment variable (in Cloud Foundry). As of version `2.6.2` you can implement it like that: +2. Instead, provide your own implementation of `XsuaaSecurityConfiguration` interface to access the **primary Xsuaa service configuration** of your application (chose the service instance of plan `application` here), which are exposed in the `VCAP_SERVICES` system environment variable (in Cloud Foundry). +Starting with version `2.6.2` you can implement it like that: ```java import com.sap.cloud.security.xsuaa.XsuaaCredentials; @@ -137,7 +138,7 @@ In case of multiple bindings you need to adapt your **Spring Security Configurat } ``` -3. You need to overwrite `JwtDecoder` bean so that the `AudienceValidator` checks the JWT audience not only against the client id of the primary Xsuaa service instance, but also of the binding of plan `broker`. As of version `2.6.2` you can implement it like that: +3. You need to overwrite `JwtDecoder` bean so that the `AudienceValidator` checks the JWT audience not only against the client id of the primary Xsuaa service instance, but also of the binding of plan `broker`. Starting with version `2.6.2` you can implement it like that: ```java @Bean @ConfigurationProperties("vcap.services.<>.credentials") diff --git a/spring-xsuaa/README.md b/spring-xsuaa/README.md index a55f25a7f3..c48dfc6496 100644 --- a/spring-xsuaa/README.md +++ b/spring-xsuaa/README.md @@ -39,7 +39,7 @@ These (spring) dependencies need to be provided: com.sap.cloud.security.xsuaa spring-xsuaa - 3.3.2 + 3.3.4 org.apache.logging.log4j @@ -53,7 +53,7 @@ These (spring) dependencies need to be provided: com.sap.cloud.security.xsuaa xsuaa-spring-boot-starter - 3.3.2 + 3.3.4 ``` diff --git a/spring-xsuaa/pom.xml b/spring-xsuaa/pom.xml index c8b377b13f..5d06323750 100644 --- a/spring-xsuaa/pom.xml +++ b/spring-xsuaa/pom.xml @@ -9,7 +9,7 @@ com.sap.cloud.security.xsuaa parent - 3.3.2 + 3.3.4 spring-xsuaa @@ -164,7 +164,7 @@ uk.org.webcompere system-stubs-jupiter - 2.1.5 + 2.1.6 test diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/IasToken.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/IasToken.java index c8bf030c9e..0429b29744 100644 --- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/IasToken.java +++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/IasToken.java @@ -102,6 +102,7 @@ public String getZoneId() { @Override public String getAppTid() { - return decodedToken.hasClaim(SAP_GLOBAL_APP_TID) ? decodedToken.getClaimAsString(SAP_GLOBAL_APP_TID) : decodedToken.getClaimAsString(SAP_GLOBAL_ZONE_ID); + return decodedToken.hasClaim(SAP_GLOBAL_APP_TID) ? decodedToken.getClaimAsString(SAP_GLOBAL_APP_TID) + : decodedToken.getClaimAsString(SAP_GLOBAL_ZONE_ID); } } diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/SpringSecurityContext.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/SpringSecurityContext.java index ca0187c790..d60f700665 100644 --- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/SpringSecurityContext.java +++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/SpringSecurityContext.java @@ -41,7 +41,8 @@ public static Token getToken() { return (Token) principal; } throw new AccessDeniedException( - "Access forbidden: SecurityContextHolder does not contain a principal of type 'Token'. Found instead a principal of type " + principal.getClass()); + "Access forbidden: SecurityContextHolder does not contain a principal of type 'Token'. Found instead a principal of type " + + principal.getClass()); } /** diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoder.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoder.java index df79eb550d..7854f41049 100644 --- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoder.java +++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoder.java @@ -146,9 +146,9 @@ private static String getZid(JWT jwt) { TokenClaims.CLAIM_ZONE_ID); } catch (ParseException e) { - zid =null; + zid = null; } - if (zid != null && zid.isBlank()){ + if (zid != null && zid.isBlank()) { zid = null; } return zid; @@ -194,8 +194,9 @@ private void canVerifyWithKey(String kid, String uaadomain) { private String composeJku(String uaaDomain, String zid) { String zidQueryParam = zid != null ? "?zid=" + zid : ""; - // uaaDomain in configuration is always without a schema, but for testing purpose http schema can be used - if (uaaDomain.startsWith("http://")){ + // uaaDomain in configuration is always without a schema, but for testing + // purpose http schema can be used + if (uaaDomain.startsWith("http://")) { return uaaDomain + "/token_keys" + zidQueryParam; } return "https://" + uaaDomain + "/token_keys" + zidQueryParam; diff --git a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/XsuaaLocalhostJkuFactory.java b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/XsuaaLocalhostJkuFactory.java index 6e546e40ad..65e19e542a 100644 --- a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/XsuaaLocalhostJkuFactory.java +++ b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/XsuaaLocalhostJkuFactory.java @@ -9,20 +9,20 @@ public class XsuaaLocalhostJkuFactory implements XsuaaJkuFactory { - @Override - public String create(String token) { - String tokenJku; - try { - JWT jwt = JWTParser.parse(token); - tokenJku = (String) jwt.getHeader().toJSONObject().get(TokenHeader.JWKS_URL); - } catch (ParseException e) { - throw new RuntimeException(e); - } + @Override + public String create(String token) { + String tokenJku; + try { + JWT jwt = JWTParser.parse(token); + tokenJku = (String) jwt.getHeader().toJSONObject().get(TokenHeader.JWKS_URL); + } catch (ParseException e) { + throw new RuntimeException(e); + } - if (tokenJku == null || tokenJku.contains("localhost") || tokenJku.contains("127.0.0.1")) { - return tokenJku; - } + if (tokenJku == null || tokenJku.contains("localhost") || tokenJku.contains("127.0.0.1")) { + return tokenJku; + } - throw new IllegalArgumentException("JKU is not trusted because it does not target localhost."); - } + throw new IllegalArgumentException("JKU is not trusted because it does not target localhost."); + } } \ No newline at end of file diff --git a/token-client/README.md b/token-client/README.md index acbfdcc00e..a8dd6e6425 100644 --- a/token-client/README.md +++ b/token-client/README.md @@ -49,7 +49,7 @@ In context of a Spring Boot application you can leverage autoconfiguration provi com.sap.cloud.security resourceserver-security-spring-boot-starter - 3.3.2 + 3.3.4 ``` In context of Spring Applications you will need the following dependencies: @@ -57,7 +57,7 @@ In context of Spring Applications you will need the following dependencies: com.sap.cloud.security.xsuaa token-client - 3.3.2 + 3.3.4 org.apache.httpcomponents @@ -124,7 +124,7 @@ See the [OAuth2ServiceConfiguration](#oauth2serviceconfiguration) section and [H com.sap.cloud.security.xsuaa token-client - 3.3.2 + 3.3.4 org.apache.httpcomponents diff --git a/token-client/pom.xml b/token-client/pom.xml index 466c64daa4..d24caf93ea 100644 --- a/token-client/pom.xml +++ b/token-client/pom.xml @@ -9,7 +9,7 @@ com.sap.cloud.security.xsuaa parent - 3.3.2 + 3.3.4 token-client @@ -111,12 +111,6 @@ 1.4.14 test - - io.github.hakky54 - logcaptor - 2.9.2 - test - org.wiremock wiremock-standalone diff --git a/token-client/src/main/java/com/sap/cloud/security/client/DefaultHttpClientFactory.java b/token-client/src/main/java/com/sap/cloud/security/client/DefaultHttpClientFactory.java index 980d1e4cbe..7337928b9b 100644 --- a/token-client/src/main/java/com/sap/cloud/security/client/DefaultHttpClientFactory.java +++ b/token-client/src/main/java/com/sap/cloud/security/client/DefaultHttpClientFactory.java @@ -16,15 +16,10 @@ import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import javax.net.ssl.SSLContext; import java.io.IOException; import java.security.GeneralSecurityException; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @@ -41,7 +36,6 @@ * implementation of {@link HttpClientFactory}. */ public class DefaultHttpClientFactory implements HttpClientFactory { - private static final Logger LOGGER = LoggerFactory.getLogger(DefaultHttpClientFactory.class); private static final int DEFAULT_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(5); private static final int DEFAULT_SOCKET_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(30); @@ -49,8 +43,6 @@ public class DefaultHttpClientFactory implements HttpClientFactory { private static final int MAX_CONNECTIONS = 200; private final ConcurrentHashMap sslConnectionPool = new ConcurrentHashMap<>(); private final org.apache.http.client.config.RequestConfig requestConfig; - // reuse ssl connections - final Set httpClientsCreated = Collections.synchronizedSet(new HashSet<>()); public DefaultHttpClientFactory() { requestConfig = org.apache.http.client.config.RequestConfig.custom() @@ -64,15 +56,11 @@ public DefaultHttpClientFactory() { @Override public CloseableHttpClient createClient(ClientIdentity clientIdentity) throws HttpClientException { String clientId = clientIdentity != null ? clientIdentity.getId() : null; - if (httpClientsCreated.contains(clientId)) { - LOGGER.warn("Application has already created HttpClient for clientId = {}, please check.", clientId); - } - httpClientsCreated.add(clientId); HttpClientBuilder httpClientBuilder = HttpClients.custom().setDefaultRequestConfig(requestConfig); if (clientId != null && clientIdentity.isCertificateBased()) { - SslConnection connectionPool = sslConnectionPool.computeIfAbsent(clientId, - s -> new SslConnection(clientIdentity)); + SslConnection connectionPool = sslConnectionPool.compute(clientId, + (s, c) -> new SslConnection(clientIdentity)); return httpClientBuilder .setConnectionManager(connectionPool.poolingConnectionManager) .setSSLContext(connectionPool.context) diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/DefaultOAuth2TokenKeyService.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/DefaultOAuth2TokenKeyService.java index a658752782..8389adc94c 100644 --- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/DefaultOAuth2TokenKeyService.java +++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/DefaultOAuth2TokenKeyService.java @@ -41,11 +41,12 @@ public DefaultOAuth2TokenKeyService(@Nonnull CloseableHttpClient httpClient) { } @Override - public String retrieveTokenKeys(@Nonnull URI tokenKeysEndpointUri, Map params) throws OAuth2ServiceException { + public String retrieveTokenKeys(@Nonnull URI tokenKeysEndpointUri, Map params) + throws OAuth2ServiceException { Assertions.assertNotNull(tokenKeysEndpointUri, "Token key endpoint must not be null!"); HttpUriRequest request = new HttpGet(tokenKeysEndpointUri); - for(Map.Entry p : params.entrySet()) { + for (Map.Entry p : params.entrySet()) { request.addHeader(p.getKey(), p.getValue()); } request.addHeader(HttpHeaders.USER_AGENT, HttpClientUtil.getUserAgent()); @@ -58,10 +59,12 @@ public String retrieveTokenKeys(@Nonnull URI tokenKeysEndpointUri, Map headers) { @@ -70,6 +70,7 @@ public Integer getHttpStatusCode() { /** * Returns the HTTP headers of the failed OAuth2 service request + * * @return list of HTTP headers */ public List getHeaders() { diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenKeyService.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenKeyService.java index 8f3e100516..43d45bbdaf 100644 --- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenKeyService.java +++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenKeyService.java @@ -26,37 +26,37 @@ public interface OAuth2TokenKeyService { * @param tokenKeysEndpointUri * the token endpoint URI (jku). * @param tenantId - * the unique identifier of the tenant. Obligatory parameter in context of - * multi-tenant IAS applications to make sure that the tenant id - * belongs to the IAS tenant. - * @return list of JSON Web Token (JWT) keys as - * JSON string. + * the unique identifier of the tenant. Obligatory parameter in + * context of multi-tenant IAS applications to make sure that the + * tenant id belongs to the IAS tenant. + * @return list of JSON Web Token (JWT) keys as JSON string. * @throws OAuth2ServiceException * in case of an error during the http request. */ - default String retrieveTokenKeys(@Nonnull URI tokenKeysEndpointUri, @Nullable String tenantId) throws OAuth2ServiceException { + default String retrieveTokenKeys(@Nonnull URI tokenKeysEndpointUri, @Nullable String tenantId) + throws OAuth2ServiceException { return retrieveTokenKeys(tokenKeysEndpointUri, Collections.singletonMap(HttpHeaders.X_APP_TID, tenantId)); } /** - * @deprecated Use {@link OAuth2TokenKeyService#retrieveTokenKeys(URI, Map)} instead - * Requests token web key set from IAS OAuth Server. + * @deprecated Use {@link OAuth2TokenKeyService#retrieveTokenKeys(URI, Map)} + * instead Requests token web key set from IAS OAuth Server. * * @param tokenKeysEndpointUri * the token endpoint URI (jku). * @param tenantId - * the unique identifier of the tenant. Obligatory parameter in context of - * multi-tenant IAS applications to make sure that the tenant id - * belongs to the IAS tenant. + * the unique identifier of the tenant. Obligatory parameter in + * context of multi-tenant IAS applications to make sure that the + * tenant id belongs to the IAS tenant. * @param clientId - * clientId from the service binding - * @return list of JSON Web Token (JWT) keys as - * JSON string. + * clientId from the service binding + * @return list of JSON Web Token (JWT) keys as JSON string. * @throws OAuth2ServiceException * in case of an error during the http request. */ @Deprecated - default String retrieveTokenKeys(@Nonnull URI tokenKeysEndpointUri, @Nullable String tenantId, @Nullable String clientId) throws OAuth2ServiceException { + default String retrieveTokenKeys(@Nonnull URI tokenKeysEndpointUri, @Nullable String tenantId, + @Nullable String clientId) throws OAuth2ServiceException { Map params = new HashMap<>(2, 1); params.put(HttpHeaders.X_APP_TID, tenantId); params.put(HttpHeaders.X_CLIENT_ID, clientId); @@ -67,10 +67,15 @@ default String retrieveTokenKeys(@Nonnull URI tokenKeysEndpointUri, @Nullable St /** * Retrieves the JWKS (JSON Web Key Set) from the OAuth2 Server. * - * @param tokenKeysEndpointUri the JWKS endpoint URI. - * @param params additional header parameters that are sent along with the request. Use constants from {@link HttpHeaders} for the header keys. + * @param tokenKeysEndpointUri + * the JWKS endpoint URI. + * @param params + * additional header parameters that are sent along with the request. + * Use constants from {@link HttpHeaders} for the header keys. * @return a JWKS in JSON format. - * @throws OAuth2ServiceException in case of an error during the http request. + * @throws OAuth2ServiceException + * in case of an error during the http request. */ - String retrieveTokenKeys(@Nonnull URI tokenKeysEndpointUri, Map params) throws OAuth2ServiceException; + String retrieveTokenKeys(@Nonnull URI tokenKeysEndpointUri, Map params) + throws OAuth2ServiceException; } diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenService.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenService.java index b7cea77371..013fbdef80 100644 --- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenService.java +++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenService.java @@ -13,9 +13,12 @@ import java.util.Map; /** - * Retrieves OAuth2 Access Tokens as documented on - * Cloud Foundry UAA.
- * Note that the XSUAA API might differ slightly from these specs which is why not all parameters from the Cloud Foundry UAA documentation are configurable via this library. + * Retrieves OAuth2 Access Tokens as documented on Cloud + * Foundry UAA.
+ * Note that the XSUAA API might differ slightly from these specs which is why + * not all parameters from the Cloud Foundry UAA documentation are configurable + * via this library. */ public interface OAuth2TokenService { diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/SpringOAuth2TokenKeyService.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/SpringOAuth2TokenKeyService.java index bd08cba1ad..2bff1b6151 100644 --- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/SpringOAuth2TokenKeyService.java +++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/SpringOAuth2TokenKeyService.java @@ -40,7 +40,7 @@ public String retrieveTokenKeys(@Nonnull URI tokenKeysEndpointUri, Map p : params.entrySet()) { + for (Map.Entry p : params.entrySet()) { headers.set(p.getKey(), p.getValue()); } @@ -52,12 +52,12 @@ public String retrieveTokenKeys(@Nonnull URI tokenKeysEndpointUri, Map h.getKey() + ": " + String.join(",", h.getValue())) - .collect(Collectors.joining(", ")) + "]") + "Error retrieving token keys. Request headers [" + headers.entrySet().stream() + .map(h -> h.getKey() + ": " + String.join(",", h.getValue())) + .collect(Collectors.joining(", ")) + "]") .withUri(tokenKeysEndpointUri) .withHeaders(response.getHeaders().size() != 0 ? response.getHeaders().entrySet().stream().map( - h -> h.getKey() + ": " + String.join(",", h.getValue())) + h -> h.getKey() + ": " + String.join(",", h.getValue())) .toArray(String[]::new) : null) .withStatusCode(response.getStatusCode().value()) .withResponseBody(response.getBody()) @@ -65,11 +65,11 @@ public String retrieveTokenKeys(@Nonnull URI tokenKeysEndpointUri, Map h.getKey() + ": " + String.join(",", h.getValue()))) .withUri(tokenKeysEndpointUri) .withHeaders(ex.getResponseHeaders() != null ? ex.getResponseHeaders().entrySet().stream().map( - h -> h.getKey() + ": " + String.join(",", h.getValue())) + h -> h.getKey() + ": " + String.join(",", h.getValue())) .toArray(String[]::new) : null) .withStatusCode(ex.getStatusCode().value()) .withResponseBody(ex.getResponseBodyAsString()) diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenService.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenService.java index dbee575890..7079ea0f90 100644 --- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenService.java +++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenService.java @@ -72,12 +72,14 @@ protected OAuth2TokenResponse requestAccessToken(URI tokenEndpointUri, HttpHeade String warningMsg = String.format( "Error retrieving JWT token. Received status code %s. Call to XSUAA was not successful: %s", ex.getStatusCode(), ex.getResponseBodyAsString()); - throw new OAuth2ServiceException(warningMsg, ex.getStatusCode().value(), getHeaders(ex.getResponseHeaders())); + throw new OAuth2ServiceException(warningMsg, ex.getStatusCode().value(), + getHeaders(ex.getResponseHeaders())); } catch (HttpServerErrorException ex) { String warningMsg = String.format("Server error while obtaining access token from XSUAA (%s): %s", ex.getStatusCode(), ex.getResponseBodyAsString()); LOGGER.error(warningMsg, ex); - throw new OAuth2ServiceException(warningMsg, ex.getStatusCode().value(), getHeaders(ex.getResponseHeaders())); + throw new OAuth2ServiceException(warningMsg, ex.getStatusCode().value(), + getHeaders(ex.getResponseHeaders())); } catch (ResourceAccessException ex) { String warningMsg = String.format( "RestClient isn't configured properly - Error while obtaining access token from XSUAA (%s): %s", @@ -98,7 +100,7 @@ protected OAuth2TokenResponse requestAccessToken(URI tokenEndpointUri, HttpHeade } private static List getHeaders(org.springframework.http.HttpHeaders ex) { - if (ex != null){ + if (ex != null) { return ex.toSingleValueMap().entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).toList(); } return Collections.emptyList(); diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/http/HttpHeaders.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/http/HttpHeaders.java index da5b70c93a..2bb01e6455 100644 --- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/http/HttpHeaders.java +++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/http/HttpHeaders.java @@ -20,15 +20,14 @@ public class HttpHeaders { /** * @deprecated use {@link #X_APP_TID} instead * - * will be removed with next major release 4.0.0 + * will be removed with next major release 4.0.0 */ - @Deprecated(forRemoval = true ) + @Deprecated(forRemoval = true) public static final String X_ZONE_UUID = "x-zone_uuid"; public static final String X_APP_TID = "x-app_tid"; public static final String X_CLIENT_ID = "x-client_id"; public static final String X_AZP = "x-azp"; - private final Set headers; public HttpHeaders(HttpHeader... headers) { diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/JwtBearerTokenFlow.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/JwtBearerTokenFlow.java index ce63075e93..2760f3d689 100644 --- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/JwtBearerTokenFlow.java +++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/JwtBearerTokenFlow.java @@ -8,10 +8,7 @@ import java.util.*; import static com.sap.cloud.security.xsuaa.Assertions.assertNotNull; -import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.AUTHORITIES; -import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.SCOPE; -import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.TOKEN_FORMAT; -import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.TOKEN_TYPE_OPAQUE; +import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.*; import static com.sap.cloud.security.xsuaa.tokenflows.XsuaaTokenFlowsUtils.buildAdditionalAuthoritiesJson; /** @@ -145,7 +142,8 @@ public JwtBearerTokenFlow disableCache(boolean disableCache) { /** * Can be used to change the format of the returned token. * - * @param opaque enables opaque token format when set to {@code true}. + * @param opaque + * enables opaque token format when set to {@code true}. * @return this builder. */ public JwtBearerTokenFlow setOpaqueTokenFormat(boolean opaque) { diff --git a/token-client/src/test/java/com/sap/cloud/security/client/DefaultHttpClientFactoryTest.java b/token-client/src/test/java/com/sap/cloud/security/client/DefaultHttpClientFactoryTest.java index 14a8e19bcc..0ea0741b73 100644 --- a/token-client/src/test/java/com/sap/cloud/security/client/DefaultHttpClientFactoryTest.java +++ b/token-client/src/test/java/com/sap/cloud/security/client/DefaultHttpClientFactoryTest.java @@ -8,15 +8,12 @@ import com.github.tomakehurst.wiremock.WireMockServer; import com.sap.cloud.security.config.ClientCredentials; import com.sap.cloud.security.config.ClientIdentity; -import nl.altindag.log.LogCaptor; import org.apache.commons.io.IOUtils; import org.apache.http.HttpHeaders; import org.apache.http.HttpStatus; -import org.apache.http.client.HttpClient; import org.apache.http.client.ResponseHandler; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -25,9 +22,7 @@ import java.nio.charset.StandardCharsets; import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.mockito.Mockito.when; class DefaultHttpClientFactoryTest { @@ -37,7 +32,6 @@ class DefaultHttpClientFactoryTest { private static final ClientIdentity config = Mockito.mock(ClientIdentity.class); private static final ClientIdentity config2 = Mockito.mock(ClientIdentity.class); private final DefaultHttpClientFactory cut = new DefaultHttpClientFactory(); - private static LogCaptor logCaptor; @BeforeAll static void setup() throws IOException { @@ -50,47 +44,6 @@ static void setup() throws IOException { when(config2.getKey()).thenReturn(readFromFile("/privateRSAKey.txt")); when(config2.getCertificate()).thenReturn(readFromFile("/certificates.txt")); when(config2.isCertificateBased()).thenCallRealMethod(); - - logCaptor = LogCaptor.forClass(DefaultHttpClientFactory.class); - } - - @AfterEach - void tearDown() { - logCaptor.clearLogs(); - } - - @Test - void createHttpClient_sameClientId() { - HttpClient client1 = cut.createClient(config); - HttpClient client2 = cut.createClient(config); - - assertNotSame(client1, client2); - - assertEquals(1, cut.httpClientsCreated.size()); - } - - @Test - void createHttpClient_differentClientId() { - HttpClient client1 = cut.createClient(config); - HttpClient client2 = cut.createClient(config2); - - assertNotSame(client1, client2); - - assertEquals(2, cut.httpClientsCreated.size()); - } - - @Test - void assertWarnWhenCalledMoreThanOnce() { - cut.createClient(config); - cut.createClient(config2); - assertThat(logCaptor.getWarnLogs()).isEmpty(); - - cut.createClient(config); - assertThat(logCaptor.getWarnLogs()).hasSize(1); - assertThat(logCaptor.getWarnLogs().get(0)) - .startsWith("Application has already created HttpClient for clientId = theClientId, please check."); - - logCaptor.clearLogs(); } private static String readFromFile(String file) throws IOException { diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/OAuth2ServiceExceptionTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/OAuth2ServiceExceptionTest.java index 5530d6193b..a4ff00cd8e 100644 --- a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/OAuth2ServiceExceptionTest.java +++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/OAuth2ServiceExceptionTest.java @@ -8,28 +8,29 @@ import static org.junit.jupiter.api.Assertions.*; class OAuth2ServiceExceptionTest { - public static final String SERVICE_EXCEPTION = "Service Exception"; - private static List headers; - private static OAuth2ServiceException builtWithHeaders; - private static OAuth2ServiceException createdWithHeaders; + public static final String SERVICE_EXCEPTION = "Service Exception"; + private static List headers; + private static OAuth2ServiceException builtWithHeaders; + private static OAuth2ServiceException createdWithHeaders; - @BeforeAll - static void setup() { - headers = List.of("header1=value1", "header2=value2"); - builtWithHeaders = OAuth2ServiceException.builder(SERVICE_EXCEPTION).withHeaders(headers.toArray(String[]::new)).withStatusCode(400).build(); - createdWithHeaders = new OAuth2ServiceException(SERVICE_EXCEPTION, 400, headers); - } + @BeforeAll + static void setup() { + headers = List.of("header1=value1", "header2=value2"); + builtWithHeaders = OAuth2ServiceException.builder(SERVICE_EXCEPTION).withHeaders(headers.toArray(String[]::new)) + .withStatusCode(400).build(); + createdWithHeaders = new OAuth2ServiceException(SERVICE_EXCEPTION, 400, headers); + } - @Test - void testWithHeaders() { - assertIterableEquals(headers, builtWithHeaders.getHeaders()); - assertTrue(builtWithHeaders.getMessage().contains(SERVICE_EXCEPTION)); - assertTrue(builtWithHeaders.getMessage().contains("[header1=value1, header2=value2]")); - assertEquals(400, builtWithHeaders.getHttpStatusCode()); + @Test + void testWithHeaders() { + assertIterableEquals(headers, builtWithHeaders.getHeaders()); + assertTrue(builtWithHeaders.getMessage().contains(SERVICE_EXCEPTION)); + assertTrue(builtWithHeaders.getMessage().contains("[header1=value1, header2=value2]")); + assertEquals(400, builtWithHeaders.getHttpStatusCode()); - assertIterableEquals(headers, createdWithHeaders.getHeaders()); - assertTrue(createdWithHeaders.getMessage().contains(SERVICE_EXCEPTION)); - assertFalse(createdWithHeaders.getMessage().contains("[header1=value1, header2=value2]")); - assertEquals(400, createdWithHeaders.getHttpStatusCode()); - } + assertIterableEquals(headers, createdWithHeaders.getHeaders()); + assertTrue(createdWithHeaders.getMessage().contains(SERVICE_EXCEPTION)); + assertFalse(createdWithHeaders.getMessage().contains("[header1=value1, header2=value2]")); + assertEquals(400, createdWithHeaders.getHttpStatusCode()); + } } \ No newline at end of file diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/SpringOAuth2TokenKeyServiceTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/SpringOAuth2TokenKeyServiceTest.java index 987b34c24b..c69ef0e1be 100644 --- a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/SpringOAuth2TokenKeyServiceTest.java +++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/SpringOAuth2TokenKeyServiceTest.java @@ -120,6 +120,7 @@ private ArgumentMatcher httpEntityContainsMandatoryHeaders() { boolean correctAppTid = httpGet.getHeaders().get(HttpHeaders.X_APP_TID).get(0).equals(APP_TID); boolean correctAzp = httpGet.getHeaders().get(HttpHeaders.X_AZP).get(0).equals(AZP); return correctAppTid && correctClientId && correctAzp; - }; } + }; + } } \ No newline at end of file diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/JwtBearerTokenFlowTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/JwtBearerTokenFlowTest.java index d68cc12e2d..f5226bc2b5 100644 --- a/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/JwtBearerTokenFlowTest.java +++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/JwtBearerTokenFlowTest.java @@ -138,7 +138,6 @@ public void execute_withOpaqueTokenFormat() throws TokenFlowException, OAuth2Ser optionalParametersCaptor.capture(), anyBoolean()); assertThat(optionalParametersCaptor.getValue()).doesNotContainEntry(TOKEN_FORMAT, OPAQUE); - cut.setOpaqueTokenFormat(true).execute(); verify(tokenService, times(2)) .retrieveAccessTokenViaJwtBearerTokenGrant(any(), any(), any(), any(),