GUACAMOLE-1364: Refactor all SSO extensions beneath common base.

This commit is contained in:
Michael Jumper
2021-11-25 17:54:08 -08:00
parent ea657099f5
commit 36a02c1f90
86 changed files with 326 additions and 62 deletions

View File

@@ -0,0 +1,3 @@
*~
target/
src/main/resources/generated/

View File

@@ -0,0 +1 @@
src/main/resources/html/*.html

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso-saml</artifactId>
<packaging>jar</packaging>
<version>1.3.0</version>
<name>guacamole-auth-sso-saml</name>
<url>http://guacamole.apache.org/</url>
<parent>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso</artifactId>
<version>1.3.0</version>
<relativePath>../../</relativePath>
</parent>
<dependencies>
<!-- Guacamole Extension API -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-ext</artifactId>
</dependency>
<!-- Core SSO support -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso-base</artifactId>
</dependency>
<!-- Guice -->
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
</dependency>
<!-- Java servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</dependency>
<!-- JAX-RS Annotations -->
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
</dependency>
<!-- OneLogin SAML Library -->
<dependency>
<groupId>com.onelogin</groupId>
<artifactId>java-saml</artifactId>
<version>2.6.0</version>
<exclusions>
<!-- Force consistent version of commons-codec (see below) -->
<exclusion>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Force single, specific version of commons-codec (multiple
dependencies reference multiple versions of this). -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
<id>dist</id>
<baseDirectory>${project.artifactId}-${project.version}</baseDirectory>
<!-- Output tar.gz -->
<formats>
<format>tar.gz</format>
</formats>
<!-- Include licenses and extension .jar -->
<fileSets>
<!-- Include licenses -->
<fileSet>
<outputDirectory></outputDirectory>
<directory>target/licenses</directory>
</fileSet>
<!-- Include extension .jar -->
<fileSet>
<directory>target</directory>
<outputDirectory></outputDirectory>
<includes>
<include>*.jar</include>
</includes>
</fileSet>
</fileSets>
</assembly>

View File

@@ -0,0 +1,119 @@
/*
* 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 java.net.URI;
import java.util.Arrays;
import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.auth.saml.user.SAMLAuthenticatedUser;
import org.apache.guacamole.GuacamoleException;
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.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;
/**
* Service that authenticates Guacamole users by processing the responses of
* SAML identity providers.
*/
public class AuthenticationProviderService {
/**
* The name of the query parameter that identifies an active authentication
* session (in-progress SAML authentication attempt).
*/
public static final String AUTH_SESSION_QUERY_PARAM = "state";
/**
* Provider for AuthenticatedUser objects.
*/
@Inject
private Provider<SAMLAuthenticatedUser> authenticatedUserProvider;
/**
* Manager of active SAML authentication attempts.
*/
@Inject
private AuthenticationSessionManager sessionManager;
/**
* Service for processing SAML requests/responses.
*/
@Inject
private SAMLService 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 {
// No authentication can be attempted without a corresponding HTTP
// request
HttpServletRequest request = credentials.getRequest();
if (request == null)
return null;
// Use established SAML identity if already provided by the SAML IdP
AssertedIdentity identity = sessionManager.getIdentity(request.getParameter(AUTH_SESSION_QUERY_PARAM));
if (identity != null) {
// Back-port the username to the credentials
credentials.setUsername(identity.getUsername());
// Configure the AuthenticatedUser and return it
SAMLAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
authenticatedUser.init(identity, credentials);
return authenticatedUser;
}
// Redirect to SAML IdP if no SAML identity is associated with the
// Guacamole authentication request
URI authUri = saml.createRequest();
throw new GuacamoleInsufficientCredentialsException("Redirecting to SAML IdP.",
new CredentialsInfo(Arrays.asList(new Field[] {
new RedirectField("samlRedirect", authUri, new TranslatableMessage("LOGIN.INFO_SAML_REDIRECT_PENDING"))
}))
);
}
}

View File

@@ -0,0 +1,102 @@
/*
* 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.Guice;
import com.google.inject.Injector;
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.AbstractAuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.TokenInjectingUserContext;
import org.apache.guacamole.net.auth.UserContext;
/**
* AuthenticationProvider implementation that authenticates Guacamole users
* against a SAML SSO Identity Provider (IdP). This module does not provide any
* storage for connection information, and must be layered with other modules
* for authenticated users to have access to Guacamole connections.
*/
public class SAMLAuthenticationProvider extends AbstractAuthenticationProvider {
/**
* Injector which will manage the object graph of this authentication
* provider.
*/
private final Injector injector;
/**
* Creates a new SAMLAuthenticationProvider that authenticates users
* against a SAML IdP.
*/
public SAMLAuthenticationProvider() {
// Set up Guice injector.
injector = Guice.createInjector(
new SAMLAuthenticationProviderModule(this)
);
}
@Override
public String getIdentifier() {
return "saml";
}
@Override
public Object getResource() throws GuacamoleException {
return injector.getInstance(AssertionConsumerServiceResource.class);
}
@Override
public AuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException {
// Attempt to authenticate user with given credentials
AuthenticationProviderService authProviderService =
injector.getInstance(AuthenticationProviderService.class);
return authProviderService.authenticateUser(credentials);
}
@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
public void shutdown() {
injector.getInstance(AuthenticationSessionManager.class).shutdown();
}
}

View File

@@ -0,0 +1,81 @@
/*
* 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.AbstractModule;
import org.apache.guacamole.auth.saml.conf.ConfigurationService;
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.LocalEnvironment;
import org.apache.guacamole.net.auth.AuthenticationProvider;
/**
* Guice module which configures SAML-specific injections.
*/
public class SAMLAuthenticationProviderModule extends AbstractModule {
/**
* Guacamole server environment.
*/
private final Environment environment;
/**
* A reference to the SAMLAuthenticationProvider on behalf of which this
* module has configured injection.
*/
private final AuthenticationProvider authProvider;
/**
* Creates a new SAML authentication provider module which configures
* injection for the SAMLAuthenticationProvider.
*
* @param authProvider
* The AuthenticationProvider for which injection is being configured.
*/
public SAMLAuthenticationProviderModule(AuthenticationProvider authProvider) {
// Get local environment
this.environment = LocalEnvironment.getInstance();
// Store associated auth provider
this.authProvider = authProvider;
}
@Override
protected void configure() {
// Bind core implementations of guacamole-ext classes
bind(AuthenticationProvider.class).toInstance(authProvider);
bind(Environment.class).toInstance(environment);
// Bind SAML-specific services
bind(AssertionConsumerServiceResource.class);
bind(AuthenticationSessionManager.class);
bind(ConfigurationService.class);
bind(IdentifierGenerator.class);
bind(SAMLService.class);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,394 @@
/*
* 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.conf;
import com.google.inject.Inject;
import com.onelogin.saml2.settings.IdPMetadataParser;
import com.onelogin.saml2.settings.Saml2Settings;
import com.onelogin.saml2.settings.SettingsBuilder;
import com.onelogin.saml2.util.Constants;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.core.UriBuilder;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.properties.BooleanGuacamoleProperty;
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
import org.apache.guacamole.properties.StringGuacamoleProperty;
import org.apache.guacamole.properties.URIGuacamoleProperty;
/**
* Service for retrieving configuration information regarding the SAML
* authentication module.
*/
public class ConfigurationService {
/**
* The URI of the file containing the XML Metadata associated with the
* SAML IdP.
*/
private static final URIGuacamoleProperty SAML_IDP_METADATA_URL =
new URIGuacamoleProperty() {
@Override
public String getName() { return "saml-idp-metadata-url"; }
};
/**
* The URL of the SAML IdP.
*/
private static final URIGuacamoleProperty SAML_IDP_URL =
new URIGuacamoleProperty() {
@Override
public String getName() { return "saml-idp-url"; }
};
/**
* The URL identifier for this SAML client.
*/
private static final URIGuacamoleProperty SAML_ENTITY_ID =
new URIGuacamoleProperty() {
@Override
public String getName() { return "saml-entity-id"; }
};
/**
* The callback URL to use for SAML IdP, normally the base
* of the Guacamole install. The SAML extensions callback
* endpoint will be appended to this value.
*/
private static final URIGuacamoleProperty SAML_CALLBACK_URL =
new URIGuacamoleProperty() {
@Override
public String getName() { return "saml-callback-url"; }
};
/**
* Whether or not debugging should be enabled in the SAML library to help
* track down errors.
*/
private static final BooleanGuacamoleProperty SAML_DEBUG =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "saml-debug"; }
};
/**
* Whether or not to enable compression for the SAML request.
*/
private static final BooleanGuacamoleProperty SAML_COMPRESS_REQUEST =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "saml-compress-request"; }
};
/**
* Whether or not to enable compression for the SAML response.
*/
private static final BooleanGuacamoleProperty SAML_COMPRESS_RESPONSE =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "saml-compress-response"; }
};
/**
* Whether or not to enforce strict SAML security during processing.
*/
private static final BooleanGuacamoleProperty SAML_STRICT =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "saml-strict"; }
};
/**
* The property that defines what attribute the SAML provider will return
* that contains group membership for the authenticated user.
*/
private static final StringGuacamoleProperty SAML_GROUP_ATTRIBUTE =
new StringGuacamoleProperty() {
@Override
public String getName() { return "saml-group-attribute"; }
};
/**
* 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.
*/
@Inject
private Environment environment;
/**
* Returns the URL to be submitted as the client ID to the SAML IdP, as
* configured in guacamole.properties.
*
* @return
* The URL to send to the SAML IdP as the Client Identifier.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
private URI getEntityId() throws GuacamoleException {
return environment.getProperty(SAML_ENTITY_ID);
}
/**
* The URI that contains the metadata that the SAML client should
* use to communicate with the SAML IdP. This can either be a remote
* URL of a server that provides this, or can be a URI to a file on the
* local filesystem. The metadata file is usually generated by the SAML IdP
* and should be uploaded to the system where the Guacamole client is
* running.
*
* @return
* The URI of the file containing the metadata used by the SAML client
* when it communicates with the SAML IdP.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed, or if the client
* metadata is missing.
*/
private URI getIdpMetadata() throws GuacamoleException {
return environment.getProperty(SAML_IDP_METADATA_URL);
}
/**
* Return the URL used to log in to the SAML IdP.
*
* @return
* The URL used to log in to the SAML IdP.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
private URI getIdpUrl() throws GuacamoleException {
return environment.getProperty(SAML_IDP_URL);
}
/**
* The callback URL used for the SAML IdP to POST a response
* to upon completion of authentication, normally the base
* of the Guacamole install.
*
* @return
* The callback URL to be sent to the SAML IdP that will
* be POSTed to upon completion of SAML authentication.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed, or the property
* is missing.
*/
public URI getCallbackUrl() throws GuacamoleException {
return environment.getRequiredProperty(SAML_CALLBACK_URL);
}
/**
* Return the Boolean value that indicates whether SAML client debugging
* will be enabled, as configured in guacamole.properties. The default is
* false, and debug information will not be generated or logged.
*
* @return
* True if debugging should be enabled in the SAML library, otherwise
* false.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
private boolean getDebug() throws GuacamoleException {
return environment.getProperty(SAML_DEBUG, false);
}
/**
* Return the Boolean value that indicates whether or not compression of
* SAML requests to the IdP should be enabled or not, as configured in
* guacamole.properties. The default is to enable compression.
*
* @return
* True if compression should be enabled when sending the SAML request,
* otherwise false.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
private boolean getCompressRequest() throws GuacamoleException {
return environment.getProperty(SAML_COMPRESS_REQUEST, true);
}
/**
* Return a Boolean value that indicates whether or not the SAML login
* should enforce strict security controls, as configured in
* guacamole.properties. By default this is true, and should be set to
* true in any production environment.
*
* @return
* True if the SAML login should enforce strict security checks,
* otherwise false.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
private boolean getStrict() throws GuacamoleException {
return environment.getProperty(SAML_STRICT, true);
}
/**
* Return a Boolean value that indicates whether or not compression should
* be requested from the server when the SAML response is returned, as
* configured in guacamole.properties. The default is to request that the
* response be compressed.
*
* @return
* True if compression should be requested from the server for the SAML
* response.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
private boolean getCompressResponse() throws GuacamoleException {
return environment.getProperty(SAML_COMPRESS_RESPONSE, true);
}
/**
* Return the name of the attribute that will be supplied by the identity
* provider that contains the groups of which this user is a member.
*
* @return
* The name of the attribute that contains the user groups.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public String getGroupAttribute() throws GuacamoleException {
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.
*
* @return
* The collection of SAML settings used to initialize the SAML client.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed or if required parameters
* are missing.
*/
public Saml2Settings getSamlSettings() throws GuacamoleException {
// Try to get the XML file, first.
URI idpMetadata = getIdpMetadata();
Map<String, Object> samlMap;
if (idpMetadata != null) {
try {
samlMap = IdPMetadataParser.parseRemoteXML(idpMetadata.toURL());
}
catch (Exception e) {
throw new GuacamoleServerException(
"Could not parse SAML IdP Metadata file.", e);
}
}
// If no XML metadata is provided, fall-back to individual values.
else {
samlMap = new HashMap<>();
samlMap.put(SettingsBuilder.IDP_ENTITYID_PROPERTY_KEY,
getIdpUrl().toString());
samlMap.put(SettingsBuilder.IDP_SINGLE_SIGN_ON_SERVICE_URL_PROPERTY_KEY,
getIdpUrl().toString());
samlMap.put(SettingsBuilder.IDP_SINGLE_SIGN_ON_SERVICE_BINDING_PROPERTY_KEY,
Constants.BINDING_HTTP_REDIRECT);
}
// Read entity ID from properties if not provided within metadata XML
if (!samlMap.containsKey(SettingsBuilder.SP_ENTITYID_PROPERTY_KEY)) {
URI entityId = getEntityId();
if (entityId == null)
throw new GuacamoleServerException("SAML Entity ID was not found"
+ " in either the metadata XML file or guacamole.properties");
samlMap.put(SettingsBuilder.SP_ENTITYID_PROPERTY_KEY, entityId.toString());
}
// Derive ACS URL from properties if not provided within metadata XML
if (!samlMap.containsKey(SettingsBuilder.SP_ASSERTION_CONSUMER_SERVICE_URL_PROPERTY_KEY)) {
samlMap.put(SettingsBuilder.SP_ASSERTION_CONSUMER_SERVICE_URL_PROPERTY_KEY,
UriBuilder.fromUri(getCallbackUrl()).path("api/ext/saml/callback").build().toString());
}
SettingsBuilder samlBuilder = new SettingsBuilder();
Saml2Settings samlSettings = samlBuilder.fromValues(samlMap).build();
samlSettings.setStrict(getStrict());
samlSettings.setDebug(getDebug());
samlSettings.setCompressRequest(getCompressRequest());
samlSettings.setCompressResponse(getCompressResponse());
return samlSettings;
}
}

View File

@@ -0,0 +1,178 @@
/*
* 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.user;
import com.google.inject.Inject;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
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.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.token.TokenName;
/**
* A SAML-specific implementation of AuthenticatedUser, associating a SAML
* identity and particular set of credentials with the SAML authentication
* provider.
*/
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
* authenticated user.
*/
@Inject
private AuthenticationProvider authProvider;
/**
* The credentials provided when this user was authenticated.
*/
private Credentials credentials;
/**
* The effective groups of the authenticated user.
*/
private Set<String> effectiveGroups;
/**
* Tokens associated with the authenticated user.
*/
private Map<String, String> tokens;
/**
* Returns a Map of all parameter tokens that should be made available for
* 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 identity
* 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 Collections.unmodifiableMap(identity.getAttributes().entrySet()
.stream()
.filter((entry) -> !entry.getValue().isEmpty())
.collect(Collectors.toMap(
(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
* The credentials provided when this user was authenticated.
*
* @throws GuacamoleException
* If configuration information required for processing the user's
* identity and group memberships cannot be read.
*/
public void init(AssertedIdentity identity, Credentials credentials)
throws GuacamoleException {
this.credentials = credentials;
this.effectiveGroups = getGroups(identity);
this.tokens = getTokens(identity);
setIdentifier(identity.getUsername());
}
/**
* Returns a Map of tokens associated with this authenticated user.
*
* @return
* A map of token names and values available from this user account.
*/
public Map<String, String> getTokens() {
return tokens;
}
@Override
public AuthenticationProvider getAuthenticationProvider() {
return authProvider;
}
@Override
public Credentials getCredentials() {
return credentials;
}
@Override
public Set<String> getEffectiveUserGroups() {
return effectiveGroups;
}
}

View File

@@ -0,0 +1,20 @@
{
"guacamoleVersion" : "1.3.0",
"name" : "SAML Authentication Extension",
"namespace" : "saml",
"authProviders" : [
"org.apache.guacamole.auth.saml.SAMLAuthenticationProvider"
],
"translations" : [
"translations/ca.json",
"translations/en.json",
"translations/ko.json",
"translations/fr.json",
"translations/pt.json"
]
}

View File

@@ -0,0 +1,18 @@
/*
* 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.
*/

View File

@@ -0,0 +1,12 @@
{
"DATA_SOURCE_SAML" : {
"NAME" : "Extensión de autenticación SAML"
},
"LOGIN" : {
"FIELD_HEADER_SAML" : "",
"INFO_SAML_REDIRECT_PENDING" : "Por favor espere, redirigiendo al proveedor de identidad ..."
}
}

View File

@@ -0,0 +1,12 @@
{
"DATA_SOURCE_SAML" : {
"NAME" : "SAML Authentication Extension"
},
"LOGIN" : {
"FIELD_HEADER_SAML" : "",
"INFO_SAML_REDIRECT_PENDING" : "Please wait, redirecting to identity provider..."
}
}

View File

@@ -0,0 +1,12 @@
{
"DATA_SOURCE_SAML" : {
"NAME" : "SAML Authentication Extension"
},
"LOGIN" : {
"FIELD_HEADER_SAML" : "",
"INFO_SAML_REDIRECT_PENDING" : "Veuillez patienter, redirection vers le fournisseur d'identité..."
}
}

View File

@@ -0,0 +1,11 @@
{
"DATA_SOURCE_SAML" : {
"NAME" : "SAML 인증 확장 프로그램"
},
"LOGIN" : {
"INFO_SAML_REDIRECT_PENDING": "잠시만 기다려주십시오. ID 제공자로 리디렉션 중..."
}
}

View File

@@ -0,0 +1,12 @@
{
"DATA_SOURCE_SAML" : {
"NAME" : "SAML Authentication Extension"
},
"LOGIN" : {
"FIELD_HEADER_SAML" : "",
"INFO_SAML_REDIRECT_PENDING" : "Por favor aguarde, redirecionando para o provedor de indentidade..."
}
}