mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-08 14:11: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());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,3 +1,13 @@
|
||||
<div class="totp-code-field">
|
||||
<input type="text" ng-model="model" autocorrect="off" autocapitalize="off"/>
|
||||
|
||||
<!-- QR Code (if available) -->
|
||||
<div class="totp-qr-code" ng-show="field.qrCode">
|
||||
<img ng-src="{{field.qrCode}}">
|
||||
</div>
|
||||
|
||||
<!-- Field for entry of the current TOTP code -->
|
||||
<div class="totp-code">
|
||||
<input type="text" ng-model="model" autocorrect="off" autocapitalize="off"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user