blob: b256e9b702864002b8b28db7c61fc67c43bb1473 [file] [log] [blame]
/*
* 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.Inject;
import com.google.inject.Provider;
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.settings.Saml2Settings;
import com.onelogin.saml2.util.Util;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
import org.apache.guacamole.auth.saml.conf.ConfigurationService;
import org.apache.guacamole.auth.saml.user.SAMLAuthenticatedUser;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.form.Field;
import org.apache.guacamole.form.RedirectField;
import org.apache.guacamole.language.TranslatableMessage;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
import org.apache.guacamole.token.TokenName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;
/**
* Class that provides services for use by the SAMLAuthenticationProvider class.
*/
public class AuthenticationProviderService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(AuthenticationProviderService.class);
/**
* Service for retrieving SAML configuration information.
*/
@Inject
private ConfigurationService confService;
/**
* Provider for AuthenticatedUser objects.
*/
@Inject
private Provider<SAMLAuthenticatedUser> authenticatedUserProvider;
/**
* The map used to track active SAML responses.
*/
@Inject
private SAMLResponseMap samlResponseMap;
private static final String SAML_ATTRIBUTE_TOKEN_PREFIX = "SAML_";
/**
* Returns an AuthenticatedUser representing the user authenticated by the
* given credentials.
*
* @param credentials
* The credentials to use for authentication.
*
* @return
* An AuthenticatedUser representing the user authenticated by the
* given credentials.
*
* @throws GuacamoleException
* If an error occurs while authenticating the user, or if access is
* denied.
*/
public AuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException {
HttpServletRequest request = credentials.getRequest();
// Initialize and configure SAML client.
Saml2Settings samlSettings = confService.getSamlSettings();
if (request != null) {
// Look for the SAML Response parameter.
String responseHash = request.getParameter("responseHash");
if (responseHash != null) {
try {
// Generate the response object
if (!samlResponseMap.hasSamlResponse(responseHash)) {
logger.warn("SAML response was not found.");
logger.debug("SAML response hash {} not found in response map.", responseHash);
throw new GuacamoleServerException("Provided response was not found in response map.");
}
SamlResponse samlResponse = samlResponseMap.getSamlResponse(responseHash);
if (!samlResponse.validateNumAssertions()) {
logger.warn("SAML response contained other than single assertion.");
logger.debug("validateNumAssertions returned false.");
throw new GuacamoleServerException("Unable to validate SAML assertions.");
}
// Validate timestamps, generating ValidationException if this fails.
samlResponse.validateTimestamps();
// Grab the username, and, if present, finish authentication.
String username = samlResponse.getNameId().toLowerCase();
if (username != null) {
// Retrieve any provided attributes
Map<String, List<String>> attributes =
samlResponse.getAttributes();
// Back-port the username to the credentials
credentials.setUsername(username);
// Configure the AuthenticatedUser and return it
SAMLAuthenticatedUser authenticatedUser =
authenticatedUserProvider.get();
authenticatedUser.init(username, credentials,
parseTokens(attributes),
parseGroups(attributes, confService.getGroupAttribute()));
return authenticatedUser;
}
}
// Catch errors and convert to a GuacamoleExcetion.
catch (IOException e) {
logger.warn("Error during I/O while parsing SAML response: {}", e.getMessage());
logger.debug("Received IOException when trying to parse SAML response.", e);
throw new GuacamoleServerException("IOException received while processing SAML response.", e);
}
catch (ParserConfigurationException e) {
logger.warn("Error configuring XML parser: {}", e.getMessage());
logger.debug("Received ParserConfigurationException when trying to parse SAML response.", e);
throw new GuacamoleServerException("XML ParserConfigurationException received while processing SAML response.", e);
}
catch (SAXException e) {
logger.warn("Bad XML when parsing SAML response: {}", e.getMessage());
logger.debug("Received SAXException while parsing SAML response.", e);
throw new GuacamoleServerException("XML SAXException received while processing SAML response.", e);
}
catch (SettingsException e) {
logger.warn("Error with SAML settings while parsing response: {}", e.getMessage());
logger.debug("Received SettingsException while parsing SAML response.", e);
throw new GuacamoleServerException("SAML SettingsException received while process SAML response.", e);
}
catch (ValidationError e) {
logger.warn("Error validating SAML response: {}", e.getMessage());
logger.debug("Received ValidationError while parsing SAML response.", e);
throw new GuacamoleServerException("SAML ValidationError received while processing SAML response.", e);
}
catch (XPathExpressionException e) {
logger.warn("Problem with XML parsing response: {}", e.getMessage());
logger.debug("Received XPathExpressionException while processing SAML response.", e);
throw new GuacamoleServerException("XML XPathExpressionExcetion received while processing SAML response.", e);
}
catch (Exception e) {
logger.warn("Exception while getting name from SAML response: {}", e.getMessage());
logger.debug("Received Exception while retrieving name from SAML response.", e);
throw new GuacamoleServerException("Generic Exception received processing SAML response.", e);
}
}
}
// No SAML Response is present, so generate a request.
AuthnRequest samlReq = new AuthnRequest(samlSettings);
URI authUri;
try {
authUri = new URI(samlSettings.getIdpSingleSignOnServiceUrl() + "?SAMLRequest=" +
Util.urlEncoder(samlReq.getEncodedAuthnRequest()));
}
catch (IOException e) {
logger.error("Error encoding authentication request to string: {}", e.getMessage());
logger.debug("Got IOException encoding authentication request.", e);
throw new GuacamoleServerException("IOException received while generating SAML authentication URI.", e);
}
catch(URISyntaxException e) {
logger.error("Error generating URI for authentication redirect: {}", e.getMessage());
logger.debug("Got URISyntaxException generating authentication URI", e);
throw new GuacamoleServerException("URISyntaxException received while generating SAML authentication URI.", e);
}
// Redirect to SAML Identity Provider (IdP)
throw new GuacamoleInsufficientCredentialsException("Redirecting to SAML IdP.",
new CredentialsInfo(Arrays.asList(new Field[] {
new RedirectField("samlRedirect", authUri, new TranslatableMessage("LOGIN.INFO_SAML_REDIRECT_PENDING"))
}))
);
}
/**
* Generates Map of tokens that can be substituted within Guacamole
* parameters given a Map containing a List of attributes from the SAML IdP.
* Attributes that have multiple values will be reduced to a single value,
* taking the first available value and discarding the remaining values.
*
* @param attributes
* The Map containing the attributes retrieved from the SAML IdP.
*
* @return
* A Map of key and single value pairs that can be used as parameter
* tokens.
*/
private Map<String, String> parseTokens(Map<String,
List<String>> attributes) {
Map<String, String> tokens = new HashMap<>();
for (Entry<String, List<String>> entry : attributes.entrySet()) {
List<String> values = entry.getValue();
tokens.put(TokenName.canonicalize(
entry.getKey(), SAML_ATTRIBUTE_TOKEN_PREFIX),
values.get(0));
}
return tokens;
}
/**
* Returns a list of groups found in the provided Map of attributes returned
* by the SAML IdP by searching the map for the provided group attribute.
*
* @param attributes
* The Map of attributes provided by the SAML IdP.
*
* @param groupAttribute
* The name of the attribute that may be present in the Map that
* will be used to parse group membership for the authenticated user.
*
* @return
* A Set of groups of which the user is a member.
*/
private Set<String> parseGroups(Map<String, List<String>> attributes,
String groupAttribute) {
List<String> samlGroups = attributes.get(groupAttribute);
if (samlGroups != null && !samlGroups.isEmpty())
return Collections.unmodifiableSet(new HashSet<>(samlGroups));
return Collections.emptySet();
}
}