]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8416 add event log in case of error for basic, realm, jwt
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Mon, 28 Nov 2016 08:30:33 +0000 (09:30 +0100)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Wed, 14 Dec 2016 16:09:10 +0000 (17:09 +0100)
generic UnauthorizedException thrown in case of login failure is replaced by specific AuthenticationException which includes context information to generate details failure logs

36 files changed:
server/sonar-server/src/main/java/org/sonar/server/authentication/BaseContextFactory.java
server/sonar-server/src/main/java/org/sonar/server/authentication/BasicAuthenticator.java
server/sonar-server/src/main/java/org/sonar/server/authentication/CredentialsAuthenticator.java
server/sonar-server/src/main/java/org/sonar/server/authentication/JwtCsrfVerifier.java
server/sonar-server/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java
server/sonar-server/src/main/java/org/sonar/server/authentication/JwtSerializer.java
server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2CallbackFilter.java
server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java
server/sonar-server/src/main/java/org/sonar/server/authentication/OAuthCsrfVerifier.java
server/sonar-server/src/main/java/org/sonar/server/authentication/RealmAuthenticator.java
server/sonar-server/src/main/java/org/sonar/server/authentication/SsoAuthenticator.java
server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java
server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java
server/sonar-server/src/main/java/org/sonar/server/authentication/event/AuthenticationEvent.java
server/sonar-server/src/main/java/org/sonar/server/authentication/event/AuthenticationEventImpl.java
server/sonar-server/src/main/java/org/sonar/server/authentication/event/AuthenticationException.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/ws/LoginAction.java
server/sonar-server/src/main/java/org/sonar/server/authentication/ws/ValidateAction.java
server/sonar-server/src/test/java/org/sonar/server/authentication/BaseContextFactoryTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/BasicAuthenticatorTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/CredentialsAuthenticatorTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/JwtCsrfVerifierTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/JwtSerializerTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2CallbackFilterTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2ContextFactoryTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/OAuthCsrfVerifierTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/RealmAuthenticatorTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/event/AuthenticationEventImplTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/event/AuthenticationEventSourceTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/event/AuthenticationExceptionMatcher.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/authentication/event/AuthenticationExceptionTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/authentication/ws/LoginActionTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/ws/ValidateActionTest.java

index f04b06c2dc6c43df1dba0319e4def7c614ddaa5c..87ee71cc96ce66ca28b44afee8938fa856a2abbe 100644 (file)
@@ -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));
     }
index 1e1b2bdffb9ee977f29f4809cb68360ac6a32d14..d0a97a734b3f78e1453c9d04fa77042da64cb637 100644 (file)
@@ -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<String> 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);
     }
   }
 
index 7c2d168621b2ba50318df7cc745e1f15a5178b46..34c0a422ecf0df94d4f270767a7dc41f8b30f268 100644 (file)
 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> userDto = externalAuthenticator.authenticate(userLogin, userPassword, request, method);
-    if (userDto.isPresent()) {
-      return userDto.get();
+    Optional<UserDto> 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;
+  }
 }
index b3d2e0144507a3f74d343f95b54bef327c30c462..18e460a1a329a51d1a30862d376e6f4b04b95a8c 100644 (file)
@@ -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) {
index 40f6c691c6bf43a2087f1e7dadb0243be44535e3..baa29d745ffc2e13176c6d3d9baea91bc9201351 100644 (file)
@@ -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);
index 1888b74537150d69f8e98fa5b83693e7246c78d4..fe3027f31cb7f5103765cb70b1d17be0d7f0b6c9 100644 (file)
@@ -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<Claims> 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();
     }
   }
 
index e38f63adfb6eb5eeffa02d29f4220c8939dbde45..b208b9aa2818d4f7c312048771baa6429e6ddf9c 100644 (file)
@@ -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()));
index 6f29dfc07128108aa7401b3926d82b57ea5e0eae..7cf0104450e634c2c006ad5d57b07d3f71c99593 100644 (file)
@@ -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));
     }
index 3c48a481439e37635927669782c7394db44a8125..874734d859a8595091e1942f8c350e43fec85dbd 100644 (file)
@@ -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();
     }
   }
 
index f2ab92b748285cb52874fa6e278f1609377fbf7e..72c8588cf853112314a2d0ba579f1776898ad6fe 100644 (file)
@@ -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<String> 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) {
index 292e101cf9c58be415030705f4d4baedc38c4a49..9cac78e0f90e7580b5985d5e40bc2a135510fd01 100644 (file)
@@ -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
index bf2b809bdaa60bdec8f1bab9f9adfa217bcec8c6..e95302d91e3b8cbf0c3828f10028f7bcc11b6d0e 100644 (file)
@@ -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();
index 4f02daea4e09e340509b055dbf03666e5cbca858..8b1456aaacbcd833622a5d64b46b07435623fa2f 100644 (file)
@@ -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<UserDto> user = authenticate(request, response);
     if (user.isPresent()) {
index a66e0e00efd7d7ad78a09f8e55fbf046d7d29f21..497bf058b0bff872f3749756c0dcb3f2d183cd37 100644 (file)
  */
 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;
     }
 
index 5a96961cb7cbddc015e42aebf2ac708df12d7f36..413cbb99328b1b07a27229e05a99fd03fe59f17c 100644 (file)
@@ -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 (file)
index 0000000..64ae64e
--- /dev/null
@@ -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.
+ * <p>
+ * This exception contains the source of authentication and, if present, the login on which the login attempt occurred.
+ * </p>
+ * <p>
+ * Given that {@link #source} and {@link #login} will be logged to file, be very careful <strong>not to set the login
+ * when the login is a security token</strong>.
+ * </p>
+ */
+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);
+    }
+  }
+}
index ab057b0827157fd0950b1502863054092f34d861..c7afb3f134cbc2cb57675e39a8ad9feec3299be2 100644 (file)
@@ -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);
   }
index 825b245ce916f1c61666a7abe28c0634b435ff38..60791c2607a520136dd958a886deb1503b4d6bac 100644 (file)
@@ -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;
     }
   }
index 4646ccc892b35625f5be5fcc05e04093fd5ff291..e398d077fc7b352e24313e8495a1d83f6e9ecf2d 100644 (file)
@@ -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));
   }
index 0a0a30c92586d79f2564ba25c50c8fc2c9435c74..9191da50352a26ad8c1ac4146367fbedbc816081 100644 (file)
@@ -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 {
index 5b900f56c67285eeda412bc86b449b48a3db3016..cd5f1d13b6cffe8106af7fa4a5b432aa45f34f04 100644 (file)
@@ -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) {
index 65a3f7a2f19077194c8f2a6d1236d03d4621bc3e..96e1fe51347648461eef25ecc1d896d81eb75163 100644 (file)
@@ -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<Cookie> cookieArgumentCaptor = ArgumentCaptor.forClass(Cookie.class);
+  private ArgumentCaptor<Cookie> 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);
   }
 }
index 443b2405df33f3f5a004aa142ba0828221e7fb9d..49ade87abe92f31b392f497e9edea7564a7e4a9d 100644 (file)
@@ -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
index 0011a0b25651661e9a3711b606ed6a9adad21529..d54f1519dc60fb24cfff1faa822b28ec0b0859d3 100644 (file)
@@ -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);
   }
index 6ff937e9c609064745d665c16b176789217b62d1..bc1c8b935ced7df27826d1634c768061a8b1ee73 100644 (file)
@@ -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);
     }
index 57af73855cd5262a3b7ac3775a6ba9ee9c3f4f30..2ceacb0dcf7e31ec86a3e433f044d33eeee52992 100644 (file)
@@ -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() {
index 07815176e2059551852a4fad8dddef7ec3c90d92..5f1825d6b1e9a178f8907b8d92fb5457b06ddb7a 100644 (file)
@@ -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<Cookie> cookieArgumentCaptor = ArgumentCaptor.forClass(Cookie.class);
+  private ArgumentCaptor<Cookie> 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) {
index 1d5ac67de480ca9bc38e3375e7c9c047ff5ce622..7651e4cdd6e292f4badbc4cc85bdf6f52e0a08b2 100644 (file)
@@ -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<UserIdentity> userIdentityArgumentCaptor = ArgumentCaptor.forClass(UserIdentity.class);
   private ArgumentCaptor<IdentityProvider> identityProviderArgumentCaptor = ArgumentCaptor.forClass(IdentityProvider.class);
+  private ArgumentCaptor<AuthenticationEvent.Source> 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);
     }
index effa289a654ebf36810ab86c1ccb405da5d7042b..a0b7e682a0e8caeb4d59fdc07d33838210e5ec0c 100644 (file)
@@ -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());
   }
 
 }
index 36cc8e38e18845c874f0ebea1925bcf518784a14..a3f6a994e58a4893ea178c28430936421e5b6fbe 100644 (file)
@@ -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 {
index 3cff532ccb8f13e807ed639df448cdfd6ec7642b..f67c589c11ea7528431f2907e93e8500c9213097 100644 (file)
@@ -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))
index 95e6b22f5253ac5c959224c84cc0b5c70f2697df..0a2b6090f0d7af5c00b5194ea5481717b76327f8 100644 (file)
  */
 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 (file)
index 0000000..a0f1be6
--- /dev/null
@@ -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.
+ *
+ * <p>
+ * Usage:
+ * <pre>
+ * expectedException.expect(authenticationException().from(Source.local(Method.BASIC_TOKEN)).withoutLogin());
+ * </pre>
+ * </p>
+ */
+public class AuthenticationExceptionMatcher extends TypeSafeMatcher<Throwable> {
+  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 (file)
index 0000000..7a0dd2a
--- /dev/null
@@ -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);
+  }
+
+}
index 7120cd89dfccd65c21bd97659eabfb45ba1db7f9..ce6422b0b75621851ab9814029bb0c5ade2fd9d8 100644 (file)
@@ -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 {
index 53ca6d7ab2766498ab7167373b55be13197fd26c..ba0624d9d7b593ef2f26292343e699651740d5f0 100644 (file)
@@ -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);