aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-auth-saml
diff options
context:
space:
mode:
authorJulien Lancelot <julien.lancelot@sonarsource.com>2020-06-15 18:19:02 +0200
committersonartech <sonartech@sonarsource.com>2020-06-15 20:05:16 +0000
commit8c7e9ded9ad3f8f9aca79558320f319d229c547c (patch)
tree048e0b153ed4f1897c586b30bc8dbf44e92e5ed2 /server/sonar-auth-saml
parent41f7eb48fac1b3199f5a75fe504ef309b441d34a (diff)
downloadsonarqube-8c7e9ded9ad3f8f9aca79558320f319d229c547c.tar.gz
sonarqube-8c7e9ded9ad3f8f9aca79558320f319d229c547c.zip
SONAR-13327 Fix SSF-107
* SONAR-13327 Create 'SAML_MESSAGE_IDS' table and DAO * SONAR-13327 Check SAML Message id not already exist during auth * SONAR-13327 Clean expired SAML Message ids daily
Diffstat (limited to 'server/sonar-auth-saml')
-rw-r--r--server/sonar-auth-saml/build.gradle2
-rw-r--r--server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlIdentityProvider.java5
-rw-r--r--server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlMessageIdChecker.java59
-rw-r--r--server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlModule.java1
-rw-r--r--server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlIdentityProviderTest.java51
-rw-r--r--server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlMessageIdCheckerTest.java90
-rw-r--r--server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlModuleTest.java2
7 files changed, 188 insertions, 22 deletions
diff --git a/server/sonar-auth-saml/build.gradle b/server/sonar-auth-saml/build.gradle
index 5651516b7b6..dd9d2341cb1 100644
--- a/server/sonar-auth-saml/build.gradle
+++ b/server/sonar-auth-saml/build.gradle
@@ -17,10 +17,12 @@ dependencies {
compileOnly 'com.google.code.findbugs:jsr305'
compileOnly 'com.squareup.okhttp3:okhttp'
compileOnly 'javax.servlet:javax.servlet-api'
+ compileOnly project(':server:sonar-db-dao')
compileOnly project(':sonar-core')
testCompile 'com.tngtech.java:junit-dataprovider'
testCompile 'junit:junit'
testCompile 'org.assertj:assertj-core'
testCompile 'org.mockito:mockito-core'
+ testCompile testFixtures(project(':server:sonar-db-dao'))
}
diff --git a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlIdentityProvider.java b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlIdentityProvider.java
index 692e0250669..f8033bbfc56 100644
--- a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlIdentityProvider.java
+++ b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlIdentityProvider.java
@@ -56,9 +56,11 @@ public class SamlIdentityProvider implements OAuth2IdentityProvider {
private static final String STATE_REQUEST_PARAMETER = "RelayState";
private final SamlSettings samlSettings;
+ private final SamlMessageIdChecker samlMessageIdChecker;
- public SamlIdentityProvider(SamlSettings samlSettings) {
+ public SamlIdentityProvider(SamlSettings samlSettings, SamlMessageIdChecker samlMessageIdChecker) {
this.samlSettings = samlSettings;
+ this.samlMessageIdChecker = samlMessageIdChecker;
}
@Override
@@ -107,6 +109,7 @@ public class SamlIdentityProvider implements OAuth2IdentityProvider {
LOGGER.trace("Name ID : {}", auth.getNameId());
checkAuthentication(auth);
+ samlMessageIdChecker.check(auth);
LOGGER.trace("Attributes received : {}", auth.getAttributes());
String login = getNonNullFirstAttribute(auth, samlSettings.getUserLogin());
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
new file mode 100644
index 00000000000..f913b3d6f37
--- /dev/null
+++ b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlMessageIdChecker.java
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 com.onelogin.saml2.Auth;
+import org.joda.time.Instant;
+import org.sonar.api.server.ServerSide;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.user.SamlMessageIdDto;
+
+import static java.util.Objects.requireNonNull;
+
+@ServerSide
+public class SamlMessageIdChecker {
+
+ private final DbClient dbClient;
+
+ public SamlMessageIdChecker(DbClient dbClient) {
+ this.dbClient = dbClient;
+ }
+
+ 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"));
+ 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();
+ }
+ }
+
+}
diff --git a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlModule.java b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlModule.java
index 63f9732870d..aecf19200ea 100644
--- a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlModule.java
+++ b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlModule.java
@@ -29,6 +29,7 @@ public class SamlModule extends Module {
protected void configureModule() {
add(
SamlIdentityProvider.class,
+ SamlMessageIdChecker.class,
SamlSettings.class);
List<PropertyDefinition> definitions = SamlSettings.definitions();
add(definitions.toArray(new Object[definitions.size()]));
diff --git a/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlIdentityProviderTest.java b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlIdentityProviderTest.java
index 58f3e87a0ab..3bd7cd2b85c 100644
--- a/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlIdentityProviderTest.java
+++ b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlIdentityProviderTest.java
@@ -30,15 +30,16 @@ import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.junit.Rule;
import org.junit.Test;
-import org.junit.rules.ExpectedException;
import org.sonar.api.config.PropertyDefinitions;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.server.authentication.OAuth2IdentityProvider;
import org.sonar.api.server.authentication.UnauthorizedException;
import org.sonar.api.server.authentication.UserIdentity;
import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -47,11 +48,13 @@ import static org.mockito.Mockito.when;
public class SamlIdentityProviderTest {
@Rule
- public ExpectedException expectedException = ExpectedException.none();
+ public DbTester db = DbTester.create();
private MapSettings settings = new MapSettings(new PropertyDefinitions(System2.INSTANCE, SamlSettings.definitions()));
- private SamlIdentityProvider underTest = new SamlIdentityProvider(new SamlSettings(settings.asConfig()));
+ private SamlMessageIdChecker samlMessageIdChecker = mock(SamlMessageIdChecker.class);
+
+ private SamlIdentityProvider underTest = new SamlIdentityProvider(new SamlSettings(settings.asConfig()), new SamlMessageIdChecker(db.getDbClient()));
@Test
public void check_fields() {
@@ -98,10 +101,9 @@ public class SamlIdentityProviderTest {
settings.setProperty("sonar.auth.saml.loginUrl", "invalid");
DumbInitContext context = new DumbInitContext();
- expectedException.expect(IllegalStateException.class);
- expectedException.expectMessage("Fail to create Auth");
-
- underTest.init(context);
+ assertThatThrownBy(() -> underTest.init(context))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Fail to create Auth");
}
@Test
@@ -159,10 +161,10 @@ public class SamlIdentityProviderTest {
setSettings(true);
DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_response_without_login.txt");
- expectedException.expect(NullPointerException.class);
- expectedException.expectMessage("login is missing");
+ assertThatThrownBy(() -> underTest.callback(callbackContext))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage("login is missing");
- underTest.callback(callbackContext);
}
@Test
@@ -170,10 +172,9 @@ public class SamlIdentityProviderTest {
setSettings(true);
DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_response_without_name.txt");
- expectedException.expect(NullPointerException.class);
- expectedException.expectMessage("name is missing");
-
- underTest.callback(callbackContext);
+ assertThatThrownBy(() -> underTest.callback(callbackContext))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage("name is missing");
}
@Test
@@ -182,10 +183,9 @@ public class SamlIdentityProviderTest {
settings.setProperty("sonar.auth.saml.certificate.secured", "invalid");
DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_full_response.txt");
- expectedException.expect(IllegalStateException.class);
- expectedException.expectMessage("Fail to create Auth");
-
- underTest.callback(callbackContext);
+ assertThatThrownBy(() -> underTest.callback(callbackContext))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Fail to create Auth");
}
@Test
@@ -218,10 +218,21 @@ public class SamlIdentityProviderTest {
"-----END CERTIFICATE-----\n");
DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_full_response.txt");
- expectedException.expect(UnauthorizedException.class);
- expectedException.expectMessage("Signature validation failed. SAML Response rejected");
+ assertThatThrownBy(() -> underTest.callback(callbackContext))
+ .isInstanceOf(UnauthorizedException.class)
+ .hasMessage("Signature validation failed. SAML Response rejected");
+ }
+
+ @Test
+ public void fail_callback_when_message_was_already_sent() {
+ setSettings(true);
+ DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_minimal_response.txt");
underTest.callback(callbackContext);
+
+ assertThatThrownBy(() -> underTest.callback(callbackContext))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("This message has already been processed");
}
private void setSettings(boolean enabled) {
diff --git a/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlMessageIdCheckerTest.java b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlMessageIdCheckerTest.java
new file mode 100644
index 00000000000..f5657459c0f
--- /dev/null
+++ b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlMessageIdCheckerTest.java
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 com.google.common.collect.ImmutableList;
+import com.onelogin.saml2.Auth;
+import java.util.Arrays;
+import org.joda.time.Instant;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.user.SamlMessageIdDto;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class SamlMessageIdCheckerTest {
+
+ @Rule
+ public DbTester db = DbTester.create();
+
+ private DbSession dbSession = db.getSession();
+
+ private Auth auth = mock(Auth.class);
+
+ private SamlMessageIdChecker underTest = new SamlMessageIdChecker(db.getDbClient());
+
+ @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)));
+
+ 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);
+ }
+
+ @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));
+ 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/test/java/org/sonar/auth/saml/SamlModuleTest.java b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlModuleTest.java
index 2add1d96f88..01aeba26b72 100644
--- a/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlModuleTest.java
+++ b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlModuleTest.java
@@ -31,6 +31,6 @@ public class SamlModuleTest {
public void verify_count_of_added_components() {
ComponentContainer container = new ComponentContainer();
new SamlModule().configure(container);
- assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 12);
+ assertThat(container.size()).isGreaterThan(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER);
}
}