diff options
author | Aurelien Poscia <aurelien.poscia@sonarsource.com> | 2024-12-18 15:01:26 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-12-20 20:03:10 +0000 |
commit | 6a0b2f2df478e4269e4419ae83c93519b75f1781 (patch) | |
tree | e6b2ec5562624d4074c0190db0cac8d27d4ec444 | |
parent | 24b47b2d66ba46e3dd7ff421576b6eb13f7666ab (diff) | |
download | sonarqube-6a0b2f2df478e4269e4419ae83c93519b75f1781.tar.gz sonarqube-6a0b2f2df478e4269e4419ae83c93519b75f1781.zip |
SONAR-24012 Migrate replay attack prevention mechanism
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; + } +} |