return userAuthResult.get().getUserDto();
} else {
throw AuthenticationException.newBuilder()
- .setSource(AuthenticationEvent.Source.local(AuthenticationEvent.Method.BASIC_TOKEN))
+ .setSource(AuthenticationEvent.Source.local(AuthenticationEvent.Method.SONARQUBE_TOKEN))
.setMessage("User doesn't exist")
.build();
}
*/
BASIC,
/**
- * HTTP basic authentication with a security token.
+ * Authentication with SonarQube token passed either as basic credentials or bearer token.
*/
- BASIC_TOKEN,
+ SONARQUBE_TOKEN,
/**
* SQ login form authentication with a login and password.
*/
import java.util.Optional;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
+import org.apache.commons.lang.StringUtils;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.user.UserDto;
import org.sonar.server.authentication.event.AuthenticationException;
import org.sonar.server.exceptions.NotFoundException;
+import static org.apache.commons.lang.StringUtils.startsWithIgnoreCase;
import static org.sonar.api.utils.DateUtils.formatDateTime;
import static org.sonar.server.authentication.BasicAuthentication.extractCredentialsFromHeader;
public class UserTokenAuthentication {
private static final String ACCESS_LOG_TOKEN_NAME = "TOKEN_NAME";
+ private static final String BEARER_AUTHORIZATION_SCHEME = "bearer";
+ private static final String API_MONITORING_METRICS_PATH = "/api/monitoring/metrics";
+ private static final String AUTHORIZATION_HEADER = "Authorization";
private final TokenGenerator tokenGenerator;
private final DbClient dbClient;
}
public Optional<UserAuthResult> authenticate(HttpServletRequest request) {
- if (isTokenBasedAuthentication(request)) {
- Optional<Credentials> credentials = extractCredentialsFromHeader(request);
- if (credentials.isPresent()) {
- UserAuthResult userAuthResult = authenticateFromUserToken(credentials.get().getLogin(), request);
- authenticationEvent.loginSuccess(request, userAuthResult.getUserDto().getLogin(), AuthenticationEvent.Source.local(AuthenticationEvent.Method.BASIC_TOKEN));
- return Optional.of(userAuthResult);
- }
+ return findBearerToken(request)
+ .or(() -> findTokenUsedWithBasicAuthentication(request))
+ .map(userAuthResult -> login(request, userAuthResult));
+ }
+
+ private static Optional<String> findBearerToken(HttpServletRequest request) {
+ // hack necessary as #org.sonar.server.monitoring.MetricsAction and org.sonar.server.platform.ws.SafeModeMonitoringMetricAction
+ // are providing their own bearer token based authentication mechanism that we can't get rid of for backward compatibility reasons
+ if (request.getServletPath().startsWith(API_MONITORING_METRICS_PATH)) {
+ return Optional.empty();
+ }
+ String authorizationHeader = request.getHeader(AUTHORIZATION_HEADER);
+ if (startsWithIgnoreCase(authorizationHeader, BEARER_AUTHORIZATION_SCHEME)) {
+ String token = StringUtils.removeStartIgnoreCase(authorizationHeader, BEARER_AUTHORIZATION_SCHEME + " ");
+ return Optional.ofNullable(token);
+ }
+ return Optional.empty();
+ }
+
+ private static Optional<String> findTokenUsedWithBasicAuthentication(HttpServletRequest request) {
+ Credentials credentials = extractCredentialsFromHeader(request).orElse(null);
+ if (isTokenWithBasicAuthenticationMethod(credentials)) {
+ return Optional.ofNullable(credentials.getLogin());
}
return Optional.empty();
}
- public static boolean isTokenBasedAuthentication(HttpServletRequest request) {
- Optional<Credentials> credentialsOptional = extractCredentialsFromHeader(request);
- return credentialsOptional.map(credentials -> credentials.getPassword().isEmpty()).orElse(false);
+ private static boolean isTokenWithBasicAuthenticationMethod(@Nullable Credentials credentials) {
+ return Optional.ofNullable(credentials).map(c -> c.getPassword().isEmpty()).orElse(false);
+ }
+
+ private UserAuthResult login(HttpServletRequest request, String token) {
+ UserAuthResult userAuthResult = authenticateFromUserToken(token, request);
+ authenticationEvent.loginSuccess(request, userAuthResult.getUserDto().getLogin(), AuthenticationEvent.Source.local(AuthenticationEvent.Method.SONARQUBE_TOKEN));
+ return userAuthResult;
}
private UserAuthResult authenticateFromUserToken(String token, HttpServletRequest request) {
UserDto userDto = dbClient.userDao().selectByUuid(dbSession, userToken.getUserUuid());
if (userDto == null || !userDto.isActive()) {
throw AuthenticationException.newBuilder()
- .setSource(AuthenticationEvent.Source.local(AuthenticationEvent.Method.BASIC_TOKEN))
+ .setSource(AuthenticationEvent.Source.local(AuthenticationEvent.Method.SONARQUBE_TOKEN))
.setMessage("User doesn't exist")
.build();
}
request.setAttribute(ACCESS_LOG_TOKEN_NAME, userToken.getName());
return new UserAuthResult(userDto, userToken, UserAuthResult.AuthType.TOKEN);
- } catch (NotFoundException | IllegalStateException exception ) {
+ } catch (NotFoundException | IllegalStateException exception) {
throw AuthenticationException.newBuilder()
- .setSource(AuthenticationEvent.Source.local(AuthenticationEvent.Method.BASIC_TOKEN))
+ .setSource(AuthenticationEvent.Source.local(AuthenticationEvent.Method.SONARQUBE_TOKEN))
.setMessage(exception.getMessage())
.build();
}
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
-import static org.sonar.server.authentication.event.AuthenticationEvent.Source;
import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC;
-import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC_TOKEN;
+import static org.sonar.server.authentication.event.AuthenticationEvent.Method.SONARQUBE_TOKEN;
+import static org.sonar.server.authentication.event.AuthenticationEvent.Source;
public class BasicAuthenticationTest {
assertThatThrownBy(() -> underTest.authenticate(request))
.hasMessage("User doesn't exist")
.isInstanceOf(AuthenticationException.class)
- .hasFieldOrPropertyWithValue("source", Source.local(BASIC_TOKEN));
+ .hasFieldOrPropertyWithValue("source", Source.local(SONARQUBE_TOKEN));
verifyNoInteractions(authenticationEvent);
verify(request, times(0)).setAttribute(anyString(), anyString());
@Test
public void does_not_authenticate_from_user_token_when_token_does_not_match_existing_user() {
when(userTokenAuthentication.authenticate(request)).thenThrow(AuthenticationException.newBuilder()
- .setSource(AuthenticationEvent.Source.local(AuthenticationEvent.Method.BASIC_TOKEN))
+ .setSource(AuthenticationEvent.Source.local(AuthenticationEvent.Method.SONARQUBE_TOKEN))
.setMessage("User doesn't exist")
.build());
when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64("token:"));
assertThatThrownBy(() -> underTest.authenticate(request))
.hasMessageContaining("User doesn't exist")
.isInstanceOf(AuthenticationException.class)
- .hasFieldOrPropertyWithValue("source", Source.local(BASIC_TOKEN));
+ .hasFieldOrPropertyWithValue("source", Source.local(SONARQUBE_TOKEN));
verifyNoInteractions(authenticationEvent);
}
import static org.sonar.server.authentication.CredentialsAuthentication.ERROR_PASSWORD_CANNOT_BE_NULL;
import static org.sonar.server.authentication.CredentialsLocalAuthentication.ERROR_UNKNOWN_HASH_METHOD;
import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC;
-import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC_TOKEN;
+import static org.sonar.server.authentication.event.AuthenticationEvent.Method.SONARQUBE_TOKEN;
import static org.sonar.server.authentication.event.AuthenticationEvent.Source;
public class CredentialsAuthenticationTest {
@Test
public void fail_to_authenticate_external_user_when_no_external_and_ldap_authentication() {
- when(externalAuthentication.authenticate(new Credentials(LOGIN, PASSWORD), request, BASIC_TOKEN)).thenReturn(Optional.empty());
- when(ldapCredentialsAuthentication.authenticate(new Credentials(LOGIN, PASSWORD), request, BASIC_TOKEN)).thenReturn(Optional.empty());
+ when(externalAuthentication.authenticate(new Credentials(LOGIN, PASSWORD), request, SONARQUBE_TOKEN)).thenReturn(Optional.empty());
+ when(ldapCredentialsAuthentication.authenticate(new Credentials(LOGIN, PASSWORD), request, SONARQUBE_TOKEN)).thenReturn(Optional.empty());
insertUser(newUserDto()
.setLogin(LOGIN)
.setLocal(false));
- assertThatThrownBy(() -> executeAuthenticate(BASIC_TOKEN))
+ assertThatThrownBy(() -> executeAuthenticate(SONARQUBE_TOKEN))
.hasMessage("User is not local")
.isInstanceOf(AuthenticationException.class)
- .hasFieldOrPropertyWithValue("source", Source.local(BASIC_TOKEN))
+ .hasFieldOrPropertyWithValue("source", Source.local(SONARQUBE_TOKEN))
.hasFieldOrPropertyWithValue("login", LOGIN);
- verify(externalAuthentication).authenticate(new Credentials(LOGIN, PASSWORD), request, BASIC_TOKEN);
- verify(ldapCredentialsAuthentication).authenticate(new Credentials(LOGIN, PASSWORD), request, BASIC_TOKEN);
+ verify(externalAuthentication).authenticate(new Credentials(LOGIN, PASSWORD), request, SONARQUBE_TOKEN);
+ verify(ldapCredentialsAuthentication).authenticate(new Credentials(LOGIN, PASSWORD), request, SONARQUBE_TOKEN);
verifyNoInteractions(authenticationEvent);
}
.setHashMethod(CredentialsLocalAuthentication.HashMethod.PBKDF2.name())
.setLocal(true));
- assertThatThrownBy(() -> executeAuthenticate(BASIC_TOKEN))
+ assertThatThrownBy(() -> executeAuthenticate(SONARQUBE_TOKEN))
.hasMessage("null salt")
.isInstanceOf(AuthenticationException.class)
- .hasFieldOrPropertyWithValue("source", Source.local(BASIC_TOKEN))
+ .hasFieldOrPropertyWithValue("source", Source.local(SONARQUBE_TOKEN))
.hasFieldOrPropertyWithValue("login", LOGIN);
verifyNoInteractions(authenticationEvent);
.setHashMethod(DEPRECATED_HASH_METHOD)
.setLocal(true));
- assertThatThrownBy(() -> executeAuthenticate(BASIC_TOKEN))
+ assertThatThrownBy(() -> executeAuthenticate(SONARQUBE_TOKEN))
.hasMessage(format(ERROR_UNKNOWN_HASH_METHOD, DEPRECATED_HASH_METHOD))
.isInstanceOf(AuthenticationException.class)
- .hasFieldOrPropertyWithValue("source", Source.local(BASIC_TOKEN))
+ .hasFieldOrPropertyWithValue("source", Source.local(SONARQUBE_TOKEN))
.hasFieldOrPropertyWithValue("login", LOGIN);
verify(localAuthentication).generateHashToAvoidEnumerationAttack();
.setLocal(true));
Credentials credentials = new Credentials(LOGIN, null);
- assertThatThrownBy(() -> underTest.authenticate(credentials, request, BASIC_TOKEN))
+ assertThatThrownBy(() -> underTest.authenticate(credentials, request, SONARQUBE_TOKEN))
.hasMessage(ERROR_PASSWORD_CANNOT_BE_NULL)
.isInstanceOf(IllegalArgumentException.class);
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC;
-import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC_TOKEN;
+import static org.sonar.server.authentication.event.AuthenticationEvent.Method.SONARQUBE_TOKEN;
public class CredentialsExternalAuthenticationTest {
when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(new UserDetails());
- assertThatThrownBy(() -> underTest.authenticate(new Credentials(LOGIN, PASSWORD), request, BASIC_TOKEN))
+ assertThatThrownBy(() -> underTest.authenticate(new Credentials(LOGIN, PASSWORD), request, SONARQUBE_TOKEN))
.hasMessage(expectedMessage)
.isInstanceOf(AuthenticationException.class)
- .hasFieldOrPropertyWithValue("source", Source.realm(BASIC_TOKEN, REALM_NAME))
+ .hasFieldOrPropertyWithValue("source", Source.realm(SONARQUBE_TOKEN, REALM_NAME))
.hasFieldOrPropertyWithValue("login", LOGIN);
verifyNoInteractions(authenticationEvent);
import static org.sonar.auth.ldap.LdapAuthenticationResult.failed;
import static org.sonar.auth.ldap.LdapAuthenticationResult.success;
import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC;
-import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC_TOKEN;
+import static org.sonar.server.authentication.event.AuthenticationEvent.Method.SONARQUBE_TOKEN;
@RunWith(MockitoJUnitRunner.Silent.class)
public class LdapCredentialsAuthenticationTest {
doThrow(new IllegalArgumentException(expectedMessage)).when(ldapAuthenticator).doAuthenticate(any(LdapAuthenticator.Context.class));
Credentials credentials = new Credentials(LOGIN, PASSWORD);
- assertThatThrownBy(() -> underTest.authenticate(credentials, request, BASIC_TOKEN))
+ assertThatThrownBy(() -> underTest.authenticate(credentials, request, SONARQUBE_TOKEN))
.hasMessage(expectedMessage)
.isInstanceOf(AuthenticationException.class)
- .hasFieldOrPropertyWithValue("source", Source.realm(BASIC_TOKEN, LDAP_SECURITY_REALM_NAME))
+ .hasFieldOrPropertyWithValue("source", Source.realm(SONARQUBE_TOKEN, LDAP_SECURITY_REALM_NAME))
.hasFieldOrPropertyWithValue("login", LOGIN);
verifyNoInteractions(ldapUsersProvider);
@Test
public void local_creates_source_instance_with_specified_method_and_hardcoded_provider_and_provider_name() {
- Source underTest = Source.local(Method.BASIC_TOKEN);
+ Source underTest = Source.local(Method.SONARQUBE_TOKEN);
- assertThat(underTest.getMethod()).isEqualTo(Method.BASIC_TOKEN);
+ assertThat(underTest.getMethod()).isEqualTo(Method.SONARQUBE_TOKEN);
assertThat(underTest.getProvider()).isEqualTo(Provider.LOCAL);
assertThat(underTest.getProviderName()).isEqualTo("local");
}
assertThat(Source.sso()).isNotEqualTo(Source.jwt());
assertThat(Source.jwt()).isEqualTo(Source.jwt());
assertThat(Source.local(Method.BASIC)).isEqualTo(Source.local(Method.BASIC));
- assertThat(Source.local(Method.BASIC)).isNotEqualTo(Source.local(Method.BASIC_TOKEN));
+ assertThat(Source.local(Method.BASIC)).isNotEqualTo(Source.local(Method.SONARQUBE_TOKEN));
assertThat(Source.local(Method.BASIC)).isNotEqualTo(Source.sso());
assertThat(Source.local(Method.BASIC)).isNotEqualTo(Source.jwt());
assertThat(Source.local(Method.BASIC)).isNotEqualTo(Source.oauth2(newOauth2IdentityProvider("voo")));
*/
package org.sonar.server.usertoken;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Base64;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.sonar.api.utils.System2;
import org.sonar.db.DbTester;
import org.sonar.db.user.UserDto;
import static org.sonar.db.user.TokenType.GLOBAL_ANALYSIS_TOKEN;
import static org.sonar.db.user.TokenType.PROJECT_ANALYSIS_TOKEN;
import static org.sonar.db.user.TokenType.USER_TOKEN;
-import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC_TOKEN;
-import static org.sonar.server.usertoken.UserTokenAuthentication.isTokenBasedAuthentication;
+import static org.sonar.server.authentication.event.AuthenticationEvent.Method.SONARQUBE_TOKEN;
+@RunWith(DataProviderRunner.class)
public class UserTokenAuthenticationTest {
private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder();
@Before
public void before() {
when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64("token:"));
+ when(request.getServletPath()).thenReturn("/api/anypath");
when(tokenGenerator.hash(EXAMPLE_OLD_USER_TOKEN)).thenReturn(OLD_USER_TOKEN_HASH);
when(tokenGenerator.hash(EXAMPLE_NEW_USER_TOKEN)).thenReturn(NEW_USER_TOKEN_HASH);
when(tokenGenerator.hash(EXAMPLE_PROJECT_ANALYSIS_TOKEN)).thenReturn(PROJECT_ANALYSIS_TOKEN_HASH);
}
@Test
- public void return_login_when_token_hash_found_in_db() {
+ public void return_login_when_token_hash_found_in_db_and_basic_auth_used() {
String token = "known-token";
String tokenHash = "123456789";
when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(token + ":"));
verify(userLastConnectionDatesUpdater).updateLastConnectionDateIfNeeded(any(UserTokenDto.class));
}
+ @DataProvider
+ public static Object[][] bearerHeaderName() {
+ return new Object[][] {
+ {"bearer"},
+ {"BEARER"},
+ {"Bearer"},
+ {"bEarer"},
+ };
+ }
+
+ @Test
+ @UseDataProvider("bearerHeaderName")
+ public void authenticate_withDifferentBearerHeaderNameCase_succeeds(String headerName) {
+ String token = setUpValidAuthToken();
+ when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn(headerName + " " + token);
+
+ Optional<UserAuthResult> result = underTest.authenticate(request);
+
+ assertThat(result).isPresent();
+ verify(userLastConnectionDatesUpdater).updateLastConnectionDateIfNeeded(any(UserTokenDto.class));
+ }
+
+ @Test
+ public void authenticate_withValidCamelcaseBearerTokenForMetricsAction_fails() {
+ String token = setUpValidAuthToken();
+ when(request.getServletPath()).thenReturn("/api/monitoring/metrics");
+ when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Bearer " + token);
+
+ Optional<UserAuthResult> result = underTest.authenticate(request);
+
+ assertThat(result).isEmpty();
+ }
+
+ private String setUpValidAuthToken() {
+ String token = "known-token";
+ String tokenHash = "123456789";
+ when(tokenGenerator.hash(token)).thenReturn(tokenHash);
+ UserDto user1 = db.users().insertUser();
+ UserTokenDto userTokenDto = db.users().insertToken(user1, t -> t.setTokenHash(tokenHash));
+ UserDto user2 = db.users().insertUser();
+ db.users().insertToken(user2, t -> t.setTokenHash("another-token-hash"));
+ return token;
+ }
+
@Test
public void return_login_when_token_hash_found_in_db_and_future_expiration_date() {
String token = "known-token";
assertThat(result).isPresent();
assertThat(result.get().getTokenDto().getUuid()).isNotNull();
assertThat(result.get().getTokenDto().getType()).isEqualTo(GLOBAL_ANALYSIS_TOKEN.name());
- verify(authenticationEvent).loginSuccess(request, user.getLogin(), AuthenticationEvent.Source.local(BASIC_TOKEN));
- verify(request).setAttribute("TOKEN_NAME",tokenName);
+ verify(authenticationEvent).loginSuccess(request, user.getLogin(), AuthenticationEvent.Source.local(SONARQUBE_TOKEN));
+ verify(request).setAttribute("TOKEN_NAME", tokenName);
}
@Test
assertThat(result).isPresent();
assertThat(result.get().getTokenDto().getUuid()).isNotNull();
assertThat(result.get().getTokenDto().getType()).isEqualTo(USER_TOKEN.name());
- verify(authenticationEvent).loginSuccess(request, user.getLogin(), AuthenticationEvent.Source.local(BASIC_TOKEN));
- verify(request).setAttribute("TOKEN_NAME",tokenName);
+ verify(authenticationEvent).loginSuccess(request, user.getLogin(), AuthenticationEvent.Source.local(SONARQUBE_TOKEN));
+ verify(request).setAttribute("TOKEN_NAME", tokenName);
}
@Test
assertThat(result.get().getTokenDto().getUuid()).isNotNull();
assertThat(result.get().getTokenDto().getType()).isEqualTo(PROJECT_ANALYSIS_TOKEN.name());
assertThat(result.get().getTokenDto().getProjectKey()).isEqualTo("project-key");
- verify(authenticationEvent).loginSuccess(request, user.getLogin(), AuthenticationEvent.Source.local(BASIC_TOKEN));
- verify(request).setAttribute("TOKEN_NAME",tokenName);
+ verify(authenticationEvent).loginSuccess(request, user.getLogin(), AuthenticationEvent.Source.local(SONARQUBE_TOKEN));
+ verify(request).setAttribute("TOKEN_NAME", tokenName);
}
-
@Test
public void does_not_authenticate_from_user_token_when_token_does_not_match_active_user() {
UserDto user = db.users().insertDisabledUser();
assertThatThrownBy(() -> underTest.authenticate(request))
.hasMessageContaining("User doesn't exist")
.isInstanceOf(AuthenticationException.class)
- .hasFieldOrPropertyWithValue("source", AuthenticationEvent.Source.local(BASIC_TOKEN));
+ .hasFieldOrPropertyWithValue("source", AuthenticationEvent.Source.local(SONARQUBE_TOKEN));
verifyNoInteractions(authenticationEvent);
}
}
@Test
- public void identifies_if_request_uses_token_based_authentication() {
- when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64("token:"));
- assertThat(isTokenBasedAuthentication(request)).isTrue();
-
+ public void return_login_when_token_hash_found_in_db2() {
when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64("login:password"));
- assertThat(isTokenBasedAuthentication(request)).isFalse();
+ Optional<UserAuthResult> result = underTest.authenticate(request);
+
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void return_login_when_token_hash_found_in_db3() {
when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn(null);
- assertThat(isTokenBasedAuthentication(request)).isFalse();
+
+ Optional<UserAuthResult> result = underTest.authenticate(request);
+
+ assertThat(result).isEmpty();
}
private static String toBase64(String text) {