GUACAMOLE-96: Handle enrollment via QR code for unconfirmed users.

This commit is contained in:
Michael Jumper
2017-11-20 14:01:39 -08:00
parent 8ac8fec478
commit 170a11bf2a
7 changed files with 465 additions and 11 deletions

View File

@@ -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.
*

View File

@@ -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())

View File

@@ -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());
}
}

View File

@@ -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>