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 dc9999c98..ff3555ca5 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 @@ package org.apache.guacamole.auth.duo; 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 class DuoAuthenticationProvider extends AbstractAuthenticationProvider { 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 class DuoAuthenticationProvider extends AbstractAuthenticationProvider { 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 @@ public class DuoAuthenticationProvider extends AbstractAuthenticationProvider { } @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 @@ public class DuoAuthenticationProvider extends AbstractAuthenticationProvider { } + @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 000000000..6876cad12 --- /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 000000000..f2f39da2e --- /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 { + + // 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 26ab71221..6f36371f8 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.language.TranslatableMessage; 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 class UserVerificationService { 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); - } - catch (URISyntaxException e) { - throw new GuacamoleServerException("Error creating URI from Duo Authentication URL.", e); + throw new GuacamoleServerException("Duo client refused to verify " + + "the identity of the authenticating user due to an " + + "underlying error condition.", 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 f0c27bc36..5b51d2ce9 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 @@ public class TranslatableGuacamoleInsufficientCredentialsException 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 81a91d9d9..6b6fd8f21 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 @@ -42,6 +42,20 @@ public abstract class AbstractAuthenticationProvider implements AuthenticationPr return null; } + /** + * {@inheritDoc} + * + *

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} * 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 fd7d84478..680c7c382 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 @@ -62,6 +62,33 @@ public interface AuthenticationProvider { */ 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 45eebe80d..6ad0e240b 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 @@ -34,16 +34,6 @@ import javax.servlet.http.HttpSession; */ 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. */ 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 8c7669474..06ae3ea5c 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 @@ -28,95 +28,6 @@ package org.apache.guacamole.net.auth.credentials; */ 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 class GuacamoleInsufficientCredentialsException extends GuacamoleCredenti 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 class GuacamoleInsufficientCredentialsException extends GuacamoleCredenti */ 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 class GuacamoleInsufficientCredentialsException extends GuacamoleCredenti */ 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 9855cd6fd..b89b2ada6 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 @@ 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 * 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 dc8d3bb7d..7bb1e6fa8 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 @@ package org.apache.guacamole.rest.auth; 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.Logger; 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 class AuthenticationService { */ public static final String TOKEN_PARAMETER_NAME = "token"; - /** - * Map to store resumable authentication states with an expiration time. - */ - private Map resumableStateMap = new ConcurrentHashMap<>(); - /** * Attempts authentication against all AuthenticationProviders, in order, * using the provided credentials. The first authentication failure takes @@ -322,20 +313,6 @@ public class AuthenticationService { 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 @@ public class AuthenticationService { 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> iterator = resumableStateMap.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry 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 @@ public class AuthenticationService { 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 userContexts = getUserContexts(existingSession, authenticatedUser, actualCredentials); + authenticatedUser = getAuthenticatedUser(existingSession, updatedCredentials); + List userContexts = getUserContexts(existingSession, authenticatedUser, updatedCredentials); // Update existing session, if it exists if (existingSession != null) { @@ -508,7 +445,7 @@ public class AuthenticationService { // 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 1aaa7ea57..000000000 --- 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; - } -}