GUACAMOLE-1289: Refactor Duo and authentication-resumption changes to instead leverage support for updating/replacing credentials prior to auth.
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationProvider.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationProvider.java
index dc9999c..ff3555c 100644
--- a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationProvider.java
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationProvider.java
@@ -21,9 +21,11 @@
 
 import com.google.inject.Guice;
 import com.google.inject.Injector;
+import javax.servlet.http.HttpServletRequest;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.net.auth.AbstractAuthenticationProvider;
 import org.apache.guacamole.net.auth.AuthenticatedUser;
+import org.apache.guacamole.net.auth.Credentials;
 import org.apache.guacamole.net.auth.UserContext;
 
 /**
@@ -41,10 +43,16 @@
     public static String PROVIDER_IDENTIFER = "duo";
 
     /**
-     * Injector which will manage the object graph of this authentication
-     * provider.
+     * Service for verifying the identity of users that Guacamole has otherwise
+     * already authenticated.
      */
-    private final Injector injector;
+    private final UserVerificationService verificationService;
+
+    /**
+     * Session manager for storing/retrieving the state of a user's
+     * authentication attempt while they are redirected to the Duo service.
+     */
+    private final DuoAuthenticationSessionManager sessionManager;
 
     /**
      * Creates a new DuoAuthenticationProvider that verifies users
@@ -57,10 +65,13 @@
     public DuoAuthenticationProvider() throws GuacamoleException {
 
         // Set up Guice injector.
-        injector = Guice.createInjector(
+        Injector injector = Guice.createInjector(
             new DuoAuthenticationProviderModule(this)
         );
 
+        sessionManager = injector.getInstance(DuoAuthenticationSessionManager.class);
+        verificationService = injector.getInstance(UserVerificationService.class);
+
     }
 
     @Override
@@ -69,11 +80,33 @@
     }
 
     @Override
-    public UserContext getUserContext(AuthenticatedUser authenticatedUser)
+    public Credentials updateCredentials(Credentials credentials)
             throws GuacamoleException {
 
-        UserVerificationService verificationService =
-                injector.getInstance(UserVerificationService.class);
+        // Ignore requests with no corresponding authentication session ID, as
+        // there are no credentials to reconstitute if the user has not yet
+        // attempted to authenticate
+        HttpServletRequest request = credentials.getRequest();
+        String duoState = request.getParameter(UserVerificationService.DUO_STATE_PARAMETER_NAME);
+        if (duoState == null)
+            return credentials;
+
+        // Ignore requests with invalid/expired authentication session IDs
+        DuoAuthenticationSession session = sessionManager.resume(duoState);
+        if (session == null)
+            return credentials;
+
+        // Reconstitute the originally-provided credentials from the users
+        // authentication attempt prior to being redirected to Duo
+        Credentials previousCredentials = session.getCredentials();
+        previousCredentials.setRequest(request);
+        return previousCredentials;
+
+    }
+
+    @Override
+    public UserContext getUserContext(AuthenticatedUser authenticatedUser)
+            throws GuacamoleException {
 
         // Verify user against Duo service
         verificationService.verifyAuthenticatedUser(authenticatedUser);
@@ -84,4 +117,9 @@
 
     }
 
+    @Override
+    public void shutdown() {
+        sessionManager.shutdown();
+    }
+
 }
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationSession.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationSession.java
new file mode 100644
index 0000000..6876cad
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationSession.java
@@ -0,0 +1,65 @@
+/*
+ * 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.guacamole.auth.duo;
+
+import org.apache.guacamole.net.auth.AuthenticationSession;
+import org.apache.guacamole.net.auth.Credentials;
+
+/**
+ * Representation of an in-progress Duo authentication attempt.
+ */
+public class DuoAuthenticationSession extends AuthenticationSession {
+
+    /**
+     * The credentials that the user originally provided to Guacamole prior to
+     * being redirected to the Duo service.
+     */
+    private final Credentials credentials;
+
+    /**
+     * Creates a new AuthenticationSession representing an in-progress Duo
+     * authentication attempt.
+     *
+     * @param credentials
+     *     The credentials that the user originally provided to Guacamole prior
+     *     to being redirected to the Duo service.
+     *
+     * @param expires
+     *     The number of milliseconds that may elapse before this session must
+     *     be considered invalid.
+     */
+    public DuoAuthenticationSession(Credentials credentials, long expires) {
+        super(expires);
+        this.credentials = credentials;
+    }
+
+    /**
+     * Returns the credentials that the user originally provided to Guacamole
+     * prior to being redirected to the Duo service.
+     *
+     * @return
+     *     The credentials that the user originally provided to Guacamole prior
+     *     to being redirected to the Duo service.
+     */
+    public Credentials getCredentials() {
+        return credentials;
+    }
+
+}
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationSessionManager.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationSessionManager.java
new file mode 100644
index 0000000..f2f39da
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationSessionManager.java
@@ -0,0 +1,36 @@
+/*
+ * 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.guacamole.auth.duo;
+
+import com.google.inject.Singleton;
+import org.apache.guacamole.net.auth.AuthenticationSessionManager;
+
+/**
+ * Manager service that temporarily stores authentication attempts while
+ * the Duo authentication flow is underway.
+ */
+@Singleton
+public class DuoAuthenticationSessionManager
+        extends AuthenticationSessionManager<DuoAuthenticationSession> {
+
+    // Intentionally empty (the default functions inherited from the
+    // AuthenticationSessionManager base class are sufficient for our needs)
+
+}
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/UserVerificationService.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/UserVerificationService.java
index 26ab712..6f36371 100644
--- a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/UserVerificationService.java
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/UserVerificationService.java
@@ -37,35 +37,45 @@
 import org.apache.guacamole.net.auth.AuthenticatedUser;
 import org.apache.guacamole.net.auth.Credentials;
 import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.web.util.UriComponentsBuilder;
 
 /**
  * Service for verifying the identity of a user against Duo.
  */
 public class UserVerificationService {
 
-    private static final Logger logger = LoggerFactory.getLogger(UserVerificationService.class);
-    
     /**
-     * The name of the parameter which Duo will return in it's GET call-back
-     * that contains the code that the client will use to generate a token.
+     * The name of the HTTP parameter that Duo will use to communicate the
+     * result of the user's attempt to authenticate with their service. This
+     * parameter is provided in the GET request received when Duo redirects the
+     * user back to Guacamole.
      */
     public static final String DUO_CODE_PARAMETER_NAME = "duo_code";
-    
+
     /**
-     * The name of the parameter that will be used in the GET call-back that
-     * contains the session state.
+     * The name of the HTTP parameter that we will be using to hold the opaque
+     * authentication session ID. This session ID is transmitted to Duo during
+     * the initial redirect and received back from Duo via this parameter in
+     * the GET request received when Duo redirects the user back to Guacamole.
+     * The session ID is ultimately used to reconstitute the original
+     * credentials received from the user by Guacamole such that parameter
+     * tokens like GUAC_USERNAME and GUAC_PASSWORD can continue to work as
+     * expected.
      */
     public static final String DUO_STATE_PARAMETER_NAME = "state";
-    
+
     /**
      * The value that will be returned in the token if Duo authentication
      * was successful.
      */
     private static final String DUO_TOKEN_SUCCESS_VALUE = "allow";
-    
+
+    /**
+     * Session manager for storing/retrieving the state of a user's
+     * authentication attempt while they are redirected to the Duo service.
+     */
+    @Inject
+    private DuoAuthenticationSessionManager sessionManager;
+
     /**
      * Service for retrieving Duo configuration information.
      */
@@ -90,79 +100,115 @@
     public void verifyAuthenticatedUser(AuthenticatedUser authenticatedUser)
             throws GuacamoleException {
 
-        // Pull the original HTTP request used to authenticate
-        Credentials credentials = authenticatedUser.getCredentials();
-        HttpServletRequest request = credentials.getRequest();
-
-        // Ignore anonymous users
-        if (authenticatedUser.getIdentifier().equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER))
-            return;
-        
+        // Ignore anonymous users (unverifiable)
         String username = authenticatedUser.getIdentifier();
+        if (username.equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER))
+            return;
 
+        // Obtain a Duo client for redirecting the user to the Duo service and
+        // verifying any received authentication code
+        Client duoClient;
         try {
-
-            String redirectUrl = confService.getRedirectUri().toString();
-
-            String builtUrl = UriComponentsBuilder
-                    .fromUriString(redirectUrl)
-                    .queryParam(Credentials.RESUME_QUERY, DuoAuthenticationProvider.PROVIDER_IDENTIFER)
-                    .build()
-                    .toUriString();
-
-            // Set up the Duo Client
-            Client duoClient = new Client.Builder(
+            duoClient = new Client.Builder(
                     confService.getClientId(),
                     confService.getClientSecret(),
                     confService.getAPIHostname(),
-                    builtUrl)
+                    confService.getRedirectUri().toString())
                     .build();
+        }
+        catch (DuoException e) {
+            throw new GuacamoleServerException("Client for communicating with "
+                    + "the Duo authentication service could not be created.", e);
+        }
 
+        // Verify that the Duo service is healthy and available
+        try {
             duoClient.healthCheck();
+        }
+        catch (DuoException e) {
+            throw new GuacamoleServerException("Duo authentication service is "
+                    + "not currently available (failed health check).", e);
+        }
 
-            // Retrieve signed Duo Code and State from the request
-            String duoCode = request.getParameter(DUO_CODE_PARAMETER_NAME);
-            String duoState = request.getParameter(DUO_STATE_PARAMETER_NAME);
+        // Pull the original HTTP request used to authenticate, as well as any
+        // associated credentials
+        Credentials credentials = authenticatedUser.getCredentials();
+        HttpServletRequest request = credentials.getRequest();
 
-            // If no code or state is received, assume Duo MFA redirect has not occured and do it
-            if (duoCode == null || duoState == null) {
+        // Retrieve signed Duo authentication code and session state from the
+        // request (these will be absent if this is an initial authentication
+        // attempt and not a redirect back from Duo)
+        String duoCode = request.getParameter(DUO_CODE_PARAMETER_NAME);
+        String duoState = request.getParameter(DUO_STATE_PARAMETER_NAME);
 
-                // Get a new session state from the Duo client
-                duoState = duoClient.generateState();
-                long expirationTimestamp = System.currentTimeMillis() + (confService.getAuthTimeout() * 1000L);
+        // Redirect to Duo to obtain an authentication code if that redirect
+        // has not yet occurred
+        if (duoCode == null || duoState == null) {
 
-                // Request additional credentials
-                throw new TranslatableGuacamoleInsufficientCredentialsException(
-                    "Verification using Duo is required before authentication "
-                    + "can continue.", "LOGIN.INFO_DUO_AUTH_REQUIRED",
-                    new CredentialsInfo(Collections.singletonList(
-                        new RedirectField(
-                                DUO_CODE_PARAMETER_NAME,
-                                new URI(duoClient.createAuthUrl(username, duoState)),
-                                new TranslatableMessage("LOGIN.INFO_DUO_REDIRECT_PENDING")
-                        )
-                    )),
-                    duoState, DuoAuthenticationProvider.PROVIDER_IDENTIFER, 
-                    DUO_STATE_PARAMETER_NAME, expirationTimestamp
-                );
+            // Store received credentials for later retrieval leveraging Duo's
+            // opaque session state identifier (we need to maintain these
+            // credentials so that things like the GUAC_USERNAME and
+            // GUAC_PASSWORD tokens continue to work as expected despite the
+            // redirect to/from the external Duo service)
+            duoState = duoClient.generateState();
+            long expirationTimestamp = System.currentTimeMillis() + (confService.getAuthTimeout() * 1000L);
+            sessionManager.defer(new DuoAuthenticationSession(credentials, expirationTimestamp), duoState);
 
+            // Obtain authentication URL from Duo client
+            String duoAuthUrlString;
+            try {
+                duoAuthUrlString = duoClient.createAuthUrl(username, duoState);
+            }
+            catch (DuoException e) {
+                throw new GuacamoleServerException("Duo client failed to "
+                        + "generate the authentication URL necessary to "
+                        + "redirect the authenticating user to the Duo "
+                        + "service.", e);
             }
 
-            // Get the token from the DuoClient using the code and username, and check status
+            // Parse and validate URL obtained from Duo client
+            URI duoAuthUrl;
+            try {
+                duoAuthUrl = new URI(duoAuthUrlString);
+            }
+            catch (URISyntaxException e) {
+                throw new GuacamoleServerException("Authentication URL "
+                        + "generated by the Duo client is not actually a "
+                        + "valid URL and cannot be used to redirect the "
+                        + "authenticating user to the Duo service.", e);
+            }
+
+            // Request that user be redirected to the Duo service to obtain
+            // a Duo authentication code
+            throw new TranslatableGuacamoleInsufficientCredentialsException(
+                "Verification using Duo is required before authentication "
+                + "can continue.", "LOGIN.INFO_DUO_AUTH_REQUIRED",
+                new CredentialsInfo(Collections.singletonList(
+                    new RedirectField(
+                            DUO_CODE_PARAMETER_NAME, duoAuthUrl,
+                            new TranslatableMessage("LOGIN.INFO_DUO_REDIRECT_PENDING")
+                    )
+                ))
+            );
+
+        }
+
+        // Validate that the user has successfully verified their identify with
+        // the Duo service
+        try {
             Token token = duoClient.exchangeAuthorizationCodeFor2FAResult(duoCode, username);
-            if (token == null 
-                    || token.getAuth_result() == null 
+            if (token == null || token.getAuth_result() == null
                     || !DUO_TOKEN_SUCCESS_VALUE.equals(token.getAuth_result().getStatus()))
                 throw new TranslatableGuacamoleClientException("Provided Duo "
                         + "validation code is incorrect.",
                         "LOGIN.INFO_DUO_VALIDATION_CODE_INCORRECT");
         }
         catch (DuoException e) {
-            throw new GuacamoleServerException("Duo Client error.", e);
+            throw new GuacamoleServerException("Duo client refused to verify "
+                    + "the identity of the authenticating user due to an "
+                    + "underlying error condition.", e);
         }
-        catch (URISyntaxException e) {
-            throw new GuacamoleServerException("Error creating URI from Duo Authentication URL.", e);
-        }
+
     }
 
 }
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/language/TranslatableGuacamoleInsufficientCredentialsException.java b/guacamole-ext/src/main/java/org/apache/guacamole/language/TranslatableGuacamoleInsufficientCredentialsException.java
index f0c27bc..5b51d2c 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/language/TranslatableGuacamoleInsufficientCredentialsException.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/language/TranslatableGuacamoleInsufficientCredentialsException.java
@@ -136,46 +136,6 @@
         this(message, new TranslatableMessage(key), credentialsInfo);
     }
 
-    /**
-     * Creates a new TranslatableGuacamoleInsufficientCredentialsException with the specified message,
-     * translation key, the credential information required for authentication, the state token, and
-     * an expiration timestamp for the state token. The message is provided in both a non-translatable
-     * form and as a translatable key which can be used to retrieve the localized message.
-     *
-     * @param message
-     *     A human-readable description of the exception that occurred. This
-     *     message should be readable on its own and as-written, without
-     *     requiring a translation service.
-     *
-     * @param key
-     *     The arbitrary key which can be used to look up the message to be
-     *     displayed in the user's native language.
-     *
-     * @param credentialsInfo
-     *     Information describing the form of valid credentials.
-     *
-     * @param state
-     *     An opaque value that may be used by a client to maintain state across requests which are part
-     *     of the same authentication transaction.
-     * 
-     * @param providerIdentifier
-     *     The identifier of the authentication provider that this exception pertains to.
-     *
-     * @param queryIdentifier
-     *     The identifier of the specific query parameter within the
-     *     authentication process that this exception pertains to.
-     *
-     * @param expires
-     *     The timestamp after which the state token associated with the authentication process expires,
-     *     specified as the number of milliseconds since the UNIX epoch.
-     */
-    public TranslatableGuacamoleInsufficientCredentialsException(String message,
-            String key, CredentialsInfo credentialsInfo, String state, String providerIdentifier, 
-            String queryIdentifier, long expires) {
-        super(message, credentialsInfo, state, providerIdentifier, queryIdentifier, expires);
-        this.translatableMessage = new TranslatableMessage(key);
-    }
-
     @Override
     public TranslatableMessage getTranslatableMessage() {
         return translatableMessage;
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AbstractAuthenticationProvider.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AbstractAuthenticationProvider.java
index 81a91d9..6b6fd8f 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AbstractAuthenticationProvider.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AbstractAuthenticationProvider.java
@@ -45,6 +45,20 @@
     /**
      * {@inheritDoc}
      *
+     * <p>This implementation simply returns the provided {@code credentials}
+     * without performing any updates. Implementations that wish to perform
+     * credential updates for in-progress authentication requests should
+     * override this function.
+     */
+    @Override
+    public Credentials updateCredentials(Credentials credentials)
+            throws GuacamoleException {
+        return credentials;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
      * <p>This implementation performs no authentication whatsoever, ignoring
      * the provided {@code credentials} and simply returning {@code null}. Any
      * authentication attempt will thus fall through to other
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AuthenticationProvider.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AuthenticationProvider.java
index fd7d844..680c7c3 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AuthenticationProvider.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AuthenticationProvider.java
@@ -63,6 +63,33 @@
     Object getResource() throws GuacamoleException;
 
     /**
+     * Given the set of credentials that a user has submitted during
+     * authentication but has not yet been provided to the
+     * {@link #authenticateUser(org.apache.guacamole.net.auth.Credentials)} or
+     * {@link #updateAuthenticatedUser(org.apache.guacamole.net.auth.AuthenticatedUser, org.apache.guacamole.net.auth.Credentials)}
+     * functions of installed AuthenticationProviders, returns the set of
+     * credentials that should be used instead. The returned credentials may
+     * be the original credentials, with or without modifications, or may be an
+     * entirely new {@link Credentials} object.
+     *
+     * @param credentials
+     *     The credentials provided by a user during authentication.
+     *
+     * @return
+     *     The set of credentials that should be provided to all
+     *     AuthenticationProviders, including this AuthenticationProvider. This
+     *     set of credentials may optionally be entirely new or may have been
+     *     modified.
+     *
+     * @throws GuacamoleException
+     *     If an error occurs while updating the provided credentials.
+     */
+    default Credentials updateCredentials(Credentials credentials)
+            throws GuacamoleException {
+        return credentials;
+    }
+
+    /**
      * Returns an AuthenticatedUser representing the user authenticated by the
      * given credentials, if any.
      *
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Credentials.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Credentials.java
index 45eebe8..6ad0e24 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Credentials.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Credentials.java
@@ -35,16 +35,6 @@
 public class Credentials implements Serializable {
 
     /**
-     * The RESUME_QUERY is a query parameter key used to determine which
-     * authentication provider's process should be resumed during multi-step
-     * authentication. The auth provider will set this parameter before 
-     * redirecting to an external service, and it is checked upon return to 
-     * Guacamole to ensure the correct authentication state is continued 
-     * without starting over.
-     */
-    public static final String RESUME_QUERY = "provider_id";
-
-    /**
      * Unique identifier associated with this specific version of Credentials.
      */
     private static final long serialVersionUID = 1L;
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/credentials/GuacamoleInsufficientCredentialsException.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/credentials/GuacamoleInsufficientCredentialsException.java
index 8c76694..06ae3ea 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/credentials/GuacamoleInsufficientCredentialsException.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/credentials/GuacamoleInsufficientCredentialsException.java
@@ -29,95 +29,6 @@
 public class GuacamoleInsufficientCredentialsException extends GuacamoleCredentialsException {
 
     /**
-     * The default state token to use when no specific state information is provided.
-     */
-    private static final String DEFAULT_STATE = "";
-
-    /**
-     * The default provider identifier to use when no specific provider is identified.
-     * This serves as a placeholder indicating that either no specific provider is
-     * responsible for the exception or the responsible provider has not been identified.
-     */
-    private static final String DEFAULT_PROVIDER_IDENTIFIER = "";
-
-    /**
-     * The default query identifier to use when no specific query is identified.
-     * This serves as a placeholder and indicates that the specific query related to
-     * the provider's state resume operation has not been provided.
-     */
-    private static final String DEFAULT_QUERY_IDENTIFIER = "";
-
-    /**
-     * The default expiration timestamp to use when no specific expiration is provided,
-     * effectively indicating that the state token does not expire.
-     */
-    private static final long DEFAULT_EXPIRES = -1L;
-
-    /**
-     * An opaque value that may be used by a client to maintain state across requests
-     * which are part of the same authentication transaction.
-     */
-    protected final String state;
-
-    /**
-     * The identifier for the authentication provider that threw this exception.
-     * This is used to link the exception back to the originating source of the
-     * authentication attempt, allowing clients to determine which provider's
-     * authentication process should be resumed.
-     */
-    protected final String providerIdentifier;
-
-    /**
-     * An identifier for the specific query within the URL for this provider that can
-     * be checked to resume the authentication state.
-     */
-    protected final String queryIdentifier;
-
-    /**
-     * The timestamp after which the state token associated with the authentication process
-     * should no longer be considered valid, expressed as the number of milliseconds since
-     * UNIX epoch.
-     */
-    protected final long expires;
-
-    /**
-     * Creates a new GuacamoleInsufficientCredentialsException with the specified
-     * message, the credential information required for authentication, the state
-     * token associated with the authentication process, and an expiration timestamp.
-     *
-     * @param message
-     *     A human-readable description of the exception that occurred.
-     *
-     * @param credentialsInfo
-     *     Information describing the form of valid credentials.
-     *
-     * @param state
-     *     An opaque value that may be used by a client to maintain state
-     *     across requests which are part of the same authentication transaction.
-     *
-     * @param providerIdentifier
-     *     The identifier of the authentication provider that this exception pertains to.
-     *
-     * @param queryIdentifier
-     *     The identifier of the specific query parameter within the
-     *     authentication process that this exception pertains to.
-     * 
-     * @param expires
-     *     The timestamp after which the state token associated with the
-     *     authentication process should no longer be considered valid, expressed
-     *     as the number of milliseconds since UNIX epoch.
-     */
-    public GuacamoleInsufficientCredentialsException(String message,
-            CredentialsInfo credentialsInfo, String state, 
-            String providerIdentifier, String queryIdentifier, long expires) {
-        super(message, credentialsInfo);
-        this.state = state;
-        this.providerIdentifier = providerIdentifier;
-        this.queryIdentifier = queryIdentifier;
-        this.expires = expires;
-    }
-
-    /**
      * Creates a new GuacamoleInsufficientCredentialsException with the given
      * message, cause, and associated credential information.
      *
@@ -133,10 +44,6 @@
     public GuacamoleInsufficientCredentialsException(String message, Throwable cause,
             CredentialsInfo credentialsInfo) {
         super(message, cause, credentialsInfo);
-        this.state = DEFAULT_STATE;
-        this.providerIdentifier = DEFAULT_PROVIDER_IDENTIFIER;
-        this.queryIdentifier = DEFAULT_QUERY_IDENTIFIER;
-        this.expires = DEFAULT_EXPIRES;
     }
 
     /**
@@ -151,10 +58,6 @@
      */
     public GuacamoleInsufficientCredentialsException(String message, CredentialsInfo credentialsInfo) {
         super(message, credentialsInfo);
-        this.state = DEFAULT_STATE;
-        this.providerIdentifier = DEFAULT_PROVIDER_IDENTIFIER;
-        this.queryIdentifier = DEFAULT_QUERY_IDENTIFIER;
-        this.expires = DEFAULT_EXPIRES;
     }
 
     /**
@@ -169,52 +72,6 @@
      */
     public GuacamoleInsufficientCredentialsException(Throwable cause, CredentialsInfo credentialsInfo) {
         super(cause, credentialsInfo);
-        this.state = DEFAULT_STATE;
-        this.providerIdentifier = DEFAULT_PROVIDER_IDENTIFIER;
-        this.queryIdentifier = DEFAULT_QUERY_IDENTIFIER;
-        this.expires = DEFAULT_EXPIRES;
-    }
-
-    /**
-     * Retrieves the state token associated with the authentication process.
-     *
-     * @return The opaque state token used to maintain consistency across multiple
-     *         requests in the same authentication transaction.
-     */
-    public String getState() {
-        return state;
-    }
-
-    /**
-     * Retrieves the identifier of the authentication provider responsible for this exception.
-     *
-     * @return The identifier of the authentication provider, allowing clients to know
-     *         which provider's process should be resumed in response to this exception.
-     */
-    public String getProviderIdentifier() {
-        return providerIdentifier;
-    }
-
-    /**
-     * Retrieves the specific query identifier associated with the URL for the provider
-     * that can be checked to resume the authentication state.
-     *
-     * @return The query identifier that serves as a reference to a specific point or
-     *         transaction within the provider's authentication process.
-     */
-    public String getQueryIdentifier() {
-        return queryIdentifier;
-    }
-
-    /**
-     * Retrieves the expiration timestamp of the state token, specified as the
-     * number of milliseconds since the UNIX epoch.
-     *
-     * @return The expiration timestamp of the state token, or a negative value if
-     *         the token does not expire.
-     */
-    public long getExpires() {
-        return expires;
     }
 
 }
diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java b/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java
index 9855cd6..b89b2ad 100644
--- a/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java
+++ b/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java
@@ -119,6 +119,18 @@
 
     }
 
+    @Override
+    public Credentials updateCredentials(Credentials credentials) throws GuacamoleException {
+
+        // Do nothing if underlying auth provider could not be loaded
+        if (authProvider == null)
+            return credentials;
+
+        // Delegate to underlying auth provider
+        return authProvider.updateCredentials(credentials);
+
+    }
+
     /**
      * Returns whether this authentication provider should tolerate internal
      * failures during the authentication process, allowing other
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java
index dc8d3bb..7bb1e6f 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java
@@ -21,11 +21,8 @@
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
 
 import javax.inject.Inject;
-import javax.servlet.http.HttpServletRequest;
 
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleSecurityException;
@@ -47,7 +44,6 @@
 import org.slf4j.LoggerFactory;
 
 import com.google.inject.Singleton;
-import java.util.Iterator;
 
 /**
  * A service for performing authentication checks in REST endpoints.
@@ -103,11 +99,6 @@
      */
     public static final String TOKEN_PARAMETER_NAME = "token";
 
-    /** 
-     * Map to store resumable authentication states with an expiration time.
-     */ 
-    private Map<String, ResumableAuthenticationState> resumableStateMap = new ConcurrentHashMap<>();
-
     /**
      * Attempts authentication against all AuthenticationProviders, in order,
      * using the provided credentials. The first authentication failure takes
@@ -322,20 +313,6 @@
                 try {
                     userContext = authProvider.getUserContext(authenticatedUser);
                 }
-                catch (GuacamoleInsufficientCredentialsException e) {
-                    // Store state and expiration
-                    String state = e.getState();
-                    long expiration = e.getExpires();
-                    String queryIdentifier = e.getQueryIdentifier();
-                    String providerIdentifier = e.getProviderIdentifier();
-
-                    resumableStateMap.put(state, new ResumableAuthenticationState(providerIdentifier, 
-                            queryIdentifier, expiration, credentials));
-
-                    throw new GuacamoleAuthenticationProcessException("User "
-                            + "authentication aborted during initial "
-                            + "UserContext creation.", authProvider, e);
-                }
                 catch (GuacamoleException | RuntimeException | Error e) {
                     throw new GuacamoleAuthenticationProcessException("User "
                             + "authentication aborted during initial "
@@ -354,81 +331,42 @@
         return userContexts;
 
     }
-    
+
     /**
-     * Resumes authentication using given credentials if a matching resumable 
-     * state is found.
+     * Performs arbitrary and optional updates to the credentials supplied by
+     * the authenticating user as dictated by the {@link AuthenticationProvider#updateCredentials(org.apache.guacamole.net.auth.Credentials)}
+     * functions of any installed AuthenticationProvider. Each installed
+     * AuthenticationProvider is given the opportunity, in order, to make
+     * updates to the supplied credentials.
      *
-     * @param credentials 
-     *     The initial credentials containing the request object.
+     * @param credentials
+     *     The credentials to be updated.
      *
-     * @return 
-     *     Resumed credentials if a valid resumable state is found; otherwise, 
-     *     returns null.
+     * @return
+     *     The set of credentials that should be provided to all
+     *     AuthenticationProviders during authentication, now possibly updated
+     *     (or even replaced) by any number of installed
+     *     AuthenticationProviders.
+     *
+     * @throws GuacamoleAuthenticationProcessException 
+     *     If an error occurs while updating the supplied credentials.
      */
-    private Credentials resumeAuthentication(Credentials credentials) {
-        
-        Credentials resumedCredentials = null;
+    private Credentials getUpdatedCredentials(Credentials credentials)
+            throws GuacamoleAuthenticationProcessException {
 
-        // Retrieve signed State from the request
-        HttpServletRequest request = credentials.getRequest();
-        
-        // Retrieve the provider id from the query parameters
-        String resumableProviderId = request.getParameter(Credentials.RESUME_QUERY);
-        // Check if a provider id is set
-        if (resumableProviderId == null || resumableProviderId.isEmpty()) {
-            // Return if a provider id is not set
-            return null;
-        }
-
-        // Use an iterator to safely remove entries while iterating
-        Iterator<Map.Entry<String, ResumableAuthenticationState>> iterator = resumableStateMap.entrySet().iterator();
-        while (iterator.hasNext()) {
-            Map.Entry<String, ResumableAuthenticationState> entry = iterator.next();
-            ResumableAuthenticationState resumableState = entry.getValue();
-
-            // Check if the provider ID from the request matches the one in the map entry
-            boolean providerMatches = resumableProviderId.equals(resumableState.getProviderIdentifier());
-            if (!providerMatches) {
-                // If the provider doesn't match, skip to the next entry
-                continue;
+        for (AuthenticationProvider authProvider : authProviders) {
+            try {
+                credentials = authProvider.updateCredentials(credentials);
             }
-
-            // Use the query identifier from the entry to retrieve the corresponding state parameter
-            String stateQueryParameter = resumableState.getQueryIdentifier();
-            String stateFromParameter = request.getParameter(stateQueryParameter);
-
-            // Check if a state parameter is set
-            if (stateFromParameter == null || stateFromParameter.isEmpty()) {
-                // Remove and continue if`state is not provided or is empty
-                iterator.remove(); 
-                continue;
-            }
-
-            // If the key in the entry (state) matches the state parameter provided in the request
-            if (entry.getKey().equals(stateFromParameter)) {
-
-                // Remove the current entry from the map
-                iterator.remove();                
-
-                // Check if the resumableState has expired
-                if (!resumableState.isExpired()) {
-
-                    // Set the actualCredentials to the credentials from the matched entry
-                    resumedCredentials = resumableState.getCredentials();
-
-                    if (resumedCredentials != null) {
-                        resumedCredentials.setRequest(request);
-                    }
-
-                }
-
-                // Exit the loop since we've found the matching state and it's unique
-                break;
+            catch (GuacamoleException | RuntimeException | Error e) {
+                throw new GuacamoleAuthenticationProcessException("User "
+                        + "authentication aborted during credential "
+                        + "update/revision.", authProvider, e);
             }
         }
 
-        return resumedCredentials;
+        return credentials;
+
     }
 
     /**
@@ -469,16 +407,15 @@
         AuthenticatedUser authenticatedUser;
         String authToken;
 
-        // Retrieve credentials if resuming authentication
-        Credentials actualCredentials = resumeAuthentication(credentials);
-        if (actualCredentials == null)
-            actualCredentials = credentials;
-
         try {
 
+            // Allow extensions to make updated to credentials prior to
+            // actual authentication
+            Credentials updatedCredentials = getUpdatedCredentials(credentials);
+
             // Get up-to-date AuthenticatedUser and associated UserContexts
-            authenticatedUser = getAuthenticatedUser(existingSession, actualCredentials);
-            List<DecoratedUserContext> userContexts = getUserContexts(existingSession, authenticatedUser, actualCredentials);
+            authenticatedUser = getAuthenticatedUser(existingSession, updatedCredentials);
+            List<DecoratedUserContext> userContexts = getUserContexts(existingSession, authenticatedUser, updatedCredentials);
 
             // Update existing session, if it exists
             if (existingSession != null) {
@@ -508,7 +445,7 @@
         // Log and rethrow any authentication errors
         catch (GuacamoleAuthenticationProcessException e) {
 
-            listenerService.handleEvent(new AuthenticationFailureEvent(actualCredentials,
+            listenerService.handleEvent(new AuthenticationFailureEvent(credentials,
                     e.getAuthenticationProvider(), e.getCause()));
 
             // Rethrow exception
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/ResumableAuthenticationState.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/ResumableAuthenticationState.java
deleted file mode 100644
index 1aaa7ea..0000000
--- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/ResumableAuthenticationState.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * 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.guacamole.rest.auth;
-
-import org.apache.guacamole.net.auth.Credentials;
-
-/**
- * Encapsulates the state information required for resuming an authentication
- * process. This includes an expiration timestamp to determine state validity
- * and the original credentials submitted by the user.
- */
-public class ResumableAuthenticationState {
-    
-    /**
-     * The timestamp at which this state should no longer be considered valid,
-     * measured in milliseconds since the Unix epoch.
-     */
-    private long expirationTimestamp;
-    
-    /**
-     * The original user credentials that were submitted at the start of the
-     * authentication process.
-     */
-    private Credentials credentials;
-
-    /**
-     * A unique string identifying the authentication provider related to the state.
-     * This field allows the client to know which provider's authentication process
-     * should be resumed using this state.
-     */
-    private String providerIdentifier;
-
-    /**
-     * A unique string that can be used to identify a specific query within the
-     * authentication process for the identified provider. This identifier can
-     * help the resumption of an authentication process.
-     */
-    private String queryIdentifier;
-
-    /**
-     * Constructs a new ResumableAuthenticationState object with the specified
-     * expiration timestamp and user credentials.
-     *
-     * @param providerIdentifier
-     *     The identifier of the authentication provider to which this resumable state pertains.
-     *
-     * @param queryIdenifier
-     *     The identifier of the specific query within the provider's
-     *     authentication process that this state corresponds to.
-     *
-     * @param expirationTimestamp
-     *     The timestamp in milliseconds since the Unix epoch when this state
-     *     expires and can no longer be used to resume authentication.
-     *
-     * @param credentials
-     *     The Credentials object initially submitted by the user and associated
-     *     with this resumable state.
-     */
-    public ResumableAuthenticationState(String providerIdentifier, String queryIdentifier, 
-            long expirationTimestamp, Credentials credentials) {
-        this.expirationTimestamp = expirationTimestamp;
-        this.credentials = credentials;
-        this.providerIdentifier = providerIdentifier;
-        this.queryIdentifier = queryIdentifier;
-    }
-
-    /**
-     * Checks if this resumable state has expired based on the stored expiration
-     * timestamp and the current system time.
-     *
-     * @return 
-     *     True if the current system time is after the expiration timestamp,
-     *     indicating that the state is expired; false otherwise.
-     */
-    public boolean isExpired() {
-        return System.currentTimeMillis() >= expirationTimestamp;
-    }
-
-    /**
-     * Retrieves the original credentials associated with this resumable state.
-     *
-     * @return 
-     *     The Credentials object containing user details that were submitted
-     *     when the state was created.
-     */
-    public Credentials getCredentials() {
-        return this.credentials;
-    }
-
-    /**
-     * Retrieves the identifier of the authentication provider associated with this state.
-     *
-     * @return 
-     *     The identifier of the authentication provider, providing context for this state
-     *     within the overall authentication sequence.
-     */
-    public String getProviderIdentifier() {
-        return this.providerIdentifier;
-    }
-
-    /**
-     * Retrieves the identifier for a specific query in the authentication
-     * process that is associated with this state.
-     *
-     * @return 
-     *     The query identifier used for retrieving a value representing the state within
-     *     the provider's authentication process that should be resumed.
-     */
-    public String getQueryIdentifier() {
-        return this.queryIdentifier;
-    }
-}