From 2acee04f1c45fc78130ca31431de294484e45d3d Mon Sep 17 00:00:00 2001 From: =?utf8?q?S=C3=A9bastien=20Lesaint?= Date: Mon, 28 Nov 2016 09:30:33 +0100 Subject: [PATCH] SONAR-8416 add event log in case of error for basic, realm, jwt generic UnauthorizedException thrown in case of login failure is replaced by specific AuthenticationException which includes context information to generate details failure logs --- .../authentication/BaseContextFactory.java | 4 +- .../authentication/BasicAuthenticator.java | 27 ++-- .../CredentialsAuthenticator.java | 50 +++++--- .../authentication/JwtCsrfVerifier.java | 29 ++++- .../server/authentication/JwtHttpHandler.java | 2 +- .../server/authentication/JwtSerializer.java | 12 +- .../authentication/OAuth2CallbackFilter.java | 2 +- .../authentication/OAuth2ContextFactory.java | 5 +- .../authentication/OAuthCsrfVerifier.java | 17 ++- .../authentication/RealmAuthenticator.java | 34 +++-- .../authentication/SsoAuthenticator.java | 2 +- .../UserIdentityAuthenticator.java | 28 +++-- .../UserSessionInitializer.java | 23 +++- .../event/AuthenticationEvent.java | 80 ++++++++++-- .../event/AuthenticationEventImpl.java | 19 ++- .../event/AuthenticationException.java | 92 ++++++++++++++ .../server/authentication/ws/LoginAction.java | 28 ++++- .../authentication/ws/ValidateAction.java | 8 +- .../BaseContextFactoryTest.java | 30 ++--- .../BasicAuthenticatorTest.java | 11 +- .../CredentialsAuthenticatorTest.java | 35 +++--- .../authentication/JwtCsrfVerifierTest.java | 63 +++++----- .../authentication/JwtHttpHandlerTest.java | 2 +- .../authentication/JwtSerializerTest.java | 18 +-- .../OAuth2CallbackFilterTest.java | 2 +- .../OAuth2ContextFactoryTest.java | 56 ++++----- .../authentication/OAuthCsrfVerifierTest.java | 49 +++++--- .../RealmAuthenticatorTest.java | 75 ++++++----- .../UserIdentityAuthenticatorTest.java | 25 ++-- .../UserSessionInitializerTest.java | 23 ++-- .../event/AuthenticationEventImplTest.java | 81 ++++++++++++ .../event/AuthenticationEventSourceTest.java | 114 +++++++++++++++-- .../event/AuthenticationExceptionMatcher.java | 118 ++++++++++++++++++ .../event/AuthenticationExceptionTest.java | 74 +++++++++++ .../authentication/ws/LoginActionTest.java | 3 +- .../authentication/ws/ValidateActionTest.java | 10 +- 36 files changed, 986 insertions(+), 265 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/authentication/event/AuthenticationException.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/authentication/event/AuthenticationExceptionMatcher.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/authentication/event/AuthenticationExceptionTest.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/BaseContextFactory.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/BaseContextFactory.java index f04b06c2dc6..87ee71cc96c 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/BaseContextFactory.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/BaseContextFactory.java @@ -29,6 +29,8 @@ import org.sonar.db.user.UserDto; import org.sonar.server.user.ServerUserSession; import org.sonar.server.user.ThreadLocalUserSession; +import static org.sonar.server.authentication.event.AuthenticationEvent.Source; + public class BaseContextFactory { private final DbClient dbClient; @@ -78,7 +80,7 @@ public class BaseContextFactory { @Override public void authenticate(UserIdentity userIdentity) { - UserDto userDto = userIdentityAuthenticator.authenticate(userIdentity, identityProvider); + UserDto userDto = userIdentityAuthenticator.authenticate(userIdentity, identityProvider, Source.external(identityProvider)); jwtHttpHandler.generateToken(userDto, request, response); threadLocalUserSession.set(ServerUserSession.createForUser(dbClient, userDto)); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/BasicAuthenticator.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/BasicAuthenticator.java index 1e1b2bdffb9..d0a97a734b3 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/BasicAuthenticator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/BasicAuthenticator.java @@ -28,7 +28,7 @@ import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.user.UserDto; import org.sonar.server.authentication.event.AuthenticationEvent; -import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.authentication.event.AuthenticationException; import org.sonar.server.usertoken.UserTokenAuthenticator; import static java.util.Locale.ENGLISH; @@ -75,7 +75,10 @@ public class BasicAuthenticator { int semiColonPos = basicAuthDecoded.indexOf(':'); if (semiColonPos <= 0) { - throw new UnauthorizedException("Invalid credentials : " + basicAuthDecoded); + throw AuthenticationException.newBuilder() + .setSource(Source.local(Method.BASIC)) + .setMessage("decoded basic auth does not contain ':'") + .build(); } String login = basicAuthDecoded.substring(0, semiColonPos); String password = basicAuthDecoded.substring(semiColonPos + 1); @@ -86,7 +89,10 @@ public class BasicAuthenticator { try { return new String(BASE64_DECODER.decode(basicAuthEncoded.getBytes(Charsets.UTF_8)), Charsets.UTF_8); } catch (Exception e) { - throw new UnauthorizedException("Invalid basic header"); + throw AuthenticationException.newBuilder() + .setSource(Source.local(Method.BASIC)) + .setMessage("Invalid basic header") + .build(); } } @@ -103,17 +109,20 @@ public class BasicAuthenticator { private UserDto authenticateFromUserToken(String token) { Optional authenticatedLogin = userTokenAuthenticator.authenticate(token); if (!authenticatedLogin.isPresent()) { - throw new UnauthorizedException("Token doesn't exist"); + throw AuthenticationException.newBuilder() + .setSource(Source.local(Method.BASIC_TOKEN)) + .setMessage("Token doesn't exist") + .build(); } - DbSession dbSession = dbClient.openSession(false); - try { + try (DbSession dbSession = dbClient.openSession(false)) { UserDto userDto = dbClient.userDao().selectActiveUserByLogin(dbSession, authenticatedLogin.get()); if (userDto == null) { - throw new UnauthorizedException("User doesn't exist"); + throw AuthenticationException.newBuilder() + .setSource(Source.local(Method.BASIC_TOKEN)) + .setMessage("User doesn't exist") + .build(); } return userDto; - } finally { - dbClient.closeSession(dbSession); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/CredentialsAuthenticator.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/CredentialsAuthenticator.java index 7c2d168621b..34c0a422ecf 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/CredentialsAuthenticator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/CredentialsAuthenticator.java @@ -21,15 +21,18 @@ package org.sonar.server.authentication; import java.util.Optional; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.user.UserDto; import org.sonar.server.authentication.event.AuthenticationEvent; -import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.authentication.event.AuthenticationException; import static org.sonar.db.user.UserDto.encryptPassword; -import static org.sonar.server.authentication.event.AuthenticationEvent.*; +import static org.sonar.server.authentication.event.AuthenticationEvent.Method; +import static org.sonar.server.authentication.event.AuthenticationEvent.Source; public class CredentialsAuthenticator { @@ -53,27 +56,46 @@ public class CredentialsAuthenticator { } private UserDto authenticate(DbSession dbSession, String userLogin, String userPassword, HttpServletRequest request, Method method) { - UserDto user = dbClient.userDao().selectActiveUserByLogin(dbSession, userLogin); - if (user != null && user.isLocal()) { - UserDto userDto = authenticateFromDb(user, userPassword); + UserDto localUser = dbClient.userDao().selectActiveUserByLogin(dbSession, userLogin); + if (localUser != null && localUser.isLocal()) { + UserDto userDto = authenticateFromDb(localUser, userPassword, method); authenticationEvent.login(request, userLogin, Source.local(method)); return userDto; } - Optional userDto = externalAuthenticator.authenticate(userLogin, userPassword, request, method); - if (userDto.isPresent()) { - return userDto.get(); + Optional externalUser = externalAuthenticator.authenticate(userLogin, userPassword, request, method); + if (externalUser.isPresent()) { + return externalUser.get(); } - throw new UnauthorizedException(); + throw AuthenticationException.newBuilder() + .setSource(Source.local(method)) + .setLogin(userLogin) + .setMessage(localUser != null && !localUser.isLocal() ? "User is not local" : "No active user for login") + .build(); } - private static UserDto authenticateFromDb(UserDto userDto, String userPassword) { + private static UserDto authenticateFromDb(UserDto userDto, String userPassword, Method method) { String cryptedPassword = userDto.getCryptedPassword(); String salt = userDto.getSalt(); - if (cryptedPassword == null || salt == null - || !cryptedPassword.equals(encryptPassword(userPassword, salt))) { - throw new UnauthorizedException(); + String failureCause = checkPassword(cryptedPassword, salt, userPassword); + if (failureCause == null) { + return userDto; } - return userDto; + throw AuthenticationException.newBuilder() + .setSource(Source.local(method)) + .setLogin(userDto.getLogin()) + .setMessage(failureCause) + .build(); } + @CheckForNull + private static String checkPassword(@Nullable String cryptedPassword, @Nullable String salt, String userPassword) { + if (cryptedPassword == null) { + return "null password in DB"; + } else if (salt == null) { + return "null salt"; + } else if (!cryptedPassword.equals(encryptPassword(userPassword, salt))) { + return "wrong password"; + } + return null; + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/JwtCsrfVerifier.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/JwtCsrfVerifier.java index b3d2e014450..18e460a1a32 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/JwtCsrfVerifier.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/JwtCsrfVerifier.java @@ -23,14 +23,17 @@ import com.google.common.collect.ImmutableSet; import java.math.BigInteger; import java.security.SecureRandom; import java.util.Set; +import javax.annotation.CheckForNull; import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; -import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.authentication.event.AuthenticationException; import static org.apache.commons.lang.StringUtils.isBlank; import static org.sonar.server.authentication.CookieUtils.createCookie; +import static org.sonar.server.authentication.event.AuthenticationEvent.Method; +import static org.sonar.server.authentication.event.AuthenticationEvent.Source; public class JwtCsrfVerifier { @@ -59,14 +62,30 @@ public class JwtCsrfVerifier { return state; } - public void verifyState(HttpServletRequest request, @Nullable String csrfState) { + public void verifyState(HttpServletRequest request, @Nullable String csrfState, @Nullable String login) { if (!shouldRequestBeChecked(request)) { return; } - String stateInHeader = request.getHeader(CSRF_HEADER); - if (isBlank(csrfState) || !StringUtils.equals(csrfState, stateInHeader)) { - throw new UnauthorizedException(); + + String failureCause = checkCsrf(csrfState, request.getHeader(CSRF_HEADER)); + if (failureCause != null) { + throw AuthenticationException.newBuilder() + .setSource(Source.local(Method.JWT)) + .setLogin(login) + .setMessage(failureCause) + .build(); + } + } + + @CheckForNull + private static String checkCsrf(@Nullable String csrfState, @Nullable String stateInHeader) { + if (isBlank(csrfState)) { + return "missing reference CSRF value"; + } + if (!StringUtils.equals(csrfState, stateInHeader)) { + return "wrong CSFR in request"; } + return null; } public void refreshState(HttpServletRequest request, HttpServletResponse response, String csrfState, int timeoutInSeconds) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java index 40f6c691c6b..baa29d745ff 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java @@ -136,7 +136,7 @@ public class JwtHttpHandler { if (now.after(addSeconds(token.getIssuedAt(), SESSION_DISCONNECT_IN_SECONDS))) { return Optional.empty(); } - jwtCsrfVerifier.verifyState(request, (String) token.get(CSRF_JWT_PARAM)); + jwtCsrfVerifier.verifyState(request, (String) token.get(CSRF_JWT_PARAM), token.getSubject()); if (now.after(addSeconds(getLastRefreshDate(token), SESSION_REFRESH_IN_SECONDS))) { refreshToken(token, request, response); diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/JwtSerializer.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/JwtSerializer.java index 1888b745371..fe3027f31cb 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/JwtSerializer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/JwtSerializer.java @@ -40,11 +40,12 @@ import org.sonar.api.config.Settings; import org.sonar.api.server.ServerSide; import org.sonar.api.utils.System2; import org.sonar.core.util.UuidFactory; -import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.authentication.event.AuthenticationException; import static com.google.common.base.Preconditions.checkNotNull; import static io.jsonwebtoken.impl.crypto.MacProvider.generateKey; import static java.util.Objects.requireNonNull; +import static org.sonar.server.authentication.event.AuthenticationEvent.Source; /** * This class can be used to encode or decode a JWT token @@ -100,8 +101,9 @@ public class JwtSerializer implements Startable { Optional decode(String token) { checkIsStarted(); + Claims claims = null; try { - Claims claims = Jwts.parser() + claims = Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); @@ -113,7 +115,11 @@ public class JwtSerializer implements Startable { } catch (ExpiredJwtException | SignatureException e) { return Optional.empty(); } catch (Exception e) { - throw new UnauthorizedException(e.getMessage()); + throw AuthenticationException.newBuilder() + .setSource(Source.jwt()) + .setLogin(claims == null ? null : claims.getSubject()) + .setMessage(e.getMessage()) + .build(); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2CallbackFilter.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2CallbackFilter.java index e38f63adfb6..b208b9aa281 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2CallbackFilter.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2CallbackFilter.java @@ -78,7 +78,7 @@ public class OAuth2CallbackFilter extends ServletFilter { WrappedContext context = new WrappedContext(oAuth2ContextFactory.newCallback(httpRequest, (HttpServletResponse) response, oauthProvider)); oauthProvider.callback(context); if (context.isAuthenticated()) { - authenticationEvent.login(httpRequest, context.getLogin(), Source.oauth2(provider.getName())); + authenticationEvent.login(httpRequest, context.getLogin(), Source.oauth2(oauthProvider)); } } else { handleError((HttpServletResponse) response, format("Not an OAuth2IdentityProvider: %s", provider.getClass())); diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java index 6f29dfc0712..7cf0104450e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java @@ -28,6 +28,7 @@ import org.sonar.api.server.authentication.UserIdentity; import org.sonar.api.utils.MessageException; import org.sonar.db.DbClient; import org.sonar.db.user.UserDto; +import org.sonar.server.authentication.event.AuthenticationEvent; import org.sonar.server.user.ServerUserSession; import org.sonar.server.user.ThreadLocalUserSession; @@ -109,7 +110,7 @@ public class OAuth2ContextFactory { @Override public void verifyCsrfState() { - csrfVerifier.verifyState(request, response); + csrfVerifier.verifyState(request, response, identityProvider); } @Override @@ -123,7 +124,7 @@ public class OAuth2ContextFactory { @Override public void authenticate(UserIdentity userIdentity) { - UserDto userDto = userIdentityAuthenticator.authenticate(userIdentity, identityProvider); + UserDto userDto = userIdentityAuthenticator.authenticate(userIdentity, identityProvider, AuthenticationEvent.Source.oauth2(identityProvider)); jwtHttpHandler.generateToken(userDto, request, response); threadLocalUserSession.set(ServerUserSession.createForUser(dbClient, userDto)); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuthCsrfVerifier.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuthCsrfVerifier.java index 3c48a481439..874734d859a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuthCsrfVerifier.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuthCsrfVerifier.java @@ -24,13 +24,15 @@ import java.security.SecureRandom; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.api.server.authentication.OAuth2IdentityProvider; +import org.sonar.server.authentication.event.AuthenticationException; import static java.lang.String.format; import static org.apache.commons.codec.digest.DigestUtils.sha256Hex; import static org.apache.commons.lang.StringUtils.isBlank; import static org.sonar.server.authentication.CookieUtils.createCookie; import static org.sonar.server.authentication.CookieUtils.findCookie; +import static org.sonar.server.authentication.event.AuthenticationEvent.Source; public class OAuthCsrfVerifier { @@ -44,8 +46,12 @@ public class OAuthCsrfVerifier { return state; } - public void verifyState(HttpServletRequest request, HttpServletResponse response) { - Cookie cookie = findCookie(CSRF_STATE_COOKIE, request).orElseThrow(() -> new UnauthorizedException(format("Cookie '%s' is missing", CSRF_STATE_COOKIE))); + public void verifyState(HttpServletRequest request, HttpServletResponse response, OAuth2IdentityProvider provider) { + Cookie cookie = findCookie(CSRF_STATE_COOKIE, request) + .orElseThrow(() -> AuthenticationException.newBuilder() + .setSource(Source.oauth2(provider)) + .setMessage(format("Cookie '%s' is missing", CSRF_STATE_COOKIE)) + .build()); String hashInCookie = cookie.getValue(); // remove cookie @@ -53,7 +59,10 @@ public class OAuthCsrfVerifier { String stateInRequest = request.getParameter("state"); if (isBlank(stateInRequest) || !sha256Hex(stateInRequest).equals(hashInCookie)) { - throw new UnauthorizedException("CSRF state value is invalid"); + throw AuthenticationException.newBuilder() + .setSource(Source.oauth2(provider)) + .setMessage("CSRF state value is invalid") + .build(); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/RealmAuthenticator.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/RealmAuthenticator.java index f2ab92b7482..72c8588cf85 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/RealmAuthenticator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/RealmAuthenticator.java @@ -39,7 +39,7 @@ import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonar.db.user.UserDto; import org.sonar.server.authentication.event.AuthenticationEvent; -import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.authentication.event.AuthenticationException; import org.sonar.server.user.SecurityRealmFactory; import static java.util.Objects.requireNonNull; @@ -93,24 +93,42 @@ public class RealmAuthenticator implements Startable { ExternalUsersProvider.Context externalUsersProviderContext = new ExternalUsersProvider.Context(userLogin, request); UserDetails details = externalUsersProvider.doGetUserDetails(externalUsersProviderContext); if (details == null) { - throw new UnauthorizedException("No user details"); + throw AuthenticationException.newBuilder() + .setSource(realmEventSource(method)) + .setLogin(userLogin) + .setMessage("No user details") + .build(); } Authenticator.Context authenticatorContext = new Authenticator.Context(userLogin, userPassword, request); boolean status = authenticator.doAuthenticate(authenticatorContext); if (!status) { - throw new UnauthorizedException("Fail to authenticate from external provider"); + throw AuthenticationException.newBuilder() + .setSource(realmEventSource(method)) + .setLogin(userLogin) + .setMessage("realm returned authenticate=false") + .build(); } - UserDto userDto = synchronize(userLogin, details, request); - authenticationEvent.login(request, userLogin, Source.realm(method, realm.getName())); + UserDto userDto = synchronize(userLogin, details, request, method); + authenticationEvent.login(request, userLogin, realmEventSource(method)); return userDto; + } catch (AuthenticationException e) { + throw e; } catch (Exception e) { // It seems that with Realm API it's expected to log the error and to not authenticate the user LOG.error("Error during authentication", e); - throw new UnauthorizedException(); + throw AuthenticationException.newBuilder() + .setSource(realmEventSource(method)) + .setLogin(userLogin) + .setMessage(e.getMessage()) + .build(); } } - private UserDto synchronize(String userLogin, UserDetails details, HttpServletRequest request) { + private Source realmEventSource(AuthenticationEvent.Method method) { + return Source.realm(method, realm.getName()); + } + + private UserDto synchronize(String userLogin, UserDetails details, HttpServletRequest request, AuthenticationEvent.Method method) { String name = details.getName(); UserIdentity.Builder userIdentityBuilder = UserIdentity.builder() .setLogin(userLogin) @@ -122,7 +140,7 @@ public class RealmAuthenticator implements Startable { Collection groups = externalGroupsProvider.doGetGroups(context); userIdentityBuilder.setGroups(new HashSet<>(groups)); } - return userIdentityAuthenticator.authenticate(userIdentityBuilder.build(), new ExternalIdentityProvider()); + return userIdentityAuthenticator.authenticate(userIdentityBuilder.build(), new ExternalIdentityProvider(), realmEventSource(method)); } private String getLogin(String userLogin) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/SsoAuthenticator.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/SsoAuthenticator.java index 292e101cf9c..9cac78e0f90 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/SsoAuthenticator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/SsoAuthenticator.java @@ -169,7 +169,7 @@ public class SsoAuthenticator implements Startable { String groupsValue = getHeaderValue(headerValuesByNames, GROUPS_HEADER_PARAM); userIdentityBuilder.setGroups(groupsValue == null ? Collections.emptySet() : new HashSet<>(COMA_SPLITTER.splitToList(groupsValue))); } - return userIdentityAuthenticator.authenticate(userIdentityBuilder.build(), new SsoIdentityProvider()); + return userIdentityAuthenticator.authenticate(userIdentityBuilder.build(), new SsoIdentityProvider(), Source.sso()); } @CheckForNull diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java index bf2b809bdaa..e95302d91e3 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java @@ -29,7 +29,6 @@ import java.util.Objects; import java.util.Set; import javax.annotation.Nonnull; import org.sonar.api.server.authentication.IdentityProvider; -import org.sonar.api.server.authentication.UnauthorizedException; import org.sonar.api.server.authentication.UserIdentity; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; @@ -38,6 +37,8 @@ import org.sonar.db.DbSession; import org.sonar.db.user.GroupDto; import org.sonar.db.user.UserDto; import org.sonar.db.user.UserGroupDto; +import org.sonar.server.authentication.event.AuthenticationEvent; +import org.sonar.server.authentication.event.AuthenticationException; import org.sonar.server.organization.DefaultOrganization; import org.sonar.server.organization.DefaultOrganizationProvider; import org.sonar.server.user.ExternalIdentity; @@ -63,11 +64,11 @@ public class UserIdentityAuthenticator { this.defaultOrganizationProvider = defaultOrganizationProvider; } - public UserDto authenticate(UserIdentity user, IdentityProvider provider) { - return register(user, provider); + public UserDto authenticate(UserIdentity user, IdentityProvider provider, AuthenticationEvent.Source source) { + return register(user, provider, source); } - private UserDto register(UserIdentity user, IdentityProvider provider) { + private UserDto register(UserIdentity user, IdentityProvider provider, AuthenticationEvent.Source source) { DbSession dbSession = dbClient.openSession(false); try { String userLogin = user.getLogin(); @@ -76,21 +77,30 @@ public class UserIdentityAuthenticator { registerExistingUser(dbSession, userDto, user, provider); return userDto; } - return registerNewUser(dbSession, user, provider); + return registerNewUser(dbSession, user, provider, source); } finally { dbClient.closeSession(dbSession); } } - private UserDto registerNewUser(DbSession dbSession, UserIdentity user, IdentityProvider provider) { + private UserDto registerNewUser(DbSession dbSession, UserIdentity user, IdentityProvider provider, AuthenticationEvent.Source source) { if (!provider.allowsUsersToSignUp()) { - throw new UnauthorizedException(format("'%s' users are not allowed to sign up", provider.getKey())); + throw AuthenticationException.newBuilder() + .setSource(source) + .setLogin(user.getLogin()) + .setMessage(format("'%s' users are not allowed to sign up", provider.getKey())) + .build(); } String email = user.getEmail(); if (email != null && dbClient.userDao().doesEmailExist(dbSession, email)) { - throw new UnauthorizedException(format( - "You can't sign up because email '%s' is already used by an existing user. This means that you probably already registered with another account.", email)); + throw AuthenticationException.newBuilder() + .setLogin(user.getLogin()) + .setSource(source) + .setMessage(format( + "You can't sign up because email '%s' is already used by an existing user. This means that you probably already registered with another account.", + email)) + .build(); } String userLogin = user.getLogin(); diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java index 4f02daea4e0..8b1456aaacb 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java @@ -29,6 +29,8 @@ import org.sonar.api.config.Settings; import org.sonar.api.server.ServerSide; import org.sonar.db.DbClient; import org.sonar.db.user.UserDto; +import org.sonar.server.authentication.event.AuthenticationEvent; +import org.sonar.server.authentication.event.AuthenticationException; import org.sonar.server.exceptions.UnauthorizedException; import org.sonar.server.user.ServerUserSession; import org.sonar.server.user.ThreadLocalUserSession; @@ -37,6 +39,7 @@ import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; import static org.sonar.api.CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY; import static org.sonar.api.web.ServletFilter.UrlPattern; import static org.sonar.api.web.ServletFilter.UrlPattern.Builder.staticResourcePatterns; +import static org.sonar.server.authentication.AuthenticationError.handleAuthenticationError; import static org.sonar.server.authentication.ws.LoginAction.AUTH_LOGIN_URL; import static org.sonar.server.authentication.ws.ValidateAction.AUTH_VALIDATE_URL; import static org.sonar.server.user.ServerUserSession.createForAnonymous; @@ -74,15 +77,17 @@ public class UserSessionInitializer { private final BasicAuthenticator basicAuthenticator; private final SsoAuthenticator ssoAuthenticator; private final ThreadLocalUserSession threadLocalSession; + private final AuthenticationEvent authenticationEvent; public UserSessionInitializer(DbClient dbClient, Settings settings, JwtHttpHandler jwtHttpHandler, BasicAuthenticator basicAuthenticator, - SsoAuthenticator ssoAuthenticator, ThreadLocalUserSession threadLocalSession) { + SsoAuthenticator ssoAuthenticator, ThreadLocalUserSession threadLocalSession, AuthenticationEvent authenticationEvent) { this.dbClient = dbClient; this.settings = settings; this.jwtHttpHandler = jwtHttpHandler; this.basicAuthenticator = basicAuthenticator; this.ssoAuthenticator = ssoAuthenticator; this.threadLocalSession = threadLocalSession; + this.authenticationEvent = authenticationEvent; } public boolean initUserSession(HttpServletRequest request, HttpServletResponse response) { @@ -94,6 +99,14 @@ public class UserSessionInitializer { } setUserSession(request, response); return true; + } catch (AuthenticationException e) { + authenticationEvent.failure(request, e); + if (isWsUrl(path)) { + response.setStatus(HTTP_UNAUTHORIZED); + return false; + } + handleAuthenticationError(e, response); + return false; } catch (UnauthorizedException e) { response.setStatus(HTTP_UNAUTHORIZED); if (isWsUrl(path)) { @@ -104,6 +117,14 @@ public class UserSessionInitializer { } } + private static boolean shouldContinueFilterOnError(String path) { + if (isWsUrl(path)) { + return false; + } + // WS should stop here. Rails page should continue in order to deal with redirection + return true; + } + private void setUserSession(HttpServletRequest request, HttpServletResponse response) { Optional user = authenticate(request, response); if (user.isPresent()) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/event/AuthenticationEvent.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/event/AuthenticationEvent.java index a66e0e00efd..497bf058b0b 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/event/AuthenticationEvent.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/event/AuthenticationEvent.java @@ -19,9 +19,12 @@ */ package org.sonar.server.authentication.event; +import java.io.Serializable; import java.util.Objects; import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; +import org.sonar.api.server.authentication.BaseIdentityProvider; +import org.sonar.api.server.authentication.OAuth2IdentityProvider; import static com.google.common.base.Preconditions.checkArgument; import static java.util.Objects.requireNonNull; @@ -30,17 +33,66 @@ public interface AuthenticationEvent { void login(HttpServletRequest request, String login, Source source); + void failure(HttpServletRequest request, AuthenticationException e); + enum Method { - BASIC, BASIC_TOKEN, FORM, FORM_TOKEN, SSO, OAUTH2, EXTERNAL + /** + * HTTP basic authentication with a login and password. + */ + BASIC, + /** + * HTTP basic authentication with a security token. + */ + BASIC_TOKEN, + /** + * SQ login form authentication with a login and password. + */ + FORM, + /** + * SSO authentication (ie. with HTTP headers) + */ + SSO, + /** + * OAUTH2 authentication. + */ + OAUTH2, + /** + * JWT authentication (ie. with a session token). + */ + JWT, + /** + * External authentication (ie. fully implemented out of SQ's core code, see {@link BaseIdentityProvider}). + */ + EXTERNAL } enum Provider { - LOCAL, SSO, REALM, EXTERNAL + /** + * User authentication made against data in SQ's User table. + */ + LOCAL, + /** + * User authentication made by SSO provider. + */ + SSO, + /** + * User authentication made by Realm based provider (eg. LDAP). + */ + REALM, + /** + * User authentication made by JWT token information. + */ + JWT, + /** + * User authentication made by external provider (see {@link BaseIdentityProvider}). + */ + EXTERNAL } - class Source { + final class Source implements Serializable { private static final String LOCAL_PROVIDER_NAME = "local"; private static final Source SSO_INSTANCE = new Source(Method.SSO, Provider.SSO, "sso"); + private static final Source JWT_INSTANCE = new Source(Method.JWT, Provider.JWT, "jwt"); private final Method method; private final Provider provider; @@ -57,8 +109,10 @@ public interface AuthenticationEvent { return new Source(method, Provider.LOCAL, LOCAL_PROVIDER_NAME); } - public static Source oauth2(String providerName) { - return new Source(Method.OAUTH2, Provider.EXTERNAL, providerName); + public static Source oauth2(OAuth2IdentityProvider identityProvider) { + return new Source( + Method.OAUTH2, Provider.EXTERNAL, + requireNonNull(identityProvider, "identityProvider can't be null").getName()); } public static Source realm(Method method, String providerName) { @@ -69,15 +123,25 @@ public interface AuthenticationEvent { return SSO_INSTANCE; } - public Method getMethod() { + public static Source jwt() { + return JWT_INSTANCE; + } + + public static Source external(BaseIdentityProvider identityProvider) { + return new Source( + Method.EXTERNAL, Provider.EXTERNAL, + requireNonNull(identityProvider, "identityProvider can't be null").getName()); + } + + Method getMethod() { return method; } - public Provider getProvider() { + Provider getProvider() { return provider; } - public String getProviderName() { + String getProviderName() { return providerName; } diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/event/AuthenticationEventImpl.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/event/AuthenticationEventImpl.java index 5a96961cb7c..413cbb99328 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/event/AuthenticationEventImpl.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/event/AuthenticationEventImpl.java @@ -33,12 +33,27 @@ public class AuthenticationEventImpl implements AuthenticationEvent { @Override public void login(HttpServletRequest request, @Nullable String login, Source source) { LOGGER.info("login success [method|{}][provider|{}|{}][IP|{}|{}][login|{}]", - source.getMethod(), source.getProvider(), source.getProviderName(), request.getRemoteAddr(), getAllIps(request), - login == null ? "" : login); + source.getMethod(), source.getProvider(), source.getProviderName(), + request.getRemoteAddr(), getAllIps(request), + emptyIfNull(login)); } private static String getAllIps(HttpServletRequest request) { return Collections.list(request.getHeaders("X-Forwarded-For")).stream().collect(Collectors.join(Joiner.on(","))); } + @Override + public void failure(HttpServletRequest request, AuthenticationException e) { + Source source = e.getSource(); + LOGGER.info("login failure [cause|{}][method|{}][provider|{}|{}][IP|{}|{}][login|{}]", + emptyIfNull(e.getMessage()), + source.getMethod(), source.getProvider(), source.getProviderName(), + request.getRemoteAddr(), getAllIps(request), + emptyIfNull(e.getLogin())); + } + + private static String emptyIfNull(@Nullable String login) { + return login == null ? "" : login; + } + } diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/event/AuthenticationException.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/event/AuthenticationException.java new file mode 100644 index 00000000000..64ae64ea40a --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/event/AuthenticationException.java @@ -0,0 +1,92 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.event; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; + +import static java.util.Objects.requireNonNull; + +/** + * Exception thrown in case of authentication failure. + *

+ * This exception contains the source of authentication and, if present, the login on which the login attempt occurred. + *

+ *

+ * Given that {@link #source} and {@link #login} will be logged to file, be very careful not to set the login + * when the login is a security token. + *

+ */ +public class AuthenticationException extends RuntimeException { + private final AuthenticationEvent.Source source; + @CheckForNull + private final String login; + + private AuthenticationException(Builder builder) { + super(builder.message); + this.source = requireNonNull(builder.source, "source can't be null"); + this.login = builder.login; + } + + public AuthenticationEvent.Source getSource() { + return source; + } + + @CheckForNull + public String getLogin() { + return login; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + @CheckForNull + private AuthenticationEvent.Source source; + @CheckForNull + private String login; + @CheckForNull + private String message; + + private Builder() { + // use static factory method + } + + public Builder setSource(AuthenticationEvent.Source source) { + this.source = source; + return this; + } + + public Builder setLogin(@Nullable String login) { + this.login = login; + return this; + } + + public Builder setMessage(String message) { + this.message = message; + return this; + } + + public AuthenticationException build() { + return new AuthenticationException(this); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/LoginAction.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/LoginAction.java index ab057b08271..c7afb3f134c 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/LoginAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/LoginAction.java @@ -21,6 +21,7 @@ package org.sonar.server.authentication.ws; import java.io.IOException; +import javax.annotation.Nullable; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; @@ -33,13 +34,17 @@ import org.sonar.db.DbClient; import org.sonar.db.user.UserDto; import org.sonar.server.authentication.CredentialsAuthenticator; import org.sonar.server.authentication.JwtHttpHandler; +import org.sonar.server.authentication.event.AuthenticationEvent; +import org.sonar.server.authentication.event.AuthenticationException; import org.sonar.server.exceptions.UnauthorizedException; import org.sonar.server.user.ServerUserSession; import org.sonar.server.user.ThreadLocalUserSession; import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; +import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; import static org.apache.commons.lang.StringUtils.isEmpty; import static org.sonar.server.authentication.event.AuthenticationEvent.Method; +import static org.sonar.server.authentication.event.AuthenticationEvent.Source; public class LoginAction extends ServletFilter { @@ -51,12 +56,15 @@ public class LoginAction extends ServletFilter { private final CredentialsAuthenticator credentialsAuthenticator; private final JwtHttpHandler jwtHttpHandler; private final ThreadLocalUserSession threadLocalUserSession; + private final AuthenticationEvent authenticationEvent; - public LoginAction(DbClient dbClient, CredentialsAuthenticator credentialsAuthenticator, JwtHttpHandler jwtHttpHandler, ThreadLocalUserSession threadLocalUserSession) { + public LoginAction(DbClient dbClient, CredentialsAuthenticator credentialsAuthenticator, JwtHttpHandler jwtHttpHandler, + ThreadLocalUserSession threadLocalUserSession, AuthenticationEvent authenticationEvent) { this.dbClient = dbClient; this.credentialsAuthenticator = credentialsAuthenticator; this.jwtHttpHandler = jwtHttpHandler; this.threadLocalUserSession = threadLocalUserSession; + this.authenticationEvent = authenticationEvent; } @Override @@ -73,21 +81,29 @@ public class LoginAction extends ServletFilter { response.setStatus(HTTP_BAD_REQUEST); return; } + + String login = request.getParameter("login"); + String password = request.getParameter("password"); try { - UserDto userDto = authenticate(request); + UserDto userDto = authenticate(request, login, password); jwtHttpHandler.generateToken(userDto, request, response); threadLocalUserSession.set(ServerUserSession.createForUser(dbClient, userDto)); // TODO add chain.doFilter when Rack filter will not be executed after this filter (or use a Servlet) + } catch (AuthenticationException e) { + authenticationEvent.failure(request, e); + response.setStatus(HTTP_UNAUTHORIZED); } catch (UnauthorizedException e) { response.setStatus(e.httpCode()); } } - private UserDto authenticate(HttpServletRequest request) { - String login = request.getParameter("login"); - String password = request.getParameter("password"); + private UserDto authenticate(HttpServletRequest request, @Nullable String login, @Nullable String password) { if (isEmpty(login) || isEmpty(password)) { - throw new UnauthorizedException(); + throw AuthenticationException.newBuilder() + .setSource(Source.local(Method.FORM)) + .setLogin(login) + .setMessage("empty login and/or password") + .build(); } return credentialsAuthenticator.authenticate(login, password, request, Method.FORM); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/ValidateAction.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/ValidateAction.java index 825b245ce91..60791c2607a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/ValidateAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/ValidateAction.java @@ -20,8 +20,6 @@ package org.sonar.server.authentication.ws; -import static org.sonar.api.CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY; - import java.io.IOException; import java.util.Optional; import javax.servlet.FilterChain; @@ -37,9 +35,11 @@ import org.sonar.api.web.ServletFilter; import org.sonar.db.user.UserDto; import org.sonar.server.authentication.BasicAuthenticator; import org.sonar.server.authentication.JwtHttpHandler; -import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.authentication.event.AuthenticationException; import org.sonarqube.ws.MediaTypes; +import static org.sonar.api.CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY; + public class ValidateAction extends ServletFilter { public static final String AUTH_VALIDATE_URL = "/api/authentication/validate"; @@ -84,7 +84,7 @@ public class ValidateAction extends ServletFilter { return true; } return !settings.getBoolean(CORE_FORCE_AUTHENTICATION_PROPERTY); - } catch (UnauthorizedException e) { + } catch (AuthenticationException e) { return false; } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/BaseContextFactoryTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/BaseContextFactoryTest.java index 4646ccc892b..e398d077fc7 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/BaseContextFactoryTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/BaseContextFactoryTest.java @@ -33,6 +33,7 @@ import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.DbTester; import org.sonar.db.user.UserDto; +import org.sonar.server.authentication.event.AuthenticationEvent; import org.sonar.server.user.ThreadLocalUserSession; import org.sonar.server.user.UserSession; @@ -46,9 +47,9 @@ import static org.sonar.db.user.UserTesting.newUserDto; public class BaseContextFactoryTest { - static String PUBLIC_ROOT_URL = "https://mydomain.com"; + private static final String PUBLIC_ROOT_URL = "https://mydomain.com"; - static UserIdentity USER_IDENTITY = UserIdentity.builder() + private static final UserIdentity USER_IDENTITY = UserIdentity.builder() .setProviderLogin("johndoo") .setLogin("id:johndoo") .setName("John") @@ -58,21 +59,21 @@ public class BaseContextFactoryTest { @Rule public DbTester dbTester = DbTester.create(System2.INSTANCE); - DbClient dbClient = dbTester.getDbClient(); + private DbClient dbClient = dbTester.getDbClient(); - DbSession dbSession = dbTester.getSession(); + private DbSession dbSession = dbTester.getSession(); - ThreadLocalUserSession threadLocalUserSession = mock(ThreadLocalUserSession.class); + private ThreadLocalUserSession threadLocalUserSession = mock(ThreadLocalUserSession.class); - UserIdentityAuthenticator userIdentityAuthenticator = mock(UserIdentityAuthenticator.class); - Server server = mock(Server.class); + private UserIdentityAuthenticator userIdentityAuthenticator = mock(UserIdentityAuthenticator.class); + private Server server = mock(Server.class); - HttpServletRequest request = mock(HttpServletRequest.class); - HttpServletResponse response = mock(HttpServletResponse.class); - BaseIdentityProvider identityProvider = mock(BaseIdentityProvider.class); - JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class); + private HttpServletRequest request = mock(HttpServletRequest.class); + private HttpServletResponse response = mock(HttpServletResponse.class); + private BaseIdentityProvider identityProvider = mock(BaseIdentityProvider.class); + private JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class); - BaseContextFactory underTest = new BaseContextFactory(dbClient, userIdentityAuthenticator, server, jwtHttpHandler, threadLocalUserSession); + private BaseContextFactory underTest = new BaseContextFactory(dbClient, userIdentityAuthenticator, server, jwtHttpHandler, threadLocalUserSession); @Before public void setUp() throws Exception { @@ -80,7 +81,8 @@ public class BaseContextFactoryTest { UserDto userDto = dbClient.userDao().insert(dbSession, newUserDto()); dbSession.commit(); - when(userIdentityAuthenticator.authenticate(USER_IDENTITY, identityProvider)).thenReturn(userDto); + when(identityProvider.getName()).thenReturn("provIdeur Nameuh"); + when(userIdentityAuthenticator.authenticate(USER_IDENTITY, identityProvider, AuthenticationEvent.Source.external(identityProvider))).thenReturn(userDto); } @Test @@ -99,7 +101,7 @@ public class BaseContextFactoryTest { when(request.getSession()).thenReturn(session); context.authenticate(USER_IDENTITY); - verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider); + verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider, AuthenticationEvent.Source.external(identityProvider)); verify(jwtHttpHandler).generateToken(any(UserDto.class), eq(request), eq(response)); verify(threadLocalUserSession).set(any(UserSession.class)); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/BasicAuthenticatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/BasicAuthenticatorTest.java index 0a0a30c9258..9191da50352 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/BasicAuthenticatorTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/BasicAuthenticatorTest.java @@ -33,7 +33,6 @@ import org.sonar.db.DbTester; import org.sonar.db.user.UserDto; import org.sonar.db.user.UserTesting; import org.sonar.server.authentication.event.AuthenticationEvent; -import org.sonar.server.exceptions.UnauthorizedException; import org.sonar.server.usertoken.UserTokenAuthenticator; import static com.google.common.base.Charsets.UTF_8; @@ -44,9 +43,11 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; +import static org.sonar.server.authentication.event.AuthenticationEvent.Method; 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.AuthenticationExceptionMatcher.authenticationException; public class BasicAuthenticatorTest { @@ -120,7 +121,7 @@ public class BasicAuthenticatorTest { public void fail_to_authenticate_when_no_login() throws Exception { when(request.getHeader("Authorization")).thenReturn("Basic " + toBase64(":" + PASSWORD)); - expectedException.expect(UnauthorizedException.class); + expectedException.expect(authenticationException().from(Source.local(BASIC)).withoutLogin()); try { underTest.authenticate(request); } finally { @@ -132,7 +133,7 @@ public class BasicAuthenticatorTest { public void fail_to_authenticate_when_invalid_header() throws Exception { when(request.getHeader("Authorization")).thenReturn("Basic Invàlid"); - expectedException.expect(UnauthorizedException.class); + expectedException.expect(authenticationException().from(Source.local(BASIC)).withoutLogin().andNoPublicMessage()); expectedException.expectMessage("Invalid basic header"); underTest.authenticate(request); } @@ -156,7 +157,7 @@ public class BasicAuthenticatorTest { when(userTokenAuthenticator.authenticate("token")).thenReturn(Optional.empty()); when(request.getHeader("Authorization")).thenReturn("Basic " + toBase64("token:")); - expectedException.expect(UnauthorizedException.class); + expectedException.expect(authenticationException().from(Source.local(BASIC_TOKEN)).withoutLogin()); try { underTest.authenticate(request); } finally { @@ -170,7 +171,7 @@ public class BasicAuthenticatorTest { when(userTokenAuthenticator.authenticate("token")).thenReturn(Optional.of("Unknown user")); when(request.getHeader("Authorization")).thenReturn("Basic " + toBase64("token:")); - expectedException.expect(UnauthorizedException.class); + expectedException.expect(authenticationException().from(Source.local(Method.BASIC_TOKEN)).withoutLogin()); try { underTest.authenticate(request); } finally { diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/CredentialsAuthenticatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/CredentialsAuthenticatorTest.java index 5b900f56c67..cd5f1d13b6c 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/CredentialsAuthenticatorTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/CredentialsAuthenticatorTest.java @@ -31,7 +31,6 @@ import org.sonar.db.DbSession; import org.sonar.db.DbTester; import org.sonar.db.user.UserDto; import org.sonar.server.authentication.event.AuthenticationEvent; -import org.sonar.server.exceptions.UnauthorizedException; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.rules.ExpectedException.none; @@ -40,8 +39,10 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.sonar.db.user.UserTesting.newUserDto; -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.Source; +import static org.sonar.server.authentication.event.AuthenticationExceptionMatcher.authenticationException; public class CredentialsAuthenticatorTest { @@ -73,7 +74,7 @@ public class CredentialsAuthenticatorTest { .setSalt(SALT) .setLocal(true)); - UserDto userDto = executeAuthenticate(); + UserDto userDto = executeAuthenticate(BASIC); assertThat(userDto.getLogin()).isEqualTo(LOGIN); verify(authenticationEvent).login(request, LOGIN, Source.local(BASIC)); } @@ -86,9 +87,10 @@ public class CredentialsAuthenticatorTest { .setSalt("Wrong salt") .setLocal(true)); - expectedException.expect(UnauthorizedException.class); + expectedException.expect(authenticationException().from(Source.local(BASIC)).withLogin(LOGIN)); + expectedException.expectMessage("wrong password"); try { - executeAuthenticate(); + executeAuthenticate(BASIC); } finally { verifyZeroInteractions(authenticationEvent); } @@ -101,7 +103,7 @@ public class CredentialsAuthenticatorTest { .setLogin(LOGIN) .setLocal(false)); - executeAuthenticate(); + executeAuthenticate(BASIC); verify(externalAuthenticator).authenticate(LOGIN, PASSWORD, request, BASIC); verifyZeroInteractions(authenticationEvent); @@ -109,14 +111,15 @@ public class CredentialsAuthenticatorTest { @Test public void fail_to_authenticate_authenticate_external_user_when_no_external_authentication() throws Exception { - when(externalAuthenticator.authenticate(LOGIN, PASSWORD, request, BASIC)).thenReturn(Optional.empty()); + when(externalAuthenticator.authenticate(LOGIN, PASSWORD, request, BASIC_TOKEN)).thenReturn(Optional.empty()); insertUser(newUserDto() .setLogin(LOGIN) .setLocal(false)); - expectedException.expect(UnauthorizedException.class); + expectedException.expect(authenticationException().from(Source.local(BASIC_TOKEN)).withLogin(LOGIN)); + expectedException.expectMessage("User is not local"); try { - executeAuthenticate(); + executeAuthenticate(BASIC_TOKEN); } finally { verifyZeroInteractions(authenticationEvent); } @@ -130,9 +133,10 @@ public class CredentialsAuthenticatorTest { .setSalt(SALT) .setLocal(true)); - expectedException.expect(UnauthorizedException.class); + expectedException.expect(authenticationException().from(Source.local(BASIC)).withLogin(LOGIN)); + expectedException.expectMessage("null password in DB"); try { - executeAuthenticate(); + executeAuthenticate(BASIC); } finally { verifyZeroInteractions(authenticationEvent); } @@ -146,16 +150,17 @@ public class CredentialsAuthenticatorTest { .setSalt(null) .setLocal(true)); - expectedException.expect(UnauthorizedException.class); + expectedException.expect(authenticationException().from(Source.local(BASIC_TOKEN)).withLogin(LOGIN)); + expectedException.expectMessage("null salt"); try { - executeAuthenticate(); + executeAuthenticate(BASIC_TOKEN); } finally { verifyZeroInteractions(authenticationEvent); } } - private UserDto executeAuthenticate() { - return underTest.authenticate(LOGIN, PASSWORD, request, BASIC); + private UserDto executeAuthenticate(AuthenticationEvent.Method method) { + return underTest.authenticate(LOGIN, PASSWORD, request, method); } private UserDto insertUser(UserDto userDto) { diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/JwtCsrfVerifierTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/JwtCsrfVerifierTest.java index 65a3f7a2f19..96e1fe51347 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/JwtCsrfVerifierTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/JwtCsrfVerifierTest.java @@ -27,30 +27,31 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; -import org.sonar.api.platform.Server; -import org.sonar.server.exceptions.UnauthorizedException; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.sonar.server.authentication.event.AuthenticationEvent.Method; +import static org.sonar.server.authentication.event.AuthenticationEvent.Source; +import static org.sonar.server.authentication.event.AuthenticationExceptionMatcher.authenticationException; public class JwtCsrfVerifierTest { @Rule public ExpectedException thrown = ExpectedException.none(); - static final int TIMEOUT = 30; - static final String CSRF_STATE = "STATE"; - static final String JAVA_WS_URL = "/api/metrics/create"; + private static final int TIMEOUT = 30; + private static final String CSRF_STATE = "STATE"; + private static final String JAVA_WS_URL = "/api/metrics/create"; + private static final String LOGIN = "foo login"; - ArgumentCaptor cookieArgumentCaptor = ArgumentCaptor.forClass(Cookie.class); + private ArgumentCaptor cookieArgumentCaptor = ArgumentCaptor.forClass(Cookie.class); - Server server = mock(Server.class); - HttpServletResponse response = mock(HttpServletResponse.class); - HttpServletRequest request = mock(HttpServletRequest.class); + private HttpServletResponse response = mock(HttpServletResponse.class); + private HttpServletRequest request = mock(HttpServletRequest.class); - JwtCsrfVerifier underTest = new JwtCsrfVerifier(); + private JwtCsrfVerifier underTest = new JwtCsrfVerifier(); @Before public void setUp() throws Exception { @@ -71,34 +72,37 @@ public class JwtCsrfVerifierTest { mockRequestCsrf(CSRF_STATE); mockPostJavaWsRequest(); - underTest.verifyState(request, CSRF_STATE); + underTest.verifyState(request, CSRF_STATE, LOGIN); } @Test - public void fail_with_unauthorized_when_state_header_is_not_the_same_as_state_parameter() throws Exception { + public void fail_with_AuthenticationException_when_state_header_is_not_the_same_as_state_parameter() throws Exception { mockRequestCsrf("other value"); mockPostJavaWsRequest(); - thrown.expect(UnauthorizedException.class); - underTest.verifyState(request, CSRF_STATE); + thrown.expect(authenticationException().from(Source.local(Method.JWT)).withLogin(LOGIN)); + thrown.expectMessage("wrong CSFR in request"); + underTest.verifyState(request, CSRF_STATE, LOGIN); } @Test - public void fail_with_unauthorized_when_state_is_null() throws Exception { + public void fail_with_AuthenticationException_when_state_is_null() throws Exception { mockRequestCsrf(CSRF_STATE); mockPostJavaWsRequest(); - thrown.expect(UnauthorizedException.class); - underTest.verifyState(request, null); + thrown.expect(authenticationException().from(Source.local(Method.JWT)).withLogin(LOGIN)); + thrown.expectMessage("missing reference CSRF value"); + underTest.verifyState(request, null, LOGIN); } @Test - public void fail_with_unauthorized_when_state_parameter_is_empty() throws Exception { + public void fail_with_AuthenticationException_when_state_parameter_is_empty() throws Exception { mockRequestCsrf(CSRF_STATE); mockPostJavaWsRequest(); - thrown.expect(UnauthorizedException.class); - underTest.verifyState(request, ""); + thrown.expect(authenticationException().from(Source.local(Method.JWT)).withLogin(LOGIN)); + thrown.expectMessage("missing reference CSRF value"); + underTest.verifyState(request, "", LOGIN); } @Test @@ -107,8 +111,9 @@ public class JwtCsrfVerifierTest { when(request.getRequestURI()).thenReturn(JAVA_WS_URL); when(request.getMethod()).thenReturn("POST"); - thrown.expect(UnauthorizedException.class); - underTest.verifyState(request, CSRF_STATE); + thrown.expect(authenticationException().from(Source.local(Method.JWT)).withLogin(LOGIN)); + thrown.expectMessage("wrong CSFR in request"); + underTest.verifyState(request, CSRF_STATE, LOGIN); } @Test @@ -117,8 +122,9 @@ public class JwtCsrfVerifierTest { when(request.getRequestURI()).thenReturn(JAVA_WS_URL); when(request.getMethod()).thenReturn("PUT"); - thrown.expect(UnauthorizedException.class); - underTest.verifyState(request, CSRF_STATE); + thrown.expect(authenticationException().from(Source.local(Method.JWT)).withLogin(LOGIN)); + thrown.expectMessage("wrong CSFR in request"); + underTest.verifyState(request, CSRF_STATE, LOGIN); } @Test @@ -127,8 +133,9 @@ public class JwtCsrfVerifierTest { when(request.getRequestURI()).thenReturn(JAVA_WS_URL); when(request.getMethod()).thenReturn("DELETE"); - thrown.expect(UnauthorizedException.class); - underTest.verifyState(request, CSRF_STATE); + thrown.expect(authenticationException().from(Source.local(Method.JWT)).withLogin(LOGIN)); + thrown.expectMessage("wrong CSFR in request"); + underTest.verifyState(request, CSRF_STATE, LOGIN); } @Test @@ -136,7 +143,7 @@ public class JwtCsrfVerifierTest { when(request.getRequestURI()).thenReturn(JAVA_WS_URL); when(request.getMethod()).thenReturn("GET"); - underTest.verifyState(request, null); + underTest.verifyState(request, null, LOGIN); } @Test @@ -199,6 +206,6 @@ public class JwtCsrfVerifierTest { when(request.getRequestURI()).thenReturn(uri); when(request.getMethod()).thenReturn(method); - underTest.verifyState(request, null); + underTest.verifyState(request, null, LOGIN); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java index 443b2405df3..49ade87abe9 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java @@ -279,7 +279,7 @@ public class JwtHttpHandlerTest { underTest.validateToken(request, response); - verify(jwtCsrfVerifier).verifyState(request, CSRF_STATE); + verify(jwtCsrfVerifier).verifyState(request, CSRF_STATE, USER_LOGIN); } @Test diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/JwtSerializerTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/JwtSerializerTest.java index 0011a0b2565..d54f1519dc6 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/JwtSerializerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/JwtSerializerTest.java @@ -39,20 +39,20 @@ import org.sonar.api.utils.DateUtils; import org.sonar.api.utils.System2; import org.sonar.core.util.UuidFactory; import org.sonar.core.util.UuidFactoryImpl; -import org.sonar.server.exceptions.UnauthorizedException; import static org.assertj.core.api.Assertions.assertThat; import static org.sonar.server.authentication.JwtSerializer.JwtSession; +import static org.sonar.server.authentication.event.AuthenticationEvent.Source; +import static org.sonar.server.authentication.event.AuthenticationExceptionMatcher.authenticationException; public class JwtSerializerTest { + private static final String A_SECRET_KEY = "HrPSavOYLNNrwTY+SOqpChr7OwvbR/zbDLdVXRN0+Eg="; + private static final String USER_LOGIN = "john"; + @Rule public ExpectedException expectedException = ExpectedException.none(); - static final String A_SECRET_KEY = "HrPSavOYLNNrwTY+SOqpChr7OwvbR/zbDLdVXRN0+Eg="; - - static final String USER_LOGIN = "john"; - private Settings settings = new MapSettings(); private System2 system2 = System2.INSTANCE; private UuidFactory uuidFactory = UuidFactoryImpl.INSTANCE; @@ -156,7 +156,7 @@ public class JwtSerializerTest { .signWith(SignatureAlgorithm.HS256, decodeSecretKey(A_SECRET_KEY)) .compact(); - expectedException.expect(UnauthorizedException.class); + expectedException.expect(authenticationException().from(Source.jwt()).withLogin(USER_LOGIN)); expectedException.expectMessage("Token id hasn't been found"); underTest.decode(token); } @@ -174,7 +174,7 @@ public class JwtSerializerTest { .signWith(SignatureAlgorithm.HS256, decodeSecretKey(A_SECRET_KEY)) .compact(); - expectedException.expect(UnauthorizedException.class); + expectedException.expect(authenticationException().from(Source.jwt()).withoutLogin()); expectedException.expectMessage("Token subject hasn't been found"); underTest.decode(token); } @@ -192,7 +192,7 @@ public class JwtSerializerTest { .signWith(SignatureAlgorithm.HS256, decodeSecretKey(A_SECRET_KEY)) .compact(); - expectedException.expect(UnauthorizedException.class); + expectedException.expect(authenticationException().from(Source.jwt()).withLogin(USER_LOGIN)); expectedException.expectMessage("Token expiration date hasn't been found"); underTest.decode(token); } @@ -209,7 +209,7 @@ public class JwtSerializerTest { .signWith(SignatureAlgorithm.HS256, decodeSecretKey(A_SECRET_KEY)) .compact(); - expectedException.expect(UnauthorizedException.class); + expectedException.expect(authenticationException().from(Source.jwt()).withLogin(USER_LOGIN)); expectedException.expectMessage("Token creation date hasn't been found"); underTest.decode(token); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2CallbackFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2CallbackFilterTest.java index 6ff937e9c60..bc1c8b935ce 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2CallbackFilterTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2CallbackFilterTest.java @@ -162,7 +162,7 @@ public class OAuth2CallbackFilterTest { assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty(); assertThat(oAuth2IdentityProvider.isCallbackCalled()).isTrue(); if (expectLoginLog) { - verify(authenticationEvent).login(request, LOGIN, Source.oauth2(oAuth2IdentityProvider.getName())); + verify(authenticationEvent).login(request, LOGIN, Source.oauth2(oAuth2IdentityProvider)); } else { verifyZeroInteractions(authenticationEvent); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2ContextFactoryTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2ContextFactoryTest.java index 57af73855cd..2ceacb0dcf7 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2ContextFactoryTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2ContextFactoryTest.java @@ -45,43 +45,42 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.sonar.db.user.UserTesting.newUserDto; +import static org.sonar.server.authentication.event.AuthenticationEvent.Source; public class OAuth2ContextFactoryTest { + private static final String PROVIDER_KEY = "github"; + private static final String SECURED_PUBLIC_ROOT_URL = "https://mydomain.com"; + private static final String NOT_SECURED_PUBLIC_URL = "http://mydomain.com"; + private static final String PROVIDER_NAME = "provider name"; + private static final UserIdentity USER_IDENTITY = UserIdentity.builder() + .setProviderLogin("johndoo") + .setLogin("id:johndoo") + .setName("John") + .setEmail("john@email.com") + .build(); + @Rule public ExpectedException thrown = ExpectedException.none(); - static String PROVIDER_KEY = "github"; - - static String SECURED_PUBLIC_ROOT_URL = "https://mydomain.com"; - static String NOT_SECURED_PUBLIC_URL = "http://mydomain.com"; - - static UserIdentity USER_IDENTITY = UserIdentity.builder() - .setProviderLogin("johndoo") - .setLogin("id:johndoo") - .setName("John") - .setEmail("john@email.com") - .build(); - @Rule public DbTester dbTester = DbTester.create(System2.INSTANCE); - DbClient dbClient = dbTester.getDbClient(); - - DbSession dbSession = dbTester.getSession(); + private DbClient dbClient = dbTester.getDbClient(); + private DbSession dbSession = dbTester.getSession(); - ThreadLocalUserSession threadLocalUserSession = mock(ThreadLocalUserSession.class); - UserIdentityAuthenticator userIdentityAuthenticator = mock(UserIdentityAuthenticator.class); - Server server = mock(Server.class); - OAuthCsrfVerifier csrfVerifier = mock(OAuthCsrfVerifier.class); - JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class); + private ThreadLocalUserSession threadLocalUserSession = mock(ThreadLocalUserSession.class); + private UserIdentityAuthenticator userIdentityAuthenticator = mock(UserIdentityAuthenticator.class); + private Server server = mock(Server.class); + private OAuthCsrfVerifier csrfVerifier = mock(OAuthCsrfVerifier.class); + private JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class); - HttpServletRequest request = mock(HttpServletRequest.class); - HttpServletResponse response = mock(HttpServletResponse.class); - HttpSession session = mock(HttpSession.class); - OAuth2IdentityProvider identityProvider = mock(OAuth2IdentityProvider.class); + private HttpServletRequest request = mock(HttpServletRequest.class); + private HttpServletResponse response = mock(HttpServletResponse.class); + private HttpSession session = mock(HttpSession.class); + private OAuth2IdentityProvider identityProvider = mock(OAuth2IdentityProvider.class); - OAuth2ContextFactory underTest = new OAuth2ContextFactory(dbClient, threadLocalUserSession, userIdentityAuthenticator, server, csrfVerifier, jwtHttpHandler); + private OAuth2ContextFactory underTest = new OAuth2ContextFactory(dbClient, threadLocalUserSession, userIdentityAuthenticator, server, csrfVerifier, jwtHttpHandler); @Before public void setUp() throws Exception { @@ -90,7 +89,8 @@ public class OAuth2ContextFactoryTest { when(request.getSession()).thenReturn(session); when(identityProvider.getKey()).thenReturn(PROVIDER_KEY); - when(userIdentityAuthenticator.authenticate(USER_IDENTITY, identityProvider)).thenReturn(userDto); + when(identityProvider.getName()).thenReturn(PROVIDER_NAME); + when(userIdentityAuthenticator.authenticate(USER_IDENTITY, identityProvider, Source.oauth2(identityProvider))).thenReturn(userDto); } @Test @@ -150,7 +150,7 @@ public class OAuth2ContextFactoryTest { callback.authenticate(USER_IDENTITY); - verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider); + verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider, Source.oauth2(identityProvider)); verify(jwtHttpHandler).generateToken(any(UserDto.class), eq(request), eq(response)); verify(threadLocalUserSession).set(any(UserSession.class)); } @@ -181,7 +181,7 @@ public class OAuth2ContextFactoryTest { callback.verifyCsrfState(); - verify(csrfVerifier).verifyState(request, response); + verify(csrfVerifier).verifyState(request, response, identityProvider); } private OAuth2IdentityProvider.InitContext newInitContext() { diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuthCsrfVerifierTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuthCsrfVerifierTest.java index 07815176e20..5f1825d6b1e 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuthCsrfVerifierTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuthCsrfVerifierTest.java @@ -28,7 +28,8 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; import org.sonar.api.platform.Server; -import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.api.server.authentication.OAuth2IdentityProvider; +import org.sonar.server.authentication.event.AuthenticationEvent; import static org.apache.commons.codec.digest.DigestUtils.sha1Hex; import static org.apache.commons.codec.digest.DigestUtils.sha256Hex; @@ -36,23 +37,27 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.sonar.server.authentication.event.AuthenticationExceptionMatcher.authenticationException; public class OAuthCsrfVerifierTest { + private static final String PROVIDER_NAME = "provider name"; @Rule public ExpectedException thrown = ExpectedException.none(); - ArgumentCaptor cookieArgumentCaptor = ArgumentCaptor.forClass(Cookie.class); + private ArgumentCaptor cookieArgumentCaptor = ArgumentCaptor.forClass(Cookie.class); - Server server = mock(Server.class); - HttpServletResponse response = mock(HttpServletResponse.class); - HttpServletRequest request = mock(HttpServletRequest.class); + private OAuth2IdentityProvider identityProvider = mock(OAuth2IdentityProvider.class); + private Server server = mock(Server.class); + private HttpServletResponse response = mock(HttpServletResponse.class); + private HttpServletRequest request = mock(HttpServletRequest.class); - OAuthCsrfVerifier underTest = new OAuthCsrfVerifier(); + private OAuthCsrfVerifier underTest = new OAuthCsrfVerifier(); @Before public void setUp() throws Exception { when(server.getContextPath()).thenReturn(""); + when(identityProvider.getName()).thenReturn(PROVIDER_NAME); } @Test @@ -71,7 +76,7 @@ public class OAuthCsrfVerifierTest { when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("OAUTHSTATE", sha256Hex(state))}); when(request.getParameter("state")).thenReturn(state); - underTest.verifyState(request, response); + underTest.verifyState(request, response, identityProvider); verify(response).addCookie(cookieArgumentCaptor.capture()); Cookie updatedCookie = cookieArgumentCaptor.getValue(); @@ -82,30 +87,42 @@ public class OAuthCsrfVerifierTest { } @Test - public void fail_with_unauthorized_when_state_cookie_is_not_the_same_as_state_parameter() throws Exception { + public void fail_with_AuthenticationException_when_state_cookie_is_not_the_same_as_state_parameter() throws Exception { when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("OAUTHSTATE", sha1Hex("state"))}); when(request.getParameter("state")).thenReturn("other value"); - thrown.expect(UnauthorizedException.class); - underTest.verifyState(request, response); + thrown.expect(authenticationException().from(AuthenticationEvent.Source.oauth2(identityProvider)).withoutLogin()); + thrown.expectMessage("CSRF state value is invalid"); + underTest.verifyState(request, response, identityProvider); } @Test - public void fail_to_verify_state_when_state_cookie_is_null() throws Exception { + public void fail_with_AuthenticationException_when_state_cookie_is_null() throws Exception { when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("OAUTHSTATE", null)}); when(request.getParameter("state")).thenReturn("state"); - thrown.expect(UnauthorizedException.class); - underTest.verifyState(request, response); + thrown.expect(authenticationException().from(AuthenticationEvent.Source.oauth2(identityProvider)).withoutLogin()); + thrown.expectMessage("CSRF state value is invalid"); + underTest.verifyState(request, response, identityProvider); } @Test - public void fail_with_unauthorized_when_state_parameter_is_empty() throws Exception { + public void fail_with_AuthenticationException_when_state_parameter_is_empty() throws Exception { when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("OAUTHSTATE", sha1Hex("state"))}); when(request.getParameter("state")).thenReturn(""); - thrown.expect(UnauthorizedException.class); - underTest.verifyState(request, response); + thrown.expect(authenticationException().from(AuthenticationEvent.Source.oauth2(identityProvider)).withoutLogin()); + thrown.expectMessage("CSRF state value is invalid"); + underTest.verifyState(request, response, identityProvider); + } + + @Test + public void fail_with_AuthenticationException_when_cookie_is_missing() throws Exception { + when(request.getCookies()).thenReturn(new Cookie[] {}); + + thrown.expect(authenticationException().from(AuthenticationEvent.Source.oauth2(identityProvider)).withoutLogin()); + thrown.expectMessage("Cookie 'OAUTHSTATE' is missing"); + underTest.verifyState(request, response, identityProvider); } private void verifyCookie(Cookie cookie) { diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/RealmAuthenticatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/RealmAuthenticatorTest.java index 1d5ac67de48..7651e4cdd6e 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/RealmAuthenticatorTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/RealmAuthenticatorTest.java @@ -37,7 +37,6 @@ import org.sonar.api.server.authentication.IdentityProvider; import org.sonar.api.server.authentication.UserIdentity; import org.sonar.db.user.UserDto; import org.sonar.server.authentication.event.AuthenticationEvent; -import org.sonar.server.exceptions.UnauthorizedException; import org.sonar.server.user.SecurityRealmFactory; import static java.util.Arrays.asList; @@ -52,6 +51,9 @@ import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.sonar.db.user.UserTesting.newUserDto; 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.Source; +import static org.sonar.server.authentication.event.AuthenticationExceptionMatcher.authenticationException; public class RealmAuthenticatorTest { @@ -66,6 +68,7 @@ public class RealmAuthenticatorTest { private ArgumentCaptor userIdentityArgumentCaptor = ArgumentCaptor.forClass(UserIdentity.class); private ArgumentCaptor identityProviderArgumentCaptor = ArgumentCaptor.forClass(IdentityProvider.class); + private ArgumentCaptor sourceCaptor = ArgumentCaptor.forClass(Source.class); private Settings settings = new MapSettings(); @@ -95,18 +98,18 @@ public class RealmAuthenticatorTest { userDetails.setName("name"); userDetails.setEmail("email"); when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(userDetails); - when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class))).thenReturn(USER); + when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class))).thenReturn(USER); underTest.authenticate(LOGIN, PASSWORD, request, BASIC); - verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture()); + verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture()); UserIdentity userIdentity = userIdentityArgumentCaptor.getValue(); assertThat(userIdentity.getLogin()).isEqualTo(LOGIN); assertThat(userIdentity.getProviderLogin()).isEqualTo(LOGIN); assertThat(userIdentity.getName()).isEqualTo("name"); assertThat(userIdentity.getEmail()).isEqualTo("email"); assertThat(userIdentity.shouldSyncGroups()).isFalse(); - verify(authenticationEvent).login(request, LOGIN, AuthenticationEvent.Source.realm(BASIC, REALM_NAME)); + verify(authenticationEvent).login(request, LOGIN, Source.realm(BASIC, REALM_NAME)); } @Test @@ -117,17 +120,17 @@ public class RealmAuthenticatorTest { userDetails.setName("name"); userDetails.setEmail("email"); when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(userDetails); - when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class))).thenReturn(USER); + when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class))).thenReturn(USER); underTest.authenticate(LOGIN, PASSWORD, request, BASIC); - verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture()); + verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture()); assertThat(identityProviderArgumentCaptor.getValue().getKey()).isEqualTo("sonarqube"); assertThat(identityProviderArgumentCaptor.getValue().getName()).isEqualTo("sonarqube"); assertThat(identityProviderArgumentCaptor.getValue().getDisplay()).isNull(); assertThat(identityProviderArgumentCaptor.getValue().isEnabled()).isTrue(); - verify(authenticationEvent).login(request, LOGIN, AuthenticationEvent.Source.realm(BASIC, REALM_NAME)); + verify(authenticationEvent).login(request, LOGIN, Source.realm(BASIC, REALM_NAME)); } @Test @@ -137,28 +140,28 @@ public class RealmAuthenticatorTest { UserDetails userDetails = new UserDetails(); userDetails.setEmail("email"); when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(userDetails); - when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class))).thenReturn(USER); + when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class))).thenReturn(USER); underTest.authenticate(LOGIN, PASSWORD, request, BASIC); - verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture()); + verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture()); assertThat(identityProviderArgumentCaptor.getValue().getName()).isEqualTo("sonarqube"); - verify(authenticationEvent).login(request, LOGIN, AuthenticationEvent.Source.realm(BASIC, REALM_NAME)); + verify(authenticationEvent).login(request, LOGIN, Source.realm(BASIC, REALM_NAME)); } @Test public void authenticate_with_group_sync() throws Exception { when(externalGroupsProvider.doGetGroups(any(ExternalGroupsProvider.Context.class))).thenReturn(asList("group1", "group2")); - when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class))).thenReturn(USER); + when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class))).thenReturn(USER); executeStartWithGroupSync(); executeAuthenticate(); - verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture()); + verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture()); UserIdentity userIdentity = userIdentityArgumentCaptor.getValue(); assertThat(userIdentity.shouldSyncGroups()).isTrue(); assertThat(userIdentity.getGroups()).containsOnly("group1", "group2"); - verify(authenticationEvent).login(request, LOGIN, AuthenticationEvent.Source.realm(BASIC, REALM_NAME)); + verify(authenticationEvent).login(request, LOGIN, Source.realm(BASIC, REALM_NAME)); } @Test @@ -168,65 +171,65 @@ public class RealmAuthenticatorTest { UserDetails userDetails = new UserDetails(); userDetails.setName(null); when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(userDetails); - when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class))).thenReturn(USER); + when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class))).thenReturn(USER); underTest.authenticate(LOGIN, PASSWORD, request, BASIC); - verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture()); + verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture()); assertThat(userIdentityArgumentCaptor.getValue().getName()).isEqualTo(LOGIN); - verify(authenticationEvent).login(request, LOGIN, AuthenticationEvent.Source.realm(BASIC, REALM_NAME)); + verify(authenticationEvent).login(request, LOGIN, Source.realm(BASIC, REALM_NAME)); } @Test public void allow_to_sign_up_property() throws Exception { settings.setProperty("sonar.authenticator.createUsers", true); - when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class))).thenReturn(USER); + when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class))).thenReturn(USER); executeStartWithoutGroupSync(); executeAuthenticate(); - verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture()); + verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture()); assertThat(identityProviderArgumentCaptor.getValue().allowsUsersToSignUp()).isTrue(); - verify(authenticationEvent).login(request, LOGIN, AuthenticationEvent.Source.realm(BASIC, REALM_NAME)); + verify(authenticationEvent).login(request, LOGIN, Source.realm(BASIC, REALM_NAME)); } @Test public void does_not_allow_to_sign_up_property() throws Exception { settings.setProperty("sonar.authenticator.createUsers", false); - when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class))).thenReturn(USER); + when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class))).thenReturn(USER); executeStartWithoutGroupSync(); executeAuthenticate(); - verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture()); + verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture()); assertThat(identityProviderArgumentCaptor.getValue().allowsUsersToSignUp()).isFalse(); - verify(authenticationEvent).login(request, LOGIN, AuthenticationEvent.Source.realm(BASIC, REALM_NAME)); + verify(authenticationEvent).login(request, LOGIN, Source.realm(BASIC, REALM_NAME)); } @Test public void use_downcase_login() throws Exception { settings.setProperty("sonar.authenticator.downcase", true); - when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class))).thenReturn(USER); + when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class))).thenReturn(USER); executeStartWithoutGroupSync(); executeAuthenticate("LOGIN"); - verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture()); + verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture()); UserIdentity userIdentity = userIdentityArgumentCaptor.getValue(); assertThat(userIdentity.getLogin()).isEqualTo("login"); assertThat(userIdentity.getProviderLogin()).isEqualTo("login"); - verify(authenticationEvent).login(request, "login", AuthenticationEvent.Source.realm(BASIC, REALM_NAME)); + verify(authenticationEvent).login(request, "login", Source.realm(BASIC, REALM_NAME)); } @Test public void does_not_user_downcase_login() throws Exception { settings.setProperty("sonar.authenticator.downcase", false); - when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class))).thenReturn(USER); + when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class))).thenReturn(USER); executeStartWithoutGroupSync(); executeAuthenticate("LoGiN"); - verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture()); + verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture()); UserIdentity userIdentity = userIdentityArgumentCaptor.getValue(); assertThat(userIdentity.getLogin()).isEqualTo("LoGiN"); assertThat(userIdentity.getProviderLogin()).isEqualTo("LoGiN"); - verify(authenticationEvent).login(request, "LoGiN", AuthenticationEvent.Source.realm(BASIC, REALM_NAME)); + verify(authenticationEvent).login(request, "LoGiN", Source.realm(BASIC, REALM_NAME)); } @Test @@ -236,7 +239,8 @@ public class RealmAuthenticatorTest { when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(null); - expectedException.expect(UnauthorizedException.class); + expectedException.expect(authenticationException().from(Source.realm(BASIC, REALM_NAME)).withLogin(LOGIN)); + expectedException.expectMessage("No user details"); try { underTest.authenticate(LOGIN, PASSWORD, request, BASIC); } finally { @@ -251,7 +255,8 @@ public class RealmAuthenticatorTest { when(authenticator.doAuthenticate(any(Authenticator.Context.class))).thenReturn(false); - expectedException.expect(UnauthorizedException.class); + expectedException.expect(authenticationException().from(Source.realm(BASIC, REALM_NAME)).withLogin(LOGIN)); + expectedException.expectMessage("realm returned authenticate=false"); try { underTest.authenticate(LOGIN, PASSWORD, request, BASIC); } finally { @@ -262,13 +267,15 @@ public class RealmAuthenticatorTest { @Test public void fail_to_authenticate_when_any_exception_is_thrown() throws Exception { executeStartWithoutGroupSync(); - doThrow(IllegalArgumentException.class).when(authenticator).doAuthenticate(any(Authenticator.Context.class)); + String expectedMessage = "emulating exception in doAuthenticate"; + doThrow(new IllegalArgumentException(expectedMessage)).when(authenticator).doAuthenticate(any(Authenticator.Context.class)); - when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(null); + when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(new UserDetails()); - expectedException.expect(UnauthorizedException.class); + expectedException.expect(authenticationException().from(Source.realm(BASIC_TOKEN, REALM_NAME)).withLogin(LOGIN)); + expectedException.expectMessage(expectedMessage); try { - underTest.authenticate(LOGIN, PASSWORD, request, BASIC); + underTest.authenticate(LOGIN, PASSWORD, request, BASIC_TOKEN); } finally { verifyZeroInteractions(authenticationEvent); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java index effa289a654..a0b7e682a0e 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java @@ -27,7 +27,6 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.sonar.api.config.MapSettings; import org.sonar.api.config.Settings; -import org.sonar.api.server.authentication.UnauthorizedException; import org.sonar.api.server.authentication.UserIdentity; import org.sonar.api.utils.System2; import org.sonar.api.utils.internal.AlwaysIncreasingSystem2; @@ -47,6 +46,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.sonar.db.organization.OrganizationTesting.newOrganizationDto; import static org.sonar.db.user.UserTesting.newUserDto; +import static org.sonar.server.authentication.event.AuthenticationEvent.Method; +import static org.sonar.server.authentication.event.AuthenticationEvent.Source; +import static org.sonar.server.authentication.event.AuthenticationExceptionMatcher.authenticationException; public class UserIdentityAuthenticatorTest { @@ -62,6 +64,7 @@ public class UserIdentityAuthenticatorTest { private static TestIdentityProvider IDENTITY_PROVIDER = new TestIdentityProvider() .setKey("github") + .setName("name of github") .setEnabled(true) .setAllowsUsersToSignUp(true); @@ -91,7 +94,7 @@ public class UserIdentityAuthenticatorTest { @Test public void authenticate_new_user() throws Exception { - underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER); + underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.realm(Method.BASIC, IDENTITY_PROVIDER.getName())); UserDto user = db.users().selectUserByLogin(USER_LOGIN).get(); assertThat(user).isNotNull(); @@ -127,7 +130,7 @@ public class UserIdentityAuthenticatorTest { .setExternalIdentity("old identity") .setExternalIdentityProvider("old provide")); - underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER); + underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.local(Method.BASIC)); UserDto userDto = db.users().selectUserByLogin(USER_LOGIN).get(); assertThat(userDto.isActive()).isTrue(); @@ -147,7 +150,7 @@ public class UserIdentityAuthenticatorTest { .setExternalIdentity("old identity") .setExternalIdentityProvider("old provide")); - underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER); + underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.local(Method.BASIC_TOKEN)); UserDto userDto = db.users().selectUserByLogin(USER_LOGIN).get(); assertThat(userDto.isActive()).isTrue(); @@ -344,7 +347,7 @@ public class UserIdentityAuthenticatorTest { .setLogin(user.getLogin()) .setName(user.getName()) .setGroups(newHashSet(groupName)) - .build(), IDENTITY_PROVIDER); + .build(), IDENTITY_PROVIDER, Source.sso()); assertThat(db.users().selectGroupIdsOfUser(user)).containsOnly(groupInDefaultOrg.getId()); } @@ -356,10 +359,11 @@ public class UserIdentityAuthenticatorTest { .setName("Github") .setEnabled(true) .setAllowsUsersToSignUp(false); + Source source = Source.realm(Method.FORM, identityProvider.getName()); - thrown.expect(UnauthorizedException.class); + thrown.expect(authenticationException().from(source).withLogin(USER_IDENTITY.getLogin())); thrown.expectMessage("'github' users are not allowed to sign up"); - underTest.authenticate(USER_IDENTITY, identityProvider); + underTest.authenticate(USER_IDENTITY, identityProvider, source); } @Test @@ -368,11 +372,12 @@ public class UserIdentityAuthenticatorTest { .setLogin("Existing user with same email") .setActive(true) .setEmail("john@email.com")); + Source source = Source.realm(Method.FORM, IDENTITY_PROVIDER.getName()); - thrown.expect(UnauthorizedException.class); + thrown.expect(authenticationException().from(source).withLogin(USER_IDENTITY.getLogin())); thrown.expectMessage("You can't sign up because email 'john@email.com' is already used by an existing user. " + "This means that you probably already registered with another account."); - underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER); + underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, source); } private void authenticate(String login, String... groups) { @@ -382,7 +387,7 @@ public class UserIdentityAuthenticatorTest { .setName("John") // No group .setGroups(Arrays.stream(groups).collect(Collectors.toSet())) - .build(), IDENTITY_PROVIDER); + .build(), IDENTITY_PROVIDER, Source.sso()); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java index 36cc8e38e18..a3f6a994e58 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java @@ -33,6 +33,7 @@ import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.DbTester; import org.sonar.db.user.UserDto; +import org.sonar.server.authentication.event.AuthenticationEvent; import org.sonar.server.exceptions.UnauthorizedException; import org.sonar.server.user.ServerUserSession; import org.sonar.server.user.ThreadLocalUserSession; @@ -55,24 +56,24 @@ public class UserSessionInitializerTest { @Rule public DbTester dbTester = DbTester.create(System2.INSTANCE); - DbClient dbClient = dbTester.getDbClient(); + private DbClient dbClient = dbTester.getDbClient(); - DbSession dbSession = dbTester.getSession(); + private DbSession dbSession = dbTester.getSession(); - ThreadLocalUserSession userSession = mock(ThreadLocalUserSession.class); + private ThreadLocalUserSession userSession = mock(ThreadLocalUserSession.class); - HttpServletRequest request = mock(HttpServletRequest.class); - HttpServletResponse response = mock(HttpServletResponse.class); + private HttpServletRequest request = mock(HttpServletRequest.class); + private HttpServletResponse response = mock(HttpServletResponse.class); - JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class); - BasicAuthenticator basicAuthenticator = mock(BasicAuthenticator.class); - SsoAuthenticator ssoAuthenticator = mock(SsoAuthenticator.class); + private JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class); + private BasicAuthenticator basicAuthenticator = mock(BasicAuthenticator.class); + private SsoAuthenticator ssoAuthenticator = mock(SsoAuthenticator.class); - Settings settings = new MapSettings(); + private Settings settings = new MapSettings(); - UserDto user = newUserDto(); + private UserDto user = newUserDto(); - UserSessionInitializer underTest = new UserSessionInitializer(dbClient, settings, jwtHttpHandler, basicAuthenticator, ssoAuthenticator, userSession); + private UserSessionInitializer underTest = new UserSessionInitializer(dbClient, settings, jwtHttpHandler, basicAuthenticator, ssoAuthenticator, userSession, mock(AuthenticationEvent.class)); @Before public void setUp() throws Exception { diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/event/AuthenticationEventImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/event/AuthenticationEventImplTest.java index 3cff532ccb8..f67c589c11e 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/event/AuthenticationEventImplTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/event/AuthenticationEventImplTest.java @@ -37,6 +37,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.sonar.server.authentication.event.AuthenticationEvent.Method; import static org.sonar.server.authentication.event.AuthenticationEvent.Source; +import static org.sonar.server.authentication.event.AuthenticationException.newBuilder; public class AuthenticationEventImplTest { @Rule @@ -97,6 +98,86 @@ public class AuthenticationEventImplTest { verifyLog("login success [method|EXTERNAL][provider|REALM|bar][IP|1.2.3.4|2.3.4.5,6.5.4.3,9.5.6.7,6.3.2.4][login|foo]"); } + @Test + public void failure_fails_with_NPE_if_request_is_null() { + expectedException.expect(NullPointerException.class); + + underTest.failure(null, newBuilder().setSource(Source.sso()).build()); + } + + @Test + public void failure_fails_with_NPE_if_AuthenticationException_is_null() { + expectedException.expect(NullPointerException.class); + + underTest.failure(mock(HttpServletRequest.class), null); + } + + @Test + public void failure_creates_INFO_log_with_empty_login_if_AuthenticationException_has_no_login() { + AuthenticationException exception = newBuilder().setSource(Source.sso()).setMessage("message").build(); + underTest.failure(mockRequest(), exception); + + verifyLog("login failure [cause|message][method|SSO][provider|SSO|sso][IP||][login|]"); + } + + @Test + public void failure_creates_INFO_log_with_empty_cause_if_AuthenticationException_has_no_message() { + AuthenticationException exception = newBuilder().setSource(Source.sso()).setLogin("FoO").build(); + underTest.failure(mockRequest(), exception); + + verifyLog("login failure [cause|][method|SSO][provider|SSO|sso][IP||][login|FoO]"); + } + + @Test + public void failure_creates_INFO_log_with_method_provider_and_login() { + AuthenticationException exception = newBuilder() + .setSource(Source.realm(Method.BASIC, "some provider name")) + .setMessage("something got terribly wrong") + .setLogin("BaR") + .build(); + underTest.failure(mockRequest(), exception); + + verifyLog("login failure [cause|something got terribly wrong][method|BASIC][provider|REALM|some provider name][IP||][login|BaR]"); + } + + @Test + public void failure_logs_remote_ip_from_request() { + AuthenticationException exception = newBuilder() + .setSource(Source.realm(Method.EXTERNAL, "bar")) + .setMessage("Damn it!") + .setLogin("Baaad") + .build(); + underTest.failure(mockRequest("1.2.3.4"), exception); + + verifyLog("login failure [cause|Damn it!][method|EXTERNAL][provider|REALM|bar][IP|1.2.3.4|][login|Baaad]"); + } + + @Test + public void failure_logs_X_Forwarded_For_header_from_request() { + AuthenticationException exception = newBuilder() + .setSource(Source.realm(Method.EXTERNAL, "bar")) + .setMessage("Hop la!") + .setLogin("foo") + .build(); + HttpServletRequest request = mockRequest("1.2.3.4", asList("2.3.4.5")); + underTest.failure(request, exception); + + verifyLog("login failure [cause|Hop la!][method|EXTERNAL][provider|REALM|bar][IP|1.2.3.4|2.3.4.5][login|foo]"); + } + + @Test + public void failure_logs_X_Forwarded_For_header_from_request_and_supports_multiple_headers() { + AuthenticationException exception = newBuilder() + .setSource(Source.realm(Method.EXTERNAL, "bar")) + .setMessage("Boom!") + .setLogin("foo") + .build(); + HttpServletRequest request = mockRequest("1.2.3.4", asList("2.3.4.5", "6.5.4.3"), asList("9.5.6.7"), asList("6.3.2.4")); + underTest.failure(request, exception); + + verifyLog("login failure [cause|Boom!][method|EXTERNAL][provider|REALM|bar][IP|1.2.3.4|2.3.4.5,6.5.4.3,9.5.6.7,6.3.2.4][login|foo]"); + } + private void verifyLog(String expected) { assertThat(logTester.logs()).hasSize(1); assertThat(logTester.logs(LoggerLevel.INFO)) diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/event/AuthenticationEventSourceTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/event/AuthenticationEventSourceTest.java index 95e6b22f525..0a2b6090f0d 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/event/AuthenticationEventSourceTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/event/AuthenticationEventSourceTest.java @@ -19,11 +19,16 @@ */ package org.sonar.server.authentication.event; +import java.io.Serializable; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.sonar.api.server.authentication.BaseIdentityProvider; +import org.sonar.api.server.authentication.OAuth2IdentityProvider; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.sonar.server.authentication.event.AuthenticationEvent.Method; import static org.sonar.server.authentication.event.AuthenticationEvent.Provider; import static org.sonar.server.authentication.event.AuthenticationEvent.Source; @@ -50,20 +55,19 @@ public class AuthenticationEventSourceTest { } @Test - public void oauth2_fails_with_NPE_if_providerName_is_null() { + public void oauth2_fails_with_NPE_if_provider_is_null() { expectedException.expect(NullPointerException.class); - expectedException.expectMessage("provider name can't be null"); + expectedException.expectMessage("identityProvider can't be null"); Source.oauth2(null); } @Test - public void oauth2_creates_source_instance_with_specified_provider_name_and_hardcoded_provider_and_method() { - Source underTest = Source.oauth2("some name"); + public void oauth2_fails_with_NPE_if_providerName_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("provider name can't be null"); - assertThat(underTest.getMethod()).isEqualTo(Method.OAUTH2); - assertThat(underTest.getProvider()).isEqualTo(Provider.EXTERNAL); - assertThat(underTest.getProviderName()).isEqualTo("some name"); + Source.oauth2(newOauth2IdentityProvider(null)); } @Test @@ -71,7 +75,16 @@ public class AuthenticationEventSourceTest { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("provider name can't be empty"); - Source.oauth2(""); + Source.oauth2(newOauth2IdentityProvider("")); + } + + @Test + public void oauth2_creates_source_instance_with_specified_provider_name_and_hardcoded_provider_and_method() { + Source underTest = Source.oauth2(newOauth2IdentityProvider("some name")); + + assertThat(underTest.getMethod()).isEqualTo(Method.OAUTH2); + assertThat(underTest.getProvider()).isEqualTo(Provider.EXTERNAL); + assertThat(underTest.getProviderName()).isEqualTo("some name"); } @Test @@ -117,4 +130,89 @@ public class AuthenticationEventSourceTest { assertThat(underTest).isSameAs(Source.sso()); } + + @Test + public void jwt_returns_source_instance_with_hardcoded_method_provider_and_providerName() { + Source underTest = Source.jwt(); + + assertThat(underTest.getMethod()).isEqualTo(Method.JWT); + assertThat(underTest.getProvider()).isEqualTo(Provider.JWT); + assertThat(underTest.getProviderName()).isEqualTo("jwt"); + + assertThat(underTest).isSameAs(Source.jwt()); + } + + @Test + public void external_fails_with_NPE_if_provider_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("identityProvider can't be null"); + + Source.external(null); + } + + @Test + public void external_fails_with_NPE_if_providerName_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("provider name can't be null"); + + Source.external(newBasicIdentityProvider(null)); + } + + @Test + public void external_fails_with_IAE_if_providerName_is_empty() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("provider name can't be empty"); + + Source.external(newBasicIdentityProvider("")); + } + + @Test + public void external_creates_source_instance_with_specified_provider_name_and_hardcoded_provider_and_method() { + Source underTest = Source.external(newBasicIdentityProvider("some name")); + + assertThat(underTest.getMethod()).isEqualTo(Method.EXTERNAL); + assertThat(underTest.getProvider()).isEqualTo(Provider.EXTERNAL); + assertThat(underTest.getProviderName()).isEqualTo("some name"); + } + + @Test + public void source_is_serializable() { + assertThat(Serializable.class.isAssignableFrom(Source.class)).isTrue(); + } + + @Test + public void toString_displays_all_fields() { + assertThat(Source.sso().toString()) + .isEqualTo("Source{method=SSO, provider=SSO, providerName='sso'}"); + assertThat(Source.oauth2(newOauth2IdentityProvider("bou")).toString()) + .isEqualTo("Source{method=OAUTH2, provider=EXTERNAL, providerName='bou'}"); + } + + @Test + public void source_implements_equals_on_all_fields() { + assertThat(Source.sso()).isEqualTo(Source.sso()); + 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.sso()); + assertThat(Source.local(Method.BASIC)).isNotEqualTo(Source.jwt()); + assertThat(Source.local(Method.BASIC)).isNotEqualTo(Source.oauth2(newOauth2IdentityProvider("voo"))); + assertThat(Source.oauth2(newOauth2IdentityProvider("foo"))) + .isEqualTo(Source.oauth2(newOauth2IdentityProvider("foo"))); + assertThat(Source.oauth2(newOauth2IdentityProvider("foo"))) + .isNotEqualTo(Source.oauth2(newOauth2IdentityProvider("bar"))); + } + + private static OAuth2IdentityProvider newOauth2IdentityProvider(String name) { + OAuth2IdentityProvider mock = mock(OAuth2IdentityProvider.class); + when(mock.getName()).thenReturn(name); + return mock; + } + + private static BaseIdentityProvider newBasicIdentityProvider(String name) { + BaseIdentityProvider mock = mock(BaseIdentityProvider.class); + when(mock.getName()).thenReturn(name); + return mock; + } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/event/AuthenticationExceptionMatcher.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/event/AuthenticationExceptionMatcher.java new file mode 100644 index 00000000000..a0f1be6d976 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/event/AuthenticationExceptionMatcher.java @@ -0,0 +1,118 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.event; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; + +import static java.util.Objects.requireNonNull; +import static org.sonar.server.authentication.event.AuthenticationEvent.Source; + +/** + * Matcher for {@link AuthenticationException} to be used with {@link org.junit.rules.ExpectedException} JUnit Rule. + * + *

+ * Usage: + *

+ * expectedException.expect(authenticationException().from(Source.local(Method.BASIC_TOKEN)).withoutLogin());
+ * 
+ *

+ */ +public class AuthenticationExceptionMatcher extends TypeSafeMatcher { + private final Source source; + @CheckForNull + private final String login; + + private AuthenticationExceptionMatcher(Source source, @Nullable String login) { + this.source = requireNonNull(source, "source can't be null"); + this.login = login; + } + + public static Builder authenticationException() { + return new Builder(); + } + + public static class Builder { + private Source source; + + public Builder from(Source source) { + this.source = checkSource(source); + return this; + } + + public AuthenticationExceptionMatcher withLogin(String login) { + requireNonNull(login, "expected login can't be null"); + return new AuthenticationExceptionMatcher(checkSource(source), login); + } + + public AuthenticationExceptionMatcher withoutLogin() { + return new AuthenticationExceptionMatcher(checkSource(source), null); + } + + private static Source checkSource(Source source) { + return requireNonNull(source, "expected source can't be null"); + } + } + + @Override + protected boolean matchesSafely(Throwable throwable) { + return check(throwable) == null; + } + + private String check(Throwable throwable) { + if (!throwable.getClass().isAssignableFrom(AuthenticationException.class)) { + return "exception is not a AuthenticationException"; + } + AuthenticationException authenticationException = (AuthenticationException) throwable; + if (!this.source.equals(authenticationException.getSource())) { + return "source is \"" + authenticationException.getSource() + "\""; + } + + String login = authenticationException.getLogin(); + if (this.login == null) { + if (login != null) { + return "login is \"" + login + "\""; + } + } else if (login == null) { + return "login is null"; + } else if (!this.login.equals(login)) { + return "login is \"" + login + "\""; + } + + return null; + } + + @Override + public void describeTo(Description description) { + description.appendText("AuthenticationException with source ").appendText(source.toString()); + if (login == null) { + description.appendText(" and no login"); + } else { + description.appendText(" and login \"").appendText(login).appendText("\""); + } + } + + @Override + protected void describeMismatchSafely(Throwable item, Description mismatchDescription) { + mismatchDescription.appendText(check(item)); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/event/AuthenticationExceptionTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/event/AuthenticationExceptionTest.java new file mode 100644 index 00000000000..7a0dd2a4941 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/event/AuthenticationExceptionTest.java @@ -0,0 +1,74 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.event; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.server.authentication.event.AuthenticationEvent.Source; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AuthenticationExceptionTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void build_fails_with_NPE_if_source_is_null() { + AuthenticationException.Builder builder = AuthenticationException.newBuilder() + .setLogin("login") + .setMessage("message"); + + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("source can't be null"); + + builder.build(); + } + + @Test + public void build_does_not_fail_if_login_is_null() { + AuthenticationException exception = AuthenticationException.newBuilder() + .setSource(Source.sso()) + .setMessage("message") + .build(); + + assertThat(exception.getSource()).isEqualTo(Source.sso()); + assertThat(exception.getMessage()).isEqualTo("message"); + assertThat(exception.getLogin()).isNull(); + } + + @Test + public void build_does_not_fail_if_message_is_null() { + AuthenticationException exception = AuthenticationException.newBuilder() + .setSource(Source.sso()) + .setLogin("login") + .build(); + + assertThat(exception.getSource()).isEqualTo(Source.sso()); + assertThat(exception.getMessage()).isNull(); + assertThat(exception.getLogin()).isEqualTo("login"); + } + + @Test + public void builder_set_methods_do_not_fail_if_login_is_null() { + AuthenticationException.newBuilder().setSource(null).setLogin(null).setMessage(null); + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/LoginActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/LoginActionTest.java index 7120cd89dfc..ce6422b0b75 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/LoginActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/LoginActionTest.java @@ -36,6 +36,7 @@ import org.sonar.db.user.UserDto; import org.sonar.db.user.UserTesting; import org.sonar.server.authentication.CredentialsAuthenticator; import org.sonar.server.authentication.JwtHttpHandler; +import org.sonar.server.authentication.event.AuthenticationEvent; import org.sonar.server.exceptions.UnauthorizedException; import org.sonar.server.user.ThreadLocalUserSession; @@ -70,7 +71,7 @@ public class LoginActionTest { private UserDto user = UserTesting.newUserDto().setLogin(LOGIN); - private LoginAction underTest = new LoginAction(dbClient, credentialsAuthenticator, jwtHttpHandler, threadLocalUserSession); + private LoginAction underTest = new LoginAction(dbClient, credentialsAuthenticator, jwtHttpHandler, threadLocalUserSession, mock(AuthenticationEvent.class)); @Before public void setUp() throws Exception { diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/ValidateActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/ValidateActionTest.java index 53ca6d7ab27..ba0624d9d7b 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/ValidateActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/ValidateActionTest.java @@ -28,11 +28,11 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.junit.Before; import org.junit.Test; -import org.sonar.api.config.Settings; import org.sonar.api.config.MapSettings; +import org.sonar.api.config.Settings; import org.sonar.server.authentication.BasicAuthenticator; import org.sonar.server.authentication.JwtHttpHandler; -import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.authentication.event.AuthenticationException; import org.sonar.test.JsonAssert; import org.sonarqube.ws.MediaTypes; @@ -55,7 +55,7 @@ public class ValidateActionTest { Settings settings = new MapSettings(); - ValidateAction underTest = new ValidateAction(settings, basicAuthenticator, jwtHttpHandler); + ValidateAction underTest = new ValidateAction(settings, basicAuthenticator, jwtHttpHandler); @Before public void setUp() throws Exception { @@ -111,7 +111,7 @@ public class ValidateActionTest { @Test public void return_false_when_jwt_throws_unauthorized_exception() throws Exception { - doThrow(UnauthorizedException.class).when(jwtHttpHandler).validateToken(request, response); + doThrow(AuthenticationException.class).when(jwtHttpHandler).validateToken(request, response); when(basicAuthenticator.authenticate(request)).thenReturn(Optional.empty()); underTest.doFilter(request, response, chain); @@ -123,7 +123,7 @@ public class ValidateActionTest { @Test public void return_false_when_basic_authenticator_throws_unauthorized_exception() throws Exception { when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.empty()); - doThrow(UnauthorizedException.class).when(basicAuthenticator).authenticate(request); + doThrow(AuthenticationException.class).when(basicAuthenticator).authenticate(request); underTest.doFilter(request, response, chain); -- 2.39.5