GUACAMOLE-1289: Refactor Duo and authentication-resumption changes to instead leverage support for updating/replacing credentials prior to auth.

This commit is contained in:
Michael Jumper
2024-04-25 12:52:27 -07:00
parent 83111616e5
commit 6dd4766da4
12 changed files with 340 additions and 486 deletions

View File

@@ -21,9 +21,11 @@ package org.apache.guacamole.auth.duo;
import com.google.inject.Guice; import com.google.inject.Guice;
import com.google.inject.Injector; import com.google.inject.Injector;
import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.AbstractAuthenticationProvider; import org.apache.guacamole.net.auth.AbstractAuthenticationProvider;
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.UserContext; import org.apache.guacamole.net.auth.UserContext;
/** /**
@@ -41,10 +43,16 @@ public class DuoAuthenticationProvider extends AbstractAuthenticationProvider {
public static String PROVIDER_IDENTIFER = "duo"; public static String PROVIDER_IDENTIFER = "duo";
/** /**
* Injector which will manage the object graph of this authentication * Service for verifying the identity of users that Guacamole has otherwise
* provider. * already authenticated.
*/ */
private final Injector injector; private final UserVerificationService verificationService;
/**
* Session manager for storing/retrieving the state of a user's
* authentication attempt while they are redirected to the Duo service.
*/
private final DuoAuthenticationSessionManager sessionManager;
/** /**
* Creates a new DuoAuthenticationProvider that verifies users * Creates a new DuoAuthenticationProvider that verifies users
@@ -57,10 +65,13 @@ public class DuoAuthenticationProvider extends AbstractAuthenticationProvider {
public DuoAuthenticationProvider() throws GuacamoleException { public DuoAuthenticationProvider() throws GuacamoleException {
// Set up Guice injector. // Set up Guice injector.
injector = Guice.createInjector( Injector injector = Guice.createInjector(
new DuoAuthenticationProviderModule(this) new DuoAuthenticationProviderModule(this)
); );
sessionManager = injector.getInstance(DuoAuthenticationSessionManager.class);
verificationService = injector.getInstance(UserVerificationService.class);
} }
@Override @Override
@@ -69,11 +80,33 @@ public class DuoAuthenticationProvider extends AbstractAuthenticationProvider {
} }
@Override @Override
public UserContext getUserContext(AuthenticatedUser authenticatedUser) public Credentials updateCredentials(Credentials credentials)
throws GuacamoleException { throws GuacamoleException {
UserVerificationService verificationService = // Ignore requests with no corresponding authentication session ID, as
injector.getInstance(UserVerificationService.class); // there are no credentials to reconstitute if the user has not yet
// attempted to authenticate
HttpServletRequest request = credentials.getRequest();
String duoState = request.getParameter(UserVerificationService.DUO_STATE_PARAMETER_NAME);
if (duoState == null)
return credentials;
// Ignore requests with invalid/expired authentication session IDs
DuoAuthenticationSession session = sessionManager.resume(duoState);
if (session == null)
return credentials;
// Reconstitute the originally-provided credentials from the users
// authentication attempt prior to being redirected to Duo
Credentials previousCredentials = session.getCredentials();
previousCredentials.setRequest(request);
return previousCredentials;
}
@Override
public UserContext getUserContext(AuthenticatedUser authenticatedUser)
throws GuacamoleException {
// Verify user against Duo service // Verify user against Duo service
verificationService.verifyAuthenticatedUser(authenticatedUser); verificationService.verifyAuthenticatedUser(authenticatedUser);
@@ -84,4 +117,9 @@ public class DuoAuthenticationProvider extends AbstractAuthenticationProvider {
} }
@Override
public void shutdown() {
sessionManager.shutdown();
}
} }

View File

@@ -0,0 +1,65 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.duo;
import org.apache.guacamole.net.auth.AuthenticationSession;
import org.apache.guacamole.net.auth.Credentials;
/**
* Representation of an in-progress Duo authentication attempt.
*/
public class DuoAuthenticationSession extends AuthenticationSession {
/**
* The credentials that the user originally provided to Guacamole prior to
* being redirected to the Duo service.
*/
private final Credentials credentials;
/**
* Creates a new AuthenticationSession representing an in-progress Duo
* authentication attempt.
*
* @param credentials
* The credentials that the user originally provided to Guacamole prior
* to being redirected to the Duo service.
*
* @param expires
* The number of milliseconds that may elapse before this session must
* be considered invalid.
*/
public DuoAuthenticationSession(Credentials credentials, long expires) {
super(expires);
this.credentials = credentials;
}
/**
* Returns the credentials that the user originally provided to Guacamole
* prior to being redirected to the Duo service.
*
* @return
* The credentials that the user originally provided to Guacamole prior
* to being redirected to the Duo service.
*/
public Credentials getCredentials() {
return credentials;
}
}

View File

@@ -0,0 +1,36 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.duo;
import com.google.inject.Singleton;
import org.apache.guacamole.net.auth.AuthenticationSessionManager;
/**
* Manager service that temporarily stores authentication attempts while
* the Duo authentication flow is underway.
*/
@Singleton
public class DuoAuthenticationSessionManager
extends AuthenticationSessionManager<DuoAuthenticationSession> {
// Intentionally empty (the default functions inherited from the
// AuthenticationSessionManager base class are sufficient for our needs)
}

View File

@@ -37,35 +37,45 @@ 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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.util.UriComponentsBuilder;
/** /**
* 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 {
private static final Logger logger = LoggerFactory.getLogger(UserVerificationService.class);
/** /**
* The name of the parameter which Duo will return in it's GET call-back * The name of the HTTP parameter that Duo will use to communicate the
* that contains the code that the client will use to generate a token. * result of the user's attempt to authenticate with their service. This
* parameter is provided in the GET request received when Duo redirects the
* user back to Guacamole.
*/ */
public static final String DUO_CODE_PARAMETER_NAME = "duo_code"; public static final String DUO_CODE_PARAMETER_NAME = "duo_code";
/** /**
* The name of the parameter that will be used in the GET call-back that * The name of the HTTP parameter that we will be using to hold the opaque
* contains the session state. * authentication session ID. This session ID is transmitted to Duo during
* the initial redirect and received back from Duo via this parameter in
* the GET request received when Duo redirects the user back to Guacamole.
* The session ID is ultimately used to reconstitute the original
* credentials received from the user by Guacamole such that parameter
* tokens like GUAC_USERNAME and GUAC_PASSWORD can continue to work as
* expected.
*/ */
public static final String DUO_STATE_PARAMETER_NAME = "state"; public static final String DUO_STATE_PARAMETER_NAME = "state";
/** /**
* The value that will be returned in the token if Duo authentication * The value that will be returned in the token if Duo authentication
* was successful. * was successful.
*/ */
private static final String DUO_TOKEN_SUCCESS_VALUE = "allow"; private static final String DUO_TOKEN_SUCCESS_VALUE = "allow";
/**
* Session manager for storing/retrieving the state of a user's
* authentication attempt while they are redirected to the Duo service.
*/
@Inject
private DuoAuthenticationSessionManager sessionManager;
/** /**
* Service for retrieving Duo configuration information. * Service for retrieving Duo configuration information.
*/ */
@@ -90,79 +100,115 @@ public class UserVerificationService {
public void verifyAuthenticatedUser(AuthenticatedUser authenticatedUser) public void verifyAuthenticatedUser(AuthenticatedUser authenticatedUser)
throws GuacamoleException { throws GuacamoleException {
// Pull the original HTTP request used to authenticate // Ignore anonymous users (unverifiable)
Credentials credentials = authenticatedUser.getCredentials();
HttpServletRequest request = credentials.getRequest();
// Ignore anonymous users
if (authenticatedUser.getIdentifier().equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER))
return;
String username = authenticatedUser.getIdentifier(); String username = authenticatedUser.getIdentifier();
if (username.equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER))
return;
// Obtain a Duo client for redirecting the user to the Duo service and
// verifying any received authentication code
Client duoClient;
try { try {
duoClient = new Client.Builder(
String redirectUrl = confService.getRedirectUri().toString();
String builtUrl = UriComponentsBuilder
.fromUriString(redirectUrl)
.queryParam(Credentials.RESUME_QUERY, DuoAuthenticationProvider.PROVIDER_IDENTIFER)
.build()
.toUriString();
// Set up the Duo Client
Client duoClient = new Client.Builder(
confService.getClientId(), confService.getClientId(),
confService.getClientSecret(), confService.getClientSecret(),
confService.getAPIHostname(), confService.getAPIHostname(),
builtUrl) confService.getRedirectUri().toString())
.build(); .build();
}
catch (DuoException e) {
throw new GuacamoleServerException("Client for communicating with "
+ "the Duo authentication service could not be created.", e);
}
// Verify that the Duo service is healthy and available
try {
duoClient.healthCheck(); duoClient.healthCheck();
}
catch (DuoException e) {
throw new GuacamoleServerException("Duo authentication service is "
+ "not currently available (failed health check).", e);
}
// Retrieve signed Duo Code and State from the request // Pull the original HTTP request used to authenticate, as well as any
String duoCode = request.getParameter(DUO_CODE_PARAMETER_NAME); // associated credentials
String duoState = request.getParameter(DUO_STATE_PARAMETER_NAME); Credentials credentials = authenticatedUser.getCredentials();
HttpServletRequest request = credentials.getRequest();
// If no code or state is received, assume Duo MFA redirect has not occured and do it // Retrieve signed Duo authentication code and session state from the
if (duoCode == null || duoState == null) { // request (these will be absent if this is an initial authentication
// attempt and not a redirect back from Duo)
String duoCode = request.getParameter(DUO_CODE_PARAMETER_NAME);
String duoState = request.getParameter(DUO_STATE_PARAMETER_NAME);
// Get a new session state from the Duo client // Redirect to Duo to obtain an authentication code if that redirect
duoState = duoClient.generateState(); // has not yet occurred
long expirationTimestamp = System.currentTimeMillis() + (confService.getAuthTimeout() * 1000L); if (duoCode == null || duoState == null) {
// Request additional credentials // Store received credentials for later retrieval leveraging Duo's
throw new TranslatableGuacamoleInsufficientCredentialsException( // opaque session state identifier (we need to maintain these
"Verification using Duo is required before authentication " // credentials so that things like the GUAC_USERNAME and
+ "can continue.", "LOGIN.INFO_DUO_AUTH_REQUIRED", // GUAC_PASSWORD tokens continue to work as expected despite the
new CredentialsInfo(Collections.singletonList( // redirect to/from the external Duo service)
new RedirectField( duoState = duoClient.generateState();
DUO_CODE_PARAMETER_NAME, long expirationTimestamp = System.currentTimeMillis() + (confService.getAuthTimeout() * 1000L);
new URI(duoClient.createAuthUrl(username, duoState)), sessionManager.defer(new DuoAuthenticationSession(credentials, expirationTimestamp), duoState);
new TranslatableMessage("LOGIN.INFO_DUO_REDIRECT_PENDING")
)
)),
duoState, DuoAuthenticationProvider.PROVIDER_IDENTIFER,
DUO_STATE_PARAMETER_NAME, expirationTimestamp
);
// 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);
} }
// Get the token from the DuoClient using the code and username, and check status // 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
// the Duo service
try {
Token token = duoClient.exchangeAuthorizationCodeFor2FAResult(duoCode, username); Token token = duoClient.exchangeAuthorizationCodeFor2FAResult(duoCode, username);
if (token == null if (token == null || token.getAuth_result() == null
|| token.getAuth_result() == null
|| !DUO_TOKEN_SUCCESS_VALUE.equals(token.getAuth_result().getStatus())) || !DUO_TOKEN_SUCCESS_VALUE.equals(token.getAuth_result().getStatus()))
throw new TranslatableGuacamoleClientException("Provided Duo " throw new TranslatableGuacamoleClientException("Provided Duo "
+ "validation code is incorrect.", + "validation code is incorrect.",
"LOGIN.INFO_DUO_VALIDATION_CODE_INCORRECT"); "LOGIN.INFO_DUO_VALIDATION_CODE_INCORRECT");
} }
catch (DuoException e) { catch (DuoException e) {
throw new GuacamoleServerException("Duo Client error.", e); throw new GuacamoleServerException("Duo client refused to verify "
} + "the identity of the authenticating user due to an "
catch (URISyntaxException e) { + "underlying error condition.", e);
throw new GuacamoleServerException("Error creating URI from Duo Authentication URL.", e);
} }
} }
} }

View File

@@ -136,46 +136,6 @@ public class TranslatableGuacamoleInsufficientCredentialsException
this(message, new TranslatableMessage(key), credentialsInfo); 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 providerIdentifier
* The identifier of the authentication provider that this exception pertains to.
*
* @param queryIdentifier
* The identifier of the specific query parameter within the
* authentication process that this exception pertains to.
*
* @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, String providerIdentifier,
String queryIdentifier, long expires) {
super(message, credentialsInfo, state, providerIdentifier, queryIdentifier, expires);
this.translatableMessage = new TranslatableMessage(key);
}
@Override @Override
public TranslatableMessage getTranslatableMessage() { public TranslatableMessage getTranslatableMessage() {
return translatableMessage; return translatableMessage;

View File

@@ -42,6 +42,20 @@ public abstract class AbstractAuthenticationProvider implements AuthenticationPr
return null; return null;
} }
/**
* {@inheritDoc}
*
* <p>This implementation simply returns the provided {@code credentials}
* without performing any updates. Implementations that wish to perform
* credential updates for in-progress authentication requests should
* override this function.
*/
@Override
public Credentials updateCredentials(Credentials credentials)
throws GuacamoleException {
return credentials;
}
/** /**
* {@inheritDoc} * {@inheritDoc}
* *

View File

@@ -62,6 +62,33 @@ public interface AuthenticationProvider {
*/ */
Object getResource() throws GuacamoleException; Object getResource() throws GuacamoleException;
/**
* Given the set of credentials that a user has submitted during
* authentication but has not yet been provided to the
* {@link #authenticateUser(org.apache.guacamole.net.auth.Credentials)} or
* {@link #updateAuthenticatedUser(org.apache.guacamole.net.auth.AuthenticatedUser, org.apache.guacamole.net.auth.Credentials)}
* functions of installed AuthenticationProviders, returns the set of
* credentials that should be used instead. The returned credentials may
* be the original credentials, with or without modifications, or may be an
* entirely new {@link Credentials} object.
*
* @param credentials
* The credentials provided by a user during authentication.
*
* @return
* The set of credentials that should be provided to all
* AuthenticationProviders, including this AuthenticationProvider. This
* set of credentials may optionally be entirely new or may have been
* modified.
*
* @throws GuacamoleException
* If an error occurs while updating the provided credentials.
*/
default Credentials updateCredentials(Credentials credentials)
throws GuacamoleException {
return credentials;
}
/** /**
* Returns an AuthenticatedUser representing the user authenticated by the * Returns an AuthenticatedUser representing the user authenticated by the
* given credentials, if any. * given credentials, if any.

View File

@@ -34,16 +34,6 @@ import javax.servlet.http.HttpSession;
*/ */
public class Credentials implements Serializable { public class Credentials implements Serializable {
/**
* The RESUME_QUERY is a query parameter key used to determine which
* authentication provider's process should be resumed during multi-step
* authentication. The auth provider will set this parameter before
* redirecting to an external service, and it is checked upon return to
* Guacamole to ensure the correct authentication state is continued
* without starting over.
*/
public static final String RESUME_QUERY = "provider_id";
/** /**
* Unique identifier associated with this specific version of Credentials. * Unique identifier associated with this specific version of Credentials.
*/ */

View File

@@ -28,95 +28,6 @@ package org.apache.guacamole.net.auth.credentials;
*/ */
public class GuacamoleInsufficientCredentialsException extends GuacamoleCredentialsException { 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 provider identifier to use when no specific provider is identified.
* This serves as a placeholder indicating that either no specific provider is
* responsible for the exception or the responsible provider has not been identified.
*/
private static final String DEFAULT_PROVIDER_IDENTIFIER = "";
/**
* The default query identifier to use when no specific query is identified.
* This serves as a placeholder and indicates that the specific query related to
* the provider's state resume operation has not been provided.
*/
private static final String DEFAULT_QUERY_IDENTIFIER = "";
/**
* 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 identifier for the authentication provider that threw this exception.
* This is used to link the exception back to the originating source of the
* authentication attempt, allowing clients to determine which provider's
* authentication process should be resumed.
*/
protected final String providerIdentifier;
/**
* An identifier for the specific query within the URL for this provider that can
* be checked to resume the authentication state.
*/
protected final String queryIdentifier;
/**
* 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 providerIdentifier
* The identifier of the authentication provider that this exception pertains to.
*
* @param queryIdentifier
* The identifier of the specific query parameter within the
* authentication process that this exception pertains to.
*
* @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,
String providerIdentifier, String queryIdentifier, long expires) {
super(message, credentialsInfo);
this.state = state;
this.providerIdentifier = providerIdentifier;
this.queryIdentifier = queryIdentifier;
this.expires = expires;
}
/** /**
* Creates a new GuacamoleInsufficientCredentialsException with the given * Creates a new GuacamoleInsufficientCredentialsException with the given
* message, cause, and associated credential information. * message, cause, and associated credential information.
@@ -133,10 +44,6 @@ public class GuacamoleInsufficientCredentialsException extends GuacamoleCredenti
public GuacamoleInsufficientCredentialsException(String message, Throwable cause, public GuacamoleInsufficientCredentialsException(String message, Throwable cause,
CredentialsInfo credentialsInfo) { CredentialsInfo credentialsInfo) {
super(message, cause, credentialsInfo); super(message, cause, credentialsInfo);
this.state = DEFAULT_STATE;
this.providerIdentifier = DEFAULT_PROVIDER_IDENTIFIER;
this.queryIdentifier = DEFAULT_QUERY_IDENTIFIER;
this.expires = DEFAULT_EXPIRES;
} }
/** /**
@@ -151,10 +58,6 @@ public class GuacamoleInsufficientCredentialsException extends GuacamoleCredenti
*/ */
public GuacamoleInsufficientCredentialsException(String message, CredentialsInfo credentialsInfo) { public GuacamoleInsufficientCredentialsException(String message, CredentialsInfo credentialsInfo) {
super(message, credentialsInfo); super(message, credentialsInfo);
this.state = DEFAULT_STATE;
this.providerIdentifier = DEFAULT_PROVIDER_IDENTIFIER;
this.queryIdentifier = DEFAULT_QUERY_IDENTIFIER;
this.expires = DEFAULT_EXPIRES;
} }
/** /**
@@ -169,52 +72,6 @@ public class GuacamoleInsufficientCredentialsException extends GuacamoleCredenti
*/ */
public GuacamoleInsufficientCredentialsException(Throwable cause, CredentialsInfo credentialsInfo) { public GuacamoleInsufficientCredentialsException(Throwable cause, CredentialsInfo credentialsInfo) {
super(cause, credentialsInfo); super(cause, credentialsInfo);
this.state = DEFAULT_STATE;
this.providerIdentifier = DEFAULT_PROVIDER_IDENTIFIER;
this.queryIdentifier = DEFAULT_QUERY_IDENTIFIER;
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 identifier of the authentication provider responsible for this exception.
*
* @return The identifier of the authentication provider, allowing clients to know
* which provider's process should be resumed in response to this exception.
*/
public String getProviderIdentifier() {
return providerIdentifier;
}
/**
* Retrieves the specific query identifier associated with the URL for the provider
* that can be checked to resume the authentication state.
*
* @return The query identifier that serves as a reference to a specific point or
* transaction within the provider's authentication process.
*/
public String getQueryIdentifier() {
return queryIdentifier;
}
/**
* 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;
} }
} }

View File

@@ -119,6 +119,18 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
} }
@Override
public Credentials updateCredentials(Credentials credentials) throws GuacamoleException {
// Do nothing if underlying auth provider could not be loaded
if (authProvider == null)
return credentials;
// Delegate to underlying auth provider
return authProvider.updateCredentials(credentials);
}
/** /**
* Returns whether this authentication provider should tolerate internal * Returns whether this authentication provider should tolerate internal
* failures during the authentication process, allowing other * failures during the authentication process, allowing other

View File

@@ -21,11 +21,8 @@ package org.apache.guacamole.rest.auth;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.inject.Inject; import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleSecurityException; import org.apache.guacamole.GuacamoleSecurityException;
@@ -47,7 +44,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import java.util.Iterator;
/** /**
* A service for performing authentication checks in REST endpoints. * A service for performing authentication checks in REST endpoints.
@@ -103,11 +99,6 @@ public class AuthenticationService {
*/ */
public static final String TOKEN_PARAMETER_NAME = "token"; public static final String TOKEN_PARAMETER_NAME = "token";
/**
* Map to store resumable authentication states with an expiration time.
*/
private Map<String, ResumableAuthenticationState> resumableStateMap = new ConcurrentHashMap<>();
/** /**
* Attempts authentication against all AuthenticationProviders, in order, * Attempts authentication against all AuthenticationProviders, in order,
* using the provided credentials. The first authentication failure takes * using the provided credentials. The first authentication failure takes
@@ -322,20 +313,6 @@ public class AuthenticationService {
try { try {
userContext = authProvider.getUserContext(authenticatedUser); userContext = authProvider.getUserContext(authenticatedUser);
} }
catch (GuacamoleInsufficientCredentialsException e) {
// Store state and expiration
String state = e.getState();
long expiration = e.getExpires();
String queryIdentifier = e.getQueryIdentifier();
String providerIdentifier = e.getProviderIdentifier();
resumableStateMap.put(state, new ResumableAuthenticationState(providerIdentifier,
queryIdentifier, expiration, credentials));
throw new GuacamoleAuthenticationProcessException("User "
+ "authentication aborted during initial "
+ "UserContext creation.", authProvider, e);
}
catch (GuacamoleException | RuntimeException | Error e) { catch (GuacamoleException | RuntimeException | Error e) {
throw new GuacamoleAuthenticationProcessException("User " throw new GuacamoleAuthenticationProcessException("User "
+ "authentication aborted during initial " + "authentication aborted during initial "
@@ -354,81 +331,42 @@ public class AuthenticationService {
return userContexts; return userContexts;
} }
/** /**
* Resumes authentication using given credentials if a matching resumable * Performs arbitrary and optional updates to the credentials supplied by
* state is found. * the authenticating user as dictated by the {@link AuthenticationProvider#updateCredentials(org.apache.guacamole.net.auth.Credentials)}
* functions of any installed AuthenticationProvider. Each installed
* AuthenticationProvider is given the opportunity, in order, to make
* updates to the supplied credentials.
* *
* @param credentials * @param credentials
* The initial credentials containing the request object. * The credentials to be updated.
* *
* @return * @return
* Resumed credentials if a valid resumable state is found; otherwise, * The set of credentials that should be provided to all
* returns null. * AuthenticationProviders during authentication, now possibly updated
* (or even replaced) by any number of installed
* AuthenticationProviders.
*
* @throws GuacamoleAuthenticationProcessException
* If an error occurs while updating the supplied credentials.
*/ */
private Credentials resumeAuthentication(Credentials credentials) { private Credentials getUpdatedCredentials(Credentials credentials)
throws GuacamoleAuthenticationProcessException {
Credentials resumedCredentials = null;
// Retrieve signed State from the request for (AuthenticationProvider authProvider : authProviders) {
HttpServletRequest request = credentials.getRequest(); try {
credentials = authProvider.updateCredentials(credentials);
// Retrieve the provider id from the query parameters
String resumableProviderId = request.getParameter(Credentials.RESUME_QUERY);
// Check if a provider id is set
if (resumableProviderId == null || resumableProviderId.isEmpty()) {
// Return if a provider id is not set
return null;
}
// Use an iterator to safely remove entries while iterating
Iterator<Map.Entry<String, ResumableAuthenticationState>> iterator = resumableStateMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, ResumableAuthenticationState> entry = iterator.next();
ResumableAuthenticationState resumableState = entry.getValue();
// Check if the provider ID from the request matches the one in the map entry
boolean providerMatches = resumableProviderId.equals(resumableState.getProviderIdentifier());
if (!providerMatches) {
// If the provider doesn't match, skip to the next entry
continue;
} }
catch (GuacamoleException | RuntimeException | Error e) {
// Use the query identifier from the entry to retrieve the corresponding state parameter throw new GuacamoleAuthenticationProcessException("User "
String stateQueryParameter = resumableState.getQueryIdentifier(); + "authentication aborted during credential "
String stateFromParameter = request.getParameter(stateQueryParameter); + "update/revision.", authProvider, e);
// Check if a state parameter is set
if (stateFromParameter == null || stateFromParameter.isEmpty()) {
// Remove and continue if`state is not provided or is empty
iterator.remove();
continue;
}
// If the key in the entry (state) matches the state parameter provided in the request
if (entry.getKey().equals(stateFromParameter)) {
// Remove the current entry from the map
iterator.remove();
// Check if the resumableState has expired
if (!resumableState.isExpired()) {
// Set the actualCredentials to the credentials from the matched entry
resumedCredentials = resumableState.getCredentials();
if (resumedCredentials != null) {
resumedCredentials.setRequest(request);
}
}
// Exit the loop since we've found the matching state and it's unique
break;
} }
} }
return resumedCredentials; return credentials;
} }
/** /**
@@ -469,16 +407,15 @@ public class AuthenticationService {
AuthenticatedUser authenticatedUser; AuthenticatedUser authenticatedUser;
String authToken; String authToken;
// Retrieve credentials if resuming authentication
Credentials actualCredentials = resumeAuthentication(credentials);
if (actualCredentials == null)
actualCredentials = credentials;
try { try {
// Allow extensions to make updated to credentials prior to
// actual authentication
Credentials updatedCredentials = getUpdatedCredentials(credentials);
// Get up-to-date AuthenticatedUser and associated UserContexts // Get up-to-date AuthenticatedUser and associated UserContexts
authenticatedUser = getAuthenticatedUser(existingSession, actualCredentials); authenticatedUser = getAuthenticatedUser(existingSession, updatedCredentials);
List<DecoratedUserContext> userContexts = getUserContexts(existingSession, authenticatedUser, actualCredentials); List<DecoratedUserContext> userContexts = getUserContexts(existingSession, authenticatedUser, updatedCredentials);
// Update existing session, if it exists // Update existing session, if it exists
if (existingSession != null) { if (existingSession != null) {
@@ -508,7 +445,7 @@ public class AuthenticationService {
// Log and rethrow any authentication errors // Log and rethrow any authentication errors
catch (GuacamoleAuthenticationProcessException e) { catch (GuacamoleAuthenticationProcessException e) {
listenerService.handleEvent(new AuthenticationFailureEvent(actualCredentials, listenerService.handleEvent(new AuthenticationFailureEvent(credentials,
e.getAuthenticationProvider(), e.getCause())); e.getAuthenticationProvider(), e.getCause()));
// Rethrow exception // Rethrow exception

View File

@@ -1,128 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.rest.auth;
import org.apache.guacamole.net.auth.Credentials;
/**
* Encapsulates the state information required for resuming an authentication
* process. This includes an expiration timestamp to determine state validity
* and the original credentials submitted by the user.
*/
public class ResumableAuthenticationState {
/**
* The timestamp at which this state should no longer be considered valid,
* measured in milliseconds since the Unix epoch.
*/
private long expirationTimestamp;
/**
* The original user credentials that were submitted at the start of the
* authentication process.
*/
private Credentials credentials;
/**
* A unique string identifying the authentication provider related to the state.
* This field allows the client to know which provider's authentication process
* should be resumed using this state.
*/
private String providerIdentifier;
/**
* A unique string that can be used to identify a specific query within the
* authentication process for the identified provider. This identifier can
* help the resumption of an authentication process.
*/
private String queryIdentifier;
/**
* Constructs a new ResumableAuthenticationState object with the specified
* expiration timestamp and user credentials.
*
* @param providerIdentifier
* The identifier of the authentication provider to which this resumable state pertains.
*
* @param queryIdenifier
* The identifier of the specific query within the provider's
* authentication process that this state corresponds to.
*
* @param expirationTimestamp
* The timestamp in milliseconds since the Unix epoch when this state
* expires and can no longer be used to resume authentication.
*
* @param credentials
* The Credentials object initially submitted by the user and associated
* with this resumable state.
*/
public ResumableAuthenticationState(String providerIdentifier, String queryIdentifier,
long expirationTimestamp, Credentials credentials) {
this.expirationTimestamp = expirationTimestamp;
this.credentials = credentials;
this.providerIdentifier = providerIdentifier;
this.queryIdentifier = queryIdentifier;
}
/**
* Checks if this resumable state has expired based on the stored expiration
* timestamp and the current system time.
*
* @return
* True if the current system time is after the expiration timestamp,
* indicating that the state is expired; false otherwise.
*/
public boolean isExpired() {
return System.currentTimeMillis() >= expirationTimestamp;
}
/**
* Retrieves the original credentials associated with this resumable state.
*
* @return
* The Credentials object containing user details that were submitted
* when the state was created.
*/
public Credentials getCredentials() {
return this.credentials;
}
/**
* Retrieves the identifier of the authentication provider associated with this state.
*
* @return
* The identifier of the authentication provider, providing context for this state
* within the overall authentication sequence.
*/
public String getProviderIdentifier() {
return this.providerIdentifier;
}
/**
* Retrieves the identifier for a specific query in the authentication
* process that is associated with this state.
*
* @return
* The query identifier used for retrieving a value representing the state within
* the provider's authentication process that should be resumed.
*/
public String getQueryIdentifier() {
return this.queryIdentifier;
}
}