Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GUACAMOLE-1855: Allow MFA modules to be configured to bypass or enforce for specific hosts. #911

Merged
merged 4 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions extensions/guacamole-auth-duo/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,14 @@
<version>${kotlin.version}</version>
</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>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@
import com.duosecurity.exception.DuoException;
import com.duosecurity.model.Token;
import com.google.inject.Inject;
import inet.ipaddr.IPAddress;
import inet.ipaddr.IPAddressString;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
Expand All @@ -37,6 +40,7 @@
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.properties.IPAddressListProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -107,9 +111,41 @@ public class UserVerificationService {
public void verifyAuthenticatedUser(AuthenticatedUser authenticatedUser)
throws GuacamoleException {

// Ignore anonymous users (unverifiable)
// Pull the original HTTP request used to authenticate
Credentials credentials = authenticatedUser.getCredentials();
HttpServletRequest request = credentials.getRequest();
IPAddress clientAddr = new IPAddressString(request.getRemoteAddr()).getAddress();

// Ignore anonymous users
String username = authenticatedUser.getIdentifier();
if (username.equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER))
if (username == null || username.equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER))
return;

// Pull address lists to check from configuration. Note that the enforce
// list will override the bypass list, which means that, if the client
// address happens to be in both lists, Duo MFA will be enforced.
List<IPAddress> bypassAddresses = confService.getBypassHosts();
List<IPAddress> enforceAddresses = confService.getEnforceHosts();

// Check if the bypass list contains the client address, and set the
// enforce flag to the opposite.
boolean enforceHost = !(IPAddressListProperty.addressListContains(bypassAddresses, clientAddr));

// Only continue processing if the list is not empty
if (!enforceAddresses.isEmpty()) {

// If client address is not available or invalid, MFA will
// be enforced.
if (clientAddr == null || !clientAddr.isIPAddress())
enforceHost = true;

// Check the enforce list for the client address and set enforcement flag.
else
enforceHost = IPAddressListProperty.addressListContains(enforceAddresses, clientAddr);
}

// If the enforce flag is not true, bypass Duo MFA.
if (!enforceHost)
return;

// Obtain a Duo client for redirecting the user to the Duo service and
Expand Down Expand Up @@ -137,11 +173,6 @@ public void verifyAuthenticatedUser(AuthenticatedUser authenticatedUser)
+ "not currently available (failed health check).", e);
}

// Pull the original HTTP request used to authenticate, as well as any
// associated credentials
Credentials credentials = authenticatedUser.getCredentials();
HttpServletRequest request = credentials.getRequest();

// 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@
package org.apache.guacamole.auth.duo.conf;

import com.google.inject.Inject;
import inet.ipaddr.IPAddress;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
import org.apache.guacamole.properties.IPAddressListProperty;
import org.apache.guacamole.properties.StringGuacamoleProperty;
import org.apache.guacamole.properties.URIGuacamoleProperty;

Expand Down Expand Up @@ -105,6 +109,40 @@ public class ConfigurationService {
public String getName() { return "duo-auth-timeout"; }

};

/**
* The optional property that contains a comma-separated list of IP addresses
* or CIDRs for which the MFA requirement should be bypassed. If the Duo
* extension is installed, any/all users authenticating from clients that
* match this list will be able to successfully log in without fulfilling
* the MFA requirement. If this option is omitted or is empty, and the
* Duo module is installed, all users from all hosts will have Duo MFA
* enforced.
*/
private static final IPAddressListProperty DUO_BYPASS_HOSTS =
new IPAddressListProperty() {

@Override
public String getName() { return "duo-bypass-hosts"; }

};

/**
* The optional property that contains a comma-separated list of IP addresses
* or CIDRs for which the MFA requirement should be explicitly enforced. If
* the Duo module is enabled and this property is specified, users that log
* in from hosts that match the items in this list will have Duo MFA required,
* and all users from hosts that do not match this list will be able to log
* in without the MFA requirement. If this option is missing or empty and
* the Duo module is installed, MFA will be enforced for all users.
*/
private static final IPAddressListProperty DUO_ENFORCE_HOSTS =
new IPAddressListProperty() {

@Override
public String getName() { return "duo-enforce-hosts"; }

};

/**
* Returns the hostname of the Duo API endpoint to be used to verify user
Expand Down Expand Up @@ -188,5 +226,43 @@ public URI getRedirectUri() throws GuacamoleException {
public int getAuthenticationTimeout() throws GuacamoleException {
return environment.getProperty(DUO_AUTH_TIMEOUT, 5);
}

/**
* Returns the list of IP addresses and subnets defined in guacamole.properties
* for which Duo MFA should _not_ be enforced. Users logging in from hosts
* contained in this list will be logged in without the MFA requirement.
*
* @return
* A list of IP addresses and subnets for which Duo MFA should not be
* enforced.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed, or if an invalid IP address
* or subnet is specified.
*/
public List<IPAddress> getBypassHosts() throws GuacamoleException {
return environment.getProperty(DUO_BYPASS_HOSTS, Collections.emptyList());
}

/**
* Returns the list of IP addresses and subnets defined in guacamole.properties
* for which Duo MFA should explicitly be enforced, while logins from all
* other hosts should not enforce MFA. Users logging in from hosts
* contained in this list will be required to complete the Duo MFA authentication,
* while users from all other hosts will be logged in without the MFA requirement.
*
* @return
* A list of IP addresses and subnets for which Duo MFA should be
* explicitly enforced.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed, or if an invalid IP address
* or subnet is specified.
*/
public List<IPAddress> getEnforceHosts() throws GuacamoleException {
return environment.getProperty(DUO_ENFORCE_HOSTS, Collections.emptyList());
}



}
1 change: 1 addition & 0 deletions extensions/guacamole-auth-json/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
<groupId>com.github.seancfoley</groupId>
<artifactId>ipaddress</artifactId>
<version>5.5.0</version>
<scope>provided</scope>
</dependency>

<!-- JUnit -->
Expand Down
8 changes: 8 additions & 0 deletions extensions/guacamole-auth-totp/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,14 @@
<version>2.1.1</version>
<scope>provided</scope>
</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>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@
package org.apache.guacamole.auth.totp.conf;

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.GuacamoleServerException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.properties.EnumGuacamoleProperty;
import org.apache.guacamole.properties.IPAddressListProperty;
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
import org.apache.guacamole.properties.StringGuacamoleProperty;
import org.apache.guacamole.totp.TOTPGenerator;
Expand Down Expand Up @@ -88,6 +92,36 @@ public class ConfigurationService {
public String getName() { return "totp-mode"; }

};

/**
* 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
Expand Down Expand Up @@ -158,5 +192,39 @@ public int getPeriod() throws GuacamoleException {
public TOTPGenerator.Mode getMode() throws GuacamoleException {
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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@
import com.google.common.io.BaseEncoding;
import com.google.inject.Inject;
import com.google.inject.Provider;
import inet.ipaddr.IPAddress;
import inet.ipaddr.IPAddressString;
import java.security.InvalidKeyException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
Expand All @@ -44,6 +47,7 @@
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.auth.UserGroup;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
import org.apache.guacamole.properties.IPAddressListProperty;
import org.apache.guacamole.totp.TOTPGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -311,6 +315,45 @@ private boolean totpDisabled(UserContext context,
public void verifyIdentity(UserContext context,
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
IPAddress clientAddr = new IPAddressString(request.getRemoteAddr()).getAddress();

// Ignore anonymous users
if (authenticatedUser.getIdentifier().equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER))
return;

// Pull address lists to check from configuration. Note that the enforce
// list will override the bypass list, which means that, if the client
// address happens to be in both lists, Duo MFA will be enforced.
List<IPAddress> bypassAddresses = confService.getBypassHosts();
List<IPAddress> enforceAddresses = confService.getEnforceHosts();

// Check the bypass list for the client address, and set the enforce
// flag to the opposite.
boolean enforceHost = !(IPAddressListProperty.addressListContains(bypassAddresses, clientAddr));

// Only continue processing if the list is not empty
if (!enforceAddresses.isEmpty()) {

// If client address is not available or invalid, MFA will
// be enforced.
if (clientAddr == null || !clientAddr.isIPAddress())
enforceHost = true;

// Check the enforce list and set the flag if the client address
// is found in the list.
else
enforceHost = IPAddressListProperty.addressListContains(enforceAddresses, clientAddr);
}

// If the enforce flag is not true, bypass TOTP MFA.
if (!enforceHost)
return;

// Ignore anonymous users
String username = authenticatedUser.getIdentifier();
if (username.equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER))
Expand All @@ -325,10 +368,6 @@ public void verifyIdentity(UserContext context,
if (key == null)
return;

// Pull the original HTTP request used to authenticate
Credentials credentials = authenticatedUser.getCredentials();
HttpServletRequest request = credentials.getRequest();

// Retrieve TOTP from request
String code = request.getParameter(AuthenticationCodeField.PARAMETER_NAME);

Expand Down
7 changes: 7 additions & 0 deletions guacamole-ext/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@
<artifactId>jackson-databind</artifactId>
</dependency>

<!-- Library for unified IPv4/6 parsing and validation -->
<dependency>
<groupId>com.github.seancfoley</groupId>
<artifactId>ipaddress</artifactId>
<version>5.5.0</version>
</dependency>

</dependencies>

</project>
Loading
Loading