GUACAMOLE-103: Move SAMLResponse processing to authentication service.

This commit is contained in:
Virtually Nick
2020-03-22 20:33:24 -04:00
parent 7a44cf6014
commit 2a2172914d
6 changed files with 190 additions and 27 deletions

View File

@@ -158,9 +158,10 @@
<!-- Jersey - JAX-RS Implementation -->
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-server</artifactId>
<version>1.17.1</version>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
<version>1.1.1</version>
<scope>provided</scope>
</dependency>
<!-- OneLogin SAML Library -->

View File

@@ -25,8 +25,6 @@ import com.onelogin.saml2.authn.AuthnRequest;
import com.onelogin.saml2.authn.SamlResponse;
import com.onelogin.saml2.exception.SettingsException;
import com.onelogin.saml2.exception.ValidationError;
import com.onelogin.saml2.http.HttpRequest;
import com.onelogin.saml2.servlet.ServletUtils;
import com.onelogin.saml2.settings.Saml2Settings;
import com.onelogin.saml2.util.Util;
import java.io.IOException;
@@ -69,6 +67,12 @@ public class AuthenticationProviderService {
*/
@Inject
private Provider<SAMLAuthenticatedUser> authenticatedUserProvider;
/**
* The map used to track active SAML responses.
*/
@Inject
private SAMLResponseMap samlResponseMap;
/**
* Returns an AuthenticatedUser representing the user authenticated by the
@@ -87,7 +91,7 @@ public class AuthenticationProviderService {
*/
public AuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException {
HttpServletRequest request = credentials.getRequest();
// Initialize and configure SAML client.
@@ -96,16 +100,18 @@ public class AuthenticationProviderService {
if (request != null) {
// Look for the SAML Response parameter.
String samlResponseParam = request.getParameter("SAMLResponse");
String responseHash = Util.urlDecoder(request.getParameter("responseHash"));
if (samlResponseParam != null) {
if (responseHash != null) {
// Convert the SAML response into the version needed for the client.
HttpRequest httpRequest = ServletUtils.makeHttpRequest(request);
try {
// Generate the response object
SamlResponse samlResponse = new SamlResponse(samlSettings, httpRequest);
if (!samlResponseMap.hasSamlResponse(responseHash))
throw new GuacamoleInvalidCredentialsException("Provided response has not found.",
CredentialsInfo.USERNAME_PASSWORD);
SamlResponse samlResponse = samlResponseMap.getSamlResponse(responseHash);
if (!samlResponse.validateNumAssertions()) {
logger.warn("SAML response contained other than single assertion.");
@@ -197,7 +203,6 @@ public class AuthenticationProviderService {
}))
);
}
}

View File

@@ -72,7 +72,8 @@ public class SAMLAuthenticationProvider extends AbstractAuthenticationProvider {
throws GuacamoleException {
// Attempt to authenticate user with given credentials
AuthenticationProviderService authProviderService = injector.getInstance(AuthenticationProviderService.class);
AuthenticationProviderService authProviderService =
injector.getInstance(AuthenticationProviderService.class);
return authProviderService.authenticateUser(credentials);
}

View File

@@ -71,9 +71,10 @@ public class SAMLAuthenticationProviderModule extends AbstractModule {
bind(AuthenticationProvider.class).toInstance(authProvider);
bind(Environment.class).toInstance(environment);
// Bind saml-specific services
// Bind SAML-specific services
bind(ConfigurationService.class);
bind(SAMLAuthenticationProviderResource.class);
bind(SAMLResponseMap.class);
}

View File

@@ -20,16 +20,34 @@
package org.apache.guacamole.auth.saml;
import com.google.inject.Inject;
import com.onelogin.saml2.authn.SamlResponse;
import com.onelogin.saml2.exception.SettingsException;
import com.onelogin.saml2.exception.ValidationError;
import com.onelogin.saml2.http.HttpRequest;
import com.onelogin.saml2.servlet.ServletUtils;
import com.onelogin.saml2.settings.Saml2Settings;
import com.onelogin.saml2.util.Util;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.Response;
import javax.ws.rs.FormParam;
import javax.ws.rs.Path;
import javax.ws.rs.POST;
import javax.ws.rs.core.Context;
import javax.xml.bind.DatatypeConverter;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.auth.saml.conf.ConfigurationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;
/**
* A class that implements the REST API necessary for the
@@ -37,19 +55,36 @@ import org.apache.guacamole.auth.saml.conf.ConfigurationService;
*/
public class SAMLAuthenticationProviderResource {
/**
* Logger for this class.
*/
private final Logger logger =
LoggerFactory.getLogger(SAMLAuthenticationProviderResource.class);
/**
* The configuration service for this module.
*/
@Inject
private ConfigurationService confService;
/**
* The map used to track active responses.
*/
@Inject
private SAMLResponseMap samlResponseMap;
/**
* A REST endpoint that is POSTed to by the SAML IdP
* with the results of the SAML SSO Authentication.
*
* @param samlResponse
* @param samlResponseString
* The encoded response returned by the SAML IdP.
*
* @param consumedRequest
* The HttpServletRequest associated with the SAML response. The
* parameters of this request may not be accessible, as the request may
* have been fully consumed by JAX-RS.
*
* @return
* A HTTP Response that will redirect the user back to the
* Guacamole home page, with the SAMLResponse encoded in the
@@ -61,21 +96,61 @@ public class SAMLAuthenticationProviderResource {
*/
@POST
@Path("callback")
public Response processSamlResponse(@FormParam("SAMLResponse") String samlResponse)
public Response processSamlResponse(
@FormParam("SAMLResponse") String samlResponseString,
@Context HttpServletRequest consumedRequest)
throws GuacamoleException {
String guacBase = confService.getCallbackUrl().toString();
try {
Response redirectHome = Response.seeOther(
new URI(guacBase + "?SAMLResponse=" + Util.urlEncoder(samlResponse))).build();
return redirectHome;
}
catch (URISyntaxException e) {
throw new GuacamoleServerException("Error processing SAML response.", e);
}
String guacBase = confService.getCallbackUrl().toString();
Saml2Settings samlSettings = confService.getSamlSettings();
try {
HttpRequest request = ServletUtils
.makeHttpRequest(consumedRequest)
.addParameter("SAMLResponse", samlResponseString);
SamlResponse samlResponse = new SamlResponse(samlSettings, request);
String responseHash = hashSamlResponse(samlResponseString);
samlResponseMap.putSamlResponse(responseHash, samlResponse);
return Response.seeOther(new URI(guacBase
+ "?responseHash="
+ Util.urlEncoder(responseHash))
).build();
}
catch (IOException
| NoSuchAlgorithmException
| ParserConfigurationException
| SAXException
| SettingsException
| URISyntaxException
| ValidationError
| XPathExpressionException e) {
throw new GuacamoleServerException(e);
}
}
/**
* This is a utility method designed to generate a SHA-256 has for the
* given string representation of the SAMLResponse, throwing an exception
* if, for some reason, the Java implementation in use doesn't support
* SHA-256, and returning a hex-formatted hash value.
*
* @param samlResponse
* The String representation of the SAML response.
*
* @return
* A hex-formatted string of the SHA-256 hash.
*
* @throws NoSuchAlgorithmException
* If Java does not support SHA-256.
*/
private String hashSamlResponse(String samlResponse)
throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return DatatypeConverter.printHexBinary(
digest.digest(samlResponse.getBytes(StandardCharsets.UTF_8)));
}
}

View File

@@ -0,0 +1,80 @@
/*
* 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.saml;
import com.google.inject.Singleton;
import com.onelogin.saml2.authn.SamlResponse;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* A class that handles mapping of hashes to SAMLResponse objects.
*/
@Singleton
public class SAMLResponseMap {
/**
* The internal data structure that holds a map of SHA-256 hashes to
* SAML responses.
*/
private final ConcurrentMap<String, SamlResponse> samlResponseMap =
new ConcurrentHashMap<>();
/**
* Retrieve the SamlResponse from the map that is represented by the
* provided hash, or null if no such object exists.
*
* @param hash
* The SHA-256 hash of the SamlResponse.
*
* @return
* The SamlResponse object matching the hash provided.
*/
protected SamlResponse getSamlResponse(String hash) {
return samlResponseMap.remove(hash);
}
/**
* Place the provided mapping of hash to SamlResponse into the map.
*
* @param hash
* The hash that will be the lookup key for this SamlResponse.
*
* @param samlResponse
* The SamlResponse object.
*/
protected void putSamlResponse(String hash, SamlResponse samlResponse) {
samlResponseMap.put(hash, samlResponse);
}
/**
* Return true if the provided hash key exists in the map, otherwise false.
*
* @param hash
* The hash key to look for in the map.
*
* @return
* true if the provided hash is present, otherwise false.
*/
protected boolean hasSamlResponse(String hash) {
return samlResponseMap.containsKey(hash);
}
}