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.

SamlIdentityProvider.java 6.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  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 com.onelogin.saml2.Auth;
  22. import com.onelogin.saml2.exception.SettingsException;
  23. import com.onelogin.saml2.settings.Saml2Settings;
  24. import com.onelogin.saml2.settings.SettingsBuilder;
  25. import java.io.IOException;
  26. import java.util.Collection;
  27. import java.util.HashMap;
  28. import java.util.HashSet;
  29. import java.util.List;
  30. import java.util.Map;
  31. import java.util.Set;
  32. import javax.annotation.CheckForNull;
  33. import javax.annotation.Nullable;
  34. import javax.servlet.http.HttpServletRequest;
  35. import javax.servlet.http.HttpServletResponse;
  36. import org.sonar.api.server.ServerSide;
  37. import org.sonar.api.server.authentication.Display;
  38. import org.sonar.api.server.authentication.OAuth2IdentityProvider;
  39. import org.sonar.api.server.authentication.UnauthorizedException;
  40. import org.sonar.api.server.authentication.UserIdentity;
  41. import org.sonar.api.utils.log.Logger;
  42. import org.sonar.api.utils.log.Loggers;
  43. import static java.util.Collections.emptySet;
  44. import static java.util.Objects.requireNonNull;
  45. @ServerSide
  46. public class SamlIdentityProvider implements OAuth2IdentityProvider {
  47. private static final String KEY = "saml";
  48. private static final Logger LOGGER = Loggers.get(SamlIdentityProvider.class);
  49. private static final String ANY_URL = "http://anyurl";
  50. private static final String STATE_REQUEST_PARAMETER = "RelayState";
  51. private final SamlSettings samlSettings;
  52. public SamlIdentityProvider(SamlSettings samlSettings) {
  53. this.samlSettings = samlSettings;
  54. }
  55. @Override
  56. public String getKey() {
  57. return KEY;
  58. }
  59. @Override
  60. public String getName() {
  61. return samlSettings.getProviderName();
  62. }
  63. @Override
  64. public Display getDisplay() {
  65. return Display.builder()
  66. .setIconPath("/images/saml.png")
  67. .setBackgroundColor("#444444")
  68. .build();
  69. }
  70. @Override
  71. public boolean isEnabled() {
  72. return samlSettings.isEnabled();
  73. }
  74. @Override
  75. public boolean allowsUsersToSignUp() {
  76. return true;
  77. }
  78. @Override
  79. public void init(InitContext context) {
  80. try {
  81. Auth auth = newAuth(initSettings(context.getCallbackUrl()), context.getRequest(), context.getResponse());
  82. auth.login(context.generateCsrfState());
  83. } catch (IOException | SettingsException e) {
  84. throw new IllegalStateException("Fail to intialize SAML authentication plugin", e);
  85. }
  86. }
  87. @Override
  88. public void callback(CallbackContext context) {
  89. Auth auth = newAuth(initSettings(null), context.getRequest(), context.getResponse());
  90. processResponse(auth);
  91. context.verifyCsrfState(STATE_REQUEST_PARAMETER);
  92. LOGGER.trace("Name ID : {}", auth.getNameId());
  93. checkAuthentication(auth);
  94. LOGGER.trace("Attributes received : {}", auth.getAttributes());
  95. String login = getNonNullFirstAttribute(auth, samlSettings.getUserLogin());
  96. UserIdentity.Builder userIdentityBuilder = UserIdentity.builder()
  97. .setLogin(login)
  98. .setProviderLogin(login)
  99. .setName(getNonNullFirstAttribute(auth, samlSettings.getUserName()));
  100. samlSettings.getUserEmail().ifPresent(
  101. email -> userIdentityBuilder.setEmail(getFirstAttribute(auth, email)));
  102. samlSettings.getGroupName().ifPresent(
  103. group -> userIdentityBuilder.setGroups(getGroups(auth, group)));
  104. context.authenticate(userIdentityBuilder.build());
  105. context.redirectToRequestedPage();
  106. }
  107. private static Auth newAuth(Saml2Settings saml2Settings, HttpServletRequest request, HttpServletResponse response) {
  108. try {
  109. return new Auth(saml2Settings, request, response);
  110. } catch (SettingsException e) {
  111. throw new IllegalStateException("Fail to create Auth", e);
  112. }
  113. }
  114. private static void processResponse(Auth auth) {
  115. try {
  116. auth.processResponse();
  117. } catch (Exception e) {
  118. throw new IllegalStateException("Fail to process response", e);
  119. }
  120. }
  121. private static void checkAuthentication(Auth auth) {
  122. List<String> errors = auth.getErrors();
  123. if (auth.isAuthenticated() && errors.isEmpty()) {
  124. return;
  125. }
  126. String errorReason = auth.getLastErrorReason();
  127. throw new UnauthorizedException(errorReason != null && !errorReason.isEmpty() ? errorReason : "Unknown error reason");
  128. }
  129. private static String getNonNullFirstAttribute(Auth auth, String key) {
  130. String attribute = getFirstAttribute(auth, key);
  131. requireNonNull(attribute, String.format("%s is missing", key));
  132. return attribute;
  133. }
  134. @CheckForNull
  135. private static String getFirstAttribute(Auth auth, String key) {
  136. Collection<String> attribute = auth.getAttribute(key);
  137. if (attribute == null || attribute.isEmpty()) {
  138. return null;
  139. }
  140. return attribute.iterator().next();
  141. }
  142. private static Set<String> getGroups(Auth auth, String groupAttribute) {
  143. Collection<String> attribute = auth.getAttribute(groupAttribute);
  144. if (attribute == null || attribute.isEmpty()) {
  145. return emptySet();
  146. }
  147. return new HashSet<>(attribute);
  148. }
  149. private Saml2Settings initSettings(@Nullable String callbackUrl) {
  150. Map<String, Object> samlData = new HashMap<>();
  151. // TODO strict mode is unfortunately not compatible with HTTPS configuration on reverse proxy =>
  152. // https://jira.sonarsource.com/browse/SQAUTHSAML-8
  153. samlData.put("onelogin.saml2.strict", false);
  154. samlData.put("onelogin.saml2.idp.entityid", samlSettings.getProviderId());
  155. samlData.put("onelogin.saml2.idp.single_sign_on_service.url", samlSettings.getLoginUrl());
  156. samlData.put("onelogin.saml2.idp.x509cert", samlSettings.getCertificate());
  157. samlData.put("onelogin.saml2.sp.entityid", samlSettings.getApplicationId());
  158. // During callback, the callback URL is by definition not needed, but the Saml2Settings does never allow this setting to be empty...
  159. samlData.put("onelogin.saml2.sp.assertion_consumer_service.url", callbackUrl != null ? callbackUrl : ANY_URL);
  160. SettingsBuilder builder = new SettingsBuilder();
  161. return builder
  162. .fromValues(samlData)
  163. .build();
  164. }
  165. }