Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adapt to optimized IAS server API #1359

Merged
merged 15 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
# Change Log
All notable changes to this project will be documented in this file.

## 3.3.0
**Breaking Change [java-security-test]**: To validate mocked XSUAA tokens issued by java-security-test module, the UAA_DOMAIN property of the service configuration must now include the port of the Wiremock server.\
Likewise for validating IAS tokens, the trusted *domains* array of the service configuration also needs to include the Wiremock URL including the port.\
The full wiremock URL is available via SecurityTestContext#getWireMockServer#baseUrl.

*Note*: If you are building your configuration via SecurityTestContext#getOAuth2ServiceConfigurationBuilderFromFile, this will already be preconfigured correctly, but you must not overwrite these properties with only "localhost".

- [java-security]
- [XSUAA/IAS] Adapt optimized server API
- [spring-xsuaa]
- Adapt optimized server API

## 3.2.1
Hot fix for the CVE-2023-5072

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ The SAP Cloud Security Services Integration is published to maven central: https
<dependency>
<groupId>com.sap.cloud.security</groupId>
<artifactId>java-bom</artifactId>
<version>3.2.1</version>
<version>3.3.0</version>
<scope>import</scope>
<type>pom</type>
</dependency>
Expand Down
2 changes: 1 addition & 1 deletion bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<groupId>com.sap.cloud.security</groupId>
<artifactId>java-bom</artifactId>
<version>3.2.1</version>
<version>3.3.0</version>
<packaging>pom</packaging>
<name>java-bom</name>

Expand Down
2 changes: 1 addition & 1 deletion env/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<parent>
<groupId>com.sap.cloud.security.xsuaa</groupId>
<artifactId>parent</artifactId>
<version>3.2.1</version>
<version>3.3.0</version>
</parent>

<groupId>com.sap.cloud.security</groupId>
Expand Down
2 changes: 1 addition & 1 deletion java-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
<dependency>
<groupId>com.sap.cloud.security</groupId>
<artifactId>java-api</artifactId>
<version>3.2.1</version>
<version>3.3.0</version>
</dependency>
```
2 changes: 1 addition & 1 deletion java-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<parent>
<groupId>com.sap.cloud.security.xsuaa</groupId>
<artifactId>parent</artifactId>
<version>3.2.1</version>
<version>3.3.0</version>
</parent>

<groupId>com.sap.cloud.security</groupId>
Expand Down
2 changes: 1 addition & 1 deletion java-security-it/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<parent>
<artifactId>parent</artifactId>
<groupId>com.sap.cloud.security.xsuaa</groupId>
<version>3.2.1</version>
<version>3.3.0</version>
</parent>

<artifactId>java-security-it</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public void xsuaaTokenValidationFails_withIasCombiningValidator() {
ValidationResult result = tokenValidator.validate(token);
assertThat(result.isValid()).isFalse();
assertThat(result.getErrorDescription()).startsWith(
"Issuer is not trusted because issuer 'http://auth.com' doesn't match any of these domains '[myauth.com]' of the identity provider");
"Issuer http://auth.com was not a trusted domain or a subdomain of the trusted domains [myauth.com].");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import com.sap.cloud.security.config.OAuth2ServiceConfigurationBuilder;
import com.sap.cloud.security.config.Service;
import com.sap.cloud.security.config.ServiceConstants;
import com.sap.cloud.security.test.RSAKeys;
import com.sap.cloud.security.test.extension.SecurityTestExtension;
import com.sap.cloud.security.token.Token;
Expand Down Expand Up @@ -65,9 +64,8 @@ class JavaSSRFAttackTest {
void maliciousPartOfJwksIsNotUsedToObtainToken(String jwksUrl, boolean isValid)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
OAuth2ServiceConfigurationBuilder configuration =
extension.getContext()
.getOAuth2ServiceConfigurationBuilderFromFile("/xsuaa/vcap_services-single.json")
.withProperty(ServiceConstants.XSUAA.UAA_DOMAIN, extension.getContext().getWireMockServer().baseUrl());
extension.getContext().getOAuth2ServiceConfigurationBuilderFromFile("/xsuaa/vcap_services-single.json");

Token token;
if (isValid) {
token = extension.getContext().getJwtGeneratorFromFile("/xsuaa/token.json")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ private OAuth2ServiceConfigurationBuilder createXsuaaConfigurationBuilder() {

private OAuth2ServiceConfigurationBuilder createIasConfigurationBuilder() {
return OAuth2ServiceConfigurationBuilder.forService(IAS)
.withDomains(SecurityTest.DEFAULT_DOMAIN)
.withDomains(securityIasTest.getWireMockServer().baseUrl())
.withClientId(SecurityTest.DEFAULT_CLIENT_ID);
}
}
Expand Down
2 changes: 1 addition & 1 deletion java-security-test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ It is pre-configured with a security filter that only accepts valid tokens. Furt
<dependency>
<groupId>com.sap.cloud.security</groupId>
<artifactId>java-security-test</artifactId>
<version>3.2.1</version>
<version>3.3.0</version>
<scope>test</scope>
</dependency>
```
Expand Down
2 changes: 1 addition & 1 deletion java-security-test/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<parent>
<groupId>com.sap.cloud.security.xsuaa</groupId>
<artifactId>parent</artifactId>
<version>3.2.1</version>
<version>3.3.0</version>
</parent>

<groupId>com.sap.cloud.security</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.sap.cloud.security.config.OAuth2ServiceConfigurationBuilder;
import com.sap.cloud.security.config.Service;
import com.sap.cloud.security.config.ServiceBindingMapper;
import com.sap.cloud.security.config.ServiceConstants;
import com.sap.cloud.security.json.JsonParsingException;
import com.sap.cloud.security.test.api.ApplicationServerConfiguration;
import com.sap.cloud.security.test.api.SecurityTestContext;
Expand Down Expand Up @@ -38,7 +39,6 @@

import javax.annotation.Nullable;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPublicKey;
Expand Down Expand Up @@ -186,11 +186,15 @@ public OAuth2ServiceConfigurationBuilder getOAuth2ServiceConfigurationBuilderFro
LOGGER.warn("More than one OAuth2 service binding found in resource. Using configuration of first one!");
}

OAuth2ServiceConfigurationBuilder builder = ServiceBindingMapper
.mapToOAuth2ServiceConfigurationBuilder(serviceBindings.get(0));
ServiceBinding binding = serviceBindings.get(0);
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);
builder = builder.withDomains(issuerUrl).withUrl(issuerUrl);

if(Objects.equals(Service.from(binding.getServiceName().get()), XSUAA)) {
builder.withProperty(ServiceConstants.XSUAA.UAA_DOMAIN, wireMockServer.baseUrl());
}
}

return builder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public void getConfigurationBuilderFromFile_configurationHasCorrectUrl() {
}

@Test
public void getJwtGeneratorFromFile_setsTestingDefaults() throws IOException {
public void getJwtGeneratorFromFile_setsTestingDefaults() {
Token token = cut.getJwtGeneratorFromFile("/token.json").createToken();

String baseUrl = cut.base.wireMockServer.baseUrl();
Expand Down
2 changes: 1 addition & 1 deletion java-security/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ To be able to validate tokens it performs the following tasks:
<dependency>
<groupId>com.sap.cloud.security</groupId>
<artifactId>java-security</artifactId>
<version>3.2.1</version>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
Expand Down
2 changes: 1 addition & 1 deletion java-security/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<parent>
<groupId>com.sap.cloud.security.xsuaa</groupId>
<artifactId>parent</artifactId>
<version>3.2.1</version>
<version>3.3.0</version>
</parent>

<groupId>com.sap.cloud.security</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@
package com.sap.cloud.security.token.validation.validators;

import com.sap.cloud.security.config.OAuth2ServiceConfiguration;
import com.sap.cloud.security.config.Service;
import com.sap.cloud.security.json.JsonParsingException;
import com.sap.cloud.security.token.Token;
import com.sap.cloud.security.token.validation.ValidationResult;
import com.sap.cloud.security.token.validation.Validator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.net.URISyntaxException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;

import static com.sap.cloud.security.token.validation.ValidationResults.createInvalid;
import static com.sap.cloud.security.token.validation.ValidationResults.createValid;
Expand All @@ -36,6 +38,7 @@
* These checks are a prerequisite for using the `JwtSignatureValidator`.
*/
class JwtIssuerValidator implements Validator<Token> {
protected static final String HTTPS_SCHEME = "https://";
private final List<String> domains;
protected final Logger logger = LoggerFactory.getLogger(getClass());

Expand All @@ -54,55 +57,34 @@ class JwtIssuerValidator implements Validator<Token> {

@Override
public ValidationResult validate(Token token) {
String issuer = token.getIssuer();
if (token.getService().equals(Service.IAS) && !issuer.startsWith("http")) {
issuer = "https://" + issuer;
}
ValidationResult validationResult = validateUrl(issuer);
if (validationResult.isErroneous()) {
return validationResult;
String issuer;

try {
issuer = token.getIssuer();
} catch(JsonParsingException e) {
return createInvalid("Issuer validation can not be performed because token issuer claim was not a String value.");
}
return matchesTokenIssuerUrl(issuer);
}

private ValidationResult matchesTokenIssuerUrl(String issuer) {
URI issuerUri = URI.create(issuer);
if (issuerUri.getQuery() == null && issuerUri.getFragment() == null && issuerUri.getHost() != null) {
for (String d : domains) {
if (issuerUri.getHost().endsWith(d)) {
return createValid();
}
}
if (issuer == null || issuer.isBlank()) {
return createInvalid("Issuer validation can not be performed because token does not contain an issuer claim.");
}
return createInvalid(
"Issuer is not trusted because issuer '{}' doesn't match any of these domains '{}' of the identity provider.",
issuer, domains);
}

private ValidationResult validateUrl(String issuer) {
URI issuerUri;
String issuerUrl = issuer.startsWith(HTTPS_SCHEME) || issuer.startsWith("http://localhost") ? issuer : HTTPS_SCHEME + issuer;
try {
if (issuer == null || issuer.trim().isEmpty()) {
return createInvalid(
"Issuer validation can not be performed because Jwt token does not contain an issuer claim.");
}
if (!issuer.startsWith("http")) {
return createInvalid(
"Issuer is not trusted because issuer '{}' does not provide a valid URI (missing http scheme). Please contact your Identity Provider Administrator.",
issuer);
}
issuerUri = new URI(issuer);
if (issuerUri.getQuery() == null && issuerUri.getFragment() == null && issuerUri.getHost() != null) {
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.");
}

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 .<trustedDomain> 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)) {
return createValid();
}
} catch (URISyntaxException e) {
logger.error(
"Error: issuer claim '{}' does not provide a valid URI: {}. Please contact your Identity Provider Administrator.",
issuer, e.getMessage(), e);
}
return createInvalid(
"Issuer is not trusted because issuer does not provide a valid URI. Please contact your Identity Provider Administrator.",
issuer);
}

return createInvalid("Issuer {} was not a trusted domain or a subdomain of the trusted domains {}.", issuer, domains);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ private URI getJwksUri(Token token) throws OAuth2ServiceException {
}

if (isTenantIdCheckEnabled && !domain.equals("" + configuration.getUrl()) && token.getAppTid() == null) {
throw new IllegalArgumentException("OIDC token must provide a valid " + TokenClaims.SAP_GLOBAL_APP_TID + " header when issuer has a different domain than the url from the service credentials.");
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.");
}


Expand Down
Loading
Loading