From 57f8eaa709cd005e247b91a32f74c02a727176a1 Mon Sep 17 00:00:00 2001 From: Aurelien <100427063+aurelien-poscia-sonarsource@users.noreply.github.com> Date: Tue, 7 Feb 2023 15:11:50 +0100 Subject: [PATCH] SONAR-18395 Allow login with a bearer token in Http authentication header --- .../authentication/BasicAuthentication.java | 2 +- .../event/AuthenticationEvent.java | 4 +- .../usertoken/UserTokenAuthentication.java | 52 ++++++++--- .../BasicAuthenticationTest.java | 10 +-- .../CredentialsAuthenticationTest.java | 24 ++--- ...CredentialsExternalAuthenticationTest.java | 6 +- .../LdapCredentialsAuthenticationTest.java | 6 +- .../event/AuthenticationEventSourceTest.java | 6 +- .../UserTokenAuthenticationTest.java | 88 +++++++++++++++---- 9 files changed, 139 insertions(+), 59 deletions(-) diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/BasicAuthentication.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/BasicAuthentication.java index c9bedabe591..c3f6a619e92 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/BasicAuthentication.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/BasicAuthentication.java @@ -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(); } diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/event/AuthenticationEvent.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/event/AuthenticationEvent.java index 16dd616ce1a..236132f4076 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/event/AuthenticationEvent.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/event/AuthenticationEvent.java @@ -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. */ diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/UserTokenAuthentication.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/UserTokenAuthentication.java index 26cf30b2916..e86652beb9b 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/UserTokenAuthentication.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/UserTokenAuthentication.java @@ -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 authenticate(HttpServletRequest request) { - if (isTokenBasedAuthentication(request)) { - Optional 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 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 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 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(); } diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/BasicAuthenticationTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/BasicAuthenticationTest.java index 2f1cdec2e9c..fc3a4f9fe4e 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/BasicAuthenticationTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/BasicAuthenticationTest.java @@ -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); } diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/CredentialsAuthenticationTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/CredentialsAuthenticationTest.java index 5b0a219c4ac..fdf214ad9f4 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/CredentialsAuthenticationTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/CredentialsAuthenticationTest.java @@ -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); diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/CredentialsExternalAuthenticationTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/CredentialsExternalAuthenticationTest.java index b13e3433981..c3da6a4bc3c 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/CredentialsExternalAuthenticationTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/CredentialsExternalAuthenticationTest.java @@ -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); diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/LdapCredentialsAuthenticationTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/LdapCredentialsAuthenticationTest.java index 21f1fd8e482..53171b04110 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/LdapCredentialsAuthenticationTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/LdapCredentialsAuthenticationTest.java @@ -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); diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/event/AuthenticationEventSourceTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/event/AuthenticationEventSourceTest.java index e74ca0ff2bc..692a371d053 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/event/AuthenticationEventSourceTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/event/AuthenticationEventSourceTest.java @@ -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"))); diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticationTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticationTest.java index 541a29231c7..03b0fa8135f 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticationTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticationTest.java @@ -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 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 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 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 result = underTest.authenticate(request); + + assertThat(result).isEmpty(); } private static String toBase64(String text) { -- 2.39.5