From ed4c0ab7792606f4c0e573cdb5ce4fc44ad31ea6 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 25 Apr 2024 18:43:00 -0700 Subject: [PATCH] GUACAMOLE-1289: Redirect back to Duo in case of unexpected failures or invalid tokens. --- .../auth/duo/UserVerificationService.java | 143 +++++++++++------- 1 file changed, 88 insertions(+), 55 deletions(-) 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 a30f4d7cd8..1fe6d9979a 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 @@ -31,18 +31,25 @@ import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.auth.duo.conf.ConfigurationService; import org.apache.guacamole.form.RedirectField; -import org.apache.guacamole.language.TranslatableGuacamoleClientException; import org.apache.guacamole.language.TranslatableGuacamoleInsufficientCredentialsException; import org.apache.guacamole.language.TranslatableMessage; import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.Credentials; import org.apache.guacamole.net.auth.credentials.CredentialsInfo; +import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Service for verifying the identity of a user against Duo. */ public class UserVerificationService { + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(UserVerificationService.class); + /** * 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 @@ -143,71 +150,97 @@ public void verifyAuthenticatedUser(AuthenticatedUser authenticatedUser) // Redirect to Duo to obtain an authentication code if that redirect // has not yet occurred - if (duoCode == null || duoState == null) { - - // 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.getAuthenticationTimeout() * 60000L); - 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); - } + if (duoCode != null && duoState != null) { - // Parse and validate URL obtained from Duo client - URI duoAuthUrl; + // Validate that the user has successfully verified their identify with + // the Duo service try { - duoAuthUrl = new URI(duoAuthUrlString); + + // Note unexpected behavior (Duo is expected to always return + // a token) + Token token = duoClient.exchangeAuthorizationCodeFor2FAResult(duoCode, username); + if (token == null) { + logger.warn("Duo did not return an authentication result " + + "at all for the authentication attempt by user " + + "\"{}\". This is unexpected behavior and may be " + + "a bug in the Duo service or the Duo SDK. " + + "Guacamole will attempt to automatically work " + + "around the issue by making a fresh Duo " + + "authentication request.", username); + } + + // Warn if Duo explicitly denies authentication + else if (token.getAuth_result() == null || !DUO_TOKEN_SUCCESS_VALUE.equals(token.getAuth_result().getStatus())) { + logger.warn("Duo did not return an explicitly successful " + + "authentication result for the authentication " + + "attempt by user \"{}\". The user will now be " + + "redirected back to the Duo service to reattempt" + + "authentication.", username); + } + + // Allow user to continue authenticating with Guacamole only if + // Duo has validated their identity + else + return; + } - 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); + catch (DuoException e) { + logger.debug("The Duo client failed internally while " + + "attempting to validate the identity of user " + + "\"{}\". This is commonly caused by stale query " + + "parameters from an older Duo request remaining " + + "present in the Guacamole URL. The user will now be " + + "redirected back to the Duo service to reattempt " + + "authentication.", 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 + // 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.getAuthenticationTimeout() * 60000L); + sessionManager.defer(new DuoAuthenticationSession(credentials, expirationTimestamp), duoState); + + // Obtain authentication URL from Duo client + String duoAuthUrlString; try { - Token token = duoClient.exchangeAuthorizationCodeFor2FAResult(duoCode, username); - 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"); + duoAuthUrlString = duoClient.createAuthUrl(username, duoState); } catch (DuoException e) { - throw new GuacamoleServerException("Duo client refused to verify " - + "the identity of the authenticating user due to an " - + "underlying error condition.", e); + throw new GuacamoleServerException("Duo client failed to " + + "generate the authentication URL necessary to " + + "redirect the authenticating user to the Duo " + + "service.", e); + } + + // 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") + ) + )) + ); }