mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 05:07:41 +00:00
GUACAMOLE-197: Working RADIUS Authentication, including dealing with Challenge/Response (e.g. 2/Multi-Factor)
This commit is contained in:
committed by
Nick Couchman
parent
dbb62ded77
commit
3e994021da
@@ -258,6 +258,18 @@
|
||||
<version>1.1.5</version>
|
||||
</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>
|
||||
|
||||
|
@@ -21,9 +21,11 @@ package org.apache.guacamole.auth.radius;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
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.form.RadiusChallengeResponseField;
|
||||
import org.apache.guacamole.auth.radius.form.RadiusStateField;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.form.Field;
|
||||
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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import net.jradius.dictionary.Attr_State;
|
||||
import net.jradius.exception.UnknownAttributeException;
|
||||
import net.jradius.packet.RadiusPacket;
|
||||
import net.jradius.packet.AccessAccept;
|
||||
@@ -90,69 +93,124 @@ public class AuthenticationProviderService {
|
||||
public AuthenticatedUser authenticateUser(Credentials credentials)
|
||||
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;
|
||||
try {
|
||||
radPack = radiusService.authenticate(credentials.getUsername(),
|
||||
|
||||
// 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 {
|
||||
radPack = radiusService.authenticate(credentials.getUsername(),
|
||||
credentials.getPassword());
|
||||
}
|
||||
catch (GuacamoleException e) {
|
||||
logger.error("Cannot configure RADIUS server: {}", e.getMessage());
|
||||
logger.debug("Error configuring RADIUS server.", e);
|
||||
radPack = null;
|
||||
}
|
||||
|
||||
// If configure fails, permission to login is denied
|
||||
if (radPack == null) {
|
||||
logger.debug("Nothing in the RADIUS packet.");
|
||||
throw new GuacamoleInvalidCredentialsException("Permission denied.", CredentialsInfo.USERNAME_PASSWORD);
|
||||
}
|
||||
|
||||
// If we get back an AccessReject packet, login is denied.
|
||||
else if (radPack instanceof AccessReject) {
|
||||
logger.debug("Login has been rejected by RADIUS server.");
|
||||
throw new GuacamoleInvalidCredentialsException("Permission denied.", CredentialsInfo.USERNAME_PASSWORD);
|
||||
}
|
||||
|
||||
/**
|
||||
* If we receive an AccessChallenge package, the server needs more information -
|
||||
* We create a new form/field with the challenge message.
|
||||
*/
|
||||
else if (radPack instanceof AccessChallenge) {
|
||||
try {
|
||||
String replyMsg = radPack.getAttributeValue("Reply-Message").toString();
|
||||
String radState = radPack.getAttributeValue("State").toString();
|
||||
logger.debug("RADIUS sent challenge: {}", replyMsg);
|
||||
logger.debug("RADIUS sent state: {}", radState);
|
||||
Field radiusResponseField = new RadiusChallengeResponseField(credentials.getUsername(), replyMsg, radState);
|
||||
CredentialsInfo expectedCredentials = new CredentialsInfo(Collections.singletonList(radiusResponseField));
|
||||
throw new GuacamoleInsufficientCredentialsException("LOGIN.INFO_RADIUS_ADDL_REQUIRED", expectedCredentials);
|
||||
}
|
||||
catch(UnknownAttributeException e) {
|
||||
logger.error("Error in talks with RADIUS server.");
|
||||
logger.debug("RADIUS challenged by didn't provide right attributes.");
|
||||
throw new GuacamoleInvalidCredentialsException("Authentication error.", CredentialsInfo.USERNAME_PASSWORD);
|
||||
catch (GuacamoleException e) {
|
||||
logger.error("Cannot configure RADIUS server: {}", e.getMessage());
|
||||
logger.debug("Error configuring RADIUS server.", e);
|
||||
radPack = null;
|
||||
}
|
||||
|
||||
// If configure fails, permission to login is denied
|
||||
if (radPack == null) {
|
||||
logger.debug("Nothing in the RADIUS packet.");
|
||||
throw new GuacamoleInvalidCredentialsException("Permission denied.", CredentialsInfo.USERNAME_PASSWORD);
|
||||
}
|
||||
|
||||
// If we get back an AccessReject packet, login is denied.
|
||||
else if (radPack instanceof AccessReject) {
|
||||
logger.debug("Login has been rejected by RADIUS server.");
|
||||
throw new GuacamoleInvalidCredentialsException("Permission denied.", CredentialsInfo.USERNAME_PASSWORD);
|
||||
}
|
||||
|
||||
/**
|
||||
* If we receive an AccessChallenge package, the server needs more information -
|
||||
* We create a new form/field with the challenge message.
|
||||
*/
|
||||
else if (radPack instanceof AccessChallenge) {
|
||||
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();
|
||||
radiusState = new String(stateAttr.getValue().getBytes());
|
||||
Field radiusResponseField = new RadiusChallengeResponseField(replyMsg);
|
||||
Field radiusStateField = new RadiusStateField(radiusState);
|
||||
CredentialsInfo expectedCredentials = new CredentialsInfo(Arrays.asList(radiusResponseField,radiusStateField));
|
||||
throw new GuacamoleInsufficientCredentialsException("LOGIN.INFO_RADIUS_ADDL_REQUIRED", expectedCredentials);
|
||||
}
|
||||
catch (UnknownAttributeException e) {
|
||||
logger.error("Error in talks with RADIUS server.");
|
||||
logger.debug("RADIUS challenged by didn't provide right attributes.");
|
||||
throw new GuacamoleInvalidCredentialsException("Authentication error.", CredentialsInfo.USERNAME_PASSWORD);
|
||||
}
|
||||
}
|
||||
|
||||
// If we receive AccessAccept, authentication has succeeded
|
||||
else if (radPack instanceof AccessAccept) {
|
||||
try {
|
||||
|
||||
// Return AuthenticatedUser if bind succeeds
|
||||
AuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
|
||||
authenticatedUser.init(credentials);
|
||||
return authenticatedUser;
|
||||
}
|
||||
finally {
|
||||
radiusService.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Something unanticipated happened, so we panic
|
||||
else
|
||||
throw new GuacamoleInvalidCredentialsException("Unknown error trying to authenticate.", CredentialsInfo.USERNAME_PASSWORD);
|
||||
}
|
||||
|
||||
// If we receive AccessAccept, authentication has succeeded
|
||||
else if (radPack instanceof AccessAccept) {
|
||||
// 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 {
|
||||
|
||||
// Return AuthenticatedUser if bind succeeds
|
||||
AuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
|
||||
authenticatedUser.init(credentials);
|
||||
return authenticatedUser;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Something else we haven't thought of has happened, so we throw an error
|
||||
else
|
||||
throw new GuacamoleInvalidCredentialsException("Unknown error trying to authenticate.", CredentialsInfo.USERNAME_PASSWORD);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -178,7 +178,6 @@ public class RadiusConnectionService {
|
||||
radAttrs.add(new Attr_UserName(username));
|
||||
radAttrs.add(new Attr_UserPassword(password));
|
||||
AccessRequest radAcc = new AccessRequest(radiusClient, radAttrs);
|
||||
logger.debug("Sending authentication request to radius server for user {}.", username);
|
||||
radAuth.setupRequest(radiusClient, radAcc);
|
||||
radAuth.processRequest(radAcc);
|
||||
return radiusClient.sendReceive(radAcc, confService.getRadiusRetries());
|
||||
@@ -256,7 +255,6 @@ public class RadiusConnectionService {
|
||||
radAttrs.add(new Attr_State(state));
|
||||
radAttrs.add(new Attr_UserPassword(response));
|
||||
AccessRequest radAcc = new AccessRequest(radiusClient, radAttrs);
|
||||
logger.debug("Sending authentication response to radius server for user {}.", username);
|
||||
radAuth.setupRequest(radiusClient, radAcc);
|
||||
radAuth.processRequest(radAcc);
|
||||
return radiusClient.sendReceive(radAcc, confService.getRadiusRetries());
|
||||
|
@@ -34,23 +34,13 @@ public class RadiusChallengeResponseField extends Field {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -59,24 +49,14 @@ public class RadiusChallengeResponseField extends Field {
|
||||
/**
|
||||
* Initialize the field with the reply message and the state.
|
||||
*/
|
||||
public RadiusChallengeResponseField(String username, String replyMsg, String radiusState) {
|
||||
super(RADIUS_FIELD_NAME, RADIUS_FIELD_TYPE);
|
||||
public RadiusChallengeResponseField(String replyMsg) {
|
||||
super(PARAMETER_NAME, RADIUS_FIELD_TYPE);
|
||||
logger.debug("Initializing the RADIUS challenge/response field: {}", replyMsg);
|
||||
|
||||
this.username = username;
|
||||
this.replyMsg = replyMsg;
|
||||
this.radiusState = radiusState;
|
||||
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getRadiusState() {
|
||||
return radiusState;
|
||||
}
|
||||
|
||||
public String getReplyMsg() {
|
||||
return replyMsg;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -27,8 +27,13 @@ angular.module('guacRadius').config(['formServiceProvider',
|
||||
// Define field for the challenge from the RADIUS service
|
||||
formServiceProvider.registerFieldType('GUAC_RADIUS_CHALLENGE_RESPONSE', {
|
||||
module : 'guacRadius',
|
||||
controller : 'guacRadiusController',
|
||||
templateUrl : 'app/ext/radius/templates/radiusChallengeResponseField.html'
|
||||
controller : 'radiusResponseController',
|
||||
templateUrl : 'app/ext/radius/templates/radiusResponseField.html'
|
||||
});
|
||||
formServiceProvider.registerFieldType('GUAC_RADIUS_STATE', {
|
||||
module : 'guacRadius',
|
||||
controller : 'radiusStateController',
|
||||
template : '<input type=hidden ng-model="model" />'
|
||||
});
|
||||
|
||||
}]);
|
||||
|
@@ -22,22 +22,15 @@
|
||||
* API to prompt the user for additional credentials, ultimately receiving a
|
||||
* signed response from the Duo service.
|
||||
*/
|
||||
angular.module('guacRadius').controller('guacRadiusController', ['$scope', '$element',
|
||||
function guacRadiusController($scope, $element) {
|
||||
console.log("In guacRadiusController() method.");
|
||||
angular.module('guacRadius').controller('radiusResponseController', ['$scope', '$element',
|
||||
function radiusResponseController($scope, $element) {
|
||||
console.log("In radiusResponseController() method.");
|
||||
|
||||
// Find the area to display the challenge message
|
||||
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
|
||||
console.log("RADIUS Reply Message: " + $scope.field.replyMsg);
|
||||
radiusChellenge.html($scope.field.replyMsg);
|
||||
|
||||
// Populate the input area for the connection state
|
||||
console.log("RADIUS State: " + scope.field.radiusState);
|
||||
radiusState.value = $scope.field.radiusState;
|
||||
radiusChallenge.html($scope.field.replyMsg);
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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>
|
@@ -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>
|
Reference in New Issue
Block a user