mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-07 05:31:22 +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 com.google.common.io.BaseEncoding;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import org.apache.guacamole.GuacamoleClientException;
|
import org.apache.guacamole.GuacamoleClientException;
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.GuacamoleUnsupportedException;
|
||||||
import org.apache.guacamole.form.Field;
|
import org.apache.guacamole.form.Field;
|
||||||
import org.apache.guacamole.form.TextField;
|
import org.apache.guacamole.form.TextField;
|
||||||
import org.apache.guacamole.net.auth.AuthenticatedUser;
|
import org.apache.guacamole.net.auth.AuthenticatedUser;
|
||||||
import org.apache.guacamole.net.auth.Credentials;
|
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.UserContext;
|
||||||
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
|
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
|
||||||
import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
|
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.
|
* 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
|
* 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();
|
private static final BaseEncoding BASE32 = BaseEncoding.base32();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the base32-encoded TOTP key associated with user having the
|
* Retrieves and decodes the base32-encoded TOTP key associated with user
|
||||||
* given UserContext. If no TOTP key is associated with the user, null is
|
* 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.
|
* returned.
|
||||||
*
|
*
|
||||||
* @param context
|
* @param context
|
||||||
* The UserContext of the user whose TOTP key should be retrieved.
|
* The UserContext of the user whose TOTP key should be retrieved.
|
||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
* The base32-encoded TOTP key associated with user having the given
|
* The TOTP key associated with the user having the given UserContext,
|
||||||
* UserContext, or null if no TOTP key is associated with the user.
|
* 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();
|
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;
|
return;
|
||||||
|
|
||||||
// Ignore users which do not have an associated key
|
// Ignore users which do not have an associated key
|
||||||
String encodedKey = getKey(context);
|
UserTOTPKey key = getKey(context);
|
||||||
if (encodedKey == null)
|
if (key == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Pull the original HTTP request used to authenticate
|
// Pull the original HTTP request used to authenticate
|
||||||
@@ -133,27 +237,36 @@ public class UserVerificationService {
|
|||||||
String code = request.getParameter(TOTP_PARAMETER_NAME);
|
String code = request.getParameter(TOTP_PARAMETER_NAME);
|
||||||
|
|
||||||
// If no TOTP provided, request one
|
// If no TOTP provided, request one
|
||||||
if (code == null)
|
if (code == null) {
|
||||||
|
|
||||||
|
// FIXME: Handle key.isConfirmed() for initial prompt
|
||||||
throw new GuacamoleInsufficientCredentialsException(
|
throw new GuacamoleInsufficientCredentialsException(
|
||||||
"LOGIN.INFO_TOTP_REQUIRED", TOTP_CREDENTIALS);
|
"LOGIN.INFO_TOTP_REQUIRED", TOTP_CREDENTIALS);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// Verify provided TOTP against value produced by generator
|
// Verify provided TOTP against value produced by generator
|
||||||
byte[] key = BASE32.decode(encodedKey);
|
TOTPGenerator totp = new TOTPGenerator(key.getSecret(), TOTPGenerator.Mode.SHA1, 6);
|
||||||
TOTPGenerator totp = new TOTPGenerator(key, TOTPGenerator.Mode.SHA1, 6);
|
if (code.equals(totp.generate()) || code.equals(totp.previous())) {
|
||||||
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;
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (InvalidKeyException e) {
|
catch (InvalidKeyException e) {
|
||||||
logger.warn("User \"{}\" is associated with an invalid TOTP key.", username);
|
logger.warn("User \"{}\" is associated with an invalid TOTP key.", username);
|
||||||
logger.debug("TOTP key is not valid.", e);
|
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
|
// Provided code is not valid
|
||||||
throw new GuacamoleClientException("LOGIN.INFO_TOTP_VERIFICATION_FAILED");
|
throw new GuacamoleClientException("LOGIN.INFO_TOTP_VERIFICATION_FAILED");
|
||||||
|
Reference in New Issue
Block a user