import org.sonar.server.usertoken.UserTokenAuthentication;
import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Locale.ENGLISH;
+import static org.apache.commons.lang.StringUtils.startsWithIgnoreCase;
import static org.sonar.server.authentication.event.AuthenticationEvent.Method;
import static org.sonar.server.authentication.event.AuthenticationEvent.Source;
*/
public class BasicAuthentication {
- private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder();
-
- private static final String AUTHORIZATION_HEADER = "Authorization";
- private static final String BASIC_AUTHORIZATION = "BASIC";
-
private final DbClient dbClient;
private final CredentialsAuthentication credentialsAuthentication;
private final UserTokenAuthentication userTokenAuthentication;
}
public Optional<UserDto> authenticate(HttpServletRequest request) {
- String authorizationHeader = request.getHeader(AUTHORIZATION_HEADER);
- if (authorizationHeader == null || !authorizationHeader.toUpperCase(ENGLISH).startsWith(BASIC_AUTHORIZATION)) {
+ return extractCredentialsFromHeader(request)
+ .flatMap(credentials -> Optional.of(authenticate(credentials, request)));
+ }
+
+ public static Optional<Credentials> extractCredentialsFromHeader(HttpServletRequest request) {
+ String authorizationHeader = request.getHeader("Authorization");
+ if (authorizationHeader == null || !startsWithIgnoreCase(authorizationHeader, "BASIC")) {
return Optional.empty();
}
- Credentials credentials = extractCredentials(authorizationHeader);
- UserDto userDto = authenticate(credentials, request);
- return Optional.of(userDto);
- }
-
- private static Credentials extractCredentials(String authorizationHeader) {
String basicAuthEncoded = authorizationHeader.substring(6);
String basicAuthDecoded = getDecodedBasicAuth(basicAuthEncoded);
}
String login = basicAuthDecoded.substring(0, semiColonPos);
String password = basicAuthDecoded.substring(semiColonPos + 1);
- return new Credentials(login, password);
+ return Optional.of(new Credentials(login, password));
}
private static String getDecodedBasicAuth(String basicAuthEncoded) {
try {
- return new String(BASE64_DECODER.decode(basicAuthEncoded.getBytes(UTF_8)), UTF_8);
+ return new String(Base64.getDecoder().decode(basicAuthEncoded.getBytes(UTF_8)), UTF_8);
} catch (Exception e) {
throw AuthenticationException.newBuilder()
.setSource(Source.local(Method.BASIC))
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.server.authentication;
+
+import java.util.Optional;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.sonar.api.server.ServerSide;
+import org.sonar.server.user.UserSession;
+
+/**
+ * Authentication that can create {@link org.sonar.server.user.UserSession}
+ * that are not associated to a user.
+ * That is convenient for authenticating bots that need special permissions.
+ *
+ * This is not an extension point, plugins can not provide their own
+ * implementations.
+ */
+@ServerSide
+public interface CustomAuthentication {
+
+ Optional<UserSession> authenticate(HttpServletRequest request, HttpServletResponse response);
+
+}
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.sonar.api.server.ServerSide;
import org.sonar.server.authentication.event.AuthenticationException;
import org.sonar.server.user.UserSession;
+@ServerSide
public interface RequestAuthenticator {
UserSession authenticate(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException;
*/
package org.sonar.server.authentication;
+import java.util.Arrays;
+import java.util.List;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.sonar.db.user.UserDto;
-import org.sonar.server.authentication.event.AuthenticationException;
import org.sonar.server.user.UserSession;
import org.sonar.server.user.UserSessionFactory;
private final BasicAuthentication basicAuthentication;
private final HttpHeadersAuthentication httpHeadersAuthentication;
private final UserSessionFactory userSessionFactory;
+ private final List<CustomAuthentication> customAuthentications;
public RequestAuthenticatorImpl(JwtHttpHandler jwtHttpHandler, BasicAuthentication basicAuthentication, HttpHeadersAuthentication httpHeadersAuthentication,
- UserSessionFactory userSessionFactory) throws AuthenticationException {
+ UserSessionFactory userSessionFactory, CustomAuthentication[] customAuthentications) {
this.jwtHttpHandler = jwtHttpHandler;
this.basicAuthentication = basicAuthentication;
this.httpHeadersAuthentication = httpHeadersAuthentication;
this.userSessionFactory = userSessionFactory;
+ this.customAuthentications = Arrays.asList(customAuthentications);
+ }
+
+ public RequestAuthenticatorImpl(JwtHttpHandler jwtHttpHandler, BasicAuthentication basicAuthentication, HttpHeadersAuthentication httpHeadersAuthentication,
+ UserSessionFactory userSessionFactory) {
+ this(jwtHttpHandler, basicAuthentication, httpHeadersAuthentication, userSessionFactory, new CustomAuthentication[0]);
}
@Override
public UserSession authenticate(HttpServletRequest request, HttpServletResponse response) {
+ for (CustomAuthentication customAuthentication : customAuthentications) {
+ Optional<UserSession> session = customAuthentication.authenticate(request, response);
+ if (session.isPresent()) {
+ return session.get();
+ }
+ }
+
Optional<UserDto> userOpt = loadUser(request, response);
if (userOpt.isPresent()) {
return userSessionFactory.create(userOpt.get());
import org.junit.Before;
import org.junit.Test;
import org.sonar.db.user.UserDto;
+import org.sonar.server.authentication.event.AuthenticationEvent;
+import org.sonar.server.authentication.event.AuthenticationException;
import org.sonar.server.tester.AnonymousMockUserSession;
import org.sonar.server.tester.MockUserSession;
import org.sonar.server.user.UserSession;
private BasicAuthentication basicAuthentication = mock(BasicAuthentication.class);
private HttpHeadersAuthentication httpHeadersAuthentication = mock(HttpHeadersAuthentication.class);
private UserSessionFactory sessionFactory = mock(UserSessionFactory.class);
- private RequestAuthenticator underTest = new RequestAuthenticatorImpl(jwtHttpHandler, basicAuthentication, httpHeadersAuthentication, sessionFactory);
+ private CustomAuthentication customAuthentication1 = mock(CustomAuthentication.class);
+ private CustomAuthentication customAuthentication2 = mock(CustomAuthentication.class);
+ private RequestAuthenticator underTest = new RequestAuthenticatorImpl(jwtHttpHandler, basicAuthentication, httpHeadersAuthentication, sessionFactory,
+ new CustomAuthentication[]{customAuthentication1, customAuthentication2});
@Before
public void setUp() throws Exception {
verify(response, never()).setStatus(anyInt());
}
+ @Test
+ public void delegate_to_CustomAuthentication() {
+ when(customAuthentication1.authenticate(request, response)).thenReturn(Optional.of(new MockUserSession("foo")));
+
+ UserSession session = underTest.authenticate(request, response);
+
+ assertThat(session.getLogin()).isEqualTo("foo");
+ }
+
+ @Test
+ public void CustomAuthentication_has_priority_over_core_authentications() {
+ // use-case: both custom and core authentications check the HTTP header "Authorization".
+ // The custom authentication should be able to test the header because that the core authentication
+ // throws an exception.
+ when(customAuthentication1.authenticate(request, response)).thenReturn(Optional.of(new MockUserSession("foo")));
+ when(basicAuthentication.authenticate(request)).thenThrow(AuthenticationException.newBuilder()
+ .setSource(AuthenticationEvent.Source.sso())
+ .setMessage("message")
+ .build());
+
+ UserSession session = underTest.authenticate(request, response);
+
+ assertThat(session.getLogin()).isEqualTo("foo");
+ }
}