GUACAMOLE-197: Working RADIUS Authentication, including dealing with Challenge/Response (e.g. 2/Multi-Factor)

This commit is contained in:
Nick Couchman
2017-02-07 15:37:49 -05:00
committed by Nick Couchman
parent dbb62ded77
commit 3e994021da
10 changed files with 238 additions and 99 deletions

View File

@@ -258,6 +258,18 @@
<version>1.1.5</version> <version>1.1.5</version>
</dependency> </dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.10</version>
</dependency>
</dependencies> </dependencies>

View File

@@ -21,9 +21,11 @@ package org.apache.guacamole.auth.radius;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import java.util.Collections; import java.util.Arrays;
import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.auth.radius.user.AuthenticatedUser; import org.apache.guacamole.auth.radius.user.AuthenticatedUser;
import org.apache.guacamole.auth.radius.form.RadiusChallengeResponseField; import org.apache.guacamole.auth.radius.form.RadiusChallengeResponseField;
import org.apache.guacamole.auth.radius.form.RadiusStateField;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.form.Field; import org.apache.guacamole.form.Field;
import org.apache.guacamole.net.auth.Credentials; import org.apache.guacamole.net.auth.Credentials;
@@ -32,6 +34,7 @@ import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsExce
import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import net.jradius.dictionary.Attr_State;
import net.jradius.exception.UnknownAttributeException; import net.jradius.exception.UnknownAttributeException;
import net.jradius.packet.RadiusPacket; import net.jradius.packet.RadiusPacket;
import net.jradius.packet.AccessAccept; import net.jradius.packet.AccessAccept;
@@ -90,8 +93,27 @@ public class AuthenticationProviderService {
public AuthenticatedUser authenticateUser(Credentials credentials) public AuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException { throws GuacamoleException {
// Initialize Radius Packet and try to authenticate // Grab the HTTP Request from the credentials object
HttpServletRequest request = credentials.getRequest();
// Set up RadiusPacket object
RadiusPacket radPack; RadiusPacket radPack;
// Ignore anonymous users
if (credentials.getUsername() == null || credentials.getUsername().isEmpty())
return null;
// Password is required
if (credentials.getPassword() == null || credentials.getPassword().isEmpty())
return null;
String challengeResponse = request.getParameter(RadiusChallengeResponseField.PARAMETER_NAME);
String radiusState = request.getParameter(RadiusStateField.PARAMETER_NAME);
// We do not have a challenge response, so we proceed normally
if (challengeResponse == null || challengeResponse.isEmpty()) {
// Initialize Radius Packet and try to authenticate
try { try {
radPack = radiusService.authenticate(credentials.getUsername(), radPack = radiusService.authenticate(credentials.getUsername(),
credentials.getPassword()); credentials.getPassword());
@@ -120,15 +142,21 @@ public class AuthenticationProviderService {
*/ */
else if (radPack instanceof AccessChallenge) { else if (radPack instanceof AccessChallenge) {
try { try {
RadiusAttribute stateAttr = radPack.findAttribute(Attr_State.TYPE);
// We should have a state attribute at this point, if not, we need to quit.
if (stateAttr == null) {
logger.error("Something went wrong, state attribute not present.");
logger.debug("State Attribute turned up null, which shouldn't happen in AccessChallenge.");
throw new GuacamoleInvalidCredentialsException("Authentication error.", CredentialsInfo.USERNAME_PASSWORD);
}
String replyMsg = radPack.getAttributeValue("Reply-Message").toString(); String replyMsg = radPack.getAttributeValue("Reply-Message").toString();
String radState = radPack.getAttributeValue("State").toString(); radiusState = new String(stateAttr.getValue().getBytes());
logger.debug("RADIUS sent challenge: {}", replyMsg); Field radiusResponseField = new RadiusChallengeResponseField(replyMsg);
logger.debug("RADIUS sent state: {}", radState); Field radiusStateField = new RadiusStateField(radiusState);
Field radiusResponseField = new RadiusChallengeResponseField(credentials.getUsername(), replyMsg, radState); CredentialsInfo expectedCredentials = new CredentialsInfo(Arrays.asList(radiusResponseField,radiusStateField));
CredentialsInfo expectedCredentials = new CredentialsInfo(Collections.singletonList(radiusResponseField));
throw new GuacamoleInsufficientCredentialsException("LOGIN.INFO_RADIUS_ADDL_REQUIRED", expectedCredentials); throw new GuacamoleInsufficientCredentialsException("LOGIN.INFO_RADIUS_ADDL_REQUIRED", expectedCredentials);
} }
catch(UnknownAttributeException e) { catch (UnknownAttributeException e) {
logger.error("Error in talks with RADIUS server."); logger.error("Error in talks with RADIUS server.");
logger.debug("RADIUS challenged by didn't provide right attributes."); logger.debug("RADIUS challenged by didn't provide right attributes.");
throw new GuacamoleInvalidCredentialsException("Authentication error.", CredentialsInfo.USERNAME_PASSWORD); throw new GuacamoleInvalidCredentialsException("Authentication error.", CredentialsInfo.USERNAME_PASSWORD);
@@ -149,10 +177,40 @@ public class AuthenticationProviderService {
} }
} }
// Something else we haven't thought of has happened, so we throw an error // Something unanticipated happened, so we panic
else else
throw new GuacamoleInvalidCredentialsException("Unknown error trying to authenticate.", CredentialsInfo.USERNAME_PASSWORD); throw new GuacamoleInvalidCredentialsException("Unknown error trying to authenticate.", CredentialsInfo.USERNAME_PASSWORD);
}
// We did receive a challenge response, so we're going to send that back to the server
else {
// Initialize Radius Packet and try to authenticate
try {
radPack = radiusService.authenticate(credentials.getUsername(),
radiusState,
challengeResponse);
}
catch (GuacamoleException e) {
logger.error("Cannot configure RADIUS server: {}", e.getMessage());
logger.debug("Error configuring RADIUS server.", e);
radPack = null;
}
finally {
radiusService.disconnect();
}
if (radPack instanceof AccessAccept) {
AuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
authenticatedUser.init(credentials);
return authenticatedUser;
}
else {
logger.warn("RADIUS Challenge/Response authentication failed.");
logger.debug("Did not receive a RADIUS AccessAccept packet back from server.");
throw new GuacamoleInvalidCredentialsException("Failed to authenticate to RADIUS.", CredentialsInfo.USERNAME_PASSWORD);
}
}
} }
} }

View File

@@ -178,7 +178,6 @@ public class RadiusConnectionService {
radAttrs.add(new Attr_UserName(username)); radAttrs.add(new Attr_UserName(username));
radAttrs.add(new Attr_UserPassword(password)); radAttrs.add(new Attr_UserPassword(password));
AccessRequest radAcc = new AccessRequest(radiusClient, radAttrs); AccessRequest radAcc = new AccessRequest(radiusClient, radAttrs);
logger.debug("Sending authentication request to radius server for user {}.", username);
radAuth.setupRequest(radiusClient, radAcc); radAuth.setupRequest(radiusClient, radAcc);
radAuth.processRequest(radAcc); radAuth.processRequest(radAcc);
return radiusClient.sendReceive(radAcc, confService.getRadiusRetries()); return radiusClient.sendReceive(radAcc, confService.getRadiusRetries());
@@ -256,7 +255,6 @@ public class RadiusConnectionService {
radAttrs.add(new Attr_State(state)); radAttrs.add(new Attr_State(state));
radAttrs.add(new Attr_UserPassword(response)); radAttrs.add(new Attr_UserPassword(response));
AccessRequest radAcc = new AccessRequest(radiusClient, radAttrs); AccessRequest radAcc = new AccessRequest(radiusClient, radAttrs);
logger.debug("Sending authentication response to radius server for user {}.", username);
radAuth.setupRequest(radiusClient, radAcc); radAuth.setupRequest(radiusClient, radAcc);
radAuth.processRequest(radAcc); radAuth.processRequest(radAcc);
return radiusClient.sendReceive(radAcc, confService.getRadiusRetries()); return radiusClient.sendReceive(radAcc, confService.getRadiusRetries());

View File

@@ -34,23 +34,13 @@ public class RadiusChallengeResponseField extends Field {
/** /**
* The field returned by the RADIUS challenge/response. * The field returned by the RADIUS challenge/response.
*/ */
private static final String RADIUS_FIELD_NAME = "guac-radius-challenge-response"; public static final String PARAMETER_NAME = "guac-radius-challenge-response";
/** /**
* The type of field to initialize for the challenge/response. * The type of field to initialize for the challenge/response.
*/ */
private static final String RADIUS_FIELD_TYPE = "GUAC_RADIUS_CHALLENGE_RESPONSE"; private static final String RADIUS_FIELD_TYPE = "GUAC_RADIUS_CHALLENGE_RESPONSE";
/**
* The username used for the RADIUS authentication attempt.
*/
private final String username;
/**
* The state of the connection passed by the previous RADIUS attempt.
*/
private final String radiusState;
/** /**
* The message the RADIUS server sent back in the challenge. * The message the RADIUS server sent back in the challenge.
*/ */
@@ -59,24 +49,14 @@ public class RadiusChallengeResponseField extends Field {
/** /**
* Initialize the field with the reply message and the state. * Initialize the field with the reply message and the state.
*/ */
public RadiusChallengeResponseField(String username, String replyMsg, String radiusState) { public RadiusChallengeResponseField(String replyMsg) {
super(RADIUS_FIELD_NAME, RADIUS_FIELD_TYPE); super(PARAMETER_NAME, RADIUS_FIELD_TYPE);
logger.debug("Initializing the RADIUS challenge/response field: {}", replyMsg); logger.debug("Initializing the RADIUS challenge/response field: {}", replyMsg);
this.username = username;
this.replyMsg = replyMsg; this.replyMsg = replyMsg;
this.radiusState = radiusState;
} }
public String getUsername() {
return username;
}
public String getRadiusState() {
return radiusState;
}
public String getReplyMsg() { public String getReplyMsg() {
return replyMsg; return replyMsg;
} }

View File

@@ -0,0 +1,64 @@
/*
* 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.radius.form;
import org.apache.guacamole.form.Field;
import org.codehaus.jackson.annotate.JsonProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RadiusStateField extends Field {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(RadiusStateField.class);
/**
* The parameter returned by the RADIUS state.
*/
public static final String PARAMETER_NAME = "guac-radius-state";
/**
* The type of field to initialize for the state.
*/
private static final String RADIUS_FIELD_TYPE = "GUAC_RADIUS_STATE";
/**
* The state of the connection passed by the previous RADIUS attempt.
*/
private final String radiusState;
/**
* Initialize the field with the reply message and the state.
*/
public RadiusStateField(String radiusState) {
super(PARAMETER_NAME, RADIUS_FIELD_TYPE);
logger.debug("Initializing the RADIUS state field: {}", radiusState);
this.radiusState = radiusState;
}
public String getRadiusState() {
return radiusState;
}
}

View File

@@ -27,8 +27,13 @@ angular.module('guacRadius').config(['formServiceProvider',
// Define field for the challenge from the RADIUS service // Define field for the challenge from the RADIUS service
formServiceProvider.registerFieldType('GUAC_RADIUS_CHALLENGE_RESPONSE', { formServiceProvider.registerFieldType('GUAC_RADIUS_CHALLENGE_RESPONSE', {
module : 'guacRadius', module : 'guacRadius',
controller : 'guacRadiusController', controller : 'radiusResponseController',
templateUrl : 'app/ext/radius/templates/radiusChallengeResponseField.html' templateUrl : 'app/ext/radius/templates/radiusResponseField.html'
});
formServiceProvider.registerFieldType('GUAC_RADIUS_STATE', {
module : 'guacRadius',
controller : 'radiusStateController',
template : '<input type=hidden ng-model="model" />'
}); });
}]); }]);

View File

@@ -22,22 +22,15 @@
* API to prompt the user for additional credentials, ultimately receiving a * API to prompt the user for additional credentials, ultimately receiving a
* signed response from the Duo service. * signed response from the Duo service.
*/ */
angular.module('guacRadius').controller('guacRadiusController', ['$scope', '$element', angular.module('guacRadius').controller('radiusResponseController', ['$scope', '$element',
function guacRadiusController($scope, $element) { function radiusResponseController($scope, $element) {
console.log("In guacRadiusController() method."); console.log("In radiusResponseController() method.");
// Find the area to display the challenge message // Find the area to display the challenge message
var radiusChallenge = $element.find(document.querySelector('#radius-challenge-text')); var radiusChallenge = $element.find(document.querySelector('#radius-challenge-text'));
// Find the hidden input to put the state in
var radiusState = $element.find(document.querySelector('#radius-state'));
// Populate the reply message field // Populate the reply message field
console.log("RADIUS Reply Message: " + $scope.field.replyMsg); console.log("RADIUS Reply Message: " + $scope.field.replyMsg);
radiusChellenge.html($scope.field.replyMsg); radiusChallenge.html($scope.field.replyMsg);
// Populate the input area for the connection state
console.log("RADIUS State: " + scope.field.radiusState);
radiusState.value = $scope.field.radiusState;
}]); }]);

View File

@@ -0,0 +1,33 @@
/*
* 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.
*/
/**
* Controller for the "GUAC_RADIUS_CHALLENGE_RESPONSE" field which uses the DuoWeb
* API to prompt the user for additional credentials, ultimately receiving a
* signed response from the Duo service.
*/
angular.module('guacRadius').controller('radiusStateController', ['$scope', '$element',
function radiusStateController($scope, $element) {
console.log("In radiusStateController() method.");
// Populate the input area for the connection state
console.log("RADIUS State: " + $scope.field.radiusState);
$scope.model = $scope.field.radiusState;
}]);

View File

@@ -1,9 +0,0 @@
<div class="radius-challenge-response-field-container">
<div id="radius-challenge-text" />
<div class="password-field">
<input type="{{passwordInputType}}" ng-model="model" ng-trim="false" autocorrect="off" autocapitalize="off"/>
<div class="icon toggle-password" ng-click="togglePassword()" title="{{getTogglePasswordHelpText() | translate}}"></div>
</div>
<input type="hidden" id="radius-challenge-state" />
<input type="submit" />
</div>

View File

@@ -0,0 +1,5 @@
<div id="radius-challenge-text" />
<div class="password-field">
<input type="{{passwordInputType}}" ng-model="model" ng-trim="false" autocorrect="off" autocapitalize="off"/>
<div class="icon toggle-password" ng-click="togglePassword()" title="{{getTogglePasswordHelpText() | translate}}"></div>
</div>