You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@guacamole.apache.org by vn...@apache.org on 2018/02/05 18:04:37 UTC

[19/21] guacamole-client git commit: GUACAMOLE-96: Block external access to TOTP-internal attributes.

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 <mj...@apache.org>
Authored: Mon Nov 20 16:15:01 2017 -0800
Committer: Michael Jumper <mj...@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");
+
+    }
+
+}