GUACAMOLE-136: Remove DuoWeb Java API from codebase. Re-implement cleanly from scratch.

This commit is contained in:
Michael Jumper
2016-12-10 01:06:04 -08:00
parent cf6a2b84ab
commit 9056bb0f4f
10 changed files with 789 additions and 1889 deletions

View File

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

View File

@@ -1,8 +0,0 @@
package com.duosecurity.duoweb;
public class DuoWebException extends Exception {
public DuoWebException(String message) {
super(message);
}
}

View File

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

View File

@@ -21,6 +21,7 @@ package org.apache.guacamole.auth.duo;
import com.google.inject.AbstractModule;
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.environment.Environment;
import org.apache.guacamole.environment.LocalEnvironment;
@@ -73,7 +74,7 @@ public class DuoAuthenticationProviderModule extends AbstractModule {
// Bind Duo-specific services
bind(ConfigurationService.class);
bind(DuoWebService.class);
bind(DuoService.class);
bind(UserVerificationService.class);
}

View File

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

View File

@@ -24,6 +24,7 @@ import java.util.Collections;
import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.GuacamoleClientException;
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.form.DuoSignedResponseField;
import org.apache.guacamole.form.Field;
@@ -44,10 +45,10 @@ public class UserVerificationService {
private ConfigurationService confService;
/**
* Service for verifying users with the DuoWeb API.
* Service for verifying users against Duo.
*/
@Inject
private DuoWebService duoWebService;
private DuoService duoService;
/**
* Verifies the identity of the given user via the Duo multi-factor
@@ -86,7 +87,7 @@ public class UserVerificationService {
// Duo API endpoint
Field signedResponseField = new DuoSignedResponseField(
confService.getAPIHostname(),
duoWebService.createSignedRequest(authenticatedUser));
duoService.createSignedRequest(authenticatedUser));
// Create an overall description of the additional credentials
// required to verify identity
@@ -100,7 +101,7 @@ public class UserVerificationService {
}
// 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");
}

View File

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

View File

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

View File

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