mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-07 05:31:22 +00:00
GUACAMOLE-136: Remove DuoWeb Java API from codebase. Re-implement cleanly from scratch.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,138 +0,0 @@
|
|||||||
package com.duosecurity.duoweb;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
|
|
||||||
public final class DuoWeb {
|
|
||||||
private static final String DUO_PREFIX = "TX";
|
|
||||||
private static final String APP_PREFIX = "APP";
|
|
||||||
private static final String AUTH_PREFIX = "AUTH";
|
|
||||||
|
|
||||||
private static final int DUO_EXPIRE = 300;
|
|
||||||
private static final int APP_EXPIRE = 3600;
|
|
||||||
|
|
||||||
private static final int IKEY_LEN = 20;
|
|
||||||
private static final int SKEY_LEN = 40;
|
|
||||||
private static final int AKEY_LEN = 40;
|
|
||||||
|
|
||||||
public static final String ERR_USER = "ERR|The username passed to sign_request() is invalid.";
|
|
||||||
public static final String ERR_IKEY = "ERR|The Duo integration key passed to sign_request() is invalid.";
|
|
||||||
public static final String ERR_SKEY = "ERR|The Duo secret key passed to sign_request() is invalid.";
|
|
||||||
public static final String ERR_AKEY = "ERR|The application secret key passed to sign_request() must be at least " + AKEY_LEN + " characters.";
|
|
||||||
public static final String ERR_UNKNOWN = "ERR|An unknown error has occurred.";
|
|
||||||
|
|
||||||
public static String signRequest(final String ikey, final String skey, final String akey, final String username) {
|
|
||||||
return signRequest(ikey, skey, akey, username, System.currentTimeMillis() / 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String signRequest(final String ikey, final String skey, final String akey, final String username, final long time) {
|
|
||||||
final String duo_sig;
|
|
||||||
final String app_sig;
|
|
||||||
|
|
||||||
if (username.equals("")) {
|
|
||||||
return ERR_USER;
|
|
||||||
}
|
|
||||||
if (username.indexOf('|') != -1) {
|
|
||||||
return ERR_USER;
|
|
||||||
}
|
|
||||||
if (ikey.equals("") || ikey.length() != IKEY_LEN) {
|
|
||||||
return ERR_IKEY;
|
|
||||||
}
|
|
||||||
if (skey.equals("") || skey.length() != SKEY_LEN) {
|
|
||||||
return ERR_SKEY;
|
|
||||||
}
|
|
||||||
if (akey.equals("") || akey.length() < AKEY_LEN) {
|
|
||||||
return ERR_AKEY;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
duo_sig = signVals(skey, username, ikey, DUO_PREFIX, DUO_EXPIRE, time);
|
|
||||||
app_sig = signVals(akey, username, ikey, APP_PREFIX, APP_EXPIRE, time);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return ERR_UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
return duo_sig + ":" + app_sig;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String verifyResponse(final String ikey, final String skey, final String akey, final String sig_response)
|
|
||||||
throws DuoWebException, NoSuchAlgorithmException, InvalidKeyException, IOException {
|
|
||||||
return verifyResponse(ikey, skey, akey, sig_response, System.currentTimeMillis() / 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String verifyResponse(final String ikey, final String skey, final String akey, final String sig_response, final long time)
|
|
||||||
throws DuoWebException, NoSuchAlgorithmException, InvalidKeyException, IOException {
|
|
||||||
String auth_user = null;
|
|
||||||
String app_user = null;
|
|
||||||
|
|
||||||
final String[] sigs = sig_response.split(":");
|
|
||||||
final String auth_sig = sigs[0];
|
|
||||||
final String app_sig = sigs[1];
|
|
||||||
|
|
||||||
auth_user = parseVals(skey, auth_sig, AUTH_PREFIX, ikey, time);
|
|
||||||
app_user = parseVals(akey, app_sig, APP_PREFIX, ikey, time);
|
|
||||||
|
|
||||||
if (!auth_user.equals(app_user)) {
|
|
||||||
throw new DuoWebException("Authentication failed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return auth_user;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String signVals(final String key, final String username, final String ikey, final String prefix, final int expire, final long time)
|
|
||||||
throws InvalidKeyException, NoSuchAlgorithmException {
|
|
||||||
final long expire_ts = time + expire;
|
|
||||||
final String exp = Long.toString(expire_ts);
|
|
||||||
|
|
||||||
final String val = username + "|" + ikey + "|" + exp;
|
|
||||||
final String cookie = prefix + "|" + Base64.encodeBytes(val.getBytes());
|
|
||||||
final String sig = Util.hmacSign(key, cookie);
|
|
||||||
|
|
||||||
return cookie + "|" + sig;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String parseVals(final String key, final String val, final String prefix, final String ikey, final long time)
|
|
||||||
throws InvalidKeyException, NoSuchAlgorithmException, IOException, DuoWebException {
|
|
||||||
|
|
||||||
final String[] parts = val.split("\\|");
|
|
||||||
if (parts.length != 3) {
|
|
||||||
throw new DuoWebException("Invalid response");
|
|
||||||
}
|
|
||||||
|
|
||||||
final String u_prefix = parts[0];
|
|
||||||
final String u_b64 = parts[1];
|
|
||||||
final String u_sig = parts[2];
|
|
||||||
|
|
||||||
final String sig = Util.hmacSign(key, u_prefix + "|" + u_b64);
|
|
||||||
if (!Util.hmacSign(key, sig).equals(Util.hmacSign(key, u_sig))) {
|
|
||||||
throw new DuoWebException("Invalid response");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!u_prefix.equals(prefix)) {
|
|
||||||
throw new DuoWebException("Invalid response");
|
|
||||||
}
|
|
||||||
|
|
||||||
final byte[] decoded = Base64.decode(u_b64);
|
|
||||||
final String cookie = new String(decoded);
|
|
||||||
|
|
||||||
final String[] cookie_parts = cookie.split("\\|");
|
|
||||||
if (cookie_parts.length != 3) {
|
|
||||||
throw new DuoWebException("Invalid response");
|
|
||||||
}
|
|
||||||
final String username = cookie_parts[0];
|
|
||||||
final String u_ikey = cookie_parts[1];
|
|
||||||
final String expire = cookie_parts[2];
|
|
||||||
|
|
||||||
if (!u_ikey.equals(ikey)) {
|
|
||||||
throw new DuoWebException("Invalid response");
|
|
||||||
}
|
|
||||||
|
|
||||||
final long expire_ts = Long.parseLong(expire);
|
|
||||||
if (time >= expire_ts) {
|
|
||||||
throw new DuoWebException("Transaction has expired. Please check that the system time is correct.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return username;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,8 +0,0 @@
|
|||||||
package com.duosecurity.duoweb;
|
|
||||||
|
|
||||||
public class DuoWebException extends Exception {
|
|
||||||
|
|
||||||
public DuoWebException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,26 +0,0 @@
|
|||||||
package com.duosecurity.duoweb;
|
|
||||||
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
|
|
||||||
import javax.crypto.Mac;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
|
|
||||||
public class Util {
|
|
||||||
public static String hmacSign(String skey, String data)
|
|
||||||
throws NoSuchAlgorithmException, InvalidKeyException {
|
|
||||||
SecretKeySpec key = new SecretKeySpec(skey.getBytes(), "HmacSHA1");
|
|
||||||
Mac mac = Mac.getInstance("HmacSHA1");
|
|
||||||
mac.init(key);
|
|
||||||
byte[] raw = mac.doFinal(data.getBytes());
|
|
||||||
return bytesToHex(raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String bytesToHex(byte[] b) {
|
|
||||||
String result = "";
|
|
||||||
for (int i = 0; i < b.length; i++) {
|
|
||||||
result += Integer.toString((b[i] & 0xff) + 0x100, 16).substring(1);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -21,6 +21,7 @@ package org.apache.guacamole.auth.duo;
|
|||||||
|
|
||||||
import com.google.inject.AbstractModule;
|
import com.google.inject.AbstractModule;
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.auth.duo.api.DuoService;
|
||||||
import org.apache.guacamole.auth.duo.conf.ConfigurationService;
|
import org.apache.guacamole.auth.duo.conf.ConfigurationService;
|
||||||
import org.apache.guacamole.environment.Environment;
|
import org.apache.guacamole.environment.Environment;
|
||||||
import org.apache.guacamole.environment.LocalEnvironment;
|
import org.apache.guacamole.environment.LocalEnvironment;
|
||||||
@@ -73,7 +74,7 @@ public class DuoAuthenticationProviderModule extends AbstractModule {
|
|||||||
|
|
||||||
// Bind Duo-specific services
|
// Bind Duo-specific services
|
||||||
bind(ConfigurationService.class);
|
bind(ConfigurationService.class);
|
||||||
bind(DuoWebService.class);
|
bind(DuoService.class);
|
||||||
bind(UserVerificationService.class);
|
bind(UserVerificationService.class);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,212 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.duo;
|
|
||||||
|
|
||||||
import com.duosecurity.duoweb.DuoWeb;
|
|
||||||
import com.duosecurity.duoweb.DuoWebException;
|
|
||||||
import com.google.inject.Inject;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import org.apache.guacamole.GuacamoleClientException;
|
|
||||||
import org.apache.guacamole.GuacamoleException;
|
|
||||||
import org.apache.guacamole.GuacamoleServerException;
|
|
||||||
import org.apache.guacamole.auth.duo.conf.ConfigurationService;
|
|
||||||
import org.apache.guacamole.net.auth.AuthenticatedUser;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service which wraps the DuoWeb Java API, providing predictable behavior and
|
|
||||||
* error handling.
|
|
||||||
*/
|
|
||||||
public class DuoWebService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A regular expression which matches a valid signature part of a Duo
|
|
||||||
* signed response. A signature part may not contain pipe symbols (which
|
|
||||||
* act as delimiters between parts) nor colons (which act as delimiters
|
|
||||||
* between signatures).
|
|
||||||
*/
|
|
||||||
private final String SIGNATURE_PART = "[^:|]*";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A regular expression which matches a valid signature within a Duo
|
|
||||||
* signed response. Each signature is made up of three distinct parts,
|
|
||||||
* separated by pipe symbols.
|
|
||||||
*/
|
|
||||||
private final String SIGNATURE = SIGNATURE_PART + "\\|" + SIGNATURE_PART + "\\|" + SIGNATURE_PART;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A regular expression which matches a valid Duo signed response. Each
|
|
||||||
* response is made up of two signatures, separated by a colon.
|
|
||||||
*/
|
|
||||||
private final String RESPONSE = SIGNATURE + ":" + SIGNATURE;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Pattern which matches valid Duo signed responses. Strings which will
|
|
||||||
* be passed to DuoWeb.verifyResponse() MUST be matched against this
|
|
||||||
* Pattern. Strings which do not match this Pattern may cause
|
|
||||||
* DuoWeb.verifyResponse() to throw unchecked exceptions.
|
|
||||||
*/
|
|
||||||
private final Pattern RESPONSE_PATTERN = Pattern.compile(RESPONSE);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service for retrieving Duo configuration information.
|
|
||||||
*/
|
|
||||||
@Inject
|
|
||||||
private ConfigurationService confService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates and signs a new request to verify the identity of the given
|
|
||||||
* user. This request may ultimately be sent to Duo, resulting in a signed
|
|
||||||
* response from Duo if that verification succeeds.
|
|
||||||
*
|
|
||||||
* @param authenticatedUser
|
|
||||||
* The user whose identity should be verified.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
* A signed user verification request which can be sent to Duo.
|
|
||||||
*
|
|
||||||
* @throws GuacamoleException
|
|
||||||
* If required Duo-specific configuration options are missing or
|
|
||||||
* invalid, or if an error occurs within the DuoWeb API which prevents
|
|
||||||
* generation of the signed request.
|
|
||||||
*/
|
|
||||||
public String createSignedRequest(AuthenticatedUser authenticatedUser)
|
|
||||||
throws GuacamoleException {
|
|
||||||
|
|
||||||
// Retrieve username from externally-authenticated user
|
|
||||||
String username = authenticatedUser.getIdentifier();
|
|
||||||
|
|
||||||
// Retrieve Duo-specific keys from configuration
|
|
||||||
String ikey = confService.getIntegrationKey();
|
|
||||||
String skey = confService.getSecretKey();
|
|
||||||
String akey = confService.getApplicationKey();
|
|
||||||
|
|
||||||
// Create signed request for the provided user
|
|
||||||
String signedRequest = DuoWeb.signRequest(ikey, skey, akey, username);
|
|
||||||
|
|
||||||
if (DuoWeb.ERR_AKEY.equals(signedRequest))
|
|
||||||
throw new GuacamoleServerException("The Duo application key "
|
|
||||||
+ "must is not valid. Duo application keys must be at "
|
|
||||||
+ "least 40 characters long.");
|
|
||||||
|
|
||||||
if (DuoWeb.ERR_IKEY.equals(signedRequest))
|
|
||||||
throw new GuacamoleServerException("The provided Duo integration "
|
|
||||||
+ "key is not valid. Integration keys must be exactly 20 "
|
|
||||||
+ "characters long.");
|
|
||||||
|
|
||||||
if (DuoWeb.ERR_SKEY.equals(signedRequest))
|
|
||||||
throw new GuacamoleServerException("The provided Duo secret key "
|
|
||||||
+ "is not valid. Secret keys must be exactly 40 "
|
|
||||||
+ "characters long.");
|
|
||||||
|
|
||||||
if (DuoWeb.ERR_USER.equals(signedRequest))
|
|
||||||
throw new GuacamoleServerException("The provided username is "
|
|
||||||
+ "not valid. Duo usernames may not be blank, nor may "
|
|
||||||
+ "they contain pipe symbols (\"|\").");
|
|
||||||
|
|
||||||
if (DuoWeb.ERR_UNKNOWN.equals(signedRequest))
|
|
||||||
throw new GuacamoleServerException("An unknown error within the "
|
|
||||||
+ "DuoWeb API prevented the signed request from being "
|
|
||||||
+ "generated.");
|
|
||||||
|
|
||||||
// Return signed request if no error is indicated
|
|
||||||
return signedRequest;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether the given signed response is a valid response from Duo
|
|
||||||
* which verifies the identity of the given user. If the given response is
|
|
||||||
* invalid or does not verify the identity of the given user (including if
|
|
||||||
* it is a valid response which verifies the identity of a DIFFERENT user),
|
|
||||||
* false is returned.
|
|
||||||
*
|
|
||||||
* @param authenticatedUser
|
|
||||||
* The user that the given signed response should verify.
|
|
||||||
*
|
|
||||||
* @param signedResponse
|
|
||||||
* The signed response received from Duo in response to a signed
|
|
||||||
* request.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
* true if the signed response is a valid response from Duo AND verifies
|
|
||||||
* the identity of the given user, false otherwise.
|
|
||||||
*
|
|
||||||
* @throws GuacamoleException
|
|
||||||
* If required Duo-specific configuration options are missing or
|
|
||||||
* invalid, or if an error occurs within the DuoWeb API which prevents
|
|
||||||
* validation of the signed response.
|
|
||||||
*/
|
|
||||||
public boolean isValidSignedResponse(AuthenticatedUser authenticatedUser,
|
|
||||||
String signedResponse) throws GuacamoleException {
|
|
||||||
|
|
||||||
// Verify signature response format will not cause
|
|
||||||
// DuoWeb.verifyResponse() to fail with unchecked exceptions
|
|
||||||
Matcher responseMatcher = RESPONSE_PATTERN.matcher(signedResponse);
|
|
||||||
if (!responseMatcher.matches())
|
|
||||||
throw new GuacamoleClientException("Invalid Duo response format.");
|
|
||||||
|
|
||||||
// Retrieve username from externally-authenticated user
|
|
||||||
String username = authenticatedUser.getIdentifier();
|
|
||||||
|
|
||||||
// Retrieve Duo-specific keys from configuration
|
|
||||||
String ikey = confService.getIntegrationKey();
|
|
||||||
String skey = confService.getSecretKey();
|
|
||||||
String akey = confService.getApplicationKey();
|
|
||||||
|
|
||||||
// Verify validity of signed response
|
|
||||||
String verifiedUsername;
|
|
||||||
try {
|
|
||||||
verifiedUsername = DuoWeb.verifyResponse(ikey, skey, akey,
|
|
||||||
signedResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rethrow any errors as appropriate GuacamoleExceptions
|
|
||||||
catch (IOException e) {
|
|
||||||
throw new GuacamoleClientException("Decoding of Duo response "
|
|
||||||
+ "failed: Invalid base64 content.", e);
|
|
||||||
}
|
|
||||||
catch (NumberFormatException e) {
|
|
||||||
throw new GuacamoleClientException("Decoding of Duo response "
|
|
||||||
+ "failed: Invalid expiry timestamp.", e);
|
|
||||||
}
|
|
||||||
catch (InvalidKeyException e) {
|
|
||||||
throw new GuacamoleServerException("Unable to produce HMAC "
|
|
||||||
+ "signature: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
catch (NoSuchAlgorithmException e) {
|
|
||||||
throw new GuacamoleServerException("Environment is missing "
|
|
||||||
+ "support for producing HMAC-SHA1 signatures.", e);
|
|
||||||
}
|
|
||||||
catch (DuoWebException e) {
|
|
||||||
throw new GuacamoleClientException("Duo response verification "
|
|
||||||
+ "failed: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signed response is valid iff the associated username matches the
|
|
||||||
// user's username
|
|
||||||
return username.equals(verifiedUsername);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -24,6 +24,7 @@ import java.util.Collections;
|
|||||||
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.auth.duo.api.DuoService;
|
||||||
import org.apache.guacamole.auth.duo.conf.ConfigurationService;
|
import org.apache.guacamole.auth.duo.conf.ConfigurationService;
|
||||||
import org.apache.guacamole.auth.duo.form.DuoSignedResponseField;
|
import org.apache.guacamole.auth.duo.form.DuoSignedResponseField;
|
||||||
import org.apache.guacamole.form.Field;
|
import org.apache.guacamole.form.Field;
|
||||||
@@ -44,10 +45,10 @@ public class UserVerificationService {
|
|||||||
private ConfigurationService confService;
|
private ConfigurationService confService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for verifying users with the DuoWeb API.
|
* Service for verifying users against Duo.
|
||||||
*/
|
*/
|
||||||
@Inject
|
@Inject
|
||||||
private DuoWebService duoWebService;
|
private DuoService duoService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies the identity of the given user via the Duo multi-factor
|
* Verifies the identity of the given user via the Duo multi-factor
|
||||||
@@ -86,7 +87,7 @@ public class UserVerificationService {
|
|||||||
// Duo API endpoint
|
// Duo API endpoint
|
||||||
Field signedResponseField = new DuoSignedResponseField(
|
Field signedResponseField = new DuoSignedResponseField(
|
||||||
confService.getAPIHostname(),
|
confService.getAPIHostname(),
|
||||||
duoWebService.createSignedRequest(authenticatedUser));
|
duoService.createSignedRequest(authenticatedUser));
|
||||||
|
|
||||||
// Create an overall description of the additional credentials
|
// Create an overall description of the additional credentials
|
||||||
// required to verify identity
|
// required to verify identity
|
||||||
@@ -100,7 +101,7 @@ public class UserVerificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If signed response does not verify this user's identity, abort auth
|
// If signed response does not verify this user's identity, abort auth
|
||||||
if (!duoWebService.isValidSignedResponse(authenticatedUser, signedResponse))
|
if (!duoService.isValidSignedResponse(authenticatedUser, signedResponse))
|
||||||
throw new GuacamoleClientException("LOGIN.INFO_DUO_VALIDATION_CODE_INCORRECT");
|
throw new GuacamoleClientException("LOGIN.INFO_DUO_VALIDATION_CODE_INCORRECT");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,245 @@
|
|||||||
|
/*
|
||||||
|
* 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.duo.api;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import javax.xml.bind.DatatypeConverter;
|
||||||
|
import org.apache.guacamole.GuacamoleClientException;
|
||||||
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data which describes the identity of the user being verified by Duo.
|
||||||
|
*/
|
||||||
|
public class DuoCookie {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pattern which matches valid cookies. Each cookie is made up of three
|
||||||
|
* sections, separated from each other by pipe symbols ("|").
|
||||||
|
*/
|
||||||
|
private static final Pattern COOKIE_FORMAT = Pattern.compile("([^|]+)\\|([^|]+)\\|([0-9]+)");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The index of the capturing group within COOKIE_FORMAT which contains the
|
||||||
|
* username.
|
||||||
|
*/
|
||||||
|
private static final int USERNAME_GROUP = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The index of the capturing group within COOKIE_FORMAT which contains the
|
||||||
|
* integration key.
|
||||||
|
*/
|
||||||
|
private static final int INTEGRATION_KEY_GROUP = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The index of the capturing group within COOKIE_FORMAT which contains the
|
||||||
|
* expiration timestamp.
|
||||||
|
*/
|
||||||
|
private static final int EXPIRATION_TIMESTAMP_GROUP = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The username of the user being verified.
|
||||||
|
*/
|
||||||
|
private final String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The integration key provided by Duo and specific to this deployment of
|
||||||
|
* Guacamole.
|
||||||
|
*/
|
||||||
|
private final String integrationKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time that this cookie expires, in seconds since midnight of
|
||||||
|
* 1970-01-01 (UTC).
|
||||||
|
*/
|
||||||
|
private final long expires;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new DuoCookie which describes the identity of a user being
|
||||||
|
* verified.
|
||||||
|
*
|
||||||
|
* @param username
|
||||||
|
* The username of the user being verified.
|
||||||
|
*
|
||||||
|
* @param integrationKey
|
||||||
|
* The integration key provided by Duo and specific to this deployment
|
||||||
|
* of Guacamole.
|
||||||
|
*
|
||||||
|
* @param expires
|
||||||
|
* The time that this cookie expires, in seconds since midnight of
|
||||||
|
* 1970-01-01 (UTC).
|
||||||
|
*/
|
||||||
|
public DuoCookie(String username, String integrationKey, long expires) {
|
||||||
|
this.username = username;
|
||||||
|
this.integrationKey = integrationKey;
|
||||||
|
this.expires = expires;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the username of the user being verified.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The username of the user being verified.
|
||||||
|
*/
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the integration key provided by Duo and specific to this
|
||||||
|
* deployment of Guacamole.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The integration key provided by Duo and specific to this deployment
|
||||||
|
* of Guacamole.
|
||||||
|
*/
|
||||||
|
public String getIntegrationKey() {
|
||||||
|
return integrationKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the time that this cookie expires. The expiration time is
|
||||||
|
* represented in seconds since midnight of 1970-01-01 (UTC).
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The time that this cookie expires, in seconds since midnight of
|
||||||
|
* 1970-01-01 (UTC).
|
||||||
|
*/
|
||||||
|
public long getExpirationTimestamp(){
|
||||||
|
return expires;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current time as the number of seconds elapsed since
|
||||||
|
* midnight of 1970-01-01 (UTC).
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The current time as the number of seconds elapsed since midnight of
|
||||||
|
* 1970-01-01 (UTC).
|
||||||
|
*/
|
||||||
|
public static long currentTimestamp() {
|
||||||
|
return System.currentTimeMillis() / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether this cookie has expired (the current time has met or
|
||||||
|
* exceeded the expiration timestamp).
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* true if this cookie has expired, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isExpired() {
|
||||||
|
return currentTimestamp() >= expires;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a base64-encoded Duo cookie, producing a new DuoCookie object
|
||||||
|
* containing the data therein. If the given string is not a valid Duo
|
||||||
|
* cookie, an exception is thrown. Note that the cookie may be expired, and
|
||||||
|
* must be checked for expiration prior to actual use.
|
||||||
|
*
|
||||||
|
* @param str
|
||||||
|
* The base64-encoded Duo cookie to parse.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A new DuoCookie object containing the same data as the given
|
||||||
|
* base64-encoded Duo cookie string.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If the given string is not a valid base64-encoded Duo cookie.
|
||||||
|
*/
|
||||||
|
public static DuoCookie parseDuoCookie(String str) throws GuacamoleException {
|
||||||
|
|
||||||
|
// Attempt to decode data as base64
|
||||||
|
String data;
|
||||||
|
try {
|
||||||
|
data = new String(DatatypeConverter.parseBase64Binary(str), "UTF-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bail if invalid base64 is provided
|
||||||
|
catch (IllegalArgumentException e) {
|
||||||
|
throw new GuacamoleClientException("Username is not correctly "
|
||||||
|
+ "encoded as base64.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw hard errors if standard pieces of Java are missing
|
||||||
|
catch (UnsupportedEncodingException e) {
|
||||||
|
throw new UnsupportedOperationException("Unexpected lack of "
|
||||||
|
+ "UTF-8 support.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify format of provided data
|
||||||
|
Matcher matcher = COOKIE_FORMAT.matcher(data);
|
||||||
|
if (!matcher.matches())
|
||||||
|
throw new GuacamoleClientException("Format of base64-encoded "
|
||||||
|
+ "username is invalid.");
|
||||||
|
|
||||||
|
// Get username and key (simple strings)
|
||||||
|
String username = matcher.group(USERNAME_GROUP);
|
||||||
|
String key = matcher.group(INTEGRATION_KEY_GROUP);
|
||||||
|
|
||||||
|
// Parse expiration time
|
||||||
|
long expires;
|
||||||
|
try {
|
||||||
|
expires = Long.parseLong(matcher.group(EXPIRATION_TIMESTAMP_GROUP));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bail if expiration timestamp is not a valid long
|
||||||
|
catch (NumberFormatException e) {
|
||||||
|
throw new GuacamoleClientException("Expiration timestamp is "
|
||||||
|
+ "not valid.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return parsed cookie
|
||||||
|
return new DuoCookie(username, key, expires);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the base64-encoded string representation of this DuoCookie. The
|
||||||
|
* format used is identical to that required by the Duo service: the
|
||||||
|
* username, integration key, and expiration timestamp separated by pipe
|
||||||
|
* symbols ("|") and encoded with base64.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The base64-encoded string representation of this DuoCookie.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Separate each cookie field with pipe symbols
|
||||||
|
String data = username + "|" + integrationKey + "|" + expires;
|
||||||
|
|
||||||
|
// Encode resulting cookie string with base64
|
||||||
|
return DatatypeConverter.printBase64Binary(data.getBytes("UTF-8"));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw hard errors if standard pieces of Java are missing
|
||||||
|
catch (UnsupportedEncodingException e) {
|
||||||
|
throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,205 @@
|
|||||||
|
/*
|
||||||
|
* 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.duo.api;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.auth.duo.conf.ConfigurationService;
|
||||||
|
import org.apache.guacamole.net.auth.AuthenticatedUser;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service which produces signed requests and parses/verifies signed responses
|
||||||
|
* as required by Duo's API.
|
||||||
|
*/
|
||||||
|
public class DuoService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger for this class.
|
||||||
|
*/
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(DuoService.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pattern which matches valid Duo responses. Each response is made up of
|
||||||
|
* two sections, separated from each other by a colon, where each section
|
||||||
|
* is a signed Duo cookie.
|
||||||
|
*/
|
||||||
|
private static final Pattern RESPONSE_FORMAT = Pattern.compile("([^:]+):([^:]+)");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The index of the capturing group within RESPONSE_FORMAT which
|
||||||
|
* contains the DUO_RESPONSE cookie signed by the secret key.
|
||||||
|
*/
|
||||||
|
private static final int DUO_COOKIE_GROUP = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The index of the capturing group within RESPONSE_FORMAT which
|
||||||
|
* contains the APPLICATION cookie signed by the application key.
|
||||||
|
*/
|
||||||
|
private static final int APP_COOKIE_GROUP = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The amount of time that each generated cookie remains valid, in seconds.
|
||||||
|
*/
|
||||||
|
private static final int COOKIE_EXPIRATION_TIME = 300;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for retrieving Duo configuration information.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
private ConfigurationService confService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and signs a new request to verify the identity of the given
|
||||||
|
* user. This request may ultimately be sent to Duo, resulting in a signed
|
||||||
|
* response from Duo if that verification succeeds.
|
||||||
|
*
|
||||||
|
* @param authenticatedUser
|
||||||
|
* The user whose identity should be verified.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A signed user verification request which can be sent to Duo.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If required Duo-specific configuration options are missing or
|
||||||
|
* invalid, or if an error prevents generation of the signature.
|
||||||
|
*/
|
||||||
|
public String createSignedRequest(AuthenticatedUser authenticatedUser)
|
||||||
|
throws GuacamoleException {
|
||||||
|
|
||||||
|
// Generate a cookie associating the username with the integration key
|
||||||
|
DuoCookie cookie = new DuoCookie(authenticatedUser.getIdentifier(),
|
||||||
|
confService.getIntegrationKey(),
|
||||||
|
DuoCookie.currentTimestamp() + COOKIE_EXPIRATION_TIME);
|
||||||
|
|
||||||
|
// Sign cookie with secret key
|
||||||
|
SignedDuoCookie duoCookie = new SignedDuoCookie(cookie,
|
||||||
|
SignedDuoCookie.Type.DUO_REQUEST,
|
||||||
|
confService.getSecretKey());
|
||||||
|
|
||||||
|
// Sign cookie with application key
|
||||||
|
SignedDuoCookie appCookie = new SignedDuoCookie(cookie,
|
||||||
|
SignedDuoCookie.Type.APPLICATION,
|
||||||
|
confService.getApplicationKey());
|
||||||
|
|
||||||
|
// Return signed request containing both signed cookies, separated by
|
||||||
|
// a colon (as required by Duo)
|
||||||
|
return duoCookie + ":" + appCookie;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the given signed response is a valid response from Duo
|
||||||
|
* which verifies the identity of the given user. If the given response is
|
||||||
|
* invalid or does not verify the identity of the given user (including if
|
||||||
|
* it is a valid response which verifies the identity of a DIFFERENT user),
|
||||||
|
* false is returned.
|
||||||
|
*
|
||||||
|
* @param authenticatedUser
|
||||||
|
* The user that the given signed response should verify.
|
||||||
|
*
|
||||||
|
* @param signedResponse
|
||||||
|
* The signed response received from Duo in response to a signed
|
||||||
|
* request.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* true if the signed response is a valid response from Duo AND verifies
|
||||||
|
* the identity of the given user, false otherwise.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If required Duo-specific configuration options are missing or
|
||||||
|
* invalid, or if an error occurs prevents validation of the signature.
|
||||||
|
*/
|
||||||
|
public boolean isValidSignedResponse(AuthenticatedUser authenticatedUser,
|
||||||
|
String signedResponse) throws GuacamoleException {
|
||||||
|
|
||||||
|
SignedDuoCookie duoCookie;
|
||||||
|
SignedDuoCookie appCookie;
|
||||||
|
|
||||||
|
// Retrieve username from externally-authenticated user
|
||||||
|
String username = authenticatedUser.getIdentifier();
|
||||||
|
|
||||||
|
// Retrieve Duo-specific keys from configuration
|
||||||
|
String applicationKey = confService.getApplicationKey();
|
||||||
|
String integrationKey = confService.getIntegrationKey();
|
||||||
|
String secretKey = confService.getSecretKey();
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Verify format of response
|
||||||
|
Matcher matcher = RESPONSE_FORMAT.matcher(signedResponse);
|
||||||
|
if (!matcher.matches()) {
|
||||||
|
logger.debug("Duo response is not in correct format.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse signed cookie defining the user verified by Duo
|
||||||
|
duoCookie = SignedDuoCookie.parseSignedDuoCookie(secretKey,
|
||||||
|
matcher.group(DUO_COOKIE_GROUP));
|
||||||
|
|
||||||
|
// Parse signed cookie defining the user this application
|
||||||
|
// originally requested
|
||||||
|
appCookie = SignedDuoCookie.parseSignedDuoCookie(applicationKey,
|
||||||
|
matcher.group(APP_COOKIE_GROUP));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simply return false if signature fails to verify
|
||||||
|
catch (GuacamoleException e) {
|
||||||
|
logger.debug("Duo signature verification failed.", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify neither cookie is expired
|
||||||
|
if (duoCookie.isExpired() || appCookie.isExpired()) {
|
||||||
|
logger.debug("Duo response contained expired cookie(s).");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the cookies in the response have the correct types
|
||||||
|
if (duoCookie.getType() != SignedDuoCookie.Type.DUO_RESPONSE
|
||||||
|
|| appCookie.getType() != SignedDuoCookie.Type.APPLICATION) {
|
||||||
|
logger.debug("Duo response did not contain correct cookie type(s).");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify integration key matches both cookies
|
||||||
|
if (!duoCookie.getIntegrationKey().equals(integrationKey)
|
||||||
|
|| !appCookie.getIntegrationKey().equals(integrationKey)) {
|
||||||
|
logger.debug("Integration key of Duo response is incorrect.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify both cookies are for the current user
|
||||||
|
if (!duoCookie.getUsername().equals(username)
|
||||||
|
|| !appCookie.getUsername().equals(username)) {
|
||||||
|
logger.debug("Username of Duo response is incorrect.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All verifications tests pass
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,332 @@
|
|||||||
|
/*
|
||||||
|
* 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.duo.api;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import javax.xml.bind.DatatypeConverter;
|
||||||
|
import org.apache.guacamole.GuacamoleClientException;
|
||||||
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.GuacamoleServerException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DuoCookie which is cryptographically signed with a provided key using
|
||||||
|
* HMAC-SHA1.
|
||||||
|
*/
|
||||||
|
public class SignedDuoCookie extends DuoCookie {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pattern which matches valid signed cookies. Like unsigned cookies, each
|
||||||
|
* signed cookie is made up of three sections, separated from each other by
|
||||||
|
* pipe symbols ("|").
|
||||||
|
*/
|
||||||
|
private static final Pattern SIGNED_COOKIE_FORMAT = Pattern.compile("([^|]+)\\|([^|]+)\\|([0-9a-f]+)");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The index of the capturing group within SIGNED_COOKIE_FORMAT which
|
||||||
|
* contains the cookie type prefix.
|
||||||
|
*/
|
||||||
|
private static final int PREFIX_GROUP = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The index of the capturing group within SIGNED_COOKIE_FORMAT which
|
||||||
|
* contains the cookie's base64-encoded data.
|
||||||
|
*/
|
||||||
|
private static final int DATA_GROUP = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The index of the capturing group within SIGNED_COOKIE_FORMAT which
|
||||||
|
* contains the signature.
|
||||||
|
*/
|
||||||
|
private static final int SIGNATURE_GROUP = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The signature algorithm that should be used to sign the cookie, as
|
||||||
|
* defined by:
|
||||||
|
* http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Mac
|
||||||
|
*/
|
||||||
|
private static final String SIGNATURE_ALGORITHM = "HmacSHA1";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of a signed Duo cookie. Each signed Duo cookie has an
|
||||||
|
* associated type which determines the prefix included in the string
|
||||||
|
* representation of that cookie. As that type is included in the data
|
||||||
|
* that is signed, different types will result in different signatures,
|
||||||
|
* even if the data portion of the cookie is otherwise identical.
|
||||||
|
*/
|
||||||
|
public enum Type {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Duo cookie which has been signed with the secret key for inclusion
|
||||||
|
* in a Duo request.
|
||||||
|
*/
|
||||||
|
DUO_REQUEST("TX"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Duo cookie which has been signed with the secret key by Duo and
|
||||||
|
* was included in a Duo response.
|
||||||
|
*/
|
||||||
|
DUO_RESPONSE("AUTH"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Duo cookie which has been signed with the application key for
|
||||||
|
* inclusion in a Duo request. Such cookies are also included in Duo
|
||||||
|
* responses, for verification by the application.
|
||||||
|
*/
|
||||||
|
APPLICATION("APP");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The prefix associated with the Duo cookie type. This prefix will
|
||||||
|
* be included in the string representation of the cookie.
|
||||||
|
*/
|
||||||
|
private final String prefix;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Duo cookie type associated with the given string
|
||||||
|
* prefix. This prefix will be included in the string representation of
|
||||||
|
* the cookie.
|
||||||
|
*
|
||||||
|
* @param prefix
|
||||||
|
* The prefix to associated with the Duo cookie type.
|
||||||
|
*/
|
||||||
|
Type(String prefix) {
|
||||||
|
this.prefix = prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the prefix associated with the Duo cookie type.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The prefix to associated with this Duo cookie type.
|
||||||
|
*/
|
||||||
|
public String getPrefix() {
|
||||||
|
return prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the cookie type associated with the given prefix. If no such
|
||||||
|
* cookie type exists, null is returned.
|
||||||
|
*
|
||||||
|
* @param prefix
|
||||||
|
* The prefix of the cookie type to search for.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The cookie type associated with the given prefix, or null if no
|
||||||
|
* such cookie type exists.
|
||||||
|
*/
|
||||||
|
public static Type fromPrefix(String prefix) {
|
||||||
|
|
||||||
|
// Search through all defined cookie types for the given prefix
|
||||||
|
for (Type type : Type.values()) {
|
||||||
|
if (type.getPrefix().equals(prefix))
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No such cookie type exists
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of this Duo cookie.
|
||||||
|
*/
|
||||||
|
private final Type type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The signature produced when the cookie was signed with HMAC-SHA1. The
|
||||||
|
* signature covers the prefix of the type and the cookie's base64-encoded
|
||||||
|
* data, separated by a pipe symbol.
|
||||||
|
*/
|
||||||
|
private final String signature;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new SignedDuoCookie which describes the identity of a user
|
||||||
|
* being verified and is cryptographically signed with HMAC-SHA1 by a given
|
||||||
|
* key.
|
||||||
|
*
|
||||||
|
* @param cookie
|
||||||
|
* The cookie defining the identity being verified.
|
||||||
|
*
|
||||||
|
* @param type
|
||||||
|
* The type of the cookie being created.
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* The key to use to generate the cryptographic signature. This key
|
||||||
|
* will not be stored within the cookie.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If the given signing key is invalid.
|
||||||
|
*/
|
||||||
|
public SignedDuoCookie(DuoCookie cookie, Type type, String key)
|
||||||
|
throws GuacamoleException {
|
||||||
|
|
||||||
|
// Init underlying cookie
|
||||||
|
super(cookie.getUsername(), cookie.getIntegrationKey(),
|
||||||
|
cookie.getExpirationTimestamp());
|
||||||
|
|
||||||
|
// Store cookie type and signature
|
||||||
|
this.type = type;
|
||||||
|
this.signature = sign(key, type.getPrefix() + "|" + cookie.toString());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signs the given arbitrary string data with the given key using the
|
||||||
|
* algorithm defined by SIGNATURE_ALGORITHM. Both the data and the key will
|
||||||
|
* be interpreted as UTF-8 bytes.
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* The key which should be used to sign the given data.
|
||||||
|
*
|
||||||
|
* @param data
|
||||||
|
* The data being signed.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The signature produced by signing the given data with the given key,
|
||||||
|
* encoded as lowercase hexadecimal.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If the given signing key is invalid.
|
||||||
|
*/
|
||||||
|
private static String sign(String key, String data) throws GuacamoleException {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Attempt to sign UTF-8 bytes of provided data
|
||||||
|
Mac mac = Mac.getInstance(SIGNATURE_ALGORITHM);
|
||||||
|
mac.init(new SecretKeySpec(key.getBytes("UTF-8"), SIGNATURE_ALGORITHM));
|
||||||
|
|
||||||
|
// Return signature as hex
|
||||||
|
return DatatypeConverter.printHexBinary(mac.doFinal(data.getBytes("UTF-8"))).toLowerCase();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-throw any errors which prevent signature
|
||||||
|
catch (InvalidKeyException e){
|
||||||
|
throw new GuacamoleServerException("Signing key is invalid.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw hard errors if standard pieces of Java are missing
|
||||||
|
catch (UnsupportedEncodingException e) {
|
||||||
|
throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
|
||||||
|
}
|
||||||
|
catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new UnsupportedOperationException("Unexpected lack of support "
|
||||||
|
+ "for required signature algorithm "
|
||||||
|
+ "\"" + SIGNATURE_ALGORITHM + "\".", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the type of this Duo cookie. The Duo cookie type is dictated
|
||||||
|
* by the context of the cookie's use, and is included with the cookie's
|
||||||
|
* underlying data when generating the signature.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The type of this Duo cookie.
|
||||||
|
*/
|
||||||
|
public Type getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the signature produced when the cookie was signed with HMAC-SHA1.
|
||||||
|
* The signature covers the prefix of the cookie's type and the cookie's
|
||||||
|
* base64-encoded data, separated by a pipe symbol.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The signature produced when the cookie was signed with HMAC-SHA1.
|
||||||
|
*/
|
||||||
|
public String getSignature() {
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a signed Duo cookie string, such as that produced by the
|
||||||
|
* toString() function or received from the Duo service, producing a new
|
||||||
|
* SignedDuoCookie object containing the associated cookie data and
|
||||||
|
* signature. If the given string is not a valid Duo cookie, or if the
|
||||||
|
* signature is incorrect, an exception is thrown. Note that the cookie may
|
||||||
|
* be expired, and must be checked for expiration prior to actual use.
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* The key that was used to sign the Duo cookie.
|
||||||
|
*
|
||||||
|
* @param str
|
||||||
|
* The Duo cookie string to parse.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A new SignedDuoCookie object containing the same data and signature
|
||||||
|
* as the given Duo cookie string.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If the given string is not a valid Duo cookie string, or if the
|
||||||
|
* signature of the cookie is invalid.
|
||||||
|
*/
|
||||||
|
public static SignedDuoCookie parseSignedDuoCookie(String key, String str)
|
||||||
|
throws GuacamoleException {
|
||||||
|
|
||||||
|
// Verify format of provided data
|
||||||
|
Matcher matcher = SIGNED_COOKIE_FORMAT.matcher(str);
|
||||||
|
if (!matcher.matches())
|
||||||
|
throw new GuacamoleClientException("Format of signed Duo cookie "
|
||||||
|
+ "is invalid.");
|
||||||
|
|
||||||
|
// Parse type from prefix
|
||||||
|
Type type = Type.fromPrefix(matcher.group(PREFIX_GROUP));
|
||||||
|
if (type == null)
|
||||||
|
throw new GuacamoleClientException("Invalid Duo cookie prefix.");
|
||||||
|
|
||||||
|
// Parse cookie from base64-encoded data
|
||||||
|
DuoCookie cookie = DuoCookie.parseDuoCookie(matcher.group(DATA_GROUP));
|
||||||
|
|
||||||
|
// Verify signature of cookie
|
||||||
|
SignedDuoCookie signedCookie = new SignedDuoCookie(cookie, type, key);
|
||||||
|
if (!signedCookie.getSignature().equals(matcher.group(SIGNATURE_GROUP)))
|
||||||
|
throw new GuacamoleClientException("Duo cookie has incorrect signature.");
|
||||||
|
|
||||||
|
// Cookie has valid signature and has parsed successfully
|
||||||
|
return signedCookie;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the string representation of this SignedDuoCookie. The format
|
||||||
|
* used is identical to that required by the Duo service: the type prefix,
|
||||||
|
* base64-encoded cookie data, and HMAC-SHA1 signature separated by pipe
|
||||||
|
* symbols ("|").
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The string representation of this SignedDuoCookie.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return type.getPrefix() + "|" + super.toString() + "|" + signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user