GUACAMOLE-1289: Refactor Duo and authentication-resumption changes to instead leverage support for updating/replacing credentials prior to auth.
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationProvider.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationProvider.java
index dc9999c..ff3555c 100644
--- a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationProvider.java
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationProvider.java
@@ -21,9 +21,11 @@
import com.google.inject.Guice;
import com.google.inject.Injector;
+import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.AbstractAuthenticationProvider;
import org.apache.guacamole.net.auth.AuthenticatedUser;
+import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.UserContext;
/**
@@ -41,10 +43,16 @@
public static String PROVIDER_IDENTIFER = "duo";
/**
- * Injector which will manage the object graph of this authentication
- * provider.
+ * Service for verifying the identity of users that Guacamole has otherwise
+ * 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
@@ -57,10 +65,13 @@
public DuoAuthenticationProvider() throws GuacamoleException {
// Set up Guice injector.
- injector = Guice.createInjector(
+ Injector injector = Guice.createInjector(
new DuoAuthenticationProviderModule(this)
);
+ sessionManager = injector.getInstance(DuoAuthenticationSessionManager.class);
+ verificationService = injector.getInstance(UserVerificationService.class);
+
}
@Override
@@ -69,11 +80,33 @@
}
@Override
- public UserContext getUserContext(AuthenticatedUser authenticatedUser)
+ public Credentials updateCredentials(Credentials credentials)
throws GuacamoleException {
- UserVerificationService verificationService =
- injector.getInstance(UserVerificationService.class);
+ // Ignore requests with no corresponding authentication session ID, as
+ // 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
verificationService.verifyAuthenticatedUser(authenticatedUser);
@@ -84,4 +117,9 @@
}
+ @Override
+ public void shutdown() {
+ sessionManager.shutdown();
+ }
+
}
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationSession.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationSession.java
new file mode 100644
index 0000000..6876cad
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationSession.java
@@ -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;
+ }
+
+}
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationSessionManager.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationSessionManager.java
new file mode 100644
index 0000000..f2f39da
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationSessionManager.java
@@ -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)
+
+}
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/UserVerificationService.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/UserVerificationService.java
index 26ab712..6f36371 100644
--- a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/UserVerificationService.java
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/UserVerificationService.java
@@ -37,35 +37,45 @@
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.Credentials;
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.
*/
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
- * that contains the code that the client will use to generate a token.
+ * 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
+ * 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";
-
+
/**
- * The name of the parameter that will be used in the GET call-back that
- * contains the session state.
+ * The name of the HTTP parameter that we will be using to hold the opaque
+ * 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";
-
+
/**
* The value that will be returned in the token if Duo authentication
* was successful.
*/
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.
*/
@@ -90,79 +100,115 @@
public void verifyAuthenticatedUser(AuthenticatedUser authenticatedUser)
throws GuacamoleException {
- // Pull the original HTTP request used to authenticate
- Credentials credentials = authenticatedUser.getCredentials();
- HttpServletRequest request = credentials.getRequest();
-
- // Ignore anonymous users
- if (authenticatedUser.getIdentifier().equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER))
- return;
-
+ // Ignore anonymous users (unverifiable)
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 {
-
- 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(
+ duoClient = new Client.Builder(
confService.getClientId(),
confService.getClientSecret(),
confService.getAPIHostname(),
- builtUrl)
+ confService.getRedirectUri().toString())
.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();
+ }
+ 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
- String duoCode = request.getParameter(DUO_CODE_PARAMETER_NAME);
- String duoState = request.getParameter(DUO_STATE_PARAMETER_NAME);
+ // Pull the original HTTP request used to authenticate, as well as any
+ // associated credentials
+ 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
- if (duoCode == null || duoState == null) {
+ // 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)
+ 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
- duoState = duoClient.generateState();
- long expirationTimestamp = System.currentTimeMillis() + (confService.getAuthTimeout() * 1000L);
+ // Redirect to Duo to obtain an authentication code if that redirect
+ // has not yet occurred
+ if (duoCode == null || duoState == null) {
- // Request additional credentials
- 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,
- new URI(duoClient.createAuthUrl(username, duoState)),
- new TranslatableMessage("LOGIN.INFO_DUO_REDIRECT_PENDING")
- )
- )),
- duoState, DuoAuthenticationProvider.PROVIDER_IDENTIFER,
- DUO_STATE_PARAMETER_NAME, expirationTimestamp
- );
+ // Store received credentials for later retrieval leveraging Duo's
+ // 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.getAuthTimeout() * 1000L);
+ sessionManager.defer(new DuoAuthenticationSession(credentials, expirationTimestamp), duoState);
+ // 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);
- if (token == null
- || token.getAuth_result() == null
+ 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);
+ throw new GuacamoleServerException("Duo client refused to verify "
+ + "the identity of the authenticating user due to an "
+ + "underlying error condition.", e);
}
- catch (URISyntaxException e) {
- throw new GuacamoleServerException("Error creating URI from Duo Authentication URL.", e);
- }
+
}
}
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/language/TranslatableGuacamoleInsufficientCredentialsException.java b/guacamole-ext/src/main/java/org/apache/guacamole/language/TranslatableGuacamoleInsufficientCredentialsException.java
index f0c27bc..5b51d2c 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/language/TranslatableGuacamoleInsufficientCredentialsException.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/language/TranslatableGuacamoleInsufficientCredentialsException.java
@@ -136,46 +136,6 @@
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
public TranslatableMessage getTranslatableMessage() {
return translatableMessage;
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AbstractAuthenticationProvider.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AbstractAuthenticationProvider.java
index 81a91d9..6b6fd8f 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AbstractAuthenticationProvider.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AbstractAuthenticationProvider.java
@@ -45,6 +45,20 @@
/**
* {@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}
+ *
* <p>This implementation performs no authentication whatsoever, ignoring
* the provided {@code credentials} and simply returning {@code null}. Any
* authentication attempt will thus fall through to other
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AuthenticationProvider.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AuthenticationProvider.java
index fd7d844..680c7c3 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AuthenticationProvider.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AuthenticationProvider.java
@@ -63,6 +63,33 @@
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
* given credentials, if any.
*
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Credentials.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Credentials.java
index 45eebe8..6ad0e24 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Credentials.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Credentials.java
@@ -35,16 +35,6 @@
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.
*/
private static final long serialVersionUID = 1L;
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/credentials/GuacamoleInsufficientCredentialsException.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/credentials/GuacamoleInsufficientCredentialsException.java
index 8c76694..06ae3ea 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/credentials/GuacamoleInsufficientCredentialsException.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/credentials/GuacamoleInsufficientCredentialsException.java
@@ -29,95 +29,6 @@
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
* message, cause, and associated credential information.
*
@@ -133,10 +44,6 @@
public GuacamoleInsufficientCredentialsException(String message, Throwable cause,
CredentialsInfo 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 GuacamoleInsufficientCredentialsException(String message, CredentialsInfo 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 GuacamoleInsufficientCredentialsException(Throwable cause, CredentialsInfo 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;
}
}
diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java b/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java
index 9855cd6..b89b2ad 100644
--- a/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java
+++ b/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java
@@ -119,6 +119,18 @@
}
+ @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
* failures during the authentication process, allowing other
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java
index dc8d3bb..7bb1e6f 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java
@@ -21,11 +21,8 @@
import java.util.ArrayList;
import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
import javax.inject.Inject;
-import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleSecurityException;
@@ -47,7 +44,6 @@
import org.slf4j.LoggerFactory;
import com.google.inject.Singleton;
-import java.util.Iterator;
/**
* A service for performing authentication checks in REST endpoints.
@@ -103,11 +99,6 @@
*/
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,
* using the provided credentials. The first authentication failure takes
@@ -322,20 +313,6 @@
try {
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) {
throw new GuacamoleAuthenticationProcessException("User "
+ "authentication aborted during initial "
@@ -354,81 +331,42 @@
return userContexts;
}
-
+
/**
- * Resumes authentication using given credentials if a matching resumable
- * state is found.
+ * Performs arbitrary and optional updates to the credentials supplied by
+ * 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
- * The initial credentials containing the request object.
+ * @param credentials
+ * The credentials to be updated.
*
- * @return
- * Resumed credentials if a valid resumable state is found; otherwise,
- * returns null.
+ * @return
+ * The set of credentials that should be provided to all
+ * 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) {
-
- Credentials resumedCredentials = null;
+ private Credentials getUpdatedCredentials(Credentials credentials)
+ throws GuacamoleAuthenticationProcessException {
- // Retrieve signed State from the request
- HttpServletRequest request = credentials.getRequest();
-
- // 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;
+ for (AuthenticationProvider authProvider : authProviders) {
+ try {
+ credentials = authProvider.updateCredentials(credentials);
}
-
- // Use the query identifier from the entry to retrieve the corresponding state parameter
- String stateQueryParameter = resumableState.getQueryIdentifier();
- String stateFromParameter = request.getParameter(stateQueryParameter);
-
- // 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;
+ catch (GuacamoleException | RuntimeException | Error e) {
+ throw new GuacamoleAuthenticationProcessException("User "
+ + "authentication aborted during credential "
+ + "update/revision.", authProvider, e);
}
}
- return resumedCredentials;
+ return credentials;
+
}
/**
@@ -469,16 +407,15 @@
AuthenticatedUser authenticatedUser;
String authToken;
- // Retrieve credentials if resuming authentication
- Credentials actualCredentials = resumeAuthentication(credentials);
- if (actualCredentials == null)
- actualCredentials = credentials;
-
try {
+ // Allow extensions to make updated to credentials prior to
+ // actual authentication
+ Credentials updatedCredentials = getUpdatedCredentials(credentials);
+
// Get up-to-date AuthenticatedUser and associated UserContexts
- authenticatedUser = getAuthenticatedUser(existingSession, actualCredentials);
- List<DecoratedUserContext> userContexts = getUserContexts(existingSession, authenticatedUser, actualCredentials);
+ authenticatedUser = getAuthenticatedUser(existingSession, updatedCredentials);
+ List<DecoratedUserContext> userContexts = getUserContexts(existingSession, authenticatedUser, updatedCredentials);
// Update existing session, if it exists
if (existingSession != null) {
@@ -508,7 +445,7 @@
// Log and rethrow any authentication errors
catch (GuacamoleAuthenticationProcessException e) {
- listenerService.handleEvent(new AuthenticationFailureEvent(actualCredentials,
+ listenerService.handleEvent(new AuthenticationFailureEvent(credentials,
e.getAuthenticationProvider(), e.getCause()));
// Rethrow exception
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/ResumableAuthenticationState.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/ResumableAuthenticationState.java
deleted file mode 100644
index 1aaa7ea..0000000
--- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/ResumableAuthenticationState.java
+++ /dev/null
@@ -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;
- }
-}