Skip to content

Commit

Permalink
GUACAMOLE-1289: Handle resumable state for duo authentication.
Browse files Browse the repository at this point in the history
  • Loading branch information
aleitner committed Mar 29, 2024
1 parent 7807bb9 commit b0e5ecd
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 148 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,6 @@ public class DuoAuthenticationProviderModule extends AbstractModule {
* module has configured injection.
*/
private final AuthenticationProvider authProvider;

/**
* The session manager that stores authentication attempts.
*/
private final DuoAuthenticationSessionManager authSessionManager;

/**
* Creates a new Duo authentication provider module which configures
Expand All @@ -66,10 +61,6 @@ public DuoAuthenticationProviderModule(AuthenticationProvider authProvider)

// Store associated auth provider
this.authProvider = authProvider;

// Create a new session manager
this.authSessionManager = new DuoAuthenticationSessionManager();

}

@Override
Expand All @@ -80,11 +71,9 @@ protected void configure() {
bind(Environment.class).toInstance(environment);

// Bind Duo-specific services
bind(DuoAuthenticationSessionManager.class).toInstance(authSessionManager);
bind(ConfigurationService.class);
bind(UserVerificationService.class);


}

}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
*/
public class UserVerificationService {

private static final Logger LOGGER = LoggerFactory.getLogger(UserVerificationService.class);
private static final Logger logger = LoggerFactory.getLogger(UserVerificationService.class);

/**
* The name of the parameter which Duo will return in it's GET call-back
Expand All @@ -63,21 +63,14 @@ public class UserVerificationService {
* The value that will be returned in the token if Duo authentication
* was successful.
*/
private static final String DUO_TOKEN_SUCCESS_VALUE = "ALLOW";
private static final String DUO_TOKEN_SUCCESS_VALUE = "allow";

/**
* Service for retrieving Duo configuration information.
*/
@Inject
private ConfigurationService confService;

/**
* The authentication session manager that temporarily stores in-progress
* authentication attempts.
*/
@Inject
private DuoAuthenticationSessionManager duoSessionManager;

/**
* Verifies the identity of the given user via the Duo multi-factor
* authentication service. If a signed response from Duo has not already
Expand Down Expand Up @@ -116,8 +109,7 @@ public void verifyAuthenticatedUser(AuthenticatedUser authenticatedUser)
confService.getRedirectUrl().toString())
.build();

duoClient.healthCheck();

duoClient.healthCheck();

// Retrieve signed Duo Code and State from the request
String duoCode = request.getParameter(DUO_CODE_PARAMETER_NAME);
Expand All @@ -128,10 +120,7 @@ public void verifyAuthenticatedUser(AuthenticatedUser authenticatedUser)

// Get a new session state from the Duo client
duoState = duoClient.generateState();
LOGGER.debug(">>> DUO <<< STATE DEFER: {}", duoState);

// Add this session
duoSessionManager.defer(new DuoAuthenticationSession(confService.getAuthTimeout(), duoState, username), duoState);
long expirationTimestamp = System.currentTimeMillis() + (confService.getAuthTimeout() * 1000L);

// Request additional credentials
throw new TranslatableGuacamoleInsufficientCredentialsException(
Expand All @@ -143,27 +132,21 @@ public void verifyAuthenticatedUser(AuthenticatedUser authenticatedUser)
new URI(duoClient.createAuthUrl(username, duoState)),
new TranslatableMessage("LOGIN.INFO_DUO_REDIRECT_PENDING")
)
))
)),
duoState,
expirationTimestamp
);

}

LOGGER.debug(">>> DUO <<< STATE RESUME: {}", duoState);

// Retrieve the deferred authenticaiton attempt
DuoAuthenticationSession duoSession = duoSessionManager.resume(duoState);
if (duoSession == null)
throw new GuacamoleServerException("Failed to resume Duo authentication session.");

// Get the token from the DuoClient using the code and username, and check status
Token token = duoClient.exchangeAuthorizationCodeFor2FAResult(duoCode, duoSession.getUsername());
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");

}
catch (DuoException e) {
throw new GuacamoleServerException("Duo Client error.", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,38 @@ public TranslatableGuacamoleInsufficientCredentialsException(String message,
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 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, long expires) {
super(message, credentialsInfo, state, expires);
this.translatableMessage = new TranslatableMessage(key);
}

@Override
public TranslatableMessage getTranslatableMessage() {
return translatableMessage;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,57 @@
*/
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 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 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 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, long expires) {
super(message, credentialsInfo);
this.state = state;
this.expires = expires;
}

/**
* Creates a new GuacamoleInsufficientCredentialsException with the given
* message, cause, and associated credential information.
Expand All @@ -44,6 +95,8 @@ public class GuacamoleInsufficientCredentialsException extends GuacamoleCredenti
public GuacamoleInsufficientCredentialsException(String message, Throwable cause,
CredentialsInfo credentialsInfo) {
super(message, cause, credentialsInfo);
this.state = DEFAULT_STATE;
this.expires = DEFAULT_EXPIRES;
}

/**
Expand All @@ -58,6 +111,8 @@ public GuacamoleInsufficientCredentialsException(String message, Throwable cause
*/
public GuacamoleInsufficientCredentialsException(String message, CredentialsInfo credentialsInfo) {
super(message, credentialsInfo);
this.state = DEFAULT_STATE;
this.expires = DEFAULT_EXPIRES;
}

/**
Expand All @@ -72,6 +127,29 @@ public GuacamoleInsufficientCredentialsException(String message, CredentialsInfo
*/
public GuacamoleInsufficientCredentialsException(Throwable cause, CredentialsInfo credentialsInfo) {
super(cause, credentialsInfo);
this.state = DEFAULT_STATE;
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 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;
}

}
Loading

0 comments on commit b0e5ecd

Please sign in to comment.