GUACAMOLE-96: Add configuration parameters for details of TOTP generation.

This commit is contained in:
Michael Jumper
2017-11-20 14:29:03 -08:00
parent 170a11bf2a
commit a422fdf9c2
5 changed files with 277 additions and 29 deletions

View File

@@ -21,6 +21,7 @@ package org.apache.guacamole.auth.totp;
import com.google.inject.AbstractModule;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.totp.conf.ConfigurationService;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.environment.LocalEnvironment;
import org.apache.guacamole.net.auth.AuthenticationProvider;
@@ -71,6 +72,7 @@ public class TOTPAuthenticationProviderModule extends AbstractModule {
bind(Environment.class).toInstance(environment);
// Bind TOTP-specific services
bind(ConfigurationService.class);
bind(UserVerificationService.class);
}

View File

@@ -20,6 +20,8 @@
package org.apache.guacamole.auth.totp;
import com.google.common.io.BaseEncoding;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.security.InvalidKeyException;
import java.util.Collections;
import java.util.HashMap;
@@ -28,6 +30,7 @@ import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleUnsupportedException;
import org.apache.guacamole.auth.totp.conf.ConfigurationService;
import org.apache.guacamole.auth.totp.form.AuthenticationCodeField;
import org.apache.guacamole.form.Field;
import org.apache.guacamole.net.auth.AuthenticatedUser;
@@ -66,6 +69,18 @@ public class UserVerificationService {
*/
private static final BaseEncoding BASE32 = BaseEncoding.base32();
/**
* Service for retrieving configuration information.
*/
@Inject
private ConfigurationService confService;
/**
* Provider for AuthenticationCodeField instances.
*/
@Inject
private Provider<AuthenticationCodeField> codeFieldProvider;
/**
* Retrieves and decodes the base32-encoded TOTP key associated with user
* having the given UserContext. If no TOTP key is associated with the user,
@@ -100,7 +115,8 @@ public class UserVerificationService {
if (secret == null) {
// Generate random key for user
UserTOTPKey generated = new UserTOTPKey(username, TOTPGenerator.Mode.SHA1.getRecommendedKeyLength());
TOTPGenerator.Mode mode = confService.getMode();
UserTOTPKey generated = new UserTOTPKey(username,mode.getRecommendedKeyLength());
if (setKey(context, generated))
return generated;
@@ -223,25 +239,32 @@ public class UserVerificationService {
// If no TOTP provided, request one
if (code == null) {
AuthenticationCodeField field = codeFieldProvider.get();
// If the user hasn't completed enrollment, request that they do
if (!key.isConfirmed())
if (!key.isConfirmed()) {
field.exposeKey(key);
throw new GuacamoleInsufficientCredentialsException(
"LOGIN.INFO_TOTP_REQUIRED", new CredentialsInfo(
Collections.<Field>singletonList(new AuthenticationCodeField(key))
Collections.<Field>singletonList(field)
));
}
// Otherwise simply request the user's authentication code
throw new GuacamoleInsufficientCredentialsException(
"LOGIN.INFO_TOTP_REQUIRED", new CredentialsInfo(
Collections.<Field>singletonList(new AuthenticationCodeField())
Collections.<Field>singletonList(field)
));
}
try {
// Get generator based on user's key and provided configuration
TOTPGenerator totp = new TOTPGenerator(key.getSecret(),
confService.getMode(), confService.getDigits());
// Verify provided TOTP against value produced by generator
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

View File

@@ -0,0 +1,161 @@
/*
* 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.conf;
import com.google.inject.Inject;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
import org.apache.guacamole.properties.StringGuacamoleProperty;
import org.apache.guacamole.totp.TOTPGenerator;
/**
* Service for retrieving configuration information regarding the TOTP
* authentication extension.
*/
public class ConfigurationService {
/**
* The Guacamole server environment.
*/
@Inject
private Environment environment;
/**
* The human-readable name of the entity issuing user accounts. By default,
* this will be "Apache Guacamole".
*/
private static final StringGuacamoleProperty TOTP_ISSUER =
new StringGuacamoleProperty() {
@Override
public String getName() { return "totp-issuer"; }
};
/**
* The number of digits which should be included in each generated TOTP
* code. By default, this will be 6.
*/
private static final IntegerGuacamoleProperty TOTP_DIGITS=
new IntegerGuacamoleProperty() {
@Override
public String getName() { return "totp-digits"; }
};
/**
* The duration that each generated code should remain valid, in seconds.
* By default, this will be 30.
*/
private static final IntegerGuacamoleProperty TOTP_PERIOD =
new IntegerGuacamoleProperty() {
@Override
public String getName() { return "totp-period"; }
};
/**
* The hash algorithm that should be used to generate TOTP codes. By
* default, this will be "sha1". Legal values are "sha1", "sha256", and
* "sha512".
*/
private static final TOTPModeProperty TOTP_MODE =
new TOTPModeProperty() {
@Override
public String getName() { return "totp-mode"; }
};
/**
* Returns the human-readable name of the entity issuing user accounts. If
* not specified, "Apache Guacamole" will be used by default.
*
* @return
* The human-readable name of the entity issuing user accounts.
*
* @throws GuacamoleException
* If the "totp-issuer" property cannot be read from
* guacamole.properties.
*/
public String getIssuer() throws GuacamoleException {
return environment.getProperty(TOTP_ISSUER, "Apache Guacamole");
}
/**
* Returns the number of digits which should be included in each generated
* TOTP code. If not specified, 6 will be used by default.
*
* @return
* The number of digits which should be included in each generated
* TOTP code.
*
* @throws GuacamoleException
* If the "totp-digits" property cannot be read from
* guacamole.properties.
*/
public int getDigits() throws GuacamoleException {
// Validate legal number of digits
int digits = environment.getProperty(TOTP_DIGITS, 6);
if (digits < 6 || digits > 8)
throw new GuacamoleServerException("TOTP codes may have no fewer "
+ "than 6 digits and no more than 8 digits.");
return digits;
}
/**
* Returns the duration that each generated code should remain valid, in
* seconds. If not specified, 30 will be used by default.
*
* @return
* The duration that each generated code should remain valid, in
* seconds.
*
* @throws GuacamoleException
* If the "totp-period" property cannot be read from
* guacamole.properties.
*/
public int getPeriod() throws GuacamoleException {
return environment.getProperty(TOTP_PERIOD, 30);
}
/**
* Returns the hash algorithm that should be used to generate TOTP codes. If
* not specified, SHA1 will be used by default.
*
* @return
* The hash algorithm that should be used to generate TOTP codes.
*
* @throws GuacamoleException
* If the "totp-mode" property cannot be read from
* guacamole.properties.
*/
public TOTPGenerator.Mode getMode() throws GuacamoleException {
return environment.getProperty(TOTP_MODE, TOTPGenerator.Mode.SHA1);
}
}

View File

@@ -0,0 +1,62 @@
/*
* 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.conf;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.properties.GuacamoleProperty;
import org.apache.guacamole.totp.TOTPGenerator;
/**
* A GuacamoleProperty whose value is a TOTP generation method. The string
* values "sha1", "sha256", and "sha512" are each parsed to their corresponding
* values within the TOTPGenerator.Mode enum. All other string values result in
* parse errors.
*/
public abstract class TOTPModeProperty
implements GuacamoleProperty<TOTPGenerator.Mode> {
@Override
public TOTPGenerator.Mode parseValue(String value)
throws GuacamoleException {
// If no value provided, return null.
if (value == null)
return null;
// SHA1
if (value.equals("sha1"))
return TOTPGenerator.Mode.SHA1;
// SHA256
if (value.equals("sha256"))
return TOTPGenerator.Mode.SHA256;
// SHA512
if (value.equals("sha512"))
return TOTPGenerator.Mode.SHA512;
// The provided value is not legal
throw new GuacamoleServerException("TOTP mode must be one of "
+ "\"sha1\", \"sha256\", or \"sha512\".");
}
}

View File

@@ -20,6 +20,7 @@
package org.apache.guacamole.auth.totp.form;
import com.google.common.io.BaseEncoding;
import com.google.inject.Inject;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
@@ -32,6 +33,7 @@ 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.auth.totp.conf.ConfigurationService;
import org.apache.guacamole.form.Field;
import org.codehaus.jackson.annotate.JsonProperty;
@@ -66,31 +68,34 @@ public class AuthenticationCodeField extends Field {
*/
private static final BaseEncoding BASE32 = BaseEncoding.base32();
/**
* Service for retrieving configuration information.
*/
@Inject
private ConfigurationService confService;
/**
* 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, 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;
}
private UserTOTPKey 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() {
this(null);
super(PARAMETER_NAME, FIELD_TYPE_NAME);
}
/**
* Exposes the given key to facilitate enrollment.
*
* @param key
* The TOTP key to expose to the user for the sake of enrollment.
*/
public void exposeKey(UserTOTPKey key) {
this.key = key;
}
/**
@@ -114,20 +119,15 @@ public class AuthenticationCodeField extends Field {
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)
String issuer = confService.getIssuer();
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)
.queryParam("algorithm", confService.getMode())
.queryParam("digits", confService.getDigits())
.queryParam("period", confService.getPeriod())
.build();
}