mirror of
				https://github.com/gyurix1968/guacamole-client.git
				synced 2025-10-31 17:13:21 +00:00 
			
		
		
		
	GUACAMOLE-96: Abstract TOTP key into separate class with confirmation semantics.
This commit is contained in:
		| @@ -0,0 +1,126 @@ | ||||
| /* | ||||
|  * 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(); | ||||
|  | ||||
|     /** | ||||
|      * 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 length | ||||
|      *     The length of the key to generate, in bytes. | ||||
|      */ | ||||
|     public UserTOTPKey(int length) { | ||||
|         this(generateBytes(length), false); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates a new UserTOTPKey containing the given key and having the given | ||||
|      * confirmed state. | ||||
|      * | ||||
|      * @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(byte[] secret, boolean confirmed) { | ||||
|         this.confirmed = confirmed; | ||||
|         this.secret = secret; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -22,14 +22,17 @@ package org.apache.guacamole.auth.totp; | ||||
| import com.google.common.io.BaseEncoding; | ||||
| 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.form.Field; | ||||
| import org.apache.guacamole.form.TextField; | ||||
| 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; | ||||
| @@ -50,7 +53,13 @@ public class UserVerificationService { | ||||
|     /** | ||||
|      * The name of the user attribute which stores the TOTP key. | ||||
|      */ | ||||
|     private static final String TOTP_KEY_ATTRIBUTE_NAME = "guac-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"; | ||||
|  | ||||
|     /** | ||||
|      * The name of the HTTP parameter which will contain the TOTP code provided | ||||
| @@ -78,20 +87,115 @@ public class UserVerificationService { | ||||
|     private static final BaseEncoding BASE32 = BaseEncoding.base32(); | ||||
|  | ||||
|     /** | ||||
|      * Retrieves the base32-encoded TOTP key associated with user having the | ||||
|      * given UserContext. If no TOTP key is associated with the user, null is | ||||
|      * 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. | ||||
|      * | ||||
|      * @return | ||||
|      *     The base32-encoded TOTP key associated with user having the given | ||||
|      *     UserContext, or null if no TOTP key is associated with the user. | ||||
|      *     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. | ||||
|      */ | ||||
|     public String getKey(UserContext context){ | ||||
|     private UserTOTPKey getKey(UserContext context) throws GuacamoleException { | ||||
|  | ||||
|         // Retrieve attributes from current user | ||||
|         User self = context.self(); | ||||
|         Map<String, String> attributes = context.self().getAttributes(); | ||||
|         return attributes.get(TOTP_KEY_ATTRIBUTE_NAME); | ||||
|  | ||||
|         // 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 | ||||
|             UserTOTPKey generated = new UserTOTPKey(TOTPGenerator.Mode.SHA1.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(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; | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -121,8 +225,8 @@ public class UserVerificationService { | ||||
|             return; | ||||
|  | ||||
|         // Ignore users which do not have an associated key | ||||
|         String encodedKey = getKey(context); | ||||
|         if (encodedKey == null) | ||||
|         UserTOTPKey key = getKey(context); | ||||
|         if (key == null) | ||||
|             return; | ||||
|  | ||||
|         // Pull the original HTTP request used to authenticate | ||||
| @@ -133,27 +237,36 @@ public class UserVerificationService { | ||||
|         String code = request.getParameter(TOTP_PARAMETER_NAME); | ||||
|  | ||||
|         // If no TOTP provided, request one | ||||
|         if (code == null) | ||||
|         if (code == null) { | ||||
|  | ||||
|             // FIXME: Handle key.isConfirmed() for initial prompt | ||||
|             throw new GuacamoleInsufficientCredentialsException( | ||||
|                     "LOGIN.INFO_TOTP_REQUIRED", TOTP_CREDENTIALS); | ||||
|  | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|  | ||||
|             // Verify provided TOTP against value produced by generator | ||||
|             byte[] key = BASE32.decode(encodedKey); | ||||
|             TOTPGenerator totp = new TOTPGenerator(key, TOTPGenerator.Mode.SHA1, 6); | ||||
|             if (code.equals(totp.generate()) || code.equals(totp.previous())) | ||||
|             TOTPGenerator totp = new TOTPGenerator(key.getSecret(), TOTPGenerator.Mode.SHA1, 6); | ||||
|             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); | ||||
|         } | ||||
|         catch (IllegalArgumentException e) { | ||||
|             logger.warn("TOTP key of user \"{}\" is not valid base32.", username); | ||||
|             logger.debug("TOTP key is not valid base32.", e); | ||||
|         } | ||||
|  | ||||
|         // Provided code is not valid | ||||
|         throw new GuacamoleClientException("LOGIN.INFO_TOTP_VERIFICATION_FAILED"); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user