mirror of
				https://github.com/gyurix1968/guacamole-client.git
				synced 2025-10-31 09:03:21 +00:00 
			
		
		
		
	GUACAMOLE-96: Handle enrollment via QR code for unconfirmed users.
This commit is contained in:
		| @@ -32,6 +32,11 @@ public class UserTOTPKey { | ||||
|      */ | ||||
|     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 | ||||
| @@ -63,17 +68,23 @@ public class UserTOTPKey { | ||||
|      * 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(int length) { | ||||
|         this(generateBytes(length), false); | ||||
|     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. | ||||
|      * | ||||
| @@ -82,11 +93,22 @@ public class UserTOTPKey { | ||||
|      *     successfully generate the corresponding TOTP codes (the user has | ||||
|      *     been "enrolled"), false otherwise. | ||||
|      */ | ||||
|     public UserTOTPKey(byte[] secret, boolean confirmed) { | ||||
|     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. | ||||
|      * | ||||
|   | ||||
| @@ -76,6 +76,9 @@ public class UserVerificationService { | ||||
|      * @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 | ||||
| @@ -85,7 +88,8 @@ public class UserVerificationService { | ||||
|      *     If a new key is generated, but the extension storing the associated | ||||
|      *     user fails while updating the user account. | ||||
|      */ | ||||
|     private UserTOTPKey getKey(UserContext context) throws GuacamoleException { | ||||
|     private UserTOTPKey getKey(UserContext context, | ||||
|             String username) throws GuacamoleException { | ||||
|  | ||||
|         // Retrieve attributes from current user | ||||
|         User self = context.self(); | ||||
| @@ -96,7 +100,7 @@ public class UserVerificationService { | ||||
|         if (secret == null) { | ||||
|  | ||||
|             // Generate random key for user | ||||
|             UserTOTPKey generated = new UserTOTPKey(TOTPGenerator.Mode.SHA1.getRecommendedKeyLength()); | ||||
|             UserTOTPKey generated = new UserTOTPKey(username, TOTPGenerator.Mode.SHA1.getRecommendedKeyLength()); | ||||
|             if (setKey(context, generated)) | ||||
|                 return generated; | ||||
|  | ||||
| @@ -121,7 +125,7 @@ public class UserVerificationService { | ||||
|  | ||||
|         // Otherwise, parse value from attributes | ||||
|         boolean confirmed = "true".equals(attributes.get(TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME)); | ||||
|         return new UserTOTPKey(key, confirmed); | ||||
|         return new UserTOTPKey(username, key, confirmed); | ||||
|  | ||||
|     } | ||||
|  | ||||
| @@ -205,7 +209,7 @@ public class UserVerificationService { | ||||
|             return; | ||||
|  | ||||
|         // Ignore users which do not have an associated key | ||||
|         UserTOTPKey key = getKey(context); | ||||
|         UserTOTPKey key = getKey(context, username); | ||||
|         if (key == null) | ||||
|             return; | ||||
|  | ||||
| @@ -219,7 +223,14 @@ public class UserVerificationService { | ||||
|         // If no TOTP provided, request one | ||||
|         if (code == null) { | ||||
|  | ||||
|             // FIXME: Handle key.isConfirmed() for initial prompt | ||||
|             // If the user hasn't completed enrollment, request that they do | ||||
|             if (!key.isConfirmed()) | ||||
|                 throw new GuacamoleInsufficientCredentialsException( | ||||
|                         "LOGIN.INFO_TOTP_REQUIRED", new CredentialsInfo( | ||||
|                             Collections.<Field>singletonList(new AuthenticationCodeField(key)) | ||||
|                         )); | ||||
|  | ||||
|             // Otherwise simply request the user's authentication code | ||||
|             throw new GuacamoleInsufficientCredentialsException( | ||||
|                     "LOGIN.INFO_TOTP_REQUIRED", new CredentialsInfo( | ||||
|                         Collections.<Field>singletonList(new AuthenticationCodeField()) | ||||
|   | ||||
| @@ -19,7 +19,21 @@ | ||||
|  | ||||
| package org.apache.guacamole.auth.totp.form; | ||||
|  | ||||
| import com.google.common.io.BaseEncoding; | ||||
| import com.google.zxing.BarcodeFormat; | ||||
| import com.google.zxing.WriterException; | ||||
| import com.google.zxing.client.j2se.MatrixToImageWriter; | ||||
| import com.google.zxing.common.BitMatrix; | ||||
| import com.google.zxing.qrcode.QRCodeWriter; | ||||
| import java.io.ByteArrayOutputStream; | ||||
| import java.io.IOException; | ||||
| 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.form.Field; | ||||
| import org.codehaus.jackson.annotate.JsonProperty; | ||||
|  | ||||
| /** | ||||
|  * Field which prompts the user for an authentication code generated via TOTP. | ||||
| @@ -37,12 +51,135 @@ public class AuthenticationCodeField extends Field { | ||||
|      */ | ||||
|     private static final String FIELD_TYPE_NAME = "GUAC_TOTP_CODE"; | ||||
|  | ||||
|     /** | ||||
|      * The width of QR codes to generate, in pixels. | ||||
|      */ | ||||
|     private static final int QR_CODE_WIDTH = 256; | ||||
|  | ||||
|     /** | ||||
|      * The height of QR codes to generate, in pixels. | ||||
|      */ | ||||
|     private static final int QR_CODE_HEIGHT = 256; | ||||
|  | ||||
|     /** | ||||
|      * BaseEncoding which encodes/decodes base32. | ||||
|      */ | ||||
|     private static final BaseEncoding BASE32 = BaseEncoding.base32(); | ||||
|  | ||||
|     /** | ||||
|      * The TOTP key to expose to the user for the sake of enrollment, if any. | ||||
|      * If no such key should be exposed to the user, this will be null. | ||||
|      */ | ||||
|     private final UserTOTPKey key; | ||||
|  | ||||
|     /** | ||||
|      * Creates a new field which prompts the user for an authentication code | ||||
|      * generated via TOTP. | ||||
|      * generated via TOTP, and provide the user with their TOTP key to | ||||
|      * facilitate enrollment. | ||||
|      * | ||||
|      * @param key | ||||
|      *     The TOTP key to expose to the user for the sake of enrollment. | ||||
|      */ | ||||
|     public AuthenticationCodeField(UserTOTPKey key) { | ||||
|         super(PARAMETER_NAME, FIELD_TYPE_NAME); | ||||
|         this.key = key; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates a new field which prompts the user for an authentication code | ||||
|      * generated via TOTP. The user's TOTP key is not exposed for enrollment. | ||||
|      */ | ||||
|     public AuthenticationCodeField() { | ||||
|         super(PARAMETER_NAME, FIELD_TYPE_NAME); | ||||
|         this(null); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the "otpauth" URI for the secret key used to generate TOTP codes | ||||
|      * for the current user. If the secret key is not being exposed to | ||||
|      * facilitate enrollment, null is returned. | ||||
|      * | ||||
|      * @return | ||||
|      *     The "otpauth" URI for the secret key used to generate TOTP codes | ||||
|      *     for the current user, or null is the secret ket is not being exposed | ||||
|      *     to facilitate enrollment. | ||||
|      * | ||||
|      * @throws GuacamoleException | ||||
|      *     If the configuration information required for generating the key URI | ||||
|      *     cannot be read from guacamole.properties. | ||||
|      */ | ||||
|     @JsonProperty("keyUri") | ||||
|     public URI getKeyURI() throws GuacamoleException { | ||||
|  | ||||
|         // Do not generate a key URI if no key is being exposed | ||||
|         if (key == null) | ||||
|             return null; | ||||
|  | ||||
|         // FIXME: Pull from configuration | ||||
|         String issuer = "Some Issuer"; | ||||
|         String algorithm = "SHA1"; | ||||
|         String digits = "6"; | ||||
|         String period = "30"; | ||||
|  | ||||
|         // Format "otpauth" URL (see https://github.com/google/google-authenticator/wiki/Key-Uri-Format) | ||||
|         return UriBuilder.fromUri("otpauth://totp/") | ||||
|                 .path(issuer + ":" + key.getUsername()) | ||||
|                 .queryParam("secret", BASE32.encode(key.getSecret())) | ||||
|                 .queryParam("issuer", issuer) | ||||
|                 .queryParam("algorithm", algorithm) | ||||
|                 .queryParam("digits", digits) | ||||
|                 .queryParam("period", period) | ||||
|                 .build(); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the URL of a QR code describing the user's TOTP key and | ||||
|      * configuration. If the key is not being exposed for enrollment, null is | ||||
|      * returned. | ||||
|      * | ||||
|      * @return  | ||||
|      *     The URL of a QR code describing the user's TOTP key and | ||||
|      *     configuration, or null if the key is not being exposed for | ||||
|      *     enrollment. | ||||
|      * | ||||
|      * @throws GuacamoleException | ||||
|      *     If the configuration information required for generating the QR code | ||||
|      *     cannot be read from guacamole.properties. | ||||
|      */ | ||||
|     @JsonProperty("qrCode") | ||||
|     public String getQRCode() throws GuacamoleException { | ||||
|  | ||||
|         // Do not generate a QR code if no key is being exposed | ||||
|         URI keyURI = getKeyURI(); | ||||
|         if (keyURI == null) | ||||
|             return null; | ||||
|  | ||||
|         ByteArrayOutputStream stream = new ByteArrayOutputStream(); | ||||
|  | ||||
|         try { | ||||
|  | ||||
|             // Create QR code writer | ||||
|             QRCodeWriter writer = new QRCodeWriter(); | ||||
|             BitMatrix matrix = writer.encode(keyURI.toString(), | ||||
|                     BarcodeFormat.QR_CODE, QR_CODE_WIDTH, QR_CODE_HEIGHT); | ||||
|  | ||||
|             // Produce PNG image of TOTP key text | ||||
|             MatrixToImageWriter.writeToStream(matrix, "PNG", stream); | ||||
|  | ||||
|         } | ||||
|         catch (WriterException e) { | ||||
|             throw new IllegalArgumentException("QR code could not be " | ||||
|                     + "generated for TOTP key.", e); | ||||
|         } | ||||
|         catch (IOException e) { | ||||
|             throw new IllegalStateException("Image stream of QR code could " | ||||
|                     + "not be written.", e); | ||||
|         } | ||||
|  | ||||
|         // Return data URI for generated image | ||||
|         return "data:image/png;base64," | ||||
|                 + DatatypeConverter.printBase64Binary(stream.toByteArray()); | ||||
|  | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user