mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 05:07:41 +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 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);
|
||||
|
||||
}
|
||||
|
@@ -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 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");
|
||||
|
||||
}
|
||||
|
@@ -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