aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAurelien Poscia <aurelien.poscia@sonarsource.com>2024-12-18 15:01:26 +0100
committersonartech <sonartech@sonarsource.com>2024-12-20 20:03:10 +0000
commit6a0b2f2df478e4269e4419ae83c93519b75f1781 (patch)
treee6b2ec5562624d4074c0190db0cac8d27d4ec444
parent24b47b2d66ba46e3dd7ff421576b6eb13f7666ab (diff)
downloadsonarqube-6a0b2f2df478e4269e4419ae83c93519b75f1781.tar.gz
sonarqube-6a0b2f2df478e4269e4419ae83c93519b75f1781.zip
SONAR-24012 Migrate replay attack prevention mechanism
-rw-r--r--server/sonar-auth-saml/src/it/java/org/sonar/auth/saml/SamlMessageIdCheckerIT.java71
-rw-r--r--server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/RelyingPartyRegistrationRepositoryProvider.java2
-rw-r--r--server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlAuthenticator.java8
-rw-r--r--server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlConfiguration.java8
-rw-r--r--server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlMessageIdChecker.java51
-rw-r--r--server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlPrivateKeyConverter.java2
-rw-r--r--server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SonarqubeSaml2ResponseValidator.java45
-rw-r--r--server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlAuthenticatorTest.java6
-rw-r--r--server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlPrivateKeyConverterTest.java2
-rw-r--r--server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SonarqubeSaml2ResponseValidatorTest.java94
10 files changed, 206 insertions, 83 deletions
diff --git a/server/sonar-auth-saml/src/it/java/org/sonar/auth/saml/SamlMessageIdCheckerIT.java b/server/sonar-auth-saml/src/it/java/org/sonar/auth/saml/SamlMessageIdCheckerIT.java
index 16bc31b577a..5df9a33a0ed 100644
--- a/server/sonar-auth-saml/src/it/java/org/sonar/auth/saml/SamlMessageIdCheckerIT.java
+++ b/server/sonar-auth-saml/src/it/java/org/sonar/auth/saml/SamlMessageIdCheckerIT.java
@@ -19,61 +19,56 @@
*/
package org.sonar.auth.saml;
-import org.junit.Rule;
-import org.sonar.db.DbSession;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.Optional;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
import org.sonar.db.DbTester;
+import org.sonar.db.user.SamlMessageIdDto;
+import org.springframework.security.saml2.core.Saml2Error;
+
+import static java.time.temporal.ChronoUnit.DAYS;
+import static org.assertj.core.api.Assertions.assertThat;
public class SamlMessageIdCheckerIT {
- @Rule
- public DbTester db = DbTester.create();
+ public static final SamlMessageIdDto MESSAGE_1 = new SamlMessageIdDto().setMessageId("MESSAGE_1").setExpirationDate(1_000_000_000L);
- private DbSession dbSession = db.getSession();
+ @RegisterExtension
+ private final DbTester db = DbTester.create();
- //TODO
+ private final Clock clock = Clock.fixed(Instant.EPOCH, ZoneId.systemDefault());
-/* private Auth auth = mock(Auth.class);
+ private final SamlMessageIdChecker underTest = new SamlMessageIdChecker(db.getDbClient(), clock);
- private SamlMessageIdChecker underTest = new SamlMessageIdChecker(db.getDbClient());
+ @Test
+ public void check_fails_when_message_id_already_exist() {
+ insertMessageInDb();
+
+ Optional<Saml2Error> validationErrors = underTest.validateMessageIdWasNotAlreadyUsed("MESSAGE_1");
+ assertThat(validationErrors).isPresent();
+ Saml2Error saml2Error = validationErrors.orElseThrow();
+ assertThat(saml2Error.getErrorCode()).isEqualTo("response_id_already_used");
+ assertThat(saml2Error.getDescription()).isEqualTo("A message with the same ID was already processed");
+ }
@Test
public void check_do_not_fail_when_message_id_is_new_and_insert_saml_message_in_db() {
- db.getDbClient().samlMessageIdDao().insert(dbSession, new SamlMessageIdDto().setMessageId("MESSAGE_1").setExpirationDate(1_000_000_000L));
- db.commit();
- when(auth.getLastMessageId()).thenReturn("MESSAGE_2");
- when(auth.getLastAssertionNotOnOrAfter()).thenReturn(ImmutableList.of(Instant.ofEpochMilli(10_000_000_000L)));
+ insertMessageInDb();
- assertThatCode(() -> underTest.check(auth)).doesNotThrowAnyException();
+ Optional<Saml2Error> validationErrors = underTest.validateMessageIdWasNotAlreadyUsed("MESSAGE_2");
+ assertThat(validationErrors).isEmpty();
- SamlMessageIdDto result = db.getDbClient().samlMessageIdDao().selectByMessageId(dbSession, "MESSAGE_2").get();
+ SamlMessageIdDto result = db.getDbClient().samlMessageIdDao().selectByMessageId(db.getSession(), "MESSAGE_2").orElseThrow();
assertThat(result.getMessageId()).isEqualTo("MESSAGE_2");
- assertThat(result.getExpirationDate()).isEqualTo(10_000_000_000L);
+ assertThat(Instant.ofEpochMilli(result.getExpirationDate())).isEqualTo(Instant.EPOCH.plus(1, DAYS));
}
- @Test
- public void check_fails_when_message_id_already_exist() {
- db.getDbClient().samlMessageIdDao().insert(dbSession, new SamlMessageIdDto().setMessageId("MESSAGE_1").setExpirationDate(1_000_000_000L));
+ private void insertMessageInDb() {
+ db.getDbClient().samlMessageIdDao().insert(db.getSession(), MESSAGE_1);
db.commit();
- when(auth.getLastMessageId()).thenReturn("MESSAGE_1");
- when(auth.getLastAssertionNotOnOrAfter()).thenReturn(ImmutableList.of(Instant.ofEpochMilli(10_000_000_000L)));
-
- assertThatThrownBy(() -> underTest.check(auth))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("This message has already been processed");
}
- @Test
- public void check_insert_message_id_using_oldest_NotOnOrAfter_value() {
- db.getDbClient().samlMessageIdDao().insert(dbSession, new SamlMessageIdDto().setMessageId("MESSAGE_1").setExpirationDate(1_000_000_000L));
- db.commit();
- when(auth.getLastMessageId()).thenReturn("MESSAGE_2");
- when(auth.getLastAssertionNotOnOrAfter())
- .thenReturn(Arrays.asList(Instant.ofEpochMilli(10_000_000_000L), Instant.ofEpochMilli(30_000_000_000L), Instant.ofEpochMilli(20_000_000_000L)));
-
- assertThatCode(() -> underTest.check(auth)).doesNotThrowAnyException();
-
- SamlMessageIdDto result = db.getDbClient().samlMessageIdDao().selectByMessageId(dbSession, "MESSAGE_2").get();
- assertThat(result.getMessageId()).isEqualTo("MESSAGE_2");
- assertThat(result.getExpirationDate()).isEqualTo(10_000_000_000L);
- }*/
}
diff --git a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/RelyingPartyRegistrationRepositoryProvider.java b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/RelyingPartyRegistrationRepositoryProvider.java
index e04d342ed50..4c4d708b94b 100644
--- a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/RelyingPartyRegistrationRepositoryProvider.java
+++ b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/RelyingPartyRegistrationRepositoryProvider.java
@@ -30,7 +30,7 @@ public class RelyingPartyRegistrationRepositoryProvider {
private final SamlCertificateConverter samlCertificateConverter;
private final SamlPrivateKeyConverter samlPrivateKeyConverter;
- public RelyingPartyRegistrationRepositoryProvider(SamlSettings samlSettings, SamlCertificateConverter samlCertificateConverter, SamlPrivateKeyConverter samlPrivateKeyConverter) {
+ RelyingPartyRegistrationRepositoryProvider(SamlSettings samlSettings, SamlCertificateConverter samlCertificateConverter, SamlPrivateKeyConverter samlPrivateKeyConverter) {
this.samlSettings = samlSettings;
this.samlCertificateConverter = samlCertificateConverter;
this.samlPrivateKeyConverter = samlPrivateKeyConverter;
diff --git a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlAuthenticator.java b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlAuthenticator.java
index eb504142cff..09492f9b06e 100644
--- a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlAuthenticator.java
+++ b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlAuthenticator.java
@@ -41,15 +41,13 @@ public class SamlAuthenticator {
private static final String STATE_REQUEST_PARAMETER = "RelayState";
private final SamlSettings samlSettings;
- private final SamlMessageIdChecker samlMessageIdChecker;
private final RedirectToUrlProvider redirectToUrlProvider;
private final SamlResponseAuthenticator samlResponseAuthenticator;
private final PrincipalToUserIdentityConverter principalToUserIdentityConverter;
- public SamlAuthenticator(SamlSettings samlSettings, SamlMessageIdChecker samlMessageIdChecker, RedirectToUrlProvider redirectToUrlProvider,
+ public SamlAuthenticator(SamlSettings samlSettings, RedirectToUrlProvider redirectToUrlProvider,
SamlResponseAuthenticator samlResponseAuthenticator, PrincipalToUserIdentityConverter principalToUserIdentityConverter) {
this.samlSettings = samlSettings;
- this.samlMessageIdChecker = samlMessageIdChecker;
this.redirectToUrlProvider = redirectToUrlProvider;
this.samlResponseAuthenticator = samlResponseAuthenticator;
this.principalToUserIdentityConverter = principalToUserIdentityConverter;
@@ -69,10 +67,6 @@ public class SamlAuthenticator {
Saml2AuthenticatedPrincipal principal = samlResponseAuthenticator.authenticate(processedRequest, context.getCallbackUrl());
- //LOGGER.trace("Name ID : {}", getNameId(auth)); //TODO extract nameid
- // this.checkMessageId(auth);
-
- //TODO create class to convert Saml2AuthenticatedPrincipal to UserIdentity
LOGGER.trace("Attributes received : {}", principal.getAttributes());
return principalToUserIdentityConverter.convertToUserIdentity(principal);
}
diff --git a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlConfiguration.java b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlConfiguration.java
index 2797659a400..53fa148b05c 100644
--- a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlConfiguration.java
+++ b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlConfiguration.java
@@ -34,15 +34,15 @@ public class SamlConfiguration {
@Bean
OpenSaml4AuthenticationProvider openSaml4AuthenticationProvider(SonarqubeSaml2ResponseValidator sonarqubeSaml2ResponseValidator){
OpenSaml4AuthenticationProvider openSaml4AuthenticationProvider = new OpenSaml4AuthenticationProvider();
- openSaml4AuthenticationProvider.setAssertionValidator(createIgnoringAssertionValidator(sonarqubeSaml2ResponseValidator));
+ openSaml4AuthenticationProvider.setAssertionValidator(createIgnoringResponseToAssertionValidator(sonarqubeSaml2ResponseValidator));
openSaml4AuthenticationProvider.setResponseValidator(sonarqubeSaml2ResponseValidator);
return openSaml4AuthenticationProvider;
}
- private static Converter<OpenSaml4AuthenticationProvider.AssertionToken, Saml2ResponseValidatorResult> createIgnoringAssertionValidator(
+ private static Converter<OpenSaml4AuthenticationProvider.AssertionToken, Saml2ResponseValidatorResult> createIgnoringResponseToAssertionValidator(
Converter<OpenSaml4AuthenticationProvider.ResponseToken, Saml2ResponseValidatorResult> customResponseValidator) {
-
- return OpenSaml4AuthenticationProvider.createDefaultAssertionValidatorWithParameters(validationContextParameterConsumer(((SonarqubeSaml2ResponseValidator) customResponseValidator)));
+ Consumer<Map<String, Object>> validationContextParameters = validationContextParameterConsumer(((SonarqubeSaml2ResponseValidator) customResponseValidator));
+ return OpenSaml4AuthenticationProvider.createDefaultAssertionValidatorWithParameters(validationContextParameters);
}
private static Consumer<Map<String, Object>> validationContextParameterConsumer(SonarqubeSaml2ResponseValidator saml2CustomResponseValidator) {
diff --git a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlMessageIdChecker.java b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlMessageIdChecker.java
index a688134b297..d321bfdab7f 100644
--- a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlMessageIdChecker.java
+++ b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlMessageIdChecker.java
@@ -19,36 +19,51 @@
*/
package org.sonar.auth.saml;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.sonar.api.server.ServerSide;
import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.user.SamlMessageIdDto;
+import org.springframework.security.saml2.core.Saml2Error;
@ServerSide
public class SamlMessageIdChecker {
+ private static final Logger LOGGER = LoggerFactory.getLogger(SamlMessageIdChecker.class);
+
private final DbClient dbClient;
+ private final Clock clock;
- public SamlMessageIdChecker(DbClient dbClient) {
+ public SamlMessageIdChecker(DbClient dbClient, Clock clock) {
this.dbClient = dbClient;
+ this.clock = clock;
}
- //TODO
-/*
- public void check(Auth auth) {
- String messageId = requireNonNull(auth.getLastMessageId(), "Message ID is missing");
- Instant lastAssertionNotOnOrAfter = auth.getLastAssertionNotOnOrAfter().stream()
- .sorted()
- .findFirst()
- .orElseThrow(() -> new IllegalArgumentException("Missing NotOnOrAfter element"));
+ public Optional<Saml2Error> validateMessageIdWasNotAlreadyUsed(String responseId) {
try (DbSession dbSession = dbClient.openSession(false)) {
- dbClient.samlMessageIdDao().selectByMessageId(dbSession, messageId)
- .ifPresent(m -> {
- throw new IllegalArgumentException("This message has already been processed");
- });
- dbClient.samlMessageIdDao().insert(dbSession, new SamlMessageIdDto()
- .setMessageId(messageId)
- .setExpirationDate(lastAssertionNotOnOrAfter.getMillis()));
- dbSession.commit();
+ LOGGER.trace("Validating that response ID '{}' was not already used", responseId);
+ if (responseIdAlreadyUsed(dbSession, responseId)) {
+ return Optional.of(new Saml2Error("response_id_already_used", "A message with the same ID was already processed"));
+ }
+ persistResponseId(dbSession, responseId);
}
+ return Optional.empty();
+ }
+
+ private boolean responseIdAlreadyUsed(DbSession dbSession, String responseId) {
+ return dbClient.samlMessageIdDao().selectByMessageId(dbSession, responseId).isPresent();
}
-*/
+
+ private void persistResponseId(DbSession dbSession, String responseId) {
+ dbClient.samlMessageIdDao().insert(dbSession, new SamlMessageIdDto()
+ .setMessageId(responseId)
+ .setExpirationDate(Instant.now(clock).plus(1, ChronoUnit.DAYS).toEpochMilli()));
+ dbSession.commit();
+ }
+
}
diff --git a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlPrivateKeyConverter.java b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlPrivateKeyConverter.java
index 836a17fd997..d27e9051816 100644
--- a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlPrivateKeyConverter.java
+++ b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlPrivateKeyConverter.java
@@ -39,7 +39,7 @@ class SamlPrivateKeyConverter {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
- throw new RuntimeException(e);
+ throw new RuntimeException("Error while loading private key, please check the format", e);
}
}
diff --git a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SonarqubeSaml2ResponseValidator.java b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SonarqubeSaml2ResponseValidator.java
index 98b4d5697c5..20eebda7aea 100644
--- a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SonarqubeSaml2ResponseValidator.java
+++ b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SonarqubeSaml2ResponseValidator.java
@@ -19,8 +19,14 @@
*/
package org.sonar.auth.saml;
+import com.google.common.annotations.VisibleForTesting;
+import jakarta.inject.Inject;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.List;
import java.util.function.Supplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
@@ -31,29 +37,48 @@ import static org.springframework.security.saml2.provider.service.authentication
class SonarqubeSaml2ResponseValidator implements Converter<ResponseToken, Saml2ResponseValidatorResult> {
- private final Converter<ResponseToken, Saml2ResponseValidatorResult> delegate = createDefaultResponseValidator();
+ private static final Logger LOGGER = LoggerFactory.getLogger(SonarqubeSaml2ResponseValidator.class);
+
+ private final Converter<ResponseToken, Saml2ResponseValidatorResult> delegate;
+ private final SamlMessageIdChecker samlMessageIdChecker;
private Supplier<String> validInResponseToSupplier;
+ @Inject
+ SonarqubeSaml2ResponseValidator(SamlMessageIdChecker samlMessageIdChecker) {
+ this(samlMessageIdChecker, createDefaultResponseValidator());
+ }
+
+ @VisibleForTesting
+ SonarqubeSaml2ResponseValidator(SamlMessageIdChecker samlMessageIdChecker, Converter<ResponseToken, Saml2ResponseValidatorResult> delegate) {
+ this.samlMessageIdChecker = samlMessageIdChecker;
+ this.delegate = delegate;
+ }
+
@Override
public Saml2ResponseValidatorResult convert(ResponseToken responseToken) {
- Saml2ResponseValidatorResult result = delegate.convert(responseToken);
-
- String inResponseTo = responseToken.getResponse().getInResponseTo();
- validInResponseToSupplier = () -> inResponseTo;
+ Saml2ResponseValidatorResult validationResults = delegate.convert(responseToken);
- Collection<Saml2Error> errors = removeInResponseToErrorIfPresent(result);
+ List<Saml2Error> errors = new ArrayList<>(getValidationErrorsWithoutInResponseTo(responseToken, validationResults));
+ samlMessageIdChecker.validateMessageIdWasNotAlreadyUsed(responseToken.getResponse().getID()).ifPresent(errors::add);
+ LOGGER.debug("Saml validation errors: {}", errors);
return Saml2ResponseValidatorResult.failure(errors);
}
- public Supplier<String> getValidInResponseToSupplier() {
- return validInResponseToSupplier;
+ private Collection<Saml2Error> getValidationErrorsWithoutInResponseTo(ResponseToken responseToken, Saml2ResponseValidatorResult validationResults) {
+ String inResponseTo = responseToken.getResponse().getInResponseTo();
+ validInResponseToSupplier = () -> inResponseTo;
+ return removeInResponseToError(validationResults);
}
- private Collection<Saml2Error> removeInResponseToErrorIfPresent(Saml2ResponseValidatorResult result) {
+ private Collection<Saml2Error> removeInResponseToError(Saml2ResponseValidatorResult result) {
return result.getErrors().stream()
- .filter(error -> !error.getErrorCode().equals(INVALID_IN_RESPONSE_TO))
+ .filter(error -> !INVALID_IN_RESPONSE_TO.equals(error.getErrorCode()))
.toList();
}
+
+ public Supplier<String> getValidInResponseToSupplier() {
+ return validInResponseToSupplier;
+ }
}
diff --git a/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlAuthenticatorTest.java b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlAuthenticatorTest.java
index fa4fbfc41ff..3e4a3bdcf75 100644
--- a/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlAuthenticatorTest.java
+++ b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlAuthenticatorTest.java
@@ -41,7 +41,7 @@ public class SamlAuthenticatorTest {
private SamlSettings samlSettings = new SamlSettings(settings.asConfig());
- private final SamlAuthenticator underTest = new SamlAuthenticator(samlSettings, mock(SamlMessageIdChecker.class), null, null, null); //TODO
+ private final SamlAuthenticator underTest = new SamlAuthenticator(samlSettings, null, null, null); //TODO
@Test
public void authentication_status_with_errors_returned_when_init_fails() {
@@ -63,7 +63,7 @@ public class SamlAuthenticatorTest {
settings.setProperty("sonar.auth.saml.sp.privateKey.secured", "Not a PKCS8 key");
assertThatIllegalStateException()
- .isThrownBy(() -> underTest.initLogin("","", mock(JakartaHttpRequest.class), mock(JakartaHttpResponse.class)))
+ .isThrownBy(() -> underTest.initLogin("", "", mock(JakartaHttpRequest.class), mock(JakartaHttpResponse.class)))
.withMessage("Failed to create a SAML Auth")
.havingCause()
.withMessage("Error in parsing service provider private key, please make sure that it is in PKCS 8 format.");
@@ -77,7 +77,7 @@ public class SamlAuthenticatorTest {
settings.setProperty("sonar.auth.saml.sp.privateKey.secured", "PRIVATE_KEY");
assertThatIllegalStateException()
- .isThrownBy(() -> underTest.initLogin("","", mock(JakartaHttpRequest.class), mock(JakartaHttpResponse.class)))
+ .isThrownBy(() -> underTest.initLogin("", "", mock(JakartaHttpRequest.class), mock(JakartaHttpResponse.class)))
.withMessage("Failed to create a SAML Auth")
.havingCause()
.withMessage("Service provider certificate is missing");
diff --git a/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlPrivateKeyConverterTest.java b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlPrivateKeyConverterTest.java
index ed19072e79e..63b562843f4 100644
--- a/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlPrivateKeyConverterTest.java
+++ b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlPrivateKeyConverterTest.java
@@ -102,7 +102,7 @@ class SamlPrivateKeyConverterTest {
void toPrivateKey_whenPrivateKeyIsInvalid_throwsException() {
assertThatRuntimeException()
.isThrownBy(() -> samlPrivateKeyConverter.toPrivateKey("invalidKey"))
- .withMessage("bla");
+ .withMessage("Error while loading private key, please check the format");
}
diff --git a/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SonarqubeSaml2ResponseValidatorTest.java b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SonarqubeSaml2ResponseValidatorTest.java
new file mode 100644
index 00000000000..4ee9562a0db
--- /dev/null
+++ b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SonarqubeSaml2ResponseValidatorTest.java
@@ -0,0 +1,94 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.auth.saml;
+
+import java.util.Optional;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.security.saml2.core.Saml2Error;
+import org.springframework.security.saml2.core.Saml2ErrorCodes;
+import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
+import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class SonarqubeSaml2ResponseValidatorTest {
+
+ @Mock
+ private Converter<ResponseToken, Saml2ResponseValidatorResult> delegate;
+
+ @Mock
+ private SamlMessageIdChecker samlMessageIdChecker;
+
+ @InjectMocks
+ private SonarqubeSaml2ResponseValidator sonarqubeSaml2ResponseValidator;
+
+ @Test
+ void convert_ifResponseIdAlreadyUsed_shouldReturnFailure() {
+ ResponseToken responseToken = mockResponseToken();
+
+ when(delegate.convert(responseToken)).thenReturn(Saml2ResponseValidatorResult.success());
+ Saml2Error saml2Error = mock();
+ when(samlMessageIdChecker.validateMessageIdWasNotAlreadyUsed("responseId")).thenReturn(Optional.of(saml2Error));
+
+ Saml2ResponseValidatorResult validatorResult = sonarqubeSaml2ResponseValidator.convert(responseToken);
+
+ assertThat(validatorResult.getErrors()).containsExactly(saml2Error);
+ }
+
+ @Test
+ void convert_returnsErrorFromDelegate() {
+ ResponseToken responseToken = mockResponseToken();
+ Saml2Error saml2Error = mock(RETURNS_DEEP_STUBS);
+
+ when(delegate.convert(responseToken)).thenReturn(Saml2ResponseValidatorResult.failure(saml2Error, saml2Error));
+
+ Saml2ResponseValidatorResult validatorResult = sonarqubeSaml2ResponseValidator.convert(responseToken);
+
+ assertThat(validatorResult.getErrors()).containsExactly(saml2Error, saml2Error);
+ }
+
+ @Test
+ void convert_filtersOutInResponseToValidationErrors() {
+ ResponseToken responseToken = mockResponseToken();
+ Saml2Error inResponseToError = new Saml2Error(Saml2ErrorCodes.INVALID_IN_RESPONSE_TO, "description");
+ Saml2Error otherError = new Saml2Error("other", "description");
+
+ when(delegate.convert(responseToken)).thenReturn(Saml2ResponseValidatorResult.failure(inResponseToError, otherError));
+
+ Saml2ResponseValidatorResult validatorResult = sonarqubeSaml2ResponseValidator.convert(responseToken);
+
+ assertThat(validatorResult.getErrors()).containsExactly(otherError);
+ }
+
+ private static ResponseToken mockResponseToken() {
+ ResponseToken responseToken = mock(RETURNS_DEEP_STUBS);
+ when(responseToken.getResponse().getID()).thenReturn("responseId");
+ return responseToken;
+ }
+}