GUACAMOLE-1289: Redirect back to Duo in case of unexpected failures or invalid tokens.

This commit is contained in:
Michael Jumper
2024-04-25 18:43:00 -07:00
parent 4a0e9f310f
commit ed4c0ab779

View File

@@ -31,18 +31,25 @@ import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.auth.duo.conf.ConfigurationService; import org.apache.guacamole.auth.duo.conf.ConfigurationService;
import org.apache.guacamole.form.RedirectField; import org.apache.guacamole.form.RedirectField;
import org.apache.guacamole.language.TranslatableGuacamoleClientException;
import org.apache.guacamole.language.TranslatableGuacamoleInsufficientCredentialsException; import org.apache.guacamole.language.TranslatableGuacamoleInsufficientCredentialsException;
import org.apache.guacamole.language.TranslatableMessage; import org.apache.guacamole.language.TranslatableMessage;
import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.Credentials; import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo; 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. * Service for verifying the identity of a user against Duo.
*/ */
public class UserVerificationService { 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 * 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 * result of the user's attempt to authenticate with their service. This
@@ -143,72 +150,98 @@ public class UserVerificationService {
// Redirect to Duo to obtain an authentication code if that redirect // Redirect to Duo to obtain an authentication code if that redirect
// has not yet occurred // has not yet occurred
if (duoCode == null || duoState == null) { if (duoCode != null && duoState != null) {
// Store received credentials for later retrieval leveraging Duo's // Validate that the user has successfully verified their identify with
// opaque session state identifier (we need to maintain these // the Duo service
// 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 { try {
duoAuthUrlString = duoClient.createAuthUrl(username, duoState);
// 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 (DuoException e) { catch (DuoException e) {
throw new GuacamoleServerException("Duo client failed to " logger.debug("The Duo client failed internally while "
+ "generate the authentication URL necessary to " + "attempting to validate the identity of user "
+ "redirect the authenticating user to the Duo " + "\"{}\". This is commonly caused by stale query "
+ "service.", e); + "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);
} }
// 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 // Store received credentials for later retrieval leveraging Duo's
// the Duo service // 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 { try {
Token token = duoClient.exchangeAuthorizationCodeFor2FAResult(duoCode, username); duoAuthUrlString = duoClient.createAuthUrl(username, duoState);
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) { catch (DuoException e) {
throw new GuacamoleServerException("Duo client refused to verify " throw new GuacamoleServerException("Duo client failed to "
+ "the identity of the authenticating user due to an " + "generate the authentication URL necessary to "
+ "underlying error condition.", e); + "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")
)
))
);
} }
} }