mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 13:17:41 +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