GUACAMOLE-96: Ensure valid codes cannot be reused.

Project: http://git-wip-us.apache.org/repos/asf/guacamole-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/guacamole-client/commit/b1c23f20
Tree: http://git-wip-us.apache.org/repos/asf/guacamole-client/tree/b1c23f20
Diff: http://git-wip-us.apache.org/repos/asf/guacamole-client/diff/b1c23f20

Branch: refs/heads/master
Commit: b1c23f20d00b030cb8a8691f8aad1d53a341f8ff
Parents: 456b8a0
Author: Michael Jumper <mjum...@apache.org>
Authored: Wed Nov 22 18:53:29 2017 -0800
Committer: Michael Jumper <mjum...@apache.org>
Committed: Sun Feb 4 19:45:18 2018 -0800

----------------------------------------------------------------------
 .../auth/totp/TOTPAuthenticationProvider.java   |   3 +-
 .../totp/TOTPAuthenticationProviderModule.java  |   2 +
 .../totp/user/CodeUsageTrackingService.java     | 264 +++++++++++++++++++
 .../auth/totp/user/UserVerificationService.java |   9 +-
 4 files changed, 276 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/b1c23f20/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 28e2380..4f18304 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
@@ -23,6 +23,7 @@ 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.CodeUsageTrackingService;
 import org.apache.guacamole.auth.totp.user.TOTPUserContext;
 import org.apache.guacamole.net.auth.AuthenticatedUser;
 import org.apache.guacamole.net.auth.AuthenticationProvider;
@@ -119,7 +120,7 @@ public class TOTPAuthenticationProvider implements 
AuthenticationProvider {
 
     @Override
     public void shutdown() {
-        // Do nothing
+        injector.getInstance(CodeUsageTrackingService.class).shutdown();
     }
 
 }

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/b1c23f20/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 94b7232..d1f7f96 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
@@ -23,6 +23,7 @@ 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;
+import org.apache.guacamole.auth.totp.user.CodeUsageTrackingService;
 import org.apache.guacamole.environment.Environment;
 import org.apache.guacamole.environment.LocalEnvironment;
 import org.apache.guacamole.net.auth.AuthenticationProvider;
@@ -73,6 +74,7 @@ public class TOTPAuthenticationProviderModule extends 
AbstractModule {
         bind(Environment.class).toInstance(environment);
 
         // Bind TOTP-specific services
+        bind(CodeUsageTrackingService.class);
         bind(ConfigurationService.class);
         bind(UserVerificationService.class);
 

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/b1c23f20/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/CodeUsageTrackingService.java
----------------------------------------------------------------------
diff --git 
a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/CodeUsageTrackingService.java
 
b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/CodeUsageTrackingService.java
new file mode 100644
index 0000000..c9a94b4
--- /dev/null
+++ 
b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/CodeUsageTrackingService.java
@@ -0,0 +1,264 @@
+/*
+ * 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.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.totp.conf.ConfigurationService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Service for tracking past valid uses of TOTP codes. An internal thread
+ * periodically walks through records of past codes, removing records which
+ * should be invalid by their own nature (no longer matching codes generated by
+ * the secret key).
+ */
+@Singleton
+public class CodeUsageTrackingService {
+
+    /**
+     * The number of periods during which a previously-used code should remain
+     * unusable. Once this period has elapsed, the code can be reused again if
+     * it is otherwise valid.
+     */
+    private static final int INVALID_INTERVAL = 2;
+
+    /**
+     * Logger for this class.
+     */
+    private final Logger logger = 
LoggerFactory.getLogger(CodeUsageTrackingService.class);
+
+    /**
+     * Executor service which runs the cleanup task.
+     */
+    private final ScheduledExecutorService executor = 
Executors.newScheduledThreadPool(1);
+
+    /**
+     * Service for retrieving configuration information.
+     */
+    @Inject
+    private ConfigurationService confService;
+
+    /**
+     * Map of previously-used codes to the timestamp after which the code can
+     * be used again, providing the TOTP key legitimately generates that code.
+     */
+    private final ConcurrentMap<UsedCode, Long> invalidCodes =
+            new ConcurrentHashMap<UsedCode, Long>();
+
+    /**
+     * Creates a new CodeUsageTrackingService which tracks past valid uses of
+     * TOTP codes on a per-user basis.
+     */
+    public CodeUsageTrackingService() {
+        executor.scheduleAtFixedRate(new CodeEvictionTask(), 1, 1, 
TimeUnit.MINUTES);
+    }
+
+    /**
+     * Task which iterates through all explicitly-invalidated codes, evicting
+     * those codes which are old enough that they would fail validation against
+     * the secret key anyway.
+     */
+    private class CodeEvictionTask implements Runnable {
+
+        @Override
+        public void run() {
+
+            // Get start time of cleanup check
+            long checkStart = System.currentTimeMillis();
+
+            // For each code still being tracked, remove those which are old
+            // enough that they would fail validation against the secret key
+            Iterator<Map.Entry<UsedCode, Long>> entries = 
invalidCodes.entrySet().iterator();
+            while (entries.hasNext()) {
+
+                Map.Entry<UsedCode, Long> entry = entries.next();
+                long invalidUntil = entry.getValue();
+
+                // If code is sufficiently old, evict it and check the next one
+                if (checkStart >= invalidUntil)
+                    entries.remove();
+
+            }
+
+            // Log completion and duration
+            logger.debug("TOTP tracking cleanup check completed in {} ms.",
+                    System.currentTimeMillis() - checkStart);
+
+        }
+
+    }
+
+    /**
+     * A valid TOTP code which was previously used by a particular user.
+     */
+    private class UsedCode {
+
+        /**
+         * The username of the user which previously used this code.
+         */
+        private final String username;
+
+        /**
+         * The valid code given by the user.
+         */
+        private final String code;
+
+        /**
+         * Creates a new UsedCode which records the given code as having been
+         * used by the given user.
+         *
+         * @param username
+         *     The username of the user which previously used the given code.
+         *
+         * @param code
+         *     The valid code given by the user.
+         */
+        public UsedCode(String username, String code) {
+            this.username = username;
+            this.code = code;
+        }
+
+        /**
+         * Returns the username of the user which previously used the code
+         * associated with this UsedCode.
+         *
+         * @return
+         *     The username of the user which previously used this code.
+         */
+        public String getUsername() {
+            return username;
+        }
+
+        /**
+         * Returns the valid code given by the user when this UsedCode was
+         * created.
+         *
+         * @return
+         *     The valid code given by the user.
+         */
+        public String getCode() {
+            return code;
+        }
+
+        @Override
+        public int hashCode() {
+            int hash = 7;
+            hash = 79 * hash + this.username.hashCode();
+            hash = 79 * hash + this.code.hashCode();
+            return hash;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+
+            if (this == obj)
+                return true;
+
+            if (obj == null)
+                return false;
+
+            if (getClass() != obj.getClass())
+                return false;
+
+            final UsedCode other = (UsedCode) obj;
+            return username.equals(other.username) && code.equals(other.code);
+
+        }
+
+    }
+
+    /**
+     * Attempts to mark the given code as used. The code MUST have already been
+     * validated against the user's secret key, as this function only verifies
+     * whether the code has been previously used, not whether it is actually
+     * valid. If the code has not previously been used, the code is stored as
+     * having been used by the given user at the current time.
+     *
+     * @param username
+     *     The username of the user who has attempted to use the given valid
+     *     code.
+     *
+     * @param code
+     *     The otherwise-valid code given by the user.
+     *
+     * @return
+     *     true if the code has not previously been used by the given user and
+     *     has now been marked as previously used, false otherwise.
+     *
+     * @throws GuacamoleException
+     *     If configuration information necessary to determine the length of
+     *     time a code should be marked as invalid cannot be read from
+     *     guacamole.properties.
+     */
+    public boolean useCode(String username, String code)
+            throws GuacamoleException {
+
+        // Repeatedly attempt to use the given code until an explicit success
+        // or failure has occurred
+        UsedCode usedCode = new UsedCode(username, code);
+        for (;;) {
+
+            // Explicitly invalidate each used code for two periods after its
+            // first successful use
+            long current = System.currentTimeMillis();
+            long invalidUntil = current + confService.getPeriod() * 1000 * 
INVALID_INTERVAL;
+
+            // Try to use the given code, marking it as used within the map of
+            // now-invalidated codes
+            Long expires = invalidCodes.putIfAbsent(usedCode, invalidUntil);
+            if (expires == null)
+                return true;
+
+            // If the code was already used, fail to use the code if
+            // insufficient time has elapsed since it was last used
+            // successfully
+            if (expires > current)
+                return false;
+
+
+            // Otherwise, the code is actually valid - remove the invalidated
+            // code only if it still has the expected expiration time, and
+            // retry using the code
+            invalidCodes.remove(usedCode, expires);
+
+        }
+
+    }
+
+    /**
+     * Cleans up resources which may be in use by this service in the
+     * background, such as other threads. This function MUST be invoked during
+     * webapp shutdown to avoid leaking these resources.
+     */
+    public void shutdown() {
+        executor.shutdownNow();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/b1c23f20/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
index 8264efd..30108e1 100644
--- 
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
@@ -65,6 +65,12 @@ public class UserVerificationService {
     private ConfigurationService confService;
 
     /**
+     * Service for tracking whether TOTP codes have been used.
+     */
+    @Inject
+    private CodeUsageTrackingService codeService;
+
+    /**
      * Provider for AuthenticationCodeField instances.
      */
     @Inject
@@ -254,7 +260,8 @@ public class UserVerificationService {
                     confService.getMode(), confService.getDigits());
 
             // Verify provided TOTP against value produced by generator
-            if (code.equals(totp.generate()) || code.equals(totp.previous())) {
+            if ((code.equals(totp.generate()) || code.equals(totp.previous()))
+                    && codeService.useCode(username, code)) {
 
                 // Record key as confirmed, if it hasn't already been so 
recorded
                 if (!key.isConfirmed()) {

Reply via email to