You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SamlIdentityProviderTest.java 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2020 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.auth.saml;
  21. import java.io.IOException;
  22. import java.io.InputStream;
  23. import java.nio.charset.StandardCharsets;
  24. import java.util.HashMap;
  25. import java.util.Map;
  26. import java.util.concurrent.atomic.AtomicBoolean;
  27. import javax.servlet.http.HttpServletRequest;
  28. import javax.servlet.http.HttpServletResponse;
  29. import org.junit.Rule;
  30. import org.junit.Test;
  31. import org.junit.rules.ExpectedException;
  32. import org.sonar.api.config.PropertyDefinitions;
  33. import org.sonar.api.config.internal.MapSettings;
  34. import org.apache.commons.io.IOUtils;
  35. import org.sonar.api.server.authentication.OAuth2IdentityProvider;
  36. import org.sonar.api.server.authentication.UnauthorizedException;
  37. import org.sonar.api.server.authentication.UserIdentity;
  38. import static org.assertj.core.api.Assertions.assertThat;
  39. import static org.mockito.ArgumentMatchers.anyString;
  40. import static org.mockito.Mockito.mock;
  41. import static org.mockito.Mockito.verify;
  42. import static org.mockito.Mockito.when;
  43. public class SamlIdentityProviderTest {
  44. @Rule
  45. public ExpectedException expectedException = ExpectedException.none();
  46. private MapSettings settings = new MapSettings(new PropertyDefinitions(SamlSettings.definitions()));
  47. private SamlIdentityProvider underTest = new SamlIdentityProvider(new SamlSettings(settings.asConfig()));
  48. @Test
  49. public void check_fields() {
  50. setSettings(true);
  51. assertThat(underTest.getKey()).isEqualTo("saml");
  52. assertThat(underTest.getName()).isEqualTo("SAML");
  53. assertThat(underTest.getDisplay().getIconPath()).isEqualTo("/images/saml.png");
  54. assertThat(underTest.getDisplay().getBackgroundColor()).isEqualTo("#444444");
  55. assertThat(underTest.allowsUsersToSignUp()).isTrue();
  56. }
  57. @Test
  58. public void provider_name_is_provided_by_setting() {
  59. // Default value
  60. assertThat(underTest.getName()).isEqualTo("SAML");
  61. settings.setProperty("sonar.auth.saml.providerName", "My Provider");
  62. assertThat(underTest.getName()).isEqualTo("My Provider");
  63. }
  64. @Test
  65. public void is_enabled() {
  66. setSettings(true);
  67. assertThat(underTest.isEnabled()).isTrue();
  68. setSettings(false);
  69. assertThat(underTest.isEnabled()).isFalse();
  70. }
  71. @Test
  72. public void init() throws IOException {
  73. setSettings(true);
  74. DumbInitContext context = new DumbInitContext();
  75. underTest.init(context);
  76. verify(context.response).sendRedirect(anyString());
  77. assertThat(context.generateCsrfState.get()).isTrue();
  78. }
  79. @Test
  80. public void fail_to_init_when_login_url_is_invalid() {
  81. setSettings(true);
  82. settings.setProperty("sonar.auth.saml.loginUrl", "invalid");
  83. DumbInitContext context = new DumbInitContext();
  84. expectedException.expect(IllegalStateException.class);
  85. expectedException.expectMessage("Fail to create Auth");
  86. underTest.init(context);
  87. }
  88. @Test
  89. public void callback() {
  90. setSettings(true);
  91. DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_full_response.txt");
  92. underTest.callback(callbackContext);
  93. assertThat(callbackContext.redirectedToRequestedPage.get()).isTrue();
  94. assertThat(callbackContext.userIdentity.getProviderLogin()).isEqualTo("johndoe");
  95. assertThat(callbackContext.verifyState.get()).isTrue();
  96. }
  97. @Test
  98. public void callback_on_full_response() {
  99. setSettings(true);
  100. DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_full_response.txt");
  101. underTest.callback(callbackContext);
  102. assertThat(callbackContext.userIdentity.getName()).isEqualTo("John Doe");
  103. assertThat(callbackContext.userIdentity.getEmail()).isEqualTo("johndoe@email.com");
  104. assertThat(callbackContext.userIdentity.getProviderLogin()).isEqualTo("johndoe");
  105. assertThat(callbackContext.userIdentity.getGroups()).containsExactlyInAnyOrder("developer", "product-manager");
  106. }
  107. @Test
  108. public void callback_on_minimal_response() {
  109. setSettings(true);
  110. DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_minimal_response.txt");
  111. underTest.callback(callbackContext);
  112. assertThat(callbackContext.userIdentity.getName()).isEqualTo("John Doe");
  113. assertThat(callbackContext.userIdentity.getEmail()).isNull();
  114. assertThat(callbackContext.userIdentity.getProviderLogin()).isEqualTo("johndoe");
  115. assertThat(callbackContext.userIdentity.getGroups()).isEmpty();
  116. }
  117. @Test
  118. public void callback_does_not_sync_group_when_group_setting_is_not_set() {
  119. setSettings(true);
  120. settings.setProperty("sonar.auth.saml.group.name", (String) null);
  121. DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_full_response.txt");
  122. underTest.callback(callbackContext);
  123. assertThat(callbackContext.userIdentity.getProviderLogin()).isEqualTo("johndoe");
  124. assertThat(callbackContext.userIdentity.getGroups()).isEmpty();
  125. }
  126. @Test
  127. public void fail_to_callback_when_login_is_missing() {
  128. setSettings(true);
  129. DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_response_without_login.txt");
  130. expectedException.expect(NullPointerException.class);
  131. expectedException.expectMessage("login is missing");
  132. underTest.callback(callbackContext);
  133. }
  134. @Test
  135. public void fail_to_callback_when_name_is_missing() {
  136. setSettings(true);
  137. DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_response_without_name.txt");
  138. expectedException.expect(NullPointerException.class);
  139. expectedException.expectMessage("name is missing");
  140. underTest.callback(callbackContext);
  141. }
  142. @Test
  143. public void fail_to_callback_when_certificate_is_invalid() {
  144. setSettings(true);
  145. settings.setProperty("sonar.auth.saml.certificate.secured", "invalid");
  146. DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_full_response.txt");
  147. expectedException.expect(IllegalStateException.class);
  148. expectedException.expectMessage("Fail to create Auth");
  149. underTest.callback(callbackContext);
  150. }
  151. @Test
  152. public void fail_to_callback_when_using_wrong_certificate() {
  153. setSettings(true);
  154. settings.setProperty("sonar.auth.saml.certificate.secured", "-----BEGIN CERTIFICATE-----\n" +
  155. "MIIEIzCCAwugAwIBAgIUHUzPjy5E2TmnsmTRT2sIUBRXFF8wDQYJKoZIhvcNAQEF\n" +
  156. "BQAwXDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC1NvbmFyU291cmNlMRUwEwYDVQQL\n" +
  157. "DAxPbmVMb2dpbiBJZFAxIDAeBgNVBAMMF09uZUxvZ2luIEFjY291bnQgMTMxMTkx\n" +
  158. "MB4XDTE4MDcxOTA4NDUwNVoXDTIzMDcxOTA4NDUwNVowXDELMAkGA1UEBhMCVVMx\n" +
  159. "FDASBgNVBAoMC1NvbmFyU291cmNlMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxIDAe\n" +
  160. "BgNVBAMMF09uZUxvZ2luIEFjY291bnQgMTMxMTkxMIIBIjANBgkqhkiG9w0BAQEF\n" +
  161. "AAOCAQ8AMIIBCgKCAQEArlpKHm4EkJiQyy+4GtZBixcy7fWnreB96T7cOoWLmWkK\n" +
  162. "05FM5M/boWHZsvaNAuHsoCAMzIY3/l+55WbORzAxsloH7rvDaDrdPYQN+sU9bzsD\n" +
  163. "ZkmDGDmA3QBSm/h/p5SiMkWU5Jg34toDdM0rmzUStIOMq6Gh/Ykx3fRRSjswy48x\n" +
  164. "wfZLy+0wU7lasHqdfk54dVbb7mCm9J3iHZizvOt2lbtzGbP6vrrjpzvZm43ZRgP8\n" +
  165. "FapYA8G3lczdIaG4IaLW6kYIRORd0UwI7IAwkao3uIo12rh1T6DLVyzjOs9PdIkb\n" +
  166. "HbICN2EehB/ut3wohuPwmwp2UmqopIMVVaBSsmSlYwIDAQABo4HcMIHZMAwGA1Ud\n" +
  167. "EwEB/wQCMAAwHQYDVR0OBBYEFAXGFMKYgtpzCpfpBUPQ1H/9AeDrMIGZBgNVHSME\n" +
  168. "gZEwgY6AFAXGFMKYgtpzCpfpBUPQ1H/9AeDroWCkXjBcMQswCQYDVQQGEwJVUzEU\n" +
  169. "MBIGA1UECgwLU29uYXJTb3VyY2UxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEgMB4G\n" +
  170. "A1UEAwwXT25lTG9naW4gQWNjb3VudCAxMzExOTGCFB1Mz48uRNk5p7Jk0U9rCFAU\n" +
  171. "VxRfMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAPHgi9IdDaTxD\n" +
  172. "R5R8KHMdt385Uq8XC5pd0Li6y5RR2k6SKjThCt+eQU7D0Y2CyYU27vfCa2DQV4hJ\n" +
  173. "4v4UfQv3NR/fYfkVSsNpxjBXBI3YWouxt2yg7uwdZBdgGYd37Yv3g9PdIZenjOhr\n" +
  174. "Ck6WjdleMAWHRgJpocmB4IOESSyTfUul3jFupWnkbnn8c0ue6zwXd7LA1/yjVT2l\n" +
  175. "Yh45+lz25aIOlyyo7OUw2TD15LIl8OOIuWRS4+UWy5+VdhXMbmpSEQH+Byod90g6\n" +
  176. "A1bKpOFhRBzcxaZ6B2hB4SqjTBzS9zdmJyyFs/WNJxHri3aorcdqG9oUakjJJqqX\n" +
  177. "E13skIMV2g==\n" +
  178. "-----END CERTIFICATE-----\n");
  179. DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_full_response.txt");
  180. expectedException.expect(UnauthorizedException.class);
  181. expectedException.expectMessage("Signature validation failed. SAML Response rejected");
  182. underTest.callback(callbackContext);
  183. }
  184. private void setSettings(boolean enabled) {
  185. if (enabled) {
  186. settings.setProperty("sonar.auth.saml.applicationId", "MyApp");
  187. settings.setProperty("sonar.auth.saml.providerId", "http://localhost:8080/auth/realms/sonarqube");
  188. settings.setProperty("sonar.auth.saml.loginUrl", "http://localhost:8080/auth/realms/sonarqube/protocol/saml");
  189. settings.setProperty("sonar.auth.saml.certificate.secured",
  190. "MIICoTCCAYkCBgFksusMzTANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlzb25hcnF1YmUwHhcNMTgwNzE5MTQyMDA2WhcNMjgwNzE5MTQyMTQ2WjAUMRIwEAYDVQQDDAlzb25hcnF1YmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEOth5gxpTs1f3bFGUD8hO97eMIsDZvvE3PZeKoeTRG7mOLu6rfLXphG3fE3E6/xqUhPP5p9hJl9DwgaMewhdZhfHqtOw6/SPMCQNFVNw9FQ7lprWKg8cZygYLDxhObEvCWPek8KcMb/vlKD8c8ha374O9qET51CVogDM5ropp02q0ELxoUKXqphKH4+sGXRVnDHaEsFHxse1HnciZT5mF1G45vxDItdAnWKkXYKVHC+Et52tCieqM0ygpQF1lWVJFXVOqsi03YkMu7IkWvSSfAw+uEcfmquT7FbxJ2n5gp94odAkQB0HK3fABrHr+G+n2QvWG6WwQPJTL0Ov0w+tNAgMBAAEwDQYJKoZIhvcNAQELBQADggEBACQfOrJF98nunKz6CN+YZXXMYhzQiqTD0MlzCg+Rdhir+WC/ru3Kz8omv52W/sXEMNQbEZBksVLl8W/1xeBS41Sf1nfutU560v/j3/OmOcnCw4qebqFH7nB8RL8vA4rGx430W/PeeUMikY1mSjlwhnJGiICQ3Y8I2qM6QWEr/Df2/gFCW2YnHbnS6Q/OwRQi+UFIzKklSQQa0gAnqfM4oSKU2OMhzScinWg1buMYfJSXgd4qIhPvRsZpqBsdt/OSrU2D5Y2YfSu8oIcxBRgJoERH5BV9GdOID4fS+TYw0M0QO/ORetNw1mA/8Npsy8okF8Cn7fDgbnWC8uz+/xDc14I=");
  191. settings.setProperty("sonar.auth.saml.user.login", "login");
  192. settings.setProperty("sonar.auth.saml.user.name", "name");
  193. settings.setProperty("sonar.auth.saml.user.email", "email");
  194. settings.setProperty("sonar.auth.saml.group.name", "groups");
  195. settings.setProperty("sonar.auth.saml.enabled", true);
  196. } else {
  197. settings.setProperty("sonar.auth.saml.enabled", false);
  198. }
  199. }
  200. private static class DumbInitContext implements OAuth2IdentityProvider.InitContext {
  201. private HttpServletResponse response = mock(HttpServletResponse.class);
  202. private final AtomicBoolean generateCsrfState = new AtomicBoolean(false);
  203. @Override
  204. public String generateCsrfState() {
  205. generateCsrfState.set(true);
  206. return null;
  207. }
  208. @Override
  209. public void redirectTo(String url) {
  210. }
  211. @Override
  212. public String getCallbackUrl() {
  213. return "http://localhost/oauth/callback/saml";
  214. }
  215. @Override
  216. public HttpServletRequest getRequest() {
  217. return mock(HttpServletRequest.class);
  218. }
  219. @Override
  220. public HttpServletResponse getResponse() {
  221. return response;
  222. }
  223. }
  224. private static class DumbCallbackContext implements OAuth2IdentityProvider.CallbackContext {
  225. private HttpServletResponse response = mock(HttpServletResponse.class);
  226. private HttpServletRequest request = mock(HttpServletRequest.class);
  227. private final AtomicBoolean redirectedToRequestedPage = new AtomicBoolean(false);
  228. private final AtomicBoolean verifyState = new AtomicBoolean(false);
  229. private UserIdentity userIdentity = null;
  230. public DumbCallbackContext(String encodedResponseFile) {
  231. when(getRequest().getRequestURL()).thenReturn(new StringBuffer("http://localhost/oauth/callback/saml"));
  232. Map<String, String[]> parameterMap = new HashMap<>();
  233. parameterMap.put("SAMLResponse", new String[] {loadResponse(encodedResponseFile)});
  234. when(getRequest().getParameterMap()).thenReturn(parameterMap);
  235. }
  236. private String loadResponse(String file) {
  237. try (InputStream json = getClass().getResourceAsStream("SamlIdentityProviderTest/" + file)) {
  238. return IOUtils.toString(json, StandardCharsets.UTF_8.name());
  239. } catch (IOException e) {
  240. throw new IllegalStateException(e);
  241. }
  242. }
  243. @Override
  244. public void verifyCsrfState() {
  245. throw new IllegalStateException("This method should not be called !");
  246. }
  247. @Override
  248. public void verifyCsrfState(String parameterName) {
  249. assertThat(parameterName).isEqualTo("RelayState");
  250. verifyState.set(true);
  251. }
  252. @Override
  253. public void redirectToRequestedPage() {
  254. redirectedToRequestedPage.set(true);
  255. }
  256. @Override
  257. public void authenticate(UserIdentity userIdentity) {
  258. this.userIdentity = userIdentity;
  259. }
  260. @Override
  261. public String getCallbackUrl() {
  262. return "http://localhost/oauth/callback/saml";
  263. }
  264. @Override
  265. public HttpServletRequest getRequest() {
  266. return request;
  267. }
  268. @Override
  269. public HttpServletResponse getResponse() {
  270. return response;
  271. }
  272. }
  273. }