GUACAMOLE-96: Block external access to TOTP-internal attributes. Project: http://git-wip-us.apache.org/repos/asf/guacamole-client/repo Commit: http://git-wip-us.apache.org/repos/asf/guacamole-client/commit/96e3d029 Tree: http://git-wip-us.apache.org/repos/asf/guacamole-client/tree/96e3d029 Diff: http://git-wip-us.apache.org/repos/asf/guacamole-client/diff/96e3d029
Branch: refs/heads/master Commit: 96e3d029992ac09d27aac808c489779000fb6fe1 Parents: 2a894c4 Author: Michael Jumper <mjum...@apache.org> Authored: Mon Nov 20 16:15:01 2017 -0800 Committer: Michael Jumper <mjum...@apache.org> Committed: Sun Feb 4 19:45:18 2018 -0800 ---------------------------------------------------------------------- .../auth/totp/TOTPAuthenticationProvider.java | 6 +- .../totp/TOTPAuthenticationProviderModule.java | 1 + .../apache/guacamole/auth/totp/UserTOTPKey.java | 148 ---------- .../auth/totp/UserVerificationService.java | 292 ------------------- .../auth/totp/form/AuthenticationCodeField.java | 2 +- .../guacamole/auth/totp/user/TOTPUser.java | 102 +++++++ .../auth/totp/user/TOTPUserContext.java | 64 ++++ .../guacamole/auth/totp/user/UserTOTPKey.java | 148 ++++++++++ .../auth/totp/user/UserVerificationService.java | 281 ++++++++++++++++++ 9 files changed, 601 insertions(+), 443 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/96e3d029/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/TOTPAuthenticationProvider.java ---------------------------------------------------------------------- diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/TOTPAuthenticationProvider.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/TOTPAuthenticationProvider.java index 835ba87..28e2380 100644 --- a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/TOTPAuthenticationProvider.java +++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/TOTPAuthenticationProvider.java @@ -19,9 +19,11 @@ package org.apache.guacamole.auth.totp; +import org.apache.guacamole.auth.totp.user.UserVerificationService; import com.google.inject.Guice; import com.google.inject.Injector; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.auth.totp.user.TOTPUserContext; import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Credentials; @@ -104,7 +106,7 @@ public class TOTPAuthenticationProvider implements AuthenticationProvider { // User has been verified, and authentication should be allowed to // continue - return context; + return new TOTPUserContext(context); } @@ -112,7 +114,7 @@ public class TOTPAuthenticationProvider implements AuthenticationProvider { public UserContext redecorate(UserContext decorated, UserContext context, AuthenticatedUser authenticatedUser, Credentials credentials) throws GuacamoleException { - return context; + return new TOTPUserContext(context); } @Override http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/96e3d029/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/TOTPAuthenticationProviderModule.java ---------------------------------------------------------------------- diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/TOTPAuthenticationProviderModule.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/TOTPAuthenticationProviderModule.java index e72beec..94b7232 100644 --- a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/TOTPAuthenticationProviderModule.java +++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/TOTPAuthenticationProviderModule.java @@ -19,6 +19,7 @@ package org.apache.guacamole.auth.totp; +import org.apache.guacamole.auth.totp.user.UserVerificationService; import com.google.inject.AbstractModule; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.auth.totp.conf.ConfigurationService; http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/96e3d029/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/UserTOTPKey.java ---------------------------------------------------------------------- diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/UserTOTPKey.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/UserTOTPKey.java deleted file mode 100644 index 3de3785..0000000 --- a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/UserTOTPKey.java +++ /dev/null @@ -1,148 +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.auth.totp; - -import java.security.SecureRandom; -import java.util.Random; - -/** - * The key used to generate TOTP codes for a particular user. - */ -public class UserTOTPKey { - - /** - * Secure source of random bytes. - */ - private static final Random RANDOM = new SecureRandom(); - - /** - * The username of the user associated with this key. - */ - private final String username; - - /** - * Whether the associated secret key has been confirmed by the user. A key - * is confirmed once the user has successfully entered a valid TOTP - * derived from that key. - */ - private boolean confirmed; - - /** - * The base32-encoded TOTP key associated with the user. - */ - private byte[] secret; - - /** - * Generates the given number of random bytes. - * - * @param length - * The number of random bytes to generate. - * - * @return - * A new array of exactly the given number of random bytes. - */ - private static byte[] generateBytes(int length) { - byte[] bytes = new byte[length]; - RANDOM.nextBytes(bytes); - return bytes; - } - - /** - * Creates a new, unconfirmed, randomly-generated TOTP key having the given - * length. - * - * @param username - * The username of the user associated with this key. - * - * @param length - * The length of the key to generate, in bytes. - */ - public UserTOTPKey(String username, int length) { - this(username, generateBytes(length), false); - } - - /** - * Creates a new UserTOTPKey containing the given key and having the given - * confirmed state. - * - * @param username - * The username of the user associated with this key. - * - * @param secret - * The raw binary secret key to be used to generate TOTP codes. - * - * @param confirmed - * true if the user associated with the key has confirmed that they can - * successfully generate the corresponding TOTP codes (the user has - * been "enrolled"), false otherwise. - */ - public UserTOTPKey(String username, byte[] secret, boolean confirmed) { - this.username = username; - this.confirmed = confirmed; - this.secret = secret; - } - - /** - * Returns the username of the user associated with this key. - * - * @return - * The username of the user associated with this key. - */ - public String getUsername() { - return username; - } - - /** - * Returns the raw binary secret key to be used to generate TOTP codes. - * - * @return - * The raw binary secret key to be used to generate TOTP codes. - */ - public byte[] getSecret() { - return secret; - } - - /** - * Returns whether the user associated with the key has confirmed that they - * can successfully generate the corresponding TOTP codes (the user has - * been "enrolled"). - * - * @return - * true if the user has confirmed that they can successfully generate - * the TOTP codes generated by this key, false otherwise. - */ - public boolean isConfirmed() { - return confirmed; - } - - /** - * Sets whether the user associated with the key has confirmed that they - * can successfully generate the corresponding TOTP codes (the user has - * been "enrolled"). - * - * @param confirmed - * true if the user has confirmed that they can successfully generate - * the TOTP codes generated by this key, false otherwise. - */ - public void setConfirmed(boolean confirmed) { - this.confirmed = confirmed; - } - -} http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/96e3d029/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/UserVerificationService.java ---------------------------------------------------------------------- diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/UserVerificationService.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/UserVerificationService.java deleted file mode 100644 index 851bb94..0000000 --- a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/UserVerificationService.java +++ /dev/null @@ -1,292 +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.auth.totp; - -import com.google.common.io.BaseEncoding; -import com.google.inject.Inject; -import com.google.inject.Provider; -import java.security.InvalidKeyException; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import javax.servlet.http.HttpServletRequest; -import org.apache.guacamole.GuacamoleClientException; -import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.GuacamoleUnsupportedException; -import org.apache.guacamole.auth.totp.conf.ConfigurationService; -import org.apache.guacamole.auth.totp.form.AuthenticationCodeField; -import org.apache.guacamole.form.Field; -import org.apache.guacamole.net.auth.AuthenticatedUser; -import org.apache.guacamole.net.auth.Credentials; -import org.apache.guacamole.net.auth.User; -import org.apache.guacamole.net.auth.UserContext; -import org.apache.guacamole.net.auth.credentials.CredentialsInfo; -import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; -import org.apache.guacamole.totp.TOTPGenerator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Service for verifying the identity of a user using TOTP. - */ -public class UserVerificationService { - - /** - * Logger for this class. - */ - private final Logger logger = LoggerFactory.getLogger(UserVerificationService.class); - - /** - * The name of the user attribute which stores the TOTP key. - */ - private static final String TOTP_KEY_SECRET_ATTRIBUTE_NAME = "guac-totp-key-secret"; - - /** - * The name of the user attribute defines whether the TOTP key has been - * confirmed by the user, and the user is thus fully enrolled. - */ - private static final String TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME = "guac-totp-key-confirmed"; - - /** - * BaseEncoding instance which decoded/encodes base32. - */ - private static final BaseEncoding BASE32 = BaseEncoding.base32(); - - /** - * Service for retrieving configuration information. - */ - @Inject - private ConfigurationService confService; - - /** - * Provider for AuthenticationCodeField instances. - */ - @Inject - private Provider<AuthenticationCodeField> codeFieldProvider; - - /** - * Retrieves and decodes the base32-encoded TOTP key associated with user - * having the given UserContext. If no TOTP key is associated with the user, - * a random key is generated and associated with the user. If the extension - * storing the user does not support storage of the TOTP key, null is - * returned. - * - * @param context - * The UserContext of the user whose TOTP key should be retrieved. - * - * @param username - * The username of the user associated with the given UserContext. - * - * @return - * The TOTP key associated with the user having the given UserContext, - * or null if the extension storing the user does not support storage - * of the TOTP key. - * - * @throws GuacamoleException - * If a new key is generated, but the extension storing the associated - * user fails while updating the user account. - */ - private UserTOTPKey getKey(UserContext context, - String username) throws GuacamoleException { - - // Retrieve attributes from current user - User self = context.self(); - Map<String, String> attributes = context.self().getAttributes(); - - // If no key is defined, attempt to generate a new key - String secret = attributes.get(TOTP_KEY_SECRET_ATTRIBUTE_NAME); - if (secret == null) { - - // Generate random key for user - TOTPGenerator.Mode mode = confService.getMode(); - UserTOTPKey generated = new UserTOTPKey(username,mode.getRecommendedKeyLength()); - if (setKey(context, generated)) - return generated; - - // Fail if key cannot be set - return null; - - } - - // Parse retrieved base32 key value - byte[] key; - try { - key = BASE32.decode(secret); - } - - // If key is not valid base32, warn but otherwise pretend the key does - // not exist - catch (IllegalArgumentException e) { - logger.warn("TOTP key of user \"{}\" is not valid base32.", self.getIdentifier()); - logger.debug("TOTP key is not valid base32.", e); - return null; - } - - // Otherwise, parse value from attributes - boolean confirmed = "true".equals(attributes.get(TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME)); - return new UserTOTPKey(username, key, confirmed); - - } - - /** - * Attempts to store the given TOTP key within the user account of the user - * having the given UserContext. As not all extensions will support storage - * of arbitrary attributes, this operation may fail. - * - * @param context - * The UserContext associated with the user whose TOTP key is to be - * stored. - * - * @param key - * The TOTP key to store. - * - * @return - * true if the TOTP key was successfully stored, false if the extension - * handling storage does not support storage of the key. - * - * @throws GuacamoleException - * If the extension handling storage fails internally while attempting - * to update the user. - */ - private boolean setKey(UserContext context, UserTOTPKey key) - throws GuacamoleException { - - // Get mutable set of attributes - User self = context.self(); - Map<String, String> attributes = new HashMap<String, String>(); - - // Set/overwrite current TOTP key state - attributes.put(TOTP_KEY_SECRET_ATTRIBUTE_NAME, BASE32.encode(key.getSecret())); - attributes.put(TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME, key.isConfirmed() ? "true" : "false"); - self.setAttributes(attributes); - - // Confirm that attributes have actually been set - Map<String, String> setAttributes = self.getAttributes(); - if (!setAttributes.containsKey(TOTP_KEY_SECRET_ATTRIBUTE_NAME) - || !setAttributes.containsKey(TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME)) - return false; - - // Update user object - try { - context.getUserDirectory().update(self); - } - catch (GuacamoleUnsupportedException e) { - logger.debug("Extension storage for user is explicitly read-only. " - + "Cannot update attributes to store TOTP key.", e); - return false; - } - - // TOTP key successfully stored/updated - return true; - - } - - /** - * Verifies the identity of the given user using TOTP. If a authentication - * code from the user's TOTP device has not already been provided, a code is - * requested in the form of additional expected credentials. Any provided - * code is cryptographically verified. If no code is present, or the - * received code is invalid, an exception is thrown. - * - * @param context - * The UserContext provided for the user by another authentication - * extension. - * - * @param authenticatedUser - * The user whose identity should be verified using TOTP. - * - * @throws GuacamoleException - * If required TOTP-specific configuration options are missing or - * malformed, or if the user's identity cannot be verified. - */ - public void verifyIdentity(UserContext context, - AuthenticatedUser authenticatedUser) throws GuacamoleException { - - // Ignore anonymous users - String username = authenticatedUser.getIdentifier(); - if (username.equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER)) - return; - - // Ignore users which do not have an associated key - UserTOTPKey key = getKey(context, username); - if (key == null) - return; - - // Pull the original HTTP request used to authenticate - Credentials credentials = authenticatedUser.getCredentials(); - HttpServletRequest request = credentials.getRequest(); - - // Retrieve TOTP from request - String code = request.getParameter(AuthenticationCodeField.PARAMETER_NAME); - - // If no TOTP provided, request one - if (code == null) { - - AuthenticationCodeField field = codeFieldProvider.get(); - - // If the user hasn't completed enrollment, request that they do - if (!key.isConfirmed()) { - field.exposeKey(key); - throw new GuacamoleInsufficientCredentialsException( - "TOTP.INFO_ENROLL_REQUIRED", new CredentialsInfo( - Collections.<Field>singletonList(field) - )); - } - - // Otherwise simply request the user's authentication code - throw new GuacamoleInsufficientCredentialsException( - "TOTP.INFO_CODE_REQUIRED", new CredentialsInfo( - Collections.<Field>singletonList(field) - )); - - } - - try { - - // Get generator based on user's key and provided configuration - TOTPGenerator totp = new TOTPGenerator(key.getSecret(), - confService.getMode(), confService.getDigits()); - - // Verify provided TOTP against value produced by generator - if (code.equals(totp.generate()) || code.equals(totp.previous())) { - - // Record key as confirmed, if it hasn't already been so recorded - if (!key.isConfirmed()) { - key.setConfirmed(true); - setKey(context, key); - } - - // User has been verified - return; - - } - - } - catch (InvalidKeyException e) { - logger.warn("User \"{}\" is associated with an invalid TOTP key.", username); - logger.debug("TOTP key is not valid.", e); - } - - // Provided code is not valid - throw new GuacamoleClientException("TOTP.INFO_VERIFICATION_FAILED"); - - } - -} http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/96e3d029/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/form/AuthenticationCodeField.java ---------------------------------------------------------------------- diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/form/AuthenticationCodeField.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/form/AuthenticationCodeField.java index c3ca207..764fe95 100644 --- a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/form/AuthenticationCodeField.java +++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/form/AuthenticationCodeField.java @@ -32,7 +32,7 @@ import java.net.URI; import javax.ws.rs.core.UriBuilder; import javax.xml.bind.DatatypeConverter; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.auth.totp.UserTOTPKey; +import org.apache.guacamole.auth.totp.user.UserTOTPKey; import org.apache.guacamole.auth.totp.conf.ConfigurationService; import org.apache.guacamole.form.Field; import org.codehaus.jackson.annotate.JsonProperty; http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/96e3d029/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUser.java ---------------------------------------------------------------------- diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUser.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUser.java new file mode 100644 index 0000000..4199d43 --- /dev/null +++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUser.java @@ -0,0 +1,102 @@ +/* + * 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.totp.user; + +import java.util.HashMap; +import java.util.Map; +import org.apache.guacamole.net.auth.DelegatingUser; +import org.apache.guacamole.net.auth.User; + +/** + * TOTP-specific User implementation which wraps a User from another extension, + * hiding and blocking access to the core attributes used by TOTP. + */ +public class TOTPUser extends DelegatingUser { + + /** + * The name of the user attribute which stores the TOTP key. + */ + public static final String TOTP_KEY_SECRET_ATTRIBUTE_NAME = "guac-totp-key-secret"; + + /** + * The name of the user attribute defines whether the TOTP key has been + * confirmed by the user, and the user is thus fully enrolled. + */ + public static final String TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME = "guac-totp-key-confirmed"; + + /** + * The User object wrapped by this TOTPUser. + */ + private final User undecorated; + + /** + * Wraps the given User object, hiding and blocking access to the core + * attributes used by TOTP. + * + * @param user + * The User object to wrap. + */ + public TOTPUser(User user) { + super(user); + this.undecorated = user; + } + + /** + * Returns the User object wrapped by this TOTPUser. + * + * @return + * The wrapped User object. + */ + public User getUndecorated() { + return undecorated; + } + + @Override + public Map<String, String> getAttributes() { + + // Create independent, mutable copy of attributes + Map<String, String> attributes = + new HashMap<String, String>(super.getAttributes()); + + // Do not expose any TOTP-related attributes outside this extension + attributes.remove(TOTP_KEY_SECRET_ATTRIBUTE_NAME); + attributes.remove(TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME); + + // Expose only non-TOTP attributes + return attributes; + + } + + @Override + public void setAttributes(Map<String, String> attributes) { + + // Create independent, mutable copy of attributes + attributes = new HashMap<String, String>(attributes); + + // Do not expose any TOTP-related attributes outside this extension + attributes.remove(TOTP_KEY_SECRET_ATTRIBUTE_NAME); + attributes.remove(TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME); + + // Set only non-TOTP attributes + super.setAttributes(attributes); + + } + +} http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/96e3d029/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUserContext.java ---------------------------------------------------------------------- diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUserContext.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUserContext.java new file mode 100644 index 0000000..980bbf7 --- /dev/null +++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUserContext.java @@ -0,0 +1,64 @@ +/* + * 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.totp.user; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.DecoratingDirectory; +import org.apache.guacamole.net.auth.DelegatingUserContext; +import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.User; +import org.apache.guacamole.net.auth.UserContext; + +/** + * TOTP-specific UserContext implementation which wraps the UserContext of + * some other extension, providing (or hiding) additional data. + */ +public class TOTPUserContext extends DelegatingUserContext { + + /** + * Creates a new TOTPUserContext which wraps the given UserContext, + * providing (or hiding) additional TOTP-specific data. + * + * @param userContext + * The UserContext to wrap. + */ + public TOTPUserContext(UserContext userContext) { + super(userContext); + } + + @Override + public Directory<User> getUserDirectory() throws GuacamoleException { + return new DecoratingDirectory<User>(super.getUserDirectory()) { + + @Override + protected User decorate(User object) { + return new TOTPUser(object); + } + + @Override + protected User undecorate(User object) { + assert(object instanceof TOTPUser); + return ((TOTPUser) object).getUndecorated(); + } + + }; + } + +} http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/96e3d029/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/UserTOTPKey.java ---------------------------------------------------------------------- diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/UserTOTPKey.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/UserTOTPKey.java new file mode 100644 index 0000000..d7bc903 --- /dev/null +++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/UserTOTPKey.java @@ -0,0 +1,148 @@ +/* + * 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.totp.user; + +import java.security.SecureRandom; +import java.util.Random; + +/** + * The key used to generate TOTP codes for a particular user. + */ +public class UserTOTPKey { + + /** + * Secure source of random bytes. + */ + private static final Random RANDOM = new SecureRandom(); + + /** + * The username of the user associated with this key. + */ + private final String username; + + /** + * Whether the associated secret key has been confirmed by the user. A key + * is confirmed once the user has successfully entered a valid TOTP + * derived from that key. + */ + private boolean confirmed; + + /** + * The base32-encoded TOTP key associated with the user. + */ + private byte[] secret; + + /** + * Generates the given number of random bytes. + * + * @param length + * The number of random bytes to generate. + * + * @return + * A new array of exactly the given number of random bytes. + */ + private static byte[] generateBytes(int length) { + byte[] bytes = new byte[length]; + RANDOM.nextBytes(bytes); + return bytes; + } + + /** + * Creates a new, unconfirmed, randomly-generated TOTP key having the given + * length. + * + * @param username + * The username of the user associated with this key. + * + * @param length + * The length of the key to generate, in bytes. + */ + public UserTOTPKey(String username, int length) { + this(username, generateBytes(length), false); + } + + /** + * Creates a new UserTOTPKey containing the given key and having the given + * confirmed state. + * + * @param username + * The username of the user associated with this key. + * + * @param secret + * The raw binary secret key to be used to generate TOTP codes. + * + * @param confirmed + * true if the user associated with the key has confirmed that they can + * successfully generate the corresponding TOTP codes (the user has + * been "enrolled"), false otherwise. + */ + public UserTOTPKey(String username, byte[] secret, boolean confirmed) { + this.username = username; + this.confirmed = confirmed; + this.secret = secret; + } + + /** + * Returns the username of the user associated with this key. + * + * @return + * The username of the user associated with this key. + */ + public String getUsername() { + return username; + } + + /** + * Returns the raw binary secret key to be used to generate TOTP codes. + * + * @return + * The raw binary secret key to be used to generate TOTP codes. + */ + public byte[] getSecret() { + return secret; + } + + /** + * Returns whether the user associated with the key has confirmed that they + * can successfully generate the corresponding TOTP codes (the user has + * been "enrolled"). + * + * @return + * true if the user has confirmed that they can successfully generate + * the TOTP codes generated by this key, false otherwise. + */ + public boolean isConfirmed() { + return confirmed; + } + + /** + * Sets whether the user associated with the key has confirmed that they + * can successfully generate the corresponding TOTP codes (the user has + * been "enrolled"). + * + * @param confirmed + * true if the user has confirmed that they can successfully generate + * the TOTP codes generated by this key, false otherwise. + */ + public void setConfirmed(boolean confirmed) { + this.confirmed = confirmed; + } + +} http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/96e3d029/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/UserVerificationService.java ---------------------------------------------------------------------- diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/UserVerificationService.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/UserVerificationService.java new file mode 100644 index 0000000..8264efd --- /dev/null +++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/UserVerificationService.java @@ -0,0 +1,281 @@ +/* + * 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.totp.user; + +import com.google.common.io.BaseEncoding; +import com.google.inject.Inject; +import com.google.inject.Provider; +import java.security.InvalidKeyException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import org.apache.guacamole.GuacamoleClientException; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleUnsupportedException; +import org.apache.guacamole.auth.totp.conf.ConfigurationService; +import org.apache.guacamole.auth.totp.form.AuthenticationCodeField; +import org.apache.guacamole.form.Field; +import org.apache.guacamole.net.auth.AuthenticatedUser; +import org.apache.guacamole.net.auth.Credentials; +import org.apache.guacamole.net.auth.User; +import org.apache.guacamole.net.auth.UserContext; +import org.apache.guacamole.net.auth.credentials.CredentialsInfo; +import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; +import org.apache.guacamole.totp.TOTPGenerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Service for verifying the identity of a user using TOTP. + */ +public class UserVerificationService { + + /** + * Logger for this class. + */ + private final Logger logger = LoggerFactory.getLogger(UserVerificationService.class); + + /** + * BaseEncoding instance which decoded/encodes base32. + */ + private static final BaseEncoding BASE32 = BaseEncoding.base32(); + + /** + * Service for retrieving configuration information. + */ + @Inject + private ConfigurationService confService; + + /** + * Provider for AuthenticationCodeField instances. + */ + @Inject + private Provider<AuthenticationCodeField> codeFieldProvider; + + /** + * Retrieves and decodes the base32-encoded TOTP key associated with user + * having the given UserContext. If no TOTP key is associated with the user, + * a random key is generated and associated with the user. If the extension + * storing the user does not support storage of the TOTP key, null is + * returned. + * + * @param context + * The UserContext of the user whose TOTP key should be retrieved. + * + * @param username + * The username of the user associated with the given UserContext. + * + * @return + * The TOTP key associated with the user having the given UserContext, + * or null if the extension storing the user does not support storage + * of the TOTP key. + * + * @throws GuacamoleException + * If a new key is generated, but the extension storing the associated + * user fails while updating the user account. + */ + private UserTOTPKey getKey(UserContext context, + String username) throws GuacamoleException { + + // Retrieve attributes from current user + User self = context.self(); + Map<String, String> attributes = context.self().getAttributes(); + + // If no key is defined, attempt to generate a new key + String secret = attributes.get(TOTPUser.TOTP_KEY_SECRET_ATTRIBUTE_NAME); + if (secret == null) { + + // Generate random key for user + TOTPGenerator.Mode mode = confService.getMode(); + UserTOTPKey generated = new UserTOTPKey(username,mode.getRecommendedKeyLength()); + if (setKey(context, generated)) + return generated; + + // Fail if key cannot be set + return null; + + } + + // Parse retrieved base32 key value + byte[] key; + try { + key = BASE32.decode(secret); + } + + // If key is not valid base32, warn but otherwise pretend the key does + // not exist + catch (IllegalArgumentException e) { + logger.warn("TOTP key of user \"{}\" is not valid base32.", self.getIdentifier()); + logger.debug("TOTP key is not valid base32.", e); + return null; + } + + // Otherwise, parse value from attributes + boolean confirmed = "true".equals(attributes.get(TOTPUser.TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME)); + return new UserTOTPKey(username, key, confirmed); + + } + + /** + * Attempts to store the given TOTP key within the user account of the user + * having the given UserContext. As not all extensions will support storage + * of arbitrary attributes, this operation may fail. + * + * @param context + * The UserContext associated with the user whose TOTP key is to be + * stored. + * + * @param key + * The TOTP key to store. + * + * @return + * true if the TOTP key was successfully stored, false if the extension + * handling storage does not support storage of the key. + * + * @throws GuacamoleException + * If the extension handling storage fails internally while attempting + * to update the user. + */ + private boolean setKey(UserContext context, UserTOTPKey key) + throws GuacamoleException { + + // Get mutable set of attributes + User self = context.self(); + Map<String, String> attributes = new HashMap<String, String>(); + + // Set/overwrite current TOTP key state + attributes.put(TOTPUser.TOTP_KEY_SECRET_ATTRIBUTE_NAME, BASE32.encode(key.getSecret())); + attributes.put(TOTPUser.TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME, key.isConfirmed() ? "true" : "false"); + self.setAttributes(attributes); + + // Confirm that attributes have actually been set + Map<String, String> setAttributes = self.getAttributes(); + if (!setAttributes.containsKey(TOTPUser.TOTP_KEY_SECRET_ATTRIBUTE_NAME) + || !setAttributes.containsKey(TOTPUser.TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME)) + return false; + + // Update user object + try { + context.getUserDirectory().update(self); + } + catch (GuacamoleUnsupportedException e) { + logger.debug("Extension storage for user is explicitly read-only. " + + "Cannot update attributes to store TOTP key.", e); + return false; + } + + // TOTP key successfully stored/updated + return true; + + } + + /** + * Verifies the identity of the given user using TOTP. If a authentication + * code from the user's TOTP device has not already been provided, a code is + * requested in the form of additional expected credentials. Any provided + * code is cryptographically verified. If no code is present, or the + * received code is invalid, an exception is thrown. + * + * @param context + * The UserContext provided for the user by another authentication + * extension. + * + * @param authenticatedUser + * The user whose identity should be verified using TOTP. + * + * @throws GuacamoleException + * If required TOTP-specific configuration options are missing or + * malformed, or if the user's identity cannot be verified. + */ + public void verifyIdentity(UserContext context, + AuthenticatedUser authenticatedUser) throws GuacamoleException { + + // Ignore anonymous users + String username = authenticatedUser.getIdentifier(); + if (username.equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER)) + return; + + // Ignore users which do not have an associated key + UserTOTPKey key = getKey(context, username); + if (key == null) + return; + + // Pull the original HTTP request used to authenticate + Credentials credentials = authenticatedUser.getCredentials(); + HttpServletRequest request = credentials.getRequest(); + + // Retrieve TOTP from request + String code = request.getParameter(AuthenticationCodeField.PARAMETER_NAME); + + // If no TOTP provided, request one + if (code == null) { + + AuthenticationCodeField field = codeFieldProvider.get(); + + // If the user hasn't completed enrollment, request that they do + if (!key.isConfirmed()) { + field.exposeKey(key); + throw new GuacamoleInsufficientCredentialsException( + "TOTP.INFO_ENROLL_REQUIRED", new CredentialsInfo( + Collections.<Field>singletonList(field) + )); + } + + // Otherwise simply request the user's authentication code + throw new GuacamoleInsufficientCredentialsException( + "TOTP.INFO_CODE_REQUIRED", new CredentialsInfo( + Collections.<Field>singletonList(field) + )); + + } + + try { + + // Get generator based on user's key and provided configuration + TOTPGenerator totp = new TOTPGenerator(key.getSecret(), + confService.getMode(), confService.getDigits()); + + // Verify provided TOTP against value produced by generator + if (code.equals(totp.generate()) || code.equals(totp.previous())) { + + // Record key as confirmed, if it hasn't already been so recorded + if (!key.isConfirmed()) { + key.setConfirmed(true); + setKey(context, key); + } + + // User has been verified + return; + + } + + } + catch (InvalidKeyException e) { + logger.warn("User \"{}\" is associated with an invalid TOTP key.", username); + logger.debug("TOTP key is not valid.", e); + } + + // Provided code is not valid + throw new GuacamoleClientException("TOTP.INFO_VERIFICATION_FAILED"); + + } + +}