]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-18395 Allow login with a bearer token in Http authentication header
authorAurelien <100427063+aurelien-poscia-sonarsource@users.noreply.github.com>
Tue, 7 Feb 2023 14:11:50 +0000 (15:11 +0100)
committersonartech <sonartech@sonarsource.com>
Tue, 7 Feb 2023 20:02:53 +0000 (20:02 +0000)
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/BasicAuthentication.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/event/AuthenticationEvent.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/UserTokenAuthentication.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/BasicAuthenticationTest.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/CredentialsAuthenticationTest.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/CredentialsExternalAuthenticationTest.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/LdapCredentialsAuthenticationTest.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/event/AuthenticationEventSourceTest.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticationTest.java

index c9bedabe591032483ac233aeeef457f2062bbffb..c3f6a619e92e24ebd10431adf2bcbaf07e2d726e 100644 (file)
@@ -93,7 +93,7 @@ public class BasicAuthentication {
         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();
       }
index 16dd616ce1a02f5f21c324ec3f9a67e5b481625d..236132f4076812b27e445db8b6064b32673d0ccf 100644 (file)
@@ -46,9 +46,9 @@ public interface AuthenticationEvent {
      */
     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.
      */
index 26cf30b291691c7978cf4d69739acb11c6c452f7..e86652beb9b545907721a0b2ae963413d61a90a3 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.usertoken;
 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;
@@ -33,11 +34,15 @@ import org.sonar.server.authentication.event.AuthenticationEvent;
 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;
@@ -53,20 +58,41 @@ public class UserTokenAuthentication {
   }
 
   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) {
@@ -75,15 +101,15 @@ public class UserTokenAuthentication {
       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();
     }
index 2f1cdec2e9cf3ee1e081b1a8ac2979d287867bb9..fc3a4f9fe4ed44510625bb533831748f8f06f332 100644 (file)
@@ -44,9 +44,9 @@ import static org.mockito.Mockito.verify;
 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 {
 
@@ -161,7 +161,7 @@ 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());
@@ -170,7 +170,7 @@ public class BasicAuthenticationTest {
   @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:"));
@@ -178,7 +178,7 @@ public class BasicAuthenticationTest {
     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);
   }
index 5b0a219c4ac135c8e6c0adc0c2c2229bbaf69322..fdf214ad9f4f76ac84b4056447dc20a612203b69 100644 (file)
@@ -44,7 +44,7 @@ import static org.sonar.db.user.UserTesting.newUserDto;
 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 {
@@ -135,20 +135,20 @@ 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);
   }
 
@@ -179,10 +179,10 @@ public class CredentialsAuthenticationTest {
       .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);
@@ -197,10 +197,10 @@ public class CredentialsAuthenticationTest {
       .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();
@@ -217,7 +217,7 @@ public class CredentialsAuthenticationTest {
       .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);
 
index b13e3433981a1e2a3e43dfc3ced69eff536f9042..c3da6a4bc3cf3667f81eed31cc46752ec9f4c690 100644 (file)
@@ -44,7 +44,7 @@ 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.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 {
 
@@ -218,10 +218,10 @@ 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);
index 21f1fd8e48283ec00b2fc002e308ae64adef7006..53171b041107dae6f570e137c76330967fefa643 100644 (file)
@@ -53,7 +53,7 @@ import static org.mockito.Mockito.when;
 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 {
@@ -253,10 +253,10 @@ 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);
index e74ca0ff2bc5ee4e88b6a98048cf9c1507aeb352..692a371d053b720c46a434573ddee109cf42e47f 100644 (file)
@@ -43,9 +43,9 @@ public class AuthenticationEventSourceTest {
 
   @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");
   }
@@ -181,7 +181,7 @@ public class AuthenticationEventSourceTest {
     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")));
index 541a29231c7a119cff6151bff366d7b49a8daee3..03b0fa8135fb5fa71e1d5a5290847e8fccd04051 100644 (file)
@@ -19,6 +19,9 @@
  */
 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;
@@ -27,6 +30,7 @@ import javax.servlet.http.HttpServletRequest;
 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;
@@ -49,9 +53,9 @@ import static org.sonar.api.utils.DateUtils.formatDateTime;
 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();
@@ -80,6 +84,7 @@ public class UserTokenAuthenticationTest {
   @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);
@@ -87,7 +92,7 @@ public class UserTokenAuthenticationTest {
   }
 
   @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 + ":"));
@@ -107,6 +112,50 @@ public class UserTokenAuthenticationTest {
     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";
@@ -181,8 +230,8 @@ public class UserTokenAuthenticationTest {
     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
@@ -196,8 +245,8 @@ public class UserTokenAuthenticationTest {
     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
@@ -214,11 +263,10 @@ public class UserTokenAuthenticationTest {
     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();
@@ -229,7 +277,7 @@ public class UserTokenAuthenticationTest {
     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);
   }
@@ -248,15 +296,21 @@ public class UserTokenAuthenticationTest {
   }
 
   @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) {