GUACAMOLE-1855: Implement bypass and enforcement options in the TOTP module.

This commit is contained in:
Virtually Nick
2023-09-26 17:16:25 -04:00
parent 9139bdef02
commit 614cd550bd
3 changed files with 138 additions and 4 deletions

View File

@@ -178,6 +178,14 @@
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- Library for unified IPv4/6 parsing and validation -->
<dependency>
<groupId>com.github.seancfoley</groupId>
<artifactId>ipaddress</artifactId>
<version>5.5.0</version>
<scope>provided</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -20,10 +20,14 @@
package org.apache.guacamole.auth.totp.conf; package org.apache.guacamole.auth.totp.conf;
import com.google.inject.Inject; import com.google.inject.Inject;
import inet.ipaddr.IPAddress;
import java.util.Collections;
import java.util.List;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.environment.Environment; import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.properties.EnumGuacamoleProperty; import org.apache.guacamole.properties.EnumGuacamoleProperty;
import org.apache.guacamole.properties.IPAddressListProperty;
import org.apache.guacamole.properties.IntegerGuacamoleProperty; import org.apache.guacamole.properties.IntegerGuacamoleProperty;
import org.apache.guacamole.properties.StringGuacamoleProperty; import org.apache.guacamole.properties.StringGuacamoleProperty;
import org.apache.guacamole.totp.TOTPGenerator; import org.apache.guacamole.totp.TOTPGenerator;
@@ -89,6 +93,36 @@ public class ConfigurationService {
}; };
/**
* A property that contains a list of IP addresses and/or subnets for which
* MFA via the TOTP module should be bypassed. Users logging in from addresses
* contained in this list will not be prompted for a second authentication
* factor. If this property is empty or not defined, and the TOTP module
* is installed, all users will be prompted for MFA.
*/
private static final IPAddressListProperty TOTP_BYPASS_HOSTS =
new IPAddressListProperty() {
@Override
public String getName() { return "totp-bypass-hosts"; }
};
/**
* A property that contains a list of IP addresses and/or subnets for which
* MFA via the TOTP module should explicitly be enabled. If this property is defined,
* and the TOTP module is installed, users logging in from hosts contained
* in this list will be prompted for MFA, and users logging in from all
* other hosts will not be prompted for MFA.
*/
private static final IPAddressListProperty TOTP_ENFORCE_HOSTS =
new IPAddressListProperty() {
@Override
public String getName() { return "totp-enforce-hosts"; }
};
/** /**
* Returns the human-readable name of the entity issuing user accounts. If * Returns the human-readable name of the entity issuing user accounts. If
* not specified, "Apache Guacamole" will be used by default. * not specified, "Apache Guacamole" will be used by default.
@@ -159,4 +193,38 @@ public class ConfigurationService {
return environment.getProperty(TOTP_MODE, TOTPGenerator.Mode.SHA1); return environment.getProperty(TOTP_MODE, TOTPGenerator.Mode.SHA1);
} }
/**
* Return the list of IP addresses and/or subnets for which MFA authentication via the
* TOTP module should be bypassed, allowing users from those addresses to log in
* without the MFA requirement.
*
* @return
* A list of IP addresses and/or subnets for which MFA authentication
* should be bypassed.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed, or an invalid IP address
* or subnet is specified.
*/
public List<IPAddress> getBypassHosts() throws GuacamoleException {
return environment.getProperty(TOTP_BYPASS_HOSTS, Collections.emptyList());
}
/**
* Return the list of IP addresses and/or subnets for which MFA authentication via the TOTP
* module should be explicitly enabled, requiring users logging in from hosts specified in
* the list to complete MFA.
*
* @return
* A list of IP addresses and/or subnets for which MFA authentication
* should be explicitly enabled.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed, or an invalid IP address
* or subnet is specified.
*/
public List<IPAddress> getEnforceHosts() throws GuacamoleException {
return environment.getProperty(TOTP_ENFORCE_HOSTS, Collections.emptyList());
}
} }

View File

@@ -22,9 +22,12 @@ package org.apache.guacamole.auth.totp.user;
import com.google.common.io.BaseEncoding; import com.google.common.io.BaseEncoding;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import inet.ipaddr.IPAddress;
import inet.ipaddr.IPAddressString;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@@ -311,6 +314,65 @@ public class UserVerificationService {
public void verifyIdentity(UserContext context, public void verifyIdentity(UserContext context,
AuthenticatedUser authenticatedUser) throws GuacamoleException { AuthenticatedUser authenticatedUser) throws GuacamoleException {
// Pull the original HTTP request used to authenticate
Credentials credentials = authenticatedUser.getCredentials();
HttpServletRequest request = credentials.getRequest();
// Get the current client address
IPAddressString clientAddr = new IPAddressString(request.getRemoteAddr());
// Ignore anonymous users
if (authenticatedUser.getIdentifier().equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER))
return;
// We enforce by default
boolean enforceHost = true;
// Check for a list of addresses that should be bypassed and iterate
List<IPAddress> bypassAddresses = confService.getBypassHosts();
for (IPAddress bypassAddr : bypassAddresses) {
// If the address contains current client address, flip enforce flag
// and break out
if (clientAddr != null && clientAddr.isIPAddress()
&& bypassAddr.getIPVersion().equals(clientAddr.getIPVersion())
&& bypassAddr.contains(clientAddr.getAddress())) {
enforceHost = false;
break;
}
}
// Check for a list of addresses that should be enforced and iterate
List<IPAddress> enforceAddresses = confService.getEnforceHosts();
// Only continue processing if the list is not empty
if (!enforceAddresses.isEmpty()) {
if (clientAddr == null || !clientAddr.isIPAddress()) {
logger.warn("Client address is not valid, "
+ "MFA will be enforced.");
enforceHost = true;
}
else {
// With addresses set, this default changes to false.
enforceHost = false;
for (IPAddress enforceAddr : enforceAddresses) {
// If there's a match, flip the enforce flag and break out of the loop
if (enforceAddr.getIPVersion().equals(clientAddr.getIPVersion())
&& enforceAddr.contains(clientAddr.getAddress())) {
enforceHost = true;
break;
}
}
}
}
// If the enforce flag has been changed, exit, bypassing TOTP MFA.
if (!enforceHost)
return;
// Ignore anonymous users // Ignore anonymous users
String username = authenticatedUser.getIdentifier(); String username = authenticatedUser.getIdentifier();
if (username.equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER)) if (username.equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER))
@@ -325,10 +387,6 @@ public class UserVerificationService {
if (key == null) if (key == null)
return; return;
// Pull the original HTTP request used to authenticate
Credentials credentials = authenticatedUser.getCredentials();
HttpServletRequest request = credentials.getRequest();
// Retrieve TOTP from request // Retrieve TOTP from request
String code = request.getParameter(AuthenticationCodeField.PARAMETER_NAME); String code = request.getParameter(AuthenticationCodeField.PARAMETER_NAME);