This is an automated email from the ASF dual-hosted git repository.

jinmeiliao pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/geode.git


The following commit(s) were added to refs/heads/develop by this push:
     new ccccc5e  GEODE-9663: throw and handle AuthenticationExpiredException 
at login time (#6927)
ccccc5e is described below

commit ccccc5e8031f314c3d58df0ba7bbdf0d68704bdb
Author: Jinmei Liao <[email protected]>
AuthorDate: Tue Oct 5 17:41:59 2021 -0700

    GEODE-9663: throw and handle AuthenticationExpiredException at login time 
(#6927)
    
    * GEODE-9663: throw and handle AuthenticationExpiredException at login time
---
 .../internal/executor/connection/AuthExecutor.java | 28 +++++----
 .../executor/connection/AuthExecutorTest.java      | 70 +++++++++++++++++++++
 .../ClusterConfigurationSecurityDUnitTest.java     |  2 +-
 .../IntegratedSecurityPeerAuthDUnitTest.java       |  9 +--
 .../PeerSecurityWithEmbeddedLocatorDUnitTest.java  |  2 +-
 .../security/StartServerAuthorizationTest.java     |  2 +-
 .../SecurityWithExpirationIntegrationTest.java     |  3 +-
 .../cache/client/internal/AuthenticateUserOp.java  | 27 +++++++-
 .../internal/cache/tier/sockets/Handshake.java     |  3 +-
 .../cache/tier/sockets/ServerConnection.java       |  4 +-
 .../security/IntegratedSecurityService.java        | 12 +++-
 .../org/apache/geode/security/SecurityManager.java |  7 ++-
 .../client/internal/AuthenticateUserOpTest.java    | 73 +++++++++++++++++++++-
 .../security/IntegratedSecurityServiceTest.java    | 57 +++++++++++++++++
 .../AuthExpirationMultiServerDUnitTest.java        | 56 +++++++++++++++++
 .../geode/security/ExpirableSecurityManager.java   |  2 +-
 .../gemstone/gemfire/OldClientSupportProvider.java | 11 ++--
 ...usterManagementSecurityRestIntegrationTest.java |  2 +-
 18 files changed, 334 insertions(+), 36 deletions(-)

diff --git 
a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/executor/connection/AuthExecutor.java
 
b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/executor/connection/AuthExecutor.java
index 1edc56f..e5e8682 100644
--- 
a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/executor/connection/AuthExecutor.java
+++ 
b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/executor/connection/AuthExecutor.java
@@ -29,6 +29,7 @@ import org.apache.geode.redis.internal.executor.Executor;
 import org.apache.geode.redis.internal.executor.RedisResponse;
 import org.apache.geode.redis.internal.netty.Command;
 import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+import org.apache.geode.security.AuthenticationExpiredException;
 import org.apache.geode.security.AuthenticationFailedException;
 import org.apache.geode.security.SecurityManager;
 
@@ -36,8 +37,6 @@ public class AuthExecutor implements Executor {
 
   @Override
   public RedisResponse executeCommand(Command command, ExecutionHandlerContext 
context) {
-    List<byte[]> commandElems = command.getProcessedCommand();
-
     SecurityService securityService = context.getSecurityService();
 
     // We're deviating from Redis here in that any AUTH requests, without 
security explicitly
@@ -46,23 +45,28 @@ public class AuthExecutor implements Executor {
       return 
RedisResponse.error(ERROR_AUTH_CALLED_WITHOUT_SECURITY_CONFIGURED);
     }
 
-    Properties props = new Properties();
-    if (commandElems.size() == 2) {
-      props.setProperty(SecurityManager.USER_NAME, context.getRedisUsername());
-      props.setProperty(SecurityManager.PASSWORD, 
bytesToString(commandElems.get(1)));
-    } else {
-      props.setProperty(SecurityManager.USER_NAME, 
bytesToString(commandElems.get(1)));
-      props.setProperty(SecurityManager.PASSWORD, 
bytesToString(commandElems.get(2)));
-    }
-
+    Properties props = getSecurityProperties(command, context);
     try {
       Subject subject = securityService.login(props);
       context.setSubject(subject);
-    } catch (AuthenticationFailedException ex) {
+    } catch (AuthenticationFailedException | AuthenticationExpiredException 
ex) {
       return RedisResponse.wrongpass(ERROR_INVALID_USERNAME_OR_PASSWORD);
     }
 
     return RedisResponse.ok();
   }
 
+  Properties getSecurityProperties(Command command, ExecutionHandlerContext 
context) {
+    Properties properties = new Properties();
+    List<byte[]> commandElems = command.getProcessedCommand();
+    if (commandElems.size() == 2) {
+      properties.setProperty(SecurityManager.USER_NAME, 
context.getRedisUsername());
+      properties.setProperty(SecurityManager.PASSWORD, 
bytesToString(commandElems.get(1)));
+    } else {
+      properties.setProperty(SecurityManager.USER_NAME, 
bytesToString(commandElems.get(1)));
+      properties.setProperty(SecurityManager.PASSWORD, 
bytesToString(commandElems.get(2)));
+    }
+    return properties;
+  }
+
 }
diff --git 
a/geode-apis-compatible-with-redis/src/test/java/org/apache/geode/redis/internal/executor/connection/AuthExecutorTest.java
 
b/geode-apis-compatible-with-redis/src/test/java/org/apache/geode/redis/internal/executor/connection/AuthExecutorTest.java
new file mode 100644
index 0000000..13be66d
--- /dev/null
+++ 
b/geode-apis-compatible-with-redis/src/test/java/org/apache/geode/redis/internal/executor/connection/AuthExecutorTest.java
@@ -0,0 +1,70 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license
+ * agreements. See the NOTICE file distributed with this work for additional 
information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache 
License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the 
License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software 
distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
KIND, either express
+ * or implied. See the License for the specific language governing permissions 
and limitations under
+ * the License.
+ *
+ */
+
+package org.apache.geode.redis.internal.executor.connection;
+
+import static 
org.apache.geode.redis.internal.RedisConstants.ERROR_AUTH_CALLED_WITHOUT_SECURITY_CONFIGURED;
+import static 
org.apache.geode.redis.internal.RedisConstants.ERROR_INVALID_USERNAME_OR_PASSWORD;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import java.util.Properties;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.geode.internal.security.SecurityService;
+import org.apache.geode.redis.internal.executor.RedisResponse;
+import org.apache.geode.redis.internal.netty.Command;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+import org.apache.geode.security.AuthenticationExpiredException;
+
+public class AuthExecutorTest {
+  private AuthExecutor executor;
+  private Command command;
+  private ExecutionHandlerContext context;
+  private SecurityService securityService;
+
+  @Before
+  public void before() {
+    executor = spy(AuthExecutor.class);
+    command = mock(Command.class);
+    context = mock(ExecutionHandlerContext.class);
+    securityService = mock(SecurityService.class);
+    when(context.getSecurityService()).thenReturn(securityService);
+  }
+
+  @Test
+  public void notIntegratedService() {
+    when(securityService.isIntegratedSecurity()).thenReturn(false);
+    RedisResponse redisResponse = executor.executeCommand(command, context);
+    
assertThat(redisResponse.toString()).contains(ERROR_AUTH_CALLED_WITHOUT_SECURITY_CONFIGURED);
+  }
+
+  @Test
+  public void handleAuthExpirationException() {
+    when(securityService.isIntegratedSecurity()).thenReturn(true);
+    doReturn(new Properties()).when(executor).getSecurityProperties(command, 
context);
+    when(securityService.login(any())).thenThrow(new 
AuthenticationExpiredException("expired"));
+    RedisResponse redisResponse = executor.executeCommand(command, context);
+    
assertThat(redisResponse.toString()).contains(ERROR_INVALID_USERNAME_OR_PASSWORD);
+  }
+}
diff --git 
a/geode-core/src/distributedTest/java/org/apache/geode/security/ClusterConfigurationSecurityDUnitTest.java
 
b/geode-core/src/distributedTest/java/org/apache/geode/security/ClusterConfigurationSecurityDUnitTest.java
index 2426592..ae20b74 100644
--- 
a/geode-core/src/distributedTest/java/org/apache/geode/security/ClusterConfigurationSecurityDUnitTest.java
+++ 
b/geode-core/src/distributedTest/java/org/apache/geode/security/ClusterConfigurationSecurityDUnitTest.java
@@ -65,7 +65,7 @@ public class ClusterConfigurationSecurityDUnitTest {
     properties.put("security-password", "invalidPassword");
     assertThatThrownBy(() -> serverStarter.startServer(properties, 
locator.getPort()))
         .isInstanceOf(GemFireSecurityException.class)
-        .hasMessageContaining("Security check failed. Authentication error.");
+        .hasMessageContaining("Security check failed. invalid 
username/password");
   }
 
   @Test
diff --git 
a/geode-core/src/distributedTest/java/org/apache/geode/security/IntegratedSecurityPeerAuthDUnitTest.java
 
b/geode-core/src/distributedTest/java/org/apache/geode/security/IntegratedSecurityPeerAuthDUnitTest.java
index b147d67..a6de76e 100644
--- 
a/geode-core/src/distributedTest/java/org/apache/geode/security/IntegratedSecurityPeerAuthDUnitTest.java
+++ 
b/geode-core/src/distributedTest/java/org/apache/geode/security/IntegratedSecurityPeerAuthDUnitTest.java
@@ -44,6 +44,7 @@ import org.apache.geode.test.junit.rules.ServerStarterRule;
 @Category({SecurityTest.class})
 public class IntegratedSecurityPeerAuthDUnitTest {
 
+  private static final String SERVER_3_IS_NOT_AUTHENTICATED = "server-3 is not 
authenticated";
   @ClassRule
   public static ClusterStartupRule cluster = new ClusterStartupRule();
 
@@ -73,7 +74,7 @@ public class IntegratedSecurityPeerAuthDUnitTest {
     cluster.getVM(2).invoke(() -> {
       ServerStarterRule server = new ServerStarterRule();
       
server.withProperties(props).withConnectionToLocator(locatorPort).withAutoStart();
-      assertThatThrownBy(() -> 
server.before()).isInstanceOf(GemFireSecurityException.class)
+      
assertThatThrownBy(server::before).isInstanceOf(GemFireSecurityException.class)
           .hasMessageContaining("server-2 not authorized for CLUSTER:MANAGE");
     });
   }
@@ -87,8 +88,8 @@ public class IntegratedSecurityPeerAuthDUnitTest {
     cluster.getVM(3).invoke(() -> {
       ServerStarterRule server = new ServerStarterRule();
       
server.withProperties(props).withConnectionToLocator(locatorPort).withAutoStart();
-      assertThatThrownBy(() -> 
server.before()).isInstanceOf(GemFireSecurityException.class)
-          .hasMessageContaining("Authentication error");
+      
assertThatThrownBy(server::before).isInstanceOf(GemFireSecurityException.class)
+          .hasMessageContaining(SERVER_3_IS_NOT_AUTHENTICATED);
     });
   }
 
@@ -108,7 +109,7 @@ public class IntegratedSecurityPeerAuthDUnitTest {
       // server1 and server2 are authenticated, server3 is not
       String name = credentials.getProperty("name");
       if ("server-3".equals(name)) {
-        throw new AuthenticationFailedException("server-3 is not 
authenticated");
+        throw new AuthenticationFailedException(SERVER_3_IS_NOT_AUTHENTICATED);
       }
       return name;
     }
diff --git 
a/geode-core/src/distributedTest/java/org/apache/geode/security/PeerSecurityWithEmbeddedLocatorDUnitTest.java
 
b/geode-core/src/distributedTest/java/org/apache/geode/security/PeerSecurityWithEmbeddedLocatorDUnitTest.java
index b805e91..c236942 100644
--- 
a/geode-core/src/distributedTest/java/org/apache/geode/security/PeerSecurityWithEmbeddedLocatorDUnitTest.java
+++ 
b/geode-core/src/distributedTest/java/org/apache/geode/security/PeerSecurityWithEmbeddedLocatorDUnitTest.java
@@ -66,7 +66,7 @@ public class PeerSecurityWithEmbeddedLocatorDUnitTest {
       ClusterStartupRule.memberStarter = serverStarter;
       assertThatThrownBy(() -> serverStarter.startServer(server2Props, 
locatorPort))
           .isInstanceOf(GemFireSecurityException.class)
-          .hasMessageContaining("Security check failed. Authentication error");
+          .hasMessageContaining("Security check failed. invalid 
username/password");
     });
   }
 
diff --git 
a/geode-core/src/distributedTest/java/org/apache/geode/security/StartServerAuthorizationTest.java
 
b/geode-core/src/distributedTest/java/org/apache/geode/security/StartServerAuthorizationTest.java
index 79fd095..91d182c 100644
--- 
a/geode-core/src/distributedTest/java/org/apache/geode/security/StartServerAuthorizationTest.java
+++ 
b/geode-core/src/distributedTest/java/org/apache/geode/security/StartServerAuthorizationTest.java
@@ -58,7 +58,7 @@ public class StartServerAuthorizationTest {
 
     assertThatThrownBy(() -> serverStarter.startServer(props, 
locator.getPort()))
         .isInstanceOf(GemFireSecurityException.class).hasMessageContaining(
-            "Security check failed. Authentication error. Please check your 
credentials");
+            "Security check failed. invalid username/password");
   }
 
   @Test
diff --git 
a/geode-core/src/integrationTest/java/org/apache/geode/management/internal/security/SecurityWithExpirationIntegrationTest.java
 
b/geode-core/src/integrationTest/java/org/apache/geode/management/internal/security/SecurityWithExpirationIntegrationTest.java
index 598533a..cdab082 100644
--- 
a/geode-core/src/integrationTest/java/org/apache/geode/management/internal/security/SecurityWithExpirationIntegrationTest.java
+++ 
b/geode-core/src/integrationTest/java/org/apache/geode/management/internal/security/SecurityWithExpirationIntegrationTest.java
@@ -26,7 +26,6 @@ import org.junit.experimental.categories.Category;
 import org.apache.geode.internal.security.SecurityService;
 import org.apache.geode.internal.security.SecurityServiceFactory;
 import org.apache.geode.security.AuthenticationExpiredException;
-import org.apache.geode.security.AuthenticationFailedException;
 import org.apache.geode.security.ExpirableSecurityManager;
 import org.apache.geode.test.junit.categories.SecurityTest;
 
@@ -47,7 +46,7 @@ public class SecurityWithExpirationIntegrationTest {
   public void testAuthenticationWhenUserExpired() {
     getSecurityManager().addExpiredUser("data");
     assertThatThrownBy(() -> 
this.securityService.login(loginCredentials("data", "data")))
-        .isInstanceOf(AuthenticationFailedException.class);
+        .isInstanceOf(AuthenticationExpiredException.class);
   }
 
   @Test
diff --git 
a/geode-core/src/main/java/org/apache/geode/cache/client/internal/AuthenticateUserOp.java
 
b/geode-core/src/main/java/org/apache/geode/cache/client/internal/AuthenticateUserOp.java
index a312e2e..43bafab 100644
--- 
a/geode-core/src/main/java/org/apache/geode/cache/client/internal/AuthenticateUserOp.java
+++ 
b/geode-core/src/main/java/org/apache/geode/cache/client/internal/AuthenticateUserOp.java
@@ -21,6 +21,7 @@ import java.util.Properties;
 
 import org.apache.geode.DataSerializer;
 import org.apache.geode.InternalGemFireError;
+import org.apache.geode.annotations.VisibleForTesting;
 import org.apache.geode.cache.client.ServerOperationException;
 import org.apache.geode.distributed.DistributedMember;
 import org.apache.geode.distributed.internal.InternalDistributedSystem;
@@ -34,6 +35,7 @@ import org.apache.geode.internal.cache.tier.sockets.Part;
 import org.apache.geode.internal.cache.tier.sockets.command.PutUserCredentials;
 import org.apache.geode.internal.serialization.ByteArrayDataInput;
 import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.security.AuthenticationExpiredException;
 import org.apache.geode.security.AuthenticationFailedException;
 import org.apache.geode.security.AuthenticationRequiredException;
 import org.apache.geode.security.NotAuthorizedException;
@@ -176,13 +178,30 @@ public class AuthenticateUserOp {
 
     @Override
     public Object attempt(Connection connection) throws Exception {
-      if (connection.getServer().getRequiresCredentials()) {
-        return super.attempt(connection);
-      } else {
+      if (!connection.getServer().getRequiresCredentials()) {
         return null;
       }
+
+      try {
+        return parentAttempt(connection);
+      }
+      // if login failed for auth expired reason, try again once more
+      catch (AuthenticationExpiredException first) {
+        getMessage().clear();
+        try {
+          return parentAttempt(connection);
+        } catch (AuthenticationExpiredException second) {
+          throw new AuthenticationFailedException(second.getMessage(), second);
+        }
+      }
+    }
+
+    @VisibleForTesting
+    Object parentAttempt(Connection connection) throws Exception {
+      return super.attempt(connection);
     }
 
+
     @Override
     protected Object processResponse(Message msg, Connection connection) 
throws Exception {
       byte[] bytes;
@@ -215,6 +234,8 @@ public class AuthenticateUserOp {
           } else {
             throw new AuthenticationFailedException(s, afe);
           }
+        } else if (result instanceof AuthenticationExpiredException) {
+          throw (AuthenticationExpiredException) result;
         } else if (result instanceof AuthenticationRequiredException) {
           throw new AuthenticationRequiredException(s, 
(AuthenticationRequiredException) result);
         } else if (result instanceof NotAuthorizedException) {
diff --git 
a/geode-core/src/main/java/org/apache/geode/internal/cache/tier/sockets/Handshake.java
 
b/geode-core/src/main/java/org/apache/geode/internal/cache/tier/sockets/Handshake.java
index 6a3d221..7c7ce0c 100644
--- 
a/geode-core/src/main/java/org/apache/geode/internal/cache/tier/sockets/Handshake.java
+++ 
b/geode-core/src/main/java/org/apache/geode/internal/cache/tier/sockets/Handshake.java
@@ -44,6 +44,7 @@ import org.apache.geode.internal.security.SecurityService;
 import org.apache.geode.internal.serialization.KnownVersion;
 import org.apache.geode.logging.internal.log4j.api.LogService;
 import org.apache.geode.security.AuthInitialize;
+import org.apache.geode.security.AuthenticationExpiredException;
 import org.apache.geode.security.AuthenticationFailedException;
 import org.apache.geode.security.AuthenticationRequiredException;
 import org.apache.geode.security.Authenticator;
@@ -492,7 +493,7 @@ public abstract class Handshake {
         auth.init(securityProperties, logWriter, securityLogWriter);
         return auth.authenticate(credentials, member);
       }
-    } catch (AuthenticationFailedException ex) {
+    } catch (AuthenticationFailedException | AuthenticationExpiredException 
ex) {
       throw ex;
     } catch (Exception ex) {
       throw new AuthenticationFailedException(ex.getMessage(), ex);
diff --git 
a/geode-core/src/main/java/org/apache/geode/internal/cache/tier/sockets/ServerConnection.java
 
b/geode-core/src/main/java/org/apache/geode/internal/cache/tier/sockets/ServerConnection.java
index 06c8c6b..eae9384 100644
--- 
a/geode-core/src/main/java/org/apache/geode/internal/cache/tier/sockets/ServerConnection.java
+++ 
b/geode-core/src/main/java/org/apache/geode/internal/cache/tier/sockets/ServerConnection.java
@@ -76,6 +76,7 @@ import 
org.apache.geode.internal.serialization.ByteArrayDataInput;
 import org.apache.geode.internal.serialization.KnownVersion;
 import org.apache.geode.internal.util.Breadcrumbs;
 import org.apache.geode.logging.internal.log4j.api.LogService;
+import org.apache.geode.security.AuthenticationExpiredException;
 import org.apache.geode.security.AuthenticationFailedException;
 import org.apache.geode.security.AuthenticationRequiredException;
 import org.apache.geode.security.GemFireSecurityException;
@@ -1179,7 +1180,8 @@ public class ServerConnection implements Runnable {
 
       // create secure part which will be send in response
       return encryptId(uniqueId);
-    } catch (AuthenticationFailedException | AuthenticationRequiredException 
exception) {
+    } catch (AuthenticationFailedException | AuthenticationRequiredException
+        | AuthenticationExpiredException exception) {
       throw exception;
     } catch (Exception exception) {
       throw new AuthenticationFailedException("REPLY_REFUSED", exception);
diff --git 
a/geode-core/src/main/java/org/apache/geode/internal/security/IntegratedSecurityService.java
 
b/geode-core/src/main/java/org/apache/geode/internal/security/IntegratedSecurityService.java
index e6cf99f..c7425dd 100644
--- 
a/geode-core/src/main/java/org/apache/geode/internal/security/IntegratedSecurityService.java
+++ 
b/geode-core/src/main/java/org/apache/geode/internal/security/IntegratedSecurityService.java
@@ -42,6 +42,7 @@ import 
org.apache.geode.internal.security.shiro.SecurityManagerProvider;
 import org.apache.geode.internal.security.shiro.ShiroPrincipal;
 import org.apache.geode.internal.util.BlobHelper;
 import org.apache.geode.logging.internal.log4j.api.LogService;
+import org.apache.geode.security.AuthenticationExpiredException;
 import org.apache.geode.security.AuthenticationFailedException;
 import org.apache.geode.security.AuthenticationRequiredException;
 import org.apache.geode.security.GemFireSecurityException;
@@ -159,8 +160,17 @@ public class IntegratedSecurityService implements 
SecurityService {
       currentUser.login(token);
     } catch (ShiroException e) {
       logger.info("error logging in: " + token.getPrincipal());
+      Throwable cause = e.getCause();
+      if (cause == null) {
+        throw new AuthenticationFailedException(
+            "Authentication error. Please check your credentials.", e);
+      }
+      if (cause instanceof AuthenticationFailedException
+          || cause instanceof AuthenticationExpiredException) {
+        throw (GemFireSecurityException) cause;
+      }
       throw new AuthenticationFailedException(
-          "Authentication error. Please check your credentials.", e);
+          "Authentication error. Please check your credentials.", cause);
     }
 
     Session currentSession = currentUser.getSession();
diff --git 
a/geode-core/src/main/java/org/apache/geode/security/SecurityManager.java 
b/geode-core/src/main/java/org/apache/geode/security/SecurityManager.java
index 0b11f4f..16e0e4e 100644
--- a/geode-core/src/main/java/org/apache/geode/security/SecurityManager.java
+++ b/geode-core/src/main/java/org/apache/geode/security/SecurityManager.java
@@ -65,8 +65,13 @@ public interface SecurityManager {
    *        as keys of the properties, also the properties generated by your 
AuthInitialize
    *        interface
    * @return a serializable principal object
+   * @throw AuthenticationFailedException if the credentials are invalid, this 
exception will be
+   *        seen by the client.
+   * @throw AuthenticationExpiredException if credentials have expired, this 
will give the
+   *        client a second chance to gather new credentials and try login 
again once more.
    */
-  Object authenticate(Properties credentials) throws 
AuthenticationFailedException;
+  Object authenticate(Properties credentials)
+      throws AuthenticationFailedException, AuthenticationExpiredException;
 
   /**
    * Authorize the ResourcePermission for a given Principal
diff --git 
a/geode-core/src/test/java/org/apache/geode/cache/client/internal/AuthenticateUserOpTest.java
 
b/geode-core/src/test/java/org/apache/geode/cache/client/internal/AuthenticateUserOpTest.java
index 568ca2b..f170bf2 100644
--- 
a/geode-core/src/test/java/org/apache/geode/cache/client/internal/AuthenticateUserOpTest.java
+++ 
b/geode-core/src/test/java/org/apache/geode/cache/client/internal/AuthenticateUserOpTest.java
@@ -16,11 +16,15 @@ package org.apache.geode.cache.client.internal;
 
 import static 
org.apache.geode.cache.client.internal.AuthenticateUserOp.NOT_A_USER_ID;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -32,7 +36,10 @@ import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 
 import org.apache.geode.distributed.internal.InternalDistributedSystem;
+import org.apache.geode.distributed.internal.ServerLocation;
 import org.apache.geode.internal.cache.tier.sockets.Message;
+import org.apache.geode.security.AuthenticationExpiredException;
+import org.apache.geode.security.AuthenticationFailedException;
 
 public class AuthenticateUserOpTest {
 
@@ -41,6 +48,7 @@ public class AuthenticateUserOpTest {
   private InternalDistributedSystem system;
   private byte[] credentialBytes;
   private Message message;
+  private ServerLocation server;
 
   @Before
   public void before() throws Exception {
@@ -48,11 +56,13 @@ public class AuthenticateUserOpTest {
     system = mock(InternalDistributedSystem.class);
     credentialBytes = "test".getBytes(StandardCharsets.UTF_8);
     message = mock(Message.class);
+    server = mock(ServerLocation.class);
+    when(connection.getServer()).thenReturn(server);
+    impl = spy(new AuthenticateUserOp.AuthenticateUserOpImpl());
   }
 
   @Test
   public void constructorWithNoArg() throws Exception {
-    impl = spy(new AuthenticateUserOp.AuthenticateUserOpImpl());
     doReturn(system).when(impl).getConnectedSystem();
     Properties properties = new Properties();
     when(system.getSecurityProperties()).thenReturn(properties);
@@ -82,4 +92,65 @@ public class AuthenticateUserOpTest {
     verify(impl).getCredentialBytes(eq(connection), captor.capture());
     assertThat(captor.getValue()).isEqualTo(properties);
   }
+
+  @Test
+  public void noAttempt_if_NotRequireCredentials() throws Exception {
+    when(server.getRequiresCredentials()).thenReturn(false);
+    impl.attempt(connection);
+    verify(impl, never()).parentAttempt(connection);
+  }
+
+  @Test
+  public void callParentAttempt_IfRequireCredentials() throws Exception {
+    when(server.getRequiresCredentials()).thenReturn(true);
+    doReturn(null).when(impl).parentAttempt(connection);
+    impl.attempt(connection);
+    verify(impl).parentAttempt(connection);
+  }
+
+  @Test
+  public void whenParentAttemptThrowAuthenticationExpiredException() throws 
Exception {
+    when(server.getRequiresCredentials()).thenReturn(true);
+    doThrow(new 
AuthenticationExpiredException("expired")).when(impl).parentAttempt(connection);
+    assertThatThrownBy(() -> impl.attempt(connection))
+        .isInstanceOf(AuthenticationFailedException.class)
+        .hasCauseInstanceOf(AuthenticationExpiredException.class)
+        .hasMessageContaining("expired");
+    verify(impl, times(2)).parentAttempt(connection);
+  }
+
+  @Test
+  public void whenParentAttemptThrowAuthenticationFailedException() throws 
Exception {
+    when(server.getRequiresCredentials()).thenReturn(true);
+    doThrow(new 
AuthenticationFailedException("failed")).when(impl).parentAttempt(connection);
+    assertThatThrownBy(() -> impl.attempt(connection))
+        .isInstanceOf(AuthenticationFailedException.class)
+        .hasNoCause()
+        .hasMessageContaining("failed");
+    verify(impl).parentAttempt(connection);
+  }
+
+  @Test
+  public void 
whenParentAttemptThrowAuthenticationExpiredException_ThenAuthenticationFailedException()
+      throws Exception {
+    when(server.getRequiresCredentials()).thenReturn(true);
+    doThrow(new AuthenticationExpiredException("expired"),
+        new 
AuthenticationFailedException("failed")).when(impl).parentAttempt(connection);
+    assertThatThrownBy(() -> impl.attempt(connection))
+        .isInstanceOf(AuthenticationFailedException.class)
+        .hasNoCause()
+        .hasMessageContaining("failed");
+    verify(impl, times(2)).parentAttempt(connection);
+  }
+
+  @Test
+  public void 
whenParentAttemptThrowAuthenticationExpiredException_ThenSucceed()
+      throws Exception {
+    when(server.getRequiresCredentials()).thenReturn(true);
+    doThrow(new AuthenticationExpiredException("expired"))
+        .doReturn(null)
+        .when(impl).parentAttempt(connection);
+    impl.attempt(connection);
+    verify(impl, times(2)).parentAttempt(connection);
+  }
 }
diff --git 
a/geode-core/src/test/java/org/apache/geode/internal/security/IntegratedSecurityServiceTest.java
 
b/geode-core/src/test/java/org/apache/geode/internal/security/IntegratedSecurityServiceTest.java
index f1aa2eb..899f7ab 100644
--- 
a/geode-core/src/test/java/org/apache/geode/internal/security/IntegratedSecurityServiceTest.java
+++ 
b/geode-core/src/test/java/org/apache/geode/internal/security/IntegratedSecurityServiceTest.java
@@ -17,11 +17,13 @@ package org.apache.geode.internal.security;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import java.util.Properties;
 
+import org.apache.shiro.ShiroException;
 import org.apache.shiro.session.Session;
 import org.apache.shiro.subject.Subject;
 import org.apache.shiro.subject.SubjectContext;
@@ -32,7 +34,10 @@ import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
+import org.apache.geode.internal.security.shiro.GeodeAuthenticationToken;
 import org.apache.geode.internal.security.shiro.SecurityManagerProvider;
+import org.apache.geode.security.AuthenticationExpiredException;
+import org.apache.geode.security.AuthenticationFailedException;
 import org.apache.geode.security.AuthenticationRequiredException;
 import org.apache.geode.security.PostProcessor;
 import org.apache.geode.security.SecurityManager;
@@ -45,6 +50,8 @@ public class IntegratedSecurityServiceTest {
   private org.apache.shiro.mgt.SecurityManager shiroManager;
 
   private IntegratedSecurityService securityService;
+  private ShiroException shiroException;
+  private Properties properties;
 
   @Before
   public void before() throws Exception {
@@ -58,6 +65,9 @@ public class IntegratedSecurityServiceTest {
     when(mockSubject.getPrincipal()).thenReturn("principal");
     when(mockSubject.getSession()).thenReturn(mock(Session.class));
 
+    shiroException = mock(ShiroException.class);
+    properties = new Properties();
+
     this.securityService = new IntegratedSecurityService(provider, null);
   }
 
@@ -162,4 +172,51 @@ public class IntegratedSecurityServiceTest {
     PostProcessor postProcessor = this.securityService.getPostProcessor();
     assertThat(postProcessor).isNull();
   }
+
+  @Test
+  public void login_when_credential_isNull() throws Exception {
+    assertThatThrownBy(() -> securityService.login(null))
+        .isInstanceOf(AuthenticationRequiredException.class)
+        .hasNoCause()
+        .hasMessageContaining("credentials are null");
+  }
+
+  @Test
+  public void login_when_ShiroException_hasNoCause() throws Exception {
+    
doThrow(shiroException).when(mockSubject).login(any(GeodeAuthenticationToken.class));
+    assertThatThrownBy(() -> securityService.login(properties))
+        .isInstanceOf(AuthenticationFailedException.class)
+        .hasCauseInstanceOf(ShiroException.class)
+        .hasMessageContaining("Authentication error. Please check your 
credentials");
+  }
+
+  @Test
+  public void login_when_ShiroException_causedBy_AuthenticationFailed() throws 
Exception {
+    
doThrow(shiroException).when(mockSubject).login(any(GeodeAuthenticationToken.class));
+    when(shiroException.getCause()).thenReturn(new 
AuthenticationFailedException("failed"));
+    assertThatThrownBy(() -> securityService.login(properties))
+        .isInstanceOf(AuthenticationFailedException.class)
+        .hasNoCause()
+        .hasMessageContaining("failed");
+  }
+
+  @Test
+  public void login_when_ShiroException_causedBy_AuthenticationExpired() 
throws Exception {
+    
doThrow(shiroException).when(mockSubject).login(any(GeodeAuthenticationToken.class));
+    when(shiroException.getCause()).thenReturn(new 
AuthenticationExpiredException("expired"));
+    assertThatThrownBy(() -> securityService.login(properties))
+        .isInstanceOf(AuthenticationExpiredException.class)
+        .hasNoCause()
+        .hasMessageContaining("expired");
+  }
+
+  @Test
+  public void login_when_ShiroException_causedBy_OtherExceptions() throws 
Exception {
+    
doThrow(shiroException).when(mockSubject).login(any(GeodeAuthenticationToken.class));
+    when(shiroException.getCause()).thenReturn(new RuntimeException("other 
reasons"));
+    assertThatThrownBy(() -> securityService.login(properties))
+        .isInstanceOf(AuthenticationFailedException.class)
+        .hasCauseInstanceOf(RuntimeException.class)
+        .hasMessageContaining("Authentication error. Please check your 
credentials.");
+  }
 }
diff --git 
a/geode-core/src/upgradeTest/java/org/apache/geode/security/AuthExpirationMultiServerDUnitTest.java
 
b/geode-core/src/upgradeTest/java/org/apache/geode/security/AuthExpirationMultiServerDUnitTest.java
index 7397c03..f010f8f 100644
--- 
a/geode-core/src/upgradeTest/java/org/apache/geode/security/AuthExpirationMultiServerDUnitTest.java
+++ 
b/geode-core/src/upgradeTest/java/org/apache/geode/security/AuthExpirationMultiServerDUnitTest.java
@@ -39,6 +39,8 @@ import org.apache.geode.cache.client.ClientRegionShortcut;
 import org.apache.geode.cache.client.ServerOperationException;
 import org.apache.geode.internal.cache.InternalCache;
 import org.apache.geode.security.AuthExpirationDUnitTest.EventsCqListner;
+import org.apache.geode.test.concurrent.FileBasedCountDownLatch;
+import org.apache.geode.test.dunit.AsyncInvocation;
 import org.apache.geode.test.dunit.rules.ClientVM;
 import org.apache.geode.test.dunit.rules.ClusterStartupRule;
 import org.apache.geode.test.dunit.rules.MemberVM;
@@ -253,6 +255,60 @@ public class AuthExpirationMultiServerDUnitTest {
         .containsExactly("DATA:READ:partitionRegion:key0");
   }
 
+  @Test
+  public void consecutivePut() throws Exception {
+    FileBasedCountDownLatch latch = new FileBasedCountDownLatch(1);
+    int locatorPort = locator.getPort();
+    // do consecutive puts using a client
+    ClientVM client = cluster.startClientVM(3,
+        c -> c.withProperty(SECURITY_CLIENT_AUTH_INIT, 
UpdatableUserAuthInitialize.class.getName())
+            .withCacheSetup(ccf -> ccf.setPoolMaxConnections(2))
+            .withLocatorConnection(locatorPort));
+    AsyncInvocation<Void> invokePut = client.invokeAsync(() -> {
+      UpdatableUserAuthInitialize.setUser("user1");
+      Region<Object, Object> proxyRegion =
+          ClusterStartupRule.getClientCache()
+              .createClientRegionFactory(ClientRegionShortcut.CACHING_PROXY)
+              .create(PARTITION_REGION);
+      for (int i = 0; i < 1000; i++) {
+        // make sure at least some data is put by user2
+        if (i == 900) {
+          latch.await();
+        }
+        proxyRegion.put("key" + i, "value" + i);
+      }
+    });
+
+    client.invoke(() -> {
+      // wait till at least one of the data is in the region to expire the user
+      await().untilAsserted(() -> assertThat(getProxyRegion()).isNotNull());
+      await().untilAsserted(() -> 
assertThat(getProxyRegion().size()).isGreaterThan(1));
+      UpdatableUserAuthInitialize.setUser("user2");
+    });
+    expireUserOnAllVms("user1");
+    latch.countDown();
+    invokePut.await();
+
+    ExpirableSecurityManager securityManager = 
collectSecurityManagers(server1, server2);
+    Map<String, List<String>> authorizedOps = 
securityManager.getAuthorizedOps();
+
+    assertThat(authorizedOps).hasSize(2);
+    assertThat(authorizedOps.get("user1").size() + 
authorizedOps.get("user2").size())
+        .as(String.format("Combined sizes of user1 %s and user2 %s",
+            authorizedOps.get("user1").size(),
+            authorizedOps.get("user2").size()))
+        .isEqualTo(1000);
+    Map<String, List<String>> unAuthorizedOps = 
securityManager.getUnAuthorizedOps();
+    assertThat(unAuthorizedOps).hasSize(1);
+    // user1 may not be unauthorized for just 1 operations, puts maybe done by 
different
+    // connections
+    assertThat(unAuthorizedOps.get("user1")).isNotEmpty();
+  }
+
+  private static Region<Object, Object> getProxyRegion() {
+    return ClusterStartupRule.getClientCache().getRegion(PARTITION_REGION);
+  }
+
   private void expireUserOnAllVms(String user) {
     MemberVM.invokeInEveryMember(() -> {
       getSecurityManager().addExpiredUser(user);
diff --git 
a/geode-junit/src/main/java/org/apache/geode/security/ExpirableSecurityManager.java
 
b/geode-junit/src/main/java/org/apache/geode/security/ExpirableSecurityManager.java
index 9ad5069..cf98dd8 100644
--- 
a/geode-junit/src/main/java/org/apache/geode/security/ExpirableSecurityManager.java
+++ 
b/geode-junit/src/main/java/org/apache/geode/security/ExpirableSecurityManager.java
@@ -46,7 +46,7 @@ public class ExpirableSecurityManager extends 
SimpleSecurityManager implements S
   public Object authenticate(final Properties credentials) throws 
AuthenticationFailedException {
     Object user = super.authenticate(credentials);
     if (expired_users.contains((String) user)) {
-      throw new AuthenticationFailedException("User already expired.");
+      throw new AuthenticationExpiredException("User already expired.");
     }
     return user;
   }
diff --git 
a/geode-old-client-support/src/main/java/com/gemstone/gemfire/OldClientSupportProvider.java
 
b/geode-old-client-support/src/main/java/com/gemstone/gemfire/OldClientSupportProvider.java
index f8fb848..4730119 100644
--- 
a/geode-old-client-support/src/main/java/com/gemstone/gemfire/OldClientSupportProvider.java
+++ 
b/geode-old-client-support/src/main/java/com/gemstone/gemfire/OldClientSupportProvider.java
@@ -127,11 +127,13 @@ public class OldClientSupportProvider implements 
OldClientSupportService {
       return theThrowable;
     }
 
-    String className = theThrowable.getClass().getName();
-
     // backward compatibility for authentication expiration
     if 
(clientVersion.isOlderThan(ClientReAuthenticateMessage.RE_AUTHENTICATION_START_VERSION))
 {
-      if (className.equals(AuthenticationExpiredException.class.getName())) {
+      if (theThrowable instanceof AuthenticationExpiredException) {
+        return new AuthenticationRequiredException(USER_NOT_FOUND);
+      }
+      Throwable cause = theThrowable.getCause();
+      if (cause instanceof AuthenticationExpiredException) {
         return new AuthenticationRequiredException(USER_NOT_FOUND);
       }
     }
@@ -140,8 +142,7 @@ public class OldClientSupportProvider implements 
OldClientSupportService {
       return theThrowable;
     }
 
-
-
+    String className = theThrowable.getClass().getName();
     // this class has been renamed, so it cannot be automatically translated
     // during java deserialization
     if 
(className.equals("org.apache.geode.cache.execute.EmptyRegionFunctionException"))
 {
diff --git 
a/geode-web-management/src/integrationTest/java/org/apache/geode/management/internal/rest/ClusterManagementSecurityRestIntegrationTest.java
 
b/geode-web-management/src/integrationTest/java/org/apache/geode/management/internal/rest/ClusterManagementSecurityRestIntegrationTest.java
index 29e5892..e2bd836 100644
--- 
a/geode-web-management/src/integrationTest/java/org/apache/geode/management/internal/rest/ClusterManagementSecurityRestIntegrationTest.java
+++ 
b/geode-web-management/src/integrationTest/java/org/apache/geode/management/internal/rest/ClusterManagementSecurityRestIntegrationTest.java
@@ -159,7 +159,7 @@ public class ClusterManagementSecurityRestIntegrationTest {
         .andExpect(status().isUnauthorized())
         .andExpect(jsonPath("$.statusCode", is("UNAUTHENTICATED")))
         .andExpect(jsonPath("$.statusMessage",
-            is("Authentication error. Please check your credentials.")));
+            is("Invalid username/password.")));
   }
 
   @Test

Reply via email to