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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2019 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.sonar.api.internal.apachecommons.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.getLogin()).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.getLogin()).isEqualTo("johndoe");
  103. assertThat(callbackContext.userIdentity.getName()).isEqualTo("John Doe");
  104. assertThat(callbackContext.userIdentity.getEmail()).isEqualTo("johndoe@email.com");
  105. assertThat(callbackContext.userIdentity.getProviderLogin()).isEqualTo("johndoe");
  106. assertThat(callbackContext.userIdentity.getGroups()).containsExactlyInAnyOrder("developer", "product-manager");
  107. }
  108. @Test
  109. public void callback_on_minimal_response() {
  110. setSettings(true);
  111. DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_minimal_response.txt");
  112. underTest.callback(callbackContext);
  113. assertThat(callbackContext.userIdentity.getLogin()).isEqualTo("johndoe");
  114. assertThat(callbackContext.userIdentity.getName()).isEqualTo("John Doe");
  115. assertThat(callbackContext.userIdentity.getEmail()).isNull();
  116. assertThat(callbackContext.userIdentity.getProviderLogin()).isEqualTo("johndoe");
  117. assertThat(callbackContext.userIdentity.getGroups()).isEmpty();
  118. }
  119. @Test
  120. public void callback_does_not_sync_group_when_group_setting_is_not_set() {
  121. setSettings(true);
  122. settings.setProperty("sonar.auth.saml.group.name", (String) null);
  123. DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_full_response.txt");
  124. underTest.callback(callbackContext);
  125. assertThat(callbackContext.userIdentity.getLogin()).isEqualTo("johndoe");
  126. assertThat(callbackContext.userIdentity.getGroups()).isEmpty();
  127. }
  128. @Test
  129. public void fail_to_callback_when_login_is_missing() {
  130. setSettings(true);
  131. DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_response_without_login.txt");
  132. expectedException.expect(NullPointerException.class);
  133. expectedException.expectMessage("login is missing");
  134. underTest.callback(callbackContext);
  135. }
  136. @Test
  137. public void fail_to_callback_when_name_is_missing() {
  138. setSettings(true);
  139. DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_response_without_name.txt");
  140. expectedException.expect(NullPointerException.class);
  141. expectedException.expectMessage("name is missing");
  142. underTest.callback(callbackContext);
  143. }
  144. @Test
  145. public void fail_to_callback_when_certificate_is_invalid() {
  146. setSettings(true);
  147. settings.setProperty("sonar.auth.saml.certificate.secured", "invalid");
  148. DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_full_response.txt");
  149. expectedException.expect(IllegalStateException.class);
  150. expectedException.expectMessage("Fail to create Auth");
  151. underTest.callback(callbackContext);
  152. }
  153. @Test
  154. public void fail_to_callback_when_using_wrong_certificate() {
  155. setSettings(true);
  156. settings.setProperty("sonar.auth.saml.certificate.secured", "-----BEGIN CERTIFICATE-----\n" +
  157. "MIIEIzCCAwugAwIBAgIUHUzPjy5E2TmnsmTRT2sIUBRXFF8wDQYJKoZIhvcNAQEF\n" +
  158. "BQAwXDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC1NvbmFyU291cmNlMRUwEwYDVQQL\n" +
  159. "DAxPbmVMb2dpbiBJZFAxIDAeBgNVBAMMF09uZUxvZ2luIEFjY291bnQgMTMxMTkx\n" +
  160. "MB4XDTE4MDcxOTA4NDUwNVoXDTIzMDcxOTA4NDUwNVowXDELMAkGA1UEBhMCVVMx\n" +
  161. "FDASBgNVBAoMC1NvbmFyU291cmNlMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxIDAe\n" +
  162. "BgNVBAMMF09uZUxvZ2luIEFjY291bnQgMTMxMTkxMIIBIjANBgkqhkiG9w0BAQEF\n" +
  163. "AAOCAQ8AMIIBCgKCAQEArlpKHm4EkJiQyy+4GtZBixcy7fWnreB96T7cOoWLmWkK\n" +
  164. "05FM5M/boWHZsvaNAuHsoCAMzIY3/l+55WbORzAxsloH7rvDaDrdPYQN+sU9bzsD\n" +
  165. "ZkmDGDmA3QBSm/h/p5SiMkWU5Jg34toDdM0rmzUStIOMq6Gh/Ykx3fRRSjswy48x\n" +
  166. "wfZLy+0wU7lasHqdfk54dVbb7mCm9J3iHZizvOt2lbtzGbP6vrrjpzvZm43ZRgP8\n" +
  167. "FapYA8G3lczdIaG4IaLW6kYIRORd0UwI7IAwkao3uIo12rh1T6DLVyzjOs9PdIkb\n" +
  168. "HbICN2EehB/ut3wohuPwmwp2UmqopIMVVaBSsmSlYwIDAQABo4HcMIHZMAwGA1Ud\n" +
  169. "EwEB/wQCMAAwHQYDVR0OBBYEFAXGFMKYgtpzCpfpBUPQ1H/9AeDrMIGZBgNVHSME\n" +
  170. "gZEwgY6AFAXGFMKYgtpzCpfpBUPQ1H/9AeDroWCkXjBcMQswCQYDVQQGEwJVUzEU\n" +
  171. "MBIGA1UECgwLU29uYXJTb3VyY2UxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEgMB4G\n" +
  172. "A1UEAwwXT25lTG9naW4gQWNjb3VudCAxMzExOTGCFB1Mz48uRNk5p7Jk0U9rCFAU\n" +
  173. "VxRfMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAPHgi9IdDaTxD\n" +
  174. "R5R8KHMdt385Uq8XC5pd0Li6y5RR2k6SKjThCt+eQU7D0Y2CyYU27vfCa2DQV4hJ\n" +
  175. "4v4UfQv3NR/fYfkVSsNpxjBXBI3YWouxt2yg7uwdZBdgGYd37Yv3g9PdIZenjOhr\n" +
  176. "Ck6WjdleMAWHRgJpocmB4IOESSyTfUul3jFupWnkbnn8c0ue6zwXd7LA1/yjVT2l\n" +
  177. "Yh45+lz25aIOlyyo7OUw2TD15LIl8OOIuWRS4+UWy5+VdhXMbmpSEQH+Byod90g6\n" +
  178. "A1bKpOFhRBzcxaZ6B2hB4SqjTBzS9zdmJyyFs/WNJxHri3aorcdqG9oUakjJJqqX\n" +
  179. "E13skIMV2g==\n" +
  180. "-----END CERTIFICATE-----\n");
  181. DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_full_response.txt");
  182. expectedException.expect(UnauthorizedException.class);
  183. expectedException.expectMessage("Signature validation failed. SAML Response rejected");
  184. underTest.callback(callbackContext);
  185. }
  186. private void setSettings(boolean enabled) {
  187. if (enabled) {
  188. settings.setProperty("sonar.auth.saml.applicationId", "MyApp");
  189. settings.setProperty("sonar.auth.saml.providerId", "http://localhost:8080/auth/realms/sonarqube");
  190. settings.setProperty("sonar.auth.saml.loginUrl", "http://localhost:8080/auth/realms/sonarqube/protocol/saml");
  191. settings.setProperty("sonar.auth.saml.certificate.secured",
  192. "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=");
  193. settings.setProperty("sonar.auth.saml.user.login", "login");
  194. settings.setProperty("sonar.auth.saml.user.name", "name");
  195. settings.setProperty("sonar.auth.saml.user.email", "email");
  196. settings.setProperty("sonar.auth.saml.group.name", "groups");
  197. settings.setProperty("sonar.auth.saml.enabled", true);
  198. } else {
  199. settings.setProperty("sonar.auth.saml.enabled", false);
  200. }
  201. }
  202. private static class DumbInitContext implements OAuth2IdentityProvider.InitContext {
  203. private HttpServletResponse response = mock(HttpServletResponse.class);
  204. private final AtomicBoolean generateCsrfState = new AtomicBoolean(false);
  205. @Override
  206. public String generateCsrfState() {
  207. generateCsrfState.set(true);
  208. return null;
  209. }
  210. @Override
  211. public void redirectTo(String url) {
  212. }
  213. @Override
  214. public String getCallbackUrl() {
  215. return "http://localhost/oauth/callback/saml";
  216. }
  217. @Override
  218. public HttpServletRequest getRequest() {
  219. return mock(HttpServletRequest.class);
  220. }
  221. @Override
  222. public HttpServletResponse getResponse() {
  223. return response;
  224. }
  225. }
  226. private static class DumbCallbackContext implements OAuth2IdentityProvider.CallbackContext {
  227. private HttpServletResponse response = mock(HttpServletResponse.class);
  228. private HttpServletRequest request = mock(HttpServletRequest.class);
  229. private final AtomicBoolean redirectedToRequestedPage = new AtomicBoolean(false);
  230. private final AtomicBoolean verifyState = new AtomicBoolean(false);
  231. private UserIdentity userIdentity = null;
  232. public DumbCallbackContext(String encodedResponseFile) {
  233. when(getRequest().getRequestURL()).thenReturn(new StringBuffer("http://localhost/oauth/callback/saml"));
  234. Map<String, String[]> parameterMap = new HashMap<>();
  235. parameterMap.put("SAMLResponse", new String[] {loadResponse(encodedResponseFile)});
  236. when(getRequest().getParameterMap()).thenReturn(parameterMap);
  237. }
  238. private String loadResponse(String file) {
  239. try (InputStream json = getClass().getResourceAsStream("SamlIdentityProviderTest/" + file)) {
  240. return IOUtils.toString(json, StandardCharsets.UTF_8.name());
  241. } catch (IOException e) {
  242. throw new IllegalStateException(e);
  243. }
  244. }
  245. @Override
  246. public void verifyCsrfState() {
  247. throw new IllegalStateException("This method should not be called !");
  248. }
  249. @Override
  250. public void verifyCsrfState(String parameterName) {
  251. assertThat(parameterName).isEqualTo("RelayState");
  252. verifyState.set(true);
  253. }
  254. @Override
  255. public void redirectToRequestedPage() {
  256. redirectedToRequestedPage.set(true);
  257. }
  258. @Override
  259. public void authenticate(UserIdentity userIdentity) {
  260. this.userIdentity = userIdentity;
  261. }
  262. @Override
  263. public String getCallbackUrl() {
  264. return "http://localhost/oauth/callback/saml";
  265. }
  266. @Override
  267. public HttpServletRequest getRequest() {
  268. return request;
  269. }
  270. @Override
  271. public HttpServletResponse getResponse() {
  272. return response;
  273. }
  274. }
  275. }