mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 13:17:41 +00:00
Merge 1.4.0 changes back to master.
This commit is contained in:
@@ -21,30 +21,14 @@ package org.apache.guacamole.auth.saml;
|
|||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Provider;
|
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 java.io.IOException;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.util.Arrays;
|
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.servlet.http.HttpServletRequest;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
|
||||||
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.auth.saml.user.SAMLAuthenticatedUser;
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
import org.apache.guacamole.GuacamoleServerException;
|
import org.apache.guacamole.auth.saml.acs.AssertedIdentity;
|
||||||
|
import org.apache.guacamole.auth.saml.acs.AuthenticationSessionManager;
|
||||||
|
import org.apache.guacamole.auth.saml.acs.SAMLService;
|
||||||
import org.apache.guacamole.form.Field;
|
import org.apache.guacamole.form.Field;
|
||||||
import org.apache.guacamole.form.RedirectField;
|
import org.apache.guacamole.form.RedirectField;
|
||||||
import org.apache.guacamole.language.TranslatableMessage;
|
import org.apache.guacamole.language.TranslatableMessage;
|
||||||
@@ -52,26 +36,18 @@ import org.apache.guacamole.net.auth.AuthenticatedUser;
|
|||||||
import org.apache.guacamole.net.auth.Credentials;
|
import org.apache.guacamole.net.auth.Credentials;
|
||||||
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
|
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
|
||||||
import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
|
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.
|
* Service that authenticates Guacamole users by processing the responses of
|
||||||
|
* SAML identity providers.
|
||||||
*/
|
*/
|
||||||
public class AuthenticationProviderService {
|
public class AuthenticationProviderService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger for this class.
|
* The name of the query parameter that identifies an active authentication
|
||||||
|
* session (in-progress SAML authentication attempt).
|
||||||
*/
|
*/
|
||||||
private static final Logger logger = LoggerFactory.getLogger(AuthenticationProviderService.class);
|
public static final String AUTH_SESSION_QUERY_PARAM = "state";
|
||||||
|
|
||||||
/**
|
|
||||||
* Service for retrieving SAML configuration information.
|
|
||||||
*/
|
|
||||||
@Inject
|
|
||||||
private ConfigurationService confService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider for AuthenticatedUser objects.
|
* Provider for AuthenticatedUser objects.
|
||||||
@@ -80,12 +56,16 @@ public class AuthenticationProviderService {
|
|||||||
private Provider<SAMLAuthenticatedUser> authenticatedUserProvider;
|
private Provider<SAMLAuthenticatedUser> authenticatedUserProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The map used to track active SAML responses.
|
* Manager of active SAML authentication attempts.
|
||||||
*/
|
*/
|
||||||
@Inject
|
@Inject
|
||||||
private SAMLResponseMap samlResponseMap;
|
private AuthenticationSessionManager sessionManager;
|
||||||
|
|
||||||
private static final String SAML_ATTRIBUTE_TOKEN_PREFIX = "SAML_";
|
/**
|
||||||
|
* Service for processing SAML requests/responses.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
private SAMLService saml;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an AuthenticatedUser representing the user authenticated by the
|
* Returns an AuthenticatedUser representing the user authenticated by the
|
||||||
@@ -105,116 +85,29 @@ public class AuthenticationProviderService {
|
|||||||
public AuthenticatedUser authenticateUser(Credentials credentials)
|
public AuthenticatedUser authenticateUser(Credentials credentials)
|
||||||
throws GuacamoleException {
|
throws GuacamoleException {
|
||||||
|
|
||||||
|
// No authentication can be attempted without a corresponding HTTP
|
||||||
|
// request
|
||||||
HttpServletRequest request = credentials.getRequest();
|
HttpServletRequest request = credentials.getRequest();
|
||||||
|
if (request == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
// Initialize and configure SAML client.
|
// Use established SAML identity if already provided by the SAML IdP
|
||||||
Saml2Settings samlSettings = confService.getSamlSettings();
|
AssertedIdentity identity = sessionManager.getIdentity(request.getParameter(AUTH_SESSION_QUERY_PARAM));
|
||||||
|
if (identity != null) {
|
||||||
|
|
||||||
if (request != null) {
|
// Back-port the username to the credentials
|
||||||
|
credentials.setUsername(identity.getUsername());
|
||||||
|
|
||||||
// Look for the SAML Response parameter.
|
// Configure the AuthenticatedUser and return it
|
||||||
String responseHash = request.getParameter("responseHash");
|
SAMLAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
|
||||||
|
authenticatedUser.init(identity, credentials);
|
||||||
|
return authenticatedUser;
|
||||||
|
|
||||||
if (responseHash != null && samlResponseMap.hasSamlResponse(responseHash)) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
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();
|
|
||||||
if (username != null) {
|
|
||||||
|
|
||||||
// Canonicalize username as lowercase
|
|
||||||
username = username.toLowerCase();
|
|
||||||
|
|
||||||
// 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, or hash is not present in map.
|
// Redirect to SAML IdP if no SAML identity is associated with the
|
||||||
AuthnRequest samlReq = new AuthnRequest(samlSettings);
|
// Guacamole authentication request
|
||||||
URI authUri;
|
URI authUri = saml.createRequest();
|
||||||
try {
|
|
||||||
authUri = UriBuilder.fromUri(samlSettings.getIdpSingleSignOnServiceUrl().toURI())
|
|
||||||
.queryParam("SAMLRequest", samlReq.getEncodedAuthnRequest())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
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.",
|
throw new GuacamoleInsufficientCredentialsException("Redirecting to SAML IdP.",
|
||||||
new CredentialsInfo(Arrays.asList(new Field[] {
|
new CredentialsInfo(Arrays.asList(new Field[] {
|
||||||
new RedirectField("samlRedirect", authUri, new TranslatableMessage("LOGIN.INFO_SAML_REDIRECT_PENDING"))
|
new RedirectField("samlRedirect", authUri, new TranslatableMessage("LOGIN.INFO_SAML_REDIRECT_PENDING"))
|
||||||
@@ -223,58 +116,4 @@ public class AuthenticationProviderService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -22,15 +22,20 @@ package org.apache.guacamole.auth.saml;
|
|||||||
import com.google.inject.Guice;
|
import com.google.inject.Guice;
|
||||||
import com.google.inject.Injector;
|
import com.google.inject.Injector;
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.auth.saml.acs.AssertionConsumerServiceResource;
|
||||||
|
import org.apache.guacamole.auth.saml.acs.AuthenticationSessionManager;
|
||||||
|
import org.apache.guacamole.auth.saml.user.SAMLAuthenticatedUser;
|
||||||
import org.apache.guacamole.net.auth.AuthenticatedUser;
|
import org.apache.guacamole.net.auth.AuthenticatedUser;
|
||||||
import org.apache.guacamole.net.auth.AbstractAuthenticationProvider;
|
import org.apache.guacamole.net.auth.AbstractAuthenticationProvider;
|
||||||
import org.apache.guacamole.net.auth.Credentials;
|
import org.apache.guacamole.net.auth.Credentials;
|
||||||
|
import org.apache.guacamole.net.auth.TokenInjectingUserContext;
|
||||||
|
import org.apache.guacamole.net.auth.UserContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class when provides authentication for the Guacamole Client against a
|
* AuthenticationProvider implementation that authenticates Guacamole users
|
||||||
* SAML SSO Identity Provider (IdP). This module does not provide any
|
* against a SAML SSO Identity Provider (IdP). This module does not provide any
|
||||||
* storage for connection information, and must be layered with other
|
* storage for connection information, and must be layered with other modules
|
||||||
* modules in order to retrieve connections.
|
* for authenticated users to have access to Guacamole connections.
|
||||||
*/
|
*/
|
||||||
public class SAMLAuthenticationProvider extends AbstractAuthenticationProvider {
|
public class SAMLAuthenticationProvider extends AbstractAuthenticationProvider {
|
||||||
|
|
||||||
@@ -43,12 +48,8 @@ public class SAMLAuthenticationProvider extends AbstractAuthenticationProvider {
|
|||||||
/**
|
/**
|
||||||
* Creates a new SAMLAuthenticationProvider that authenticates users
|
* Creates a new SAMLAuthenticationProvider that authenticates users
|
||||||
* against a SAML IdP.
|
* against a SAML IdP.
|
||||||
*
|
|
||||||
* @throws GuacamoleException
|
|
||||||
* If a required property is missing, or an error occurs while parsing
|
|
||||||
* a property.
|
|
||||||
*/
|
*/
|
||||||
public SAMLAuthenticationProvider() throws GuacamoleException {
|
public SAMLAuthenticationProvider() {
|
||||||
|
|
||||||
// Set up Guice injector.
|
// Set up Guice injector.
|
||||||
injector = Guice.createInjector(
|
injector = Guice.createInjector(
|
||||||
@@ -64,7 +65,7 @@ public class SAMLAuthenticationProvider extends AbstractAuthenticationProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object getResource() throws GuacamoleException {
|
public Object getResource() throws GuacamoleException {
|
||||||
return injector.getInstance(SAMLAuthenticationProviderResource.class);
|
return injector.getInstance(AssertionConsumerServiceResource.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -78,9 +79,24 @@ public class SAMLAuthenticationProvider extends AbstractAuthenticationProvider {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserContext decorate(UserContext context,
|
||||||
|
AuthenticatedUser authenticatedUser, Credentials credentials)
|
||||||
|
throws GuacamoleException {
|
||||||
|
|
||||||
|
// Only decorate if the user authenticated with SAML
|
||||||
|
if (!(authenticatedUser instanceof SAMLAuthenticatedUser))
|
||||||
|
return context;
|
||||||
|
|
||||||
|
// Apply SAML-specific tokens to all connections / connection groups
|
||||||
|
return new TokenInjectingUserContext(context,
|
||||||
|
((SAMLAuthenticatedUser) authenticatedUser).getTokens());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
injector.getInstance(SAMLResponseMap.class).shutdown();
|
injector.getInstance(AuthenticationSessionManager.class).shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -21,7 +21,10 @@ package org.apache.guacamole.auth.saml;
|
|||||||
|
|
||||||
import com.google.inject.AbstractModule;
|
import com.google.inject.AbstractModule;
|
||||||
import org.apache.guacamole.auth.saml.conf.ConfigurationService;
|
import org.apache.guacamole.auth.saml.conf.ConfigurationService;
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.auth.saml.acs.AssertionConsumerServiceResource;
|
||||||
|
import org.apache.guacamole.auth.saml.acs.AuthenticationSessionManager;
|
||||||
|
import org.apache.guacamole.auth.saml.acs.IdentifierGenerator;
|
||||||
|
import org.apache.guacamole.auth.saml.acs.SAMLService;
|
||||||
import org.apache.guacamole.environment.Environment;
|
import org.apache.guacamole.environment.Environment;
|
||||||
import org.apache.guacamole.environment.LocalEnvironment;
|
import org.apache.guacamole.environment.LocalEnvironment;
|
||||||
import org.apache.guacamole.net.auth.AuthenticationProvider;
|
import org.apache.guacamole.net.auth.AuthenticationProvider;
|
||||||
@@ -48,13 +51,8 @@ public class SAMLAuthenticationProviderModule extends AbstractModule {
|
|||||||
*
|
*
|
||||||
* @param authProvider
|
* @param authProvider
|
||||||
* The AuthenticationProvider for which injection is being configured.
|
* The AuthenticationProvider for which injection is being configured.
|
||||||
*
|
|
||||||
* @throws GuacamoleException
|
|
||||||
* If an error occurs while retrieving the Guacamole server
|
|
||||||
* environment.
|
|
||||||
*/
|
*/
|
||||||
public SAMLAuthenticationProviderModule(AuthenticationProvider authProvider)
|
public SAMLAuthenticationProviderModule(AuthenticationProvider authProvider) {
|
||||||
throws GuacamoleException {
|
|
||||||
|
|
||||||
// Get local environment
|
// Get local environment
|
||||||
this.environment = LocalEnvironment.getInstance();
|
this.environment = LocalEnvironment.getInstance();
|
||||||
@@ -72,9 +70,11 @@ public class SAMLAuthenticationProviderModule extends AbstractModule {
|
|||||||
bind(Environment.class).toInstance(environment);
|
bind(Environment.class).toInstance(environment);
|
||||||
|
|
||||||
// Bind SAML-specific services
|
// Bind SAML-specific services
|
||||||
|
bind(AssertionConsumerServiceResource.class);
|
||||||
|
bind(AuthenticationSessionManager.class);
|
||||||
bind(ConfigurationService.class);
|
bind(ConfigurationService.class);
|
||||||
bind(SAMLAuthenticationProviderResource.class);
|
bind(IdentifierGenerator.class);
|
||||||
bind(SAMLResponseMap.class);
|
bind(SAMLService.class);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,167 +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.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 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.ws.rs.core.UriBuilder;
|
|
||||||
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
|
|
||||||
* SAML Idp to POST back its response to Guacamole.
|
|
||||||
*/
|
|
||||||
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 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
|
|
||||||
* return URL.
|
|
||||||
*
|
|
||||||
* @throws GuacamoleException
|
|
||||||
* If the Guacamole configuration cannot be read or an error occurs
|
|
||||||
* parsing a URI.
|
|
||||||
*/
|
|
||||||
@POST
|
|
||||||
@Path("callback")
|
|
||||||
public Response processSamlResponse(
|
|
||||||
@FormParam("SAMLResponse") String samlResponseString,
|
|
||||||
@Context HttpServletRequest consumedRequest)
|
|
||||||
throws GuacamoleException {
|
|
||||||
|
|
||||||
URI guacBase = confService.getCallbackUrl();
|
|
||||||
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(UriBuilder.fromUri(guacBase)
|
|
||||||
.queryParam("responseHash", responseHash)
|
|
||||||
.build()
|
|
||||||
).build();
|
|
||||||
|
|
||||||
}
|
|
||||||
catch (IOException e) {
|
|
||||||
throw new GuacamoleServerException("I/O exception processing SAML response.", e);
|
|
||||||
}
|
|
||||||
catch (NoSuchAlgorithmException e) {
|
|
||||||
throw new GuacamoleServerException("Unexpected missing SHA-256 support while generating SAML response hash.", e);
|
|
||||||
}
|
|
||||||
catch (ParserConfigurationException e) {
|
|
||||||
throw new GuacamoleServerException("Parser exception processing SAML response.", e);
|
|
||||||
}
|
|
||||||
catch (SAXException e) {
|
|
||||||
throw new GuacamoleServerException("SAX exception processing SAML response.", e);
|
|
||||||
}
|
|
||||||
catch (SettingsException e) {
|
|
||||||
throw new GuacamoleServerException("Settings exception processing SAML response.", e);
|
|
||||||
}
|
|
||||||
catch (ValidationError e) {
|
|
||||||
throw new GuacamoleServerException("Exception validating SAML response.", e);
|
|
||||||
}
|
|
||||||
catch (XPathExpressionException e) {
|
|
||||||
throw new GuacamoleServerException("XML Xpath exception validating SAML response.", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a utility method designed to generate a SHA-256 hash 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)));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,134 +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.saml;
|
|
||||||
|
|
||||||
import com.google.inject.Singleton;
|
|
||||||
import com.onelogin.saml2.authn.SamlResponse;
|
|
||||||
import com.onelogin.saml2.exception.ValidationError;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.ConcurrentMap;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executor service which runs the periodic cleanup task
|
|
||||||
*/
|
|
||||||
private final ScheduledExecutorService executor =
|
|
||||||
Executors.newScheduledThreadPool(1);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new instance of this response map and kick off the executor
|
|
||||||
* that schedules the response cleanup task to run every five minutes.
|
|
||||||
*/
|
|
||||||
public SAMLResponseMap() {
|
|
||||||
// Cleanup unclaimed responses every five minutes
|
|
||||||
executor.scheduleAtFixedRate(new SAMLResponseCleanupTask(), 5, 5, TimeUnit.MINUTES);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Task which runs every five minutes and cleans up any expired SAML
|
|
||||||
* responses that haven't been claimed and removed from the map.
|
|
||||||
*/
|
|
||||||
private class SAMLResponseCleanupTask implements Runnable {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
|
|
||||||
// Loop through responses in map and remove ones that are no longer valid.
|
|
||||||
Iterator<SamlResponse> responseIterator = samlResponseMap.values().iterator();
|
|
||||||
while (responseIterator.hasNext()) {
|
|
||||||
try {
|
|
||||||
responseIterator.next().validateTimestamps();
|
|
||||||
}
|
|
||||||
catch (ValidationError e) {
|
|
||||||
responseIterator.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shut down the executor service that periodically cleans out the
|
|
||||||
* SamlResponse Map. This must be invoked during webapp shutdown in order
|
|
||||||
* to avoid resource leaks.
|
|
||||||
*/
|
|
||||||
public void shutdown() {
|
|
||||||
executor.shutdownNow();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
* 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.acs;
|
||||||
|
|
||||||
|
import com.onelogin.saml2.authn.SamlResponse;
|
||||||
|
import com.onelogin.saml2.exception.ValidationError;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.xml.xpath.XPathExpressionException;
|
||||||
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.GuacamoleSecurityException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of a user's identity as asserted by a SAML IdP.
|
||||||
|
*/
|
||||||
|
public class AssertedIdentity {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The original SAML response received from the SAML IdP that asserted
|
||||||
|
* the user's identity.
|
||||||
|
*/
|
||||||
|
private final SamlResponse response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user's Guacamole username.
|
||||||
|
*/
|
||||||
|
private final String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All attributes included in the original SAML response. Attributes may
|
||||||
|
* possibly be associated with multiple values.
|
||||||
|
*/
|
||||||
|
private final Map<String, List<String>> attributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new AssertedIdentity representing the identity asserted by the
|
||||||
|
* SAML IdP in the given response.
|
||||||
|
*
|
||||||
|
* @param response
|
||||||
|
* The response received from the SAML IdP.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If the given SAML response cannot be parsed or is missing required
|
||||||
|
* information.
|
||||||
|
*/
|
||||||
|
public AssertedIdentity(SamlResponse response) throws GuacamoleException {
|
||||||
|
|
||||||
|
// Parse user identity from SAML response
|
||||||
|
String nameId;
|
||||||
|
try {
|
||||||
|
nameId = response.getNameId();
|
||||||
|
if (nameId == null)
|
||||||
|
throw new GuacamoleSecurityException("SAML response did not "
|
||||||
|
+ "include the relevant user's identity (no name ID).");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unfortunately, getNameId() is declared as "throws Exception", so
|
||||||
|
// this error handling has to be pretty generic
|
||||||
|
catch (Exception e) {
|
||||||
|
throw new GuacamoleSecurityException("User identity (name ID) "
|
||||||
|
+ "could not be retrieved from the SAML response: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve any provided attributes
|
||||||
|
Map<String, List<String>> responseAttributes;
|
||||||
|
try {
|
||||||
|
responseAttributes = Collections.unmodifiableMap(response.getAttributes());
|
||||||
|
}
|
||||||
|
catch (XPathExpressionException | ValidationError e) {
|
||||||
|
throw new GuacamoleSecurityException("SAML attributes could not "
|
||||||
|
+ "be parsed from the SAML response: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.response = response;
|
||||||
|
this.username = nameId.toLowerCase(); // Canonicalize username as lowercase
|
||||||
|
this.attributes = responseAttributes;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the username of the Guacamole user whose identity was asserted
|
||||||
|
* by the SAML IdP.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The username of the Guacamole user whose identity was asserted by
|
||||||
|
* the SAML IdP.
|
||||||
|
*/
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a Map containing all attributes included in the original SAML
|
||||||
|
* response that asserted this user's identity. Attributes may possibly be
|
||||||
|
* associated with multiple values.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A Map of all attributes included in the original SAML response.
|
||||||
|
*/
|
||||||
|
public Map<String, List<String>> getAttributes() {
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the identity asserted by the original SAML response is
|
||||||
|
* still valid. An asserted identity may cease to be valid after creation
|
||||||
|
* if it has expired according to the timestamps included in the response.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* true if the original SAML response is still valid, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isValid() {
|
||||||
|
return response.isValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
* 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.acs;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import java.net.URI;
|
||||||
|
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.ws.rs.core.UriBuilder;
|
||||||
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.auth.saml.AuthenticationProviderService;
|
||||||
|
import org.apache.guacamole.auth.saml.conf.ConfigurationService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST API resource that provides a SAML assertion consumer service (ACS)
|
||||||
|
* endpoint. SAML identity providers will issue an HTTP POST to this endpoint
|
||||||
|
* asserting the user's identity when the user has successfully authenticated.
|
||||||
|
*/
|
||||||
|
public class AssertionConsumerServiceResource {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger for this class.
|
||||||
|
*/
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(AssertionConsumerServiceResource.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration service for this module.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
private ConfigurationService confService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager of active SAML authentication attempts.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
private AuthenticationSessionManager sessionManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for processing SAML requests/responses.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
private SAMLService saml;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the SAML response submitted by the SAML IdP via an HTTP POST.
|
||||||
|
* If SSO has been successful, the user is redirected back to Guacamole to
|
||||||
|
* complete authentication. If SSO has failed, the user is redirected back
|
||||||
|
* to Guacamole to re-attempt authentication.
|
||||||
|
*
|
||||||
|
* @param relayState
|
||||||
|
* The "RelayState" value originally provided in the SAML request,
|
||||||
|
* which in our case is the transient the session identifier of the
|
||||||
|
* in-progress authentication attempt. The SAML standard requires that
|
||||||
|
* the identity provider include the "RelayState" value it received
|
||||||
|
* alongside its SAML response.
|
||||||
|
*
|
||||||
|
* @param samlResponse
|
||||||
|
* 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
|
||||||
|
* An HTTP Response that will redirect the user back to Guacamole,
|
||||||
|
* with an internal state identifier included in the URL such that the
|
||||||
|
* the Guacamole side of authentication can complete.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If configuration information required for processing SAML responses
|
||||||
|
* cannot be read.
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("callback")
|
||||||
|
public Response processSamlResponse(
|
||||||
|
@FormParam("RelayState") String relayState,
|
||||||
|
@FormParam("SAMLResponse") String samlResponse,
|
||||||
|
@Context HttpServletRequest consumedRequest)
|
||||||
|
throws GuacamoleException {
|
||||||
|
|
||||||
|
URI guacBase = confService.getCallbackUrl();
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Validate and parse identity asserted by SAML IdP
|
||||||
|
AuthenticationSession session = saml.processResponse(
|
||||||
|
consumedRequest.getRequestURL().toString(),
|
||||||
|
relayState, samlResponse);
|
||||||
|
|
||||||
|
// Store asserted identity for future retrieval via redirect
|
||||||
|
String identifier = sessionManager.defer(session);
|
||||||
|
|
||||||
|
// Redirect user such that Guacamole's authentication system can
|
||||||
|
// retrieve the relevant SAML identity (stored above)
|
||||||
|
return Response.seeOther(UriBuilder.fromUri(guacBase)
|
||||||
|
.queryParam(AuthenticationProviderService.AUTH_SESSION_QUERY_PARAM, identifier)
|
||||||
|
.build()
|
||||||
|
).build();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// If invalid, redirect back to main page to re-attempt authentication
|
||||||
|
catch (GuacamoleException e) {
|
||||||
|
logger.warn("Authentication attempted with an invalid SAML response: {}", e.getMessage());
|
||||||
|
logger.debug("Received SAML response failed validation.", e);
|
||||||
|
return Response.seeOther(guacBase).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* 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.acs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of an in-progress SAML authentication attempt.
|
||||||
|
*/
|
||||||
|
public class AuthenticationSession {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The absolute point in time after which this authentication session is
|
||||||
|
* invalid. This value is a UNIX epoch timestamp, as may be returned by
|
||||||
|
* {@link System#currentTimeMillis()}.
|
||||||
|
*/
|
||||||
|
private final long expirationTimestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The request ID of the SAML request associated with the authentication
|
||||||
|
* attempt.
|
||||||
|
*/
|
||||||
|
private final String requestId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identity asserted by the SAML IdP, or null if authentication has not
|
||||||
|
* yet completed successfully.
|
||||||
|
*/
|
||||||
|
private AssertedIdentity identity = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new AuthenticationSession representing an in-progress SAML
|
||||||
|
* authentication attempt.
|
||||||
|
*
|
||||||
|
* @param requestId
|
||||||
|
* The request ID of the SAML request associated with the
|
||||||
|
* authentication attempt.
|
||||||
|
*
|
||||||
|
* @param expires
|
||||||
|
* The number of milliseconds that may elapse before this session must
|
||||||
|
* be considered invalid.
|
||||||
|
*/
|
||||||
|
public AuthenticationSession(String requestId, long expires) {
|
||||||
|
this.expirationTimestamp = System.currentTimeMillis() + expires;
|
||||||
|
this.requestId = requestId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether this authentication session is still valid (has not yet
|
||||||
|
* expired). If an identity has been asserted by the SAML IdP, this
|
||||||
|
* considers also whether the SAML response asserting that identity has
|
||||||
|
* expired.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* true if this authentication session is still valid, false if it has
|
||||||
|
* expired.
|
||||||
|
*/
|
||||||
|
public boolean isValid() {
|
||||||
|
return System.currentTimeMillis() < expirationTimestamp
|
||||||
|
&& (identity == null || identity.isValid());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request ID of the SAML request associated with the
|
||||||
|
* authentication attempt.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The request ID of the SAML request associated with the
|
||||||
|
* authentication attempt.
|
||||||
|
*/
|
||||||
|
public String getRequestID() {
|
||||||
|
return requestId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks this authentication attempt as completed and successful, with the
|
||||||
|
* user having been asserted as having the given identity by the SAML IdP.
|
||||||
|
*
|
||||||
|
* @param identity
|
||||||
|
* The identity asserted by the SAML IdP.
|
||||||
|
*/
|
||||||
|
public void setIdentity(AssertedIdentity identity) {
|
||||||
|
this.identity = identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the identity asserted by the SAML IdP. If authentication has not
|
||||||
|
* yet completed successfully, this will be null.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The identity asserted by the SAML IdP, or null if authentication has
|
||||||
|
* not yet completed successfully.
|
||||||
|
*/
|
||||||
|
public AssertedIdentity getIdentity() {
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
* 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.acs;
|
||||||
|
|
||||||
|
import com.google.common.base.Predicates;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager service that temporarily stores SAML authentication attempts while
|
||||||
|
* the authentication flow is underway. Authentication attempts are represented
|
||||||
|
* as temporary authentication sessions, allowing authentication attempts to
|
||||||
|
* span multiple requests and redirects. Invalid or stale authentication
|
||||||
|
* sessions are automatically purged from storage.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class AuthenticationSessionManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generator of arbitrary, unique, unpredictable identifiers.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
private IdentifierGenerator idGenerator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of authentication session identifiers to their associated
|
||||||
|
* {@link AuthenticationSession}.
|
||||||
|
*/
|
||||||
|
private final ConcurrentMap<String, AuthenticationSession> sessions =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executor service which runs the periodic cleanup task
|
||||||
|
*/
|
||||||
|
private final ScheduledExecutorService executor =
|
||||||
|
Executors.newScheduledThreadPool(1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new AuthenticationSessionManager that manages in-progress
|
||||||
|
* SAML authentication attempts. Invalid, stale sessions are automatically
|
||||||
|
* cleaned up.
|
||||||
|
*/
|
||||||
|
public AuthenticationSessionManager() {
|
||||||
|
executor.scheduleAtFixedRate(() -> {
|
||||||
|
sessions.values().removeIf(Predicates.not(AuthenticationSession::isValid));
|
||||||
|
}, 1, 1, TimeUnit.MINUTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resumes the Guacamole side of the authentication process that was
|
||||||
|
* previously deferred through a call to defer(). Once invoked, the
|
||||||
|
* provided value ceases to be valid for future calls to resume().
|
||||||
|
*
|
||||||
|
* @param identifier
|
||||||
|
* The unique string returned by the call to defer(). For convenience,
|
||||||
|
* this value may safely be null.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The {@link AuthenticationSession} originally provided when defer()
|
||||||
|
* was invoked, or null if the session is no longer valid or no such
|
||||||
|
* value was returned by defer().
|
||||||
|
*/
|
||||||
|
public AuthenticationSession resume(String identifier) {
|
||||||
|
|
||||||
|
if (identifier != null) {
|
||||||
|
AuthenticationSession session = sessions.remove(identifier);
|
||||||
|
if (session != null && session.isValid())
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the identity finally asserted by the SAML IdP at the end of the
|
||||||
|
* authentication process represented by the authentication session with
|
||||||
|
* the given identifier. If there is no such authentication session, or no
|
||||||
|
* valid identity has been asserted by the SAML IdP for that session, null
|
||||||
|
* is returned.
|
||||||
|
*
|
||||||
|
* @param identifier
|
||||||
|
* The unique string returned by the call to defer(). For convenience,
|
||||||
|
* this value may safely be null.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The identity finally asserted by the SAML IdP at the end of the
|
||||||
|
* authentication process represented by the authentication session
|
||||||
|
* with the given identifier, or null if there is no such identity.
|
||||||
|
*/
|
||||||
|
public AssertedIdentity getIdentity(String identifier) {
|
||||||
|
|
||||||
|
AuthenticationSession session = resume(identifier);
|
||||||
|
if (session != null)
|
||||||
|
return session.getIdentity();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defers the Guacamole side of authentication for the user having the
|
||||||
|
* given authentication session such that it may be later resumed through a
|
||||||
|
* call to resume(). If authentication is never resumed, the session will
|
||||||
|
* automatically be cleaned up after it ceases to be valid.
|
||||||
|
*
|
||||||
|
* @param session
|
||||||
|
* The {@link AuthenticationSession} representing the in-progress SAML
|
||||||
|
* authentication attempt.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A unique and unpredictable string that may be used to represent the
|
||||||
|
* given session when calling resume().
|
||||||
|
*/
|
||||||
|
public String defer(AuthenticationSession session) {
|
||||||
|
String identifier = idGenerator.generateIdentifier();
|
||||||
|
sessions.put(identifier, session);
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shuts down the executor service that periodically removes all invalid
|
||||||
|
* authentication sessions. This must be invoked when the SAML extension is
|
||||||
|
* shut down in order to avoid resource leaks.
|
||||||
|
*/
|
||||||
|
public void shutdown() {
|
||||||
|
executor.shutdownNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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.acs;
|
||||||
|
|
||||||
|
import com.google.common.io.BaseEncoding;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generator of unique and unpredictable identifiers. Each generated identifier
|
||||||
|
* is an arbitrary, random string produced using a cryptographically-secure
|
||||||
|
* random number generator and consists of at least 256 bits.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class IdentifierGenerator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cryptographically-secure random number generator for generating unique
|
||||||
|
* identifiers.
|
||||||
|
*/
|
||||||
|
private final SecureRandom secureRandom = new SecureRandom();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a unique and unpredictable identifier. Each identifier is at
|
||||||
|
* least 256-bit and produced using a cryptographically-secure random
|
||||||
|
* number generator.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A unique and unpredictable identifier.
|
||||||
|
*/
|
||||||
|
public String generateIdentifier() {
|
||||||
|
byte[] bytes = new byte[33];
|
||||||
|
secureRandom.nextBytes(bytes);
|
||||||
|
return BaseEncoding.base64().encode(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,188 @@
|
|||||||
|
/*
|
||||||
|
* 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.acs;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
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 java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
|
import javax.xml.xpath.XPathExpressionException;
|
||||||
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.GuacamoleSecurityException;
|
||||||
|
import org.apache.guacamole.GuacamoleServerException;
|
||||||
|
import org.apache.guacamole.auth.saml.conf.ConfigurationService;
|
||||||
|
import org.xml.sax.SAXException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service which abstracts the internals of handling SAML requests and
|
||||||
|
* responses.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class SAMLService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for retrieving SAML configuration information.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
private ConfigurationService confService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager of active SAML authentication attempts.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
private AuthenticationSessionManager sessionManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new SAML request, beginning the overall authentication flow
|
||||||
|
* that will ultimately result in an asserted user identity if the user is
|
||||||
|
* successfully authenticated by the SAML IdP. The URI of the SSO endpoint
|
||||||
|
* of the SAML IdP that the user must be redirected to for the
|
||||||
|
* authentication process to continue is returned.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The URI of the SSO endpoint of the SAML IdP that the user must be
|
||||||
|
* redirected to.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If an error prevents the SAML request and redirect URI from being
|
||||||
|
* generated.
|
||||||
|
*/
|
||||||
|
public URI createRequest() throws GuacamoleException {
|
||||||
|
|
||||||
|
Saml2Settings samlSettings = confService.getSamlSettings();
|
||||||
|
AuthnRequest samlReq = new AuthnRequest(samlSettings);
|
||||||
|
|
||||||
|
// Create a new authentication session to represent this attempt while
|
||||||
|
// it is in progress
|
||||||
|
AuthenticationSession session = new AuthenticationSession(samlReq.getId(),
|
||||||
|
confService.getAuthenticationTimeout() * 60000L);
|
||||||
|
|
||||||
|
// Produce redirect for continuing the authentication process with
|
||||||
|
// the SAML IdP
|
||||||
|
try {
|
||||||
|
return UriBuilder.fromUri(samlSettings.getIdpSingleSignOnServiceUrl().toURI())
|
||||||
|
.queryParam("SAMLRequest", samlReq.getEncodedAuthnRequest())
|
||||||
|
.queryParam("RelayState", sessionManager.defer(session))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
throw new GuacamoleServerException("SAML authentication request "
|
||||||
|
+ "could not be encoded: " + e.getMessage());
|
||||||
|
}
|
||||||
|
catch (URISyntaxException e) {
|
||||||
|
throw new GuacamoleServerException("SAML IdP redirect could not "
|
||||||
|
+ "be generated due to an error in the URI syntax: "
|
||||||
|
+ e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the given SAML response, as received by the SAML ACS endpoint
|
||||||
|
* at the given URL, producing an {@link AuthenticationSession} that now
|
||||||
|
* includes a valid assertion of the user's identity. If the SAML response
|
||||||
|
* is invalid in any way, an exception is thrown.
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* The URL of the ACS endpoint that received the SAML response. This
|
||||||
|
* should be the URL pointing to the single POST-handling endpoint of
|
||||||
|
* {@link AssertionConsumerServiceResource}.
|
||||||
|
*
|
||||||
|
* @param relayState
|
||||||
|
* The "RelayState" value originally provided in the SAML request,
|
||||||
|
* which in our case is the transient the session identifier of the
|
||||||
|
* in-progress authentication attempt. The SAML standard requires that
|
||||||
|
* the identity provider include the "RelayState" value it received
|
||||||
|
* alongside its SAML response.
|
||||||
|
*
|
||||||
|
* @param encodedResponse
|
||||||
|
* The response received from the SAML IdP via the ACS endpoint at the
|
||||||
|
* given URL.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The {@link AuthenticationSession} associated with the in-progress
|
||||||
|
* authentication attempt, now associated with the {@link AssertedIdentity}
|
||||||
|
* representing the identity of the user asserted by the SAML IdP.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If the given SAML response is not valid, or if the configuration
|
||||||
|
* information required to validate or decrypt the response cannot be
|
||||||
|
* read.
|
||||||
|
*/
|
||||||
|
public AuthenticationSession processResponse(String url, String relayState,
|
||||||
|
String encodedResponse) throws GuacamoleException {
|
||||||
|
|
||||||
|
if (relayState == null)
|
||||||
|
throw new GuacamoleSecurityException("\"RelayState\" value "
|
||||||
|
+ "is missing from SAML response.");
|
||||||
|
|
||||||
|
AuthenticationSession session = sessionManager.resume(relayState);
|
||||||
|
if (session == null)
|
||||||
|
throw new GuacamoleSecurityException("\"RelayState\" value "
|
||||||
|
+ "included with SAML response is not valid.");
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Decode received SAML response
|
||||||
|
SamlResponse response = new SamlResponse(confService.getSamlSettings(),
|
||||||
|
url, encodedResponse);
|
||||||
|
|
||||||
|
// Validate SAML response timestamp, signature, etc.
|
||||||
|
if (!response.isValid(session.getRequestID())) {
|
||||||
|
Exception validationException = response.getValidationException();
|
||||||
|
throw new GuacamoleSecurityException("SAML response did not "
|
||||||
|
+ "pass validation: " + validationException.getMessage(),
|
||||||
|
validationException);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse identity asserted by SAML IdP
|
||||||
|
session.setIdentity(new AssertedIdentity(response));
|
||||||
|
return session;
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (ValidationError e) {
|
||||||
|
throw new GuacamoleSecurityException("SAML response did not pass "
|
||||||
|
+ "validation: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
catch (SettingsException e) {
|
||||||
|
throw new GuacamoleServerException("Current SAML settings are "
|
||||||
|
+ "insufficient to decrypt/parse the received SAML "
|
||||||
|
+ "response.", e);
|
||||||
|
}
|
||||||
|
catch (ParserConfigurationException | SAXException | XPathExpressionException e) {
|
||||||
|
throw new GuacamoleServerException("XML contents of SAML "
|
||||||
|
+ "response could not be parsed.", e);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
throw new GuacamoleServerException("Contents of SAML response "
|
||||||
|
+ "could not be decrypted/read.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -32,6 +32,7 @@ import org.apache.guacamole.GuacamoleException;
|
|||||||
import org.apache.guacamole.GuacamoleServerException;
|
import org.apache.guacamole.GuacamoleServerException;
|
||||||
import org.apache.guacamole.environment.Environment;
|
import org.apache.guacamole.environment.Environment;
|
||||||
import org.apache.guacamole.properties.BooleanGuacamoleProperty;
|
import org.apache.guacamole.properties.BooleanGuacamoleProperty;
|
||||||
|
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
|
||||||
import org.apache.guacamole.properties.StringGuacamoleProperty;
|
import org.apache.guacamole.properties.StringGuacamoleProperty;
|
||||||
import org.apache.guacamole.properties.URIGuacamoleProperty;
|
import org.apache.guacamole.properties.URIGuacamoleProperty;
|
||||||
|
|
||||||
@@ -145,6 +146,21 @@ public class ConfigurationService {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum amount of time to allow for an in-progress SAML
|
||||||
|
* authentication attempt to be completed, in minutes. A user that takes
|
||||||
|
* longer than this amount of time to complete authentication with their
|
||||||
|
* identity provider will be redirected back to the identity provider to
|
||||||
|
* try again.
|
||||||
|
*/
|
||||||
|
private static final IntegerGuacamoleProperty SAML_AUTH_TIMEOUT =
|
||||||
|
new IntegerGuacamoleProperty() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() { return "saml-auth-timeout"; }
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Guacamole server environment.
|
* The Guacamole server environment.
|
||||||
*/
|
*/
|
||||||
@@ -295,6 +311,24 @@ public class ConfigurationService {
|
|||||||
return environment.getProperty(SAML_GROUP_ATTRIBUTE, "groups");
|
return environment.getProperty(SAML_GROUP_ATTRIBUTE, "groups");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the maximum amount of time to allow for an in-progress SAML
|
||||||
|
* authentication attempt to be completed, in minutes. A user that takes
|
||||||
|
* longer than this amount of time to complete authentication with their
|
||||||
|
* identity provider will be redirected back to the identity provider to
|
||||||
|
* try again.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The maximum amount of time to allow for an in-progress SAML
|
||||||
|
* authentication attempt to be completed, in minutes.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If the authentication timeout cannot be parsed.
|
||||||
|
*/
|
||||||
|
public int getAuthenticationTimeout() throws GuacamoleException {
|
||||||
|
return environment.getProperty(SAML_AUTH_TIMEOUT, 5);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the collection of SAML settings used to initialize the client.
|
* Returns the collection of SAML settings used to initialize the client.
|
||||||
*
|
*
|
||||||
|
@@ -20,19 +20,39 @@
|
|||||||
package org.apache.guacamole.auth.saml.user;
|
package org.apache.guacamole.auth.saml.user;
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.auth.saml.acs.AssertedIdentity;
|
||||||
|
import org.apache.guacamole.auth.saml.conf.ConfigurationService;
|
||||||
import org.apache.guacamole.net.auth.AbstractAuthenticatedUser;
|
import org.apache.guacamole.net.auth.AbstractAuthenticatedUser;
|
||||||
import org.apache.guacamole.net.auth.AuthenticationProvider;
|
import org.apache.guacamole.net.auth.AuthenticationProvider;
|
||||||
import org.apache.guacamole.net.auth.Credentials;
|
import org.apache.guacamole.net.auth.Credentials;
|
||||||
|
import org.apache.guacamole.token.TokenName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An SAML-specific implementation of AuthenticatedUser, associating a
|
* A SAML-specific implementation of AuthenticatedUser, associating a SAML
|
||||||
* username and particular set of credentials with the SAML authentication
|
* identity and particular set of credentials with the SAML authentication
|
||||||
* provider.
|
* provider.
|
||||||
*/
|
*/
|
||||||
public class SAMLAuthenticatedUser extends AbstractAuthenticatedUser {
|
public class SAMLAuthenticatedUser extends AbstractAuthenticatedUser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The prefix that should be prepended to all parameter tokens generated
|
||||||
|
* from SAML attributes.
|
||||||
|
*/
|
||||||
|
private static final String SAML_ATTRIBUTE_TOKEN_PREFIX = "SAML_";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for retrieving SAML configuration information.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
private ConfigurationService confService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to the authentication provider associated with this
|
* Reference to the authentication provider associated with this
|
||||||
* authenticated user.
|
* authenticated user.
|
||||||
@@ -56,27 +76,78 @@ public class SAMLAuthenticatedUser extends AbstractAuthenticatedUser {
|
|||||||
private Map<String, String> tokens;
|
private Map<String, String> tokens;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes this AuthenticatedUser using the given username and
|
* Returns a Map of all parameter tokens that should be made available for
|
||||||
* credentials.
|
* substitution based on the given {@link AssertedIdentity}. The resulting
|
||||||
|
* Map will contain one parameter token for each SAML attribute in the
|
||||||
|
* SAML response that originally asserted the user's identity. Attributes
|
||||||
|
* that have multiple values will be reduced to a single value, taking the
|
||||||
|
* first available value and discarding the remaining values.
|
||||||
*
|
*
|
||||||
* @param username
|
* @param identity
|
||||||
* The username of the user that was authenticated.
|
* The {@link AssertedIdentity} representing the user identity
|
||||||
|
* asserted by the SAML IdP.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A Map of key and single value pairs that should be made available
|
||||||
|
* for substitution as parameter tokens.
|
||||||
|
*/
|
||||||
|
private Map<String, String> getTokens(AssertedIdentity identity) {
|
||||||
|
return identity.getAttributes().entrySet()
|
||||||
|
.stream()
|
||||||
|
.filter((entry) -> !entry.getValue().isEmpty())
|
||||||
|
.collect(Collectors.toUnmodifiableMap(
|
||||||
|
(entry) -> TokenName.canonicalize(entry.getKey(), SAML_ATTRIBUTE_TOKEN_PREFIX),
|
||||||
|
(entry) -> entry.getValue().get(0)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a set of all group memberships asserted by the SAML IdP.
|
||||||
|
*
|
||||||
|
* @param identity
|
||||||
|
* The {@link AssertedIdentity} representing the user identity
|
||||||
|
* asserted by the SAML IdP.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A set of all groups that the SAML IdP asserts this user is a
|
||||||
|
* member of.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If the configuration information necessary to retrieve group
|
||||||
|
* memberships from a SAML response cannot be read.
|
||||||
|
*/
|
||||||
|
private Set<String> getGroups(AssertedIdentity identity)
|
||||||
|
throws GuacamoleException {
|
||||||
|
|
||||||
|
List<String> samlGroups = identity.getAttributes().get(confService.getGroupAttribute());
|
||||||
|
if (samlGroups == null || samlGroups.isEmpty())
|
||||||
|
return Collections.emptySet();
|
||||||
|
|
||||||
|
return Collections.unmodifiableSet(new HashSet<>(samlGroups));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes this AuthenticatedUser using the given
|
||||||
|
* {@link AssertedIdentity} and credentials.
|
||||||
|
*
|
||||||
|
* @param identity
|
||||||
|
* The {@link AssertedIdentity} representing the user identity
|
||||||
|
* asserted by the SAML IdP.
|
||||||
*
|
*
|
||||||
* @param credentials
|
* @param credentials
|
||||||
* The credentials provided when this user was authenticated.
|
* The credentials provided when this user was authenticated.
|
||||||
*
|
*
|
||||||
* @param tokens
|
* @throws GuacamoleException
|
||||||
* The tokens available from this authentication provider.
|
* If configuration information required for processing the user's
|
||||||
*
|
* identity and group memberships cannot be read.
|
||||||
* @param effectiveGroups
|
|
||||||
* The groups of which this user is a member.
|
|
||||||
*/
|
*/
|
||||||
public void init(String username, Credentials credentials,
|
public void init(AssertedIdentity identity, Credentials credentials)
|
||||||
Map<String, String> tokens, Set<String> effectiveGroups) {
|
throws GuacamoleException {
|
||||||
this.credentials = credentials;
|
this.credentials = credentials;
|
||||||
this.effectiveGroups = effectiveGroups;
|
this.effectiveGroups = getGroups(identity);
|
||||||
this.tokens = tokens;
|
this.tokens = getTokens(identity);
|
||||||
setIdentifier(username);
|
setIdentifier(identity.getUsername());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Reference in New Issue
Block a user