GUACAMOLE-839: Implement base support for SSO using SSL/TLS authentication (certificates / smart cards).

This commit is contained in:
Michael Jumper
2023-01-26 17:18:12 -08:00
parent 6bf0b8cf63
commit e2a6947ff6
14 changed files with 1139 additions and 0 deletions

View File

@@ -12,6 +12,10 @@
"NAME" : "SAML SSO Backend" "NAME" : "SAML SSO Backend"
}, },
"DATA_SOURCE_SSL" : {
"NAME" : "SSL/TLS SSO Backend"
},
"LOGIN" : { "LOGIN" : {
"FIELD_HEADER_ID_TOKEN" : "", "FIELD_HEADER_ID_TOKEN" : "",
"FIELD_HEADER_STATE" : "", "FIELD_HEADER_STATE" : "",
@@ -20,6 +24,7 @@
"NAME_IDP_CAS" : "CAS", "NAME_IDP_CAS" : "CAS",
"NAME_IDP_OPENID" : "OpenID", "NAME_IDP_OPENID" : "OpenID",
"NAME_IDP_SAML" : "SAML", "NAME_IDP_SAML" : "SAML",
"NAME_IDP_SSL" : "Certificate / Smart Card",
"SECTION_HEADER_SSO_OPTIONS" : "Sign in with:" "SECTION_HEADER_SSO_OPTIONS" : "Sign in with:"
} }

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,74 @@
<?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-ssl</artifactId>
<packaging>jar</packaging>
<version>1.5.0</version>
<name>guacamole-auth-sso-ssl</name>
<url>http://guacamole.apache.org/</url>
<parent>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso</artifactId>
<version>1.5.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>
</dependencies>
</project>

View File

@@ -0,0 +1,413 @@
/*
* 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.ssl;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.UriBuilder;
import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.auth.ssl.conf.ConfigurationService;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.auth.sso.NonceService;
import org.apache.guacamole.auth.sso.SSOAuthenticationProviderService;
import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser;
import org.apache.guacamole.form.Field;
import org.apache.guacamole.form.RedirectField;
import org.apache.guacamole.language.TranslatableMessage;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
/**
* Service that authenticates Guacamole users using SSL/TLS authentication
* provided by an external SSL termination service.
*/
@Singleton
public class AuthenticationProviderService implements SSOAuthenticationProviderService {
/**
* Service for retrieving configuration information.
*/
@Inject
private ConfigurationService confService;
/**
* Service for validating and generating unique nonce values. Here, these
* nonces are used specifically for generating unique domains.
*/
@Inject
private NonceService subdomainNonceService;
/**
* Session manager for generating and maintaining unique tokens to
* represent the authentication flow of a user who has only partially
* authenticated. Here, these tokens represent a user that has been
* validated by SSL termination and allow the Guacamole instance that
* doesn't require SSL/TLS authentication to retrieve the user's identity
* and complete the authentication process.
*/
@Inject
private SSLAuthenticationSessionManager sessionManager;
/**
* Provider for AuthenticatedUser objects.
*/
@Inject
private Provider<SSOAuthenticatedUser> authenticatedUserProvider;
/**
* The string value that the SSL termination service uses for its client
* verification header to represent that the client certificate has been
* verified.
*/
private static final String CLIENT_VERIFIED_HEADER_SUCCESS_VALUE = "SUCCESS";
/**
* The name of the query parameter containing the temporary session token
* representing the current state of an in-progress authentication attempt.
*/
private static final String AUTH_SESSION_PARAMETER_NAME = "state";
/**
* Decodes the provided URL-encoded string as UTF-8, returning the result.
*
* @param value
* The URL-encoded string to decode.
*
* @return
* The decoded string.
*
* @throws GuacamoleException
* If the provided value is not a value URL-encoded string.
*/
private byte[] decode(String value) throws GuacamoleException {
try {
return URLDecoder.decode(value, StandardCharsets.UTF_8.name())
.getBytes(StandardCharsets.UTF_8);
}
catch (IllegalArgumentException e) {
throw new GuacamoleClientException("Invalid URL-encoded value.", e);
}
catch (UnsupportedEncodingException e) {
// This should never happen, as UTF-8 is a standard charset that
// the JVM is required to support
throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
}
}
/**
* Authenticates a user using HTTP headers containing that user's verified
* X.509 certificate. It is assumed that this certificate is being passed
* to Guacamole from an SSL termination service that has already verified
* that this certificate is valid and authorized for access to that
* Guacamole instance.
*
* @param credentials
* The credentials received by Guacamole in the authentication request.
*
* @param certificate
* The raw bytes of the X.509 certificate retrieved from the request.
*
* @return
* A new SSOAuthenticatedUser representing the identity of the user
* asserted by the SSL termination service via that user's X.509
* certificate.
*
* @throws GuacamoleException
* If the provided X.509 certificate is not valid or cannot be parsed.
* It is expected that the SSL termination service will already have
* validated the certificate; this function validates only the
* certificate timestamps.
*/
private SSOAuthenticatedUser authenticateUser(Credentials credentials,
byte[] certificate) throws GuacamoleException {
// Parse and re-verify certificate is valid with respect to timestamps
X509Certificate cert;
try (InputStream input = new ByteArrayInputStream(certificate)) {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
cert = (X509Certificate) certFactory.generateCertificate(input);
// Verify certificate is valid (it should be given pre-validation
// from SSL termination, but it's worth rechecking for sanity)
cert.checkValidity();
}
catch (CertificateException e) {
throw new GuacamoleClientException("The X.509 certificate "
+ "presented is not valid.", e);
}
catch (IOException e) {
throw new GuacamoleServerException("Provided X.509 certificate "
+ "could not be read.", e);
}
// Extract user's DN from their X.509 certificate
LdapName dn;
try {
Principal principal = cert.getSubjectX500Principal();
dn = new LdapName(principal.getName());
}
catch (InvalidNameException e) {
throw new GuacamoleClientException("The X.509 certificate "
+ "presented does not contain a valid subject DN.", e);
}
// Verify DN actually contains components
int numComponents = dn.size();
if (numComponents < 1)
throw new GuacamoleClientException("The X.509 certificate "
+ "presented contains an empty subject DN.");
// Simply use first component of DN as username (TODO: Enforce
// requirements on the attribute providing the username and the base DN,
// and consider using components following the username to determine
// group memberships)
String username = dn.getRdn(numComponents - 1).getValue().toString();
SSOAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
authenticatedUser.init(username, credentials,
Collections.emptySet(), Collections.emptyMap());
return authenticatedUser;
}
/**
* Processes the given HTTP request, returning the identity represented by
* the auth session token present in that request. If no such token is
* present, or the token does not represent a valid identity, null is
* returned.
*
* @param request
* The HTTP request to process.
*
* @return
* The identity represented by the auth session token in the request,
* or null if there is no such token or the token does not represent a
* valid identity.
*/
private SSOAuthenticatedUser processIdentity(HttpServletRequest request) {
String state = request.getParameter(AUTH_SESSION_PARAMETER_NAME);
return sessionManager.getIdentity(state);
}
/**
* Processes the X.509 certificate in the headers of the given HTTP
* request, returning an authentication session token representing the
* identity in that certificate. If the certificate is invalid or not
* present, null is returned.
*
* @param credentials
* The credentials submitted in the HTTP request being processed.
*
* @param request
* The HTTP request to process.
*
* @return
* An authentication session token representing the identity in the
* certificate in the given HTTP request, or null if the request does
* not contain a valid certificate.
*
* @throws GuacamoleException
* If any configuration parameters related to retrieving certificates
* from HTTP request cannot be parsed.
*/
private String processCertificate(Credentials credentials,
HttpServletRequest request) throws GuacamoleException {
// Verify that SSL termination has already verified the certificate
String verified = request.getHeader(confService.getClientVerifiedHeader());
if (!CLIENT_VERIFIED_HEADER_SUCCESS_VALUE.equals(verified))
return null;
String certificate = request.getHeader(confService.getClientCertificateHeader());
if (certificate == null)
return null;
SSOAuthenticatedUser authenticatedUser = authenticateUser(credentials, decode(certificate));
long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxTokenValidity());
return sessionManager.defer(new SSLAuthenticationSession(authenticatedUser, validityDuration));
}
/**
* Redirects the current user back to the main URL of the Guacamole
* instance to continue the authentication process after having identified
* themselves using SSL/TLS client authentication.
*
* @param token
* The authentication session token generated for the current user's
* identity.
*
* @throws GuacamoleException
* To redirect the user to the main URL of the Guacamole instance.
*/
private void resumeAuthenticationAtRedirectURI(String token)
throws GuacamoleException {
URI redirectURI = UriBuilder.fromUri(confService.getRedirectURI())
.queryParam(AUTH_SESSION_PARAMETER_NAME, token)
.build();
// Request that the provided credentials, now tokenized, be
// resubmitted in that tokenized form to the original host for
// authentication
throw new GuacamoleInsufficientCredentialsException("Please "
+ "resubmit your tokenized credentials using the "
+ "following URI.",
new CredentialsInfo(Arrays.asList(new Field[] {
new RedirectField(AUTH_SESSION_PARAMETER_NAME, redirectURI,
new TranslatableMessage("LOGIN.INFO_IDP_REDIRECT_PENDING"))
}))
);
}
@Override
public SSOAuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException {
//
// Overall flow:
//
// 1) Unauthenticated user is given a temporary auth session token
// and redirected to the SSL termination instance that provides
// SSL client auth. This redirect uses a unique and temporary
// subdomain to ensure each SSL client auth attempt is fresh and
// does not use cached auth details.
//
// 2) Unauthenticated user with a temporary auth session token
// is validated by SSL termination, with that SSL termination
// adding HTTP headers containing the validated certificate to the
// user's HTTP request.
//
// 3) If valid, the user is assigned a temporary token and redirected
// back to the original URL. That temporary token is accepted by
// this extension at the original URL as proof of the user's
// identity.
//
// NOTE: All SSL termination endpoints in front of Guacamole MUST
// be configured to drop these headers from any inbound requests
// or users may be able to assert arbitrary identities, since this
// extension does not validate anything but the certificate timestamps.
// It relies purely on SSL termination to validate that the certificate
// was signed by the expected CA.
//
// We can't authenticate using SSL/TLS client auth unless there's an
// associated HTTP request
HttpServletRequest request = credentials.getRequest();
if (request == null)
return null;
// We MUST have the domain associated with the request to ensure we
// always get fresh SSL sessions when validating client certificates
String host = request.getHeader("Host");
if (host == null)
return null;
//
// Handle only auth session tokens at the main redirect URI, using the
// pre-verified information from those tokens to determine user
// identity.
//
String redirectHost = confService.getRedirectURI().getHost();
if (host.equals(redirectHost)) {
SSOAuthenticatedUser user = processIdentity(request);
if (user != null)
return user;
// Redirect unauthenticated requests to the endpoint requiring
// SSL client auth to request identity verification
throw new GuacamoleInvalidCredentialsException("Invalid login.",
new CredentialsInfo(Arrays.asList(new Field[] {
new RedirectField(AUTH_SESSION_PARAMETER_NAME, getLoginURI(), // <-- Each call to getLoginURI() produces a unique subdomain that is valid only for ONE use (see below)
new TranslatableMessage("LOGIN.INFO_IDP_REDIRECT_PENDING"))
}))
);
}
//
// Process certificates only at valid single-use subdomains dedicated
// to client authentication, redirecting back to the main redirect URI
// for final authentication if that processing is successful.
//
// NOTE: This is CRITICAL. If unique subdomains are not generated and
// tied to strictly one authentication attempt, then those subdomains
// could be reused by a user on a shared machine to assume the cached
// credentials of another user that used that machine earlier. The
// browser and/or OS may cache the certificate so that it can be reused
// for future SSL sessions to that same domain. Here, we ensure each
// generated domain is unique and only valid for certificate processing
// ONCE. The domain may still be valid with DNS, but will no longer be
// usable for certificate authentication.
//
else if (subdomainNonceService.isValid(confService.getClientAuthenticationSubdomain(host))) {
String token = processCertificate(credentials, request);
if (token != null)
resumeAuthenticationAtRedirectURI(token);
}
// All other requests are not allowed - refuse to authenticate
throw new GuacamoleClientException("Direct authentication against "
+ "this endpoint is not valid without first requesting to "
+ "authenticate at the primary URL of this Guacamole "
+ "instance.");
}
@Override
public URI getLoginURI() throws GuacamoleException {
long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxDomainValidity());
String uniqueSubdomain = subdomainNonceService.generate(validityDuration);
return confService.getClientAuthenticationURI(uniqueSubdomain);
}
@Override
public void shutdown() {
sessionManager.shutdown();
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.ssl;
import org.apache.guacamole.auth.sso.SSOAuthenticationProvider;
import org.apache.guacamole.auth.sso.SSOResource;
/**
* Guacamole authentication backend which authenticates users using SSL/TLS
* client authentication provided by some external SSL termination system. This
* SSL termination system must be configured to provide access to this same
* instance of Guacamole and must have both a wildcard certificate and wildcard
* DNS. No storage for connections is provided - only authentication. Storage
* must be provided by some other extension.
*/
public class SSLAuthenticationProvider extends SSOAuthenticationProvider {
/**
* Creates a new SSLAuthenticationProvider that authenticates users against
* an external SSL termination system using SSL/TLS client authentication.
*/
public SSLAuthenticationProvider() {
super(AuthenticationProviderService.class, SSOResource.class,
new SSLAuthenticationProviderModule());
}
@Override
public String getIdentifier() {
return "ssl";
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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.ssl;
import com.google.inject.AbstractModule;
import org.apache.guacamole.auth.ssl.conf.ConfigurationService;
import org.apache.guacamole.auth.sso.NonceService;
/**
* Guice module which configures injections specific to SSO using SSL/TLS
* client authentication.
*/
public class SSLAuthenticationProviderModule extends AbstractModule {
@Override
protected void configure() {
bind(ConfigurationService.class);
bind(NonceService.class);
bind(SSLAuthenticationSessionManager.class);
}
}

View File

@@ -0,0 +1,65 @@
/*
* 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.ssl;
import org.apache.guacamole.auth.sso.AuthenticationSession;
import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser;
/**
* Representation of an in-progress SSL/TLS authentication attempt.
*/
public class SSLAuthenticationSession extends AuthenticationSession {
/**
* The identity asserted by the external SSL termination service.
*/
private final SSOAuthenticatedUser identity;
/**
* Creates a new AuthenticationSession representing an in-progress SSL/TLS
* authentication attempt.
*
* @param identity
* The identity asserted by the external SSL termination service. This
* MAY NOT be null.
*
* @param expires
* The number of milliseconds that may elapse before this session must
* be considered invalid.
*/
public SSLAuthenticationSession(SSOAuthenticatedUser identity, long expires) {
super(expires);
this.identity = identity;
}
/**
* Returns the identity asserted by the external SSL termination service.
* As authentication will have completed with respect to the SSL
* termination service by the time this session is created, this will
* always be non-null.
*
* @return
* The identity asserted by the external SSL termination service.
*/
public SSOAuthenticatedUser getIdentity() {
return identity;
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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.ssl;
import com.google.inject.Singleton;
import org.apache.guacamole.auth.sso.AuthenticationSessionManager;
import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser;
/**
* Manager service that temporarily stores SSL/TLS authentication attempts
* while the authentication flow is underway.
*/
@Singleton
public class SSLAuthenticationSessionManager
extends AuthenticationSessionManager<SSLAuthenticationSession> {
/**
* Returns the identity asserted by the external SSL termination service 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 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 asserted by the external SSL termination service 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 SSOAuthenticatedUser getIdentity(String identifier) {
SSLAuthenticationSession session = resume(identifier);
if (session != null)
return session.getIdentity();
return null;
}
}

View File

@@ -0,0 +1,325 @@
/*
* 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.ssl.conf;
import com.google.inject.Inject;
import java.net.URI;
import javax.ws.rs.core.UriBuilder;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
import org.apache.guacamole.properties.StringGuacamoleProperty;
import org.apache.guacamole.properties.URIGuacamoleProperty;
/**
* Service for retrieving configuration information regarding SSO using SSL/TLS
* authentication.
*/
public class ConfigurationService {
/**
* The default name of the header to use to retrieve the URL-encoded client
* certificate from an HTTP request received from an SSL termination
* service providing SSL/TLS client authentication.
*/
private static String DEFAULT_CLIENT_CERTIFICATE_HEADER = "X-Client-Certificate";
/**
* The default name of the header to use to retrieve the verification
* status of the certificate an HTTP request received from an SSL
* termination service providing SSL/TLS client authentication.
*/
private static String DEFAULT_CLIENT_VERIFIED_HEADER = "X-Client-Verified";
/**
* The default amount of time that a temporary authentication token for
* SSL/TLS authentication may remain valid, in minutes.
*/
private static int DEFAULT_MAX_TOKEN_VALIDITY = 5;
/**
* The default amount of time that the temporary, unique subdomain
* generated for SSL/TLS authentication may remain valid, in minutes.
*/
private static int DEFAULT_MAX_DOMAIN_VALIDITY = 5;
/**
* The property representing the URI that should be used to authenticate
* users with SSL/TLS client authentication. This must be a URI that points
* to THIS instance of Guacamole, but behind SSL termination that requires
* SSL/TLS client authentication.
*/
private static final WildcardURIGuacamoleProperty SSL_CLIENT_AUTH_URI =
new WildcardURIGuacamoleProperty() {
@Override
public String getName() { return "ssl-client-auth-uri"; }
};
/**
* The property representing the URI of this instance without SSL/TLS
* client authentication required. This must be a URI that points
* to THIS instance of Guacamole, but behind SSL termination that DOES NOT
* require or request SSL/TLS client authentication.
*/
private static final URIGuacamoleProperty SSL_REDIRECT_URI =
new URIGuacamoleProperty() {
@Override
public String getName() { return "ssl-redirect-uri"; }
};
/**
* The property representing the name of the header to use to retrieve the
* URL-encoded client certificate from an HTTP request received from an
* SSL termination service providing SSL/TLS client authentication.
*/
private static final StringGuacamoleProperty SSL_CLIENT_CERTIFICATE_HEADER =
new StringGuacamoleProperty() {
@Override
public String getName() { return "ssl-client-certificate-header"; }
};
/**
* The property representing the name of the header to use to retrieve the
* verification status of the certificate an HTTP request received from an
* SSL termination service providing SSL/TLS client authentication. This
* value of this header must be "SUCCESS" (all uppercase) if the
* certificate was successfully verified.
*/
private static final StringGuacamoleProperty SSL_CLIENT_VERIFIED_HEADER =
new StringGuacamoleProperty() {
@Override
public String getName() { return "ssl-client-verified-header"; }
};
/**
* The property representing the amount of time that a temporary
* authentication token for SSL/TLS authentication may remain valid, in
* minutes. This token is used to represent the user's asserted identity
* after it has been verified by the SSL termination service. This interval
* must be long enough to allow for network delays in redirecting the user
* back to the main Guacamole URL, but short enough that unused tokens do
* not consume unnecessary server resources and cannot potentially be
* guessed while the token is still valid. These tokens are 256-bit secure
* random values.
*/
private static final IntegerGuacamoleProperty SSL_MAX_TOKEN_VALIDITY =
new IntegerGuacamoleProperty() {
@Override
public String getName() { return "ssl-max-token-validity"; }
};
/**
* The property representing the amount of time that the temporary, unique
* subdomain generated for SSL/TLS authentication may remain valid, in
* minutes. This subdomain is used to ensure each SSL/TLS authentication
* attempt is fresh and does not potentially reuse a previous
* authentication attempt that was cached by the browser or OS. This
* interval must be long enough to allow for network delays in redirecting
* the user to the SSL termination service enforcing SSL/TLS
* authentication, but short enough that an unused domain does not consume
* unnecessary server resources and cannot potentially be guessed while
* that subdomain is still valid. These subdomains are 128-bit secure
* random values.
*/
private static final IntegerGuacamoleProperty SSL_MAX_DOMAIN_VALIDITY =
new IntegerGuacamoleProperty() {
@Override
public String getName() { return "ssl-max-domain-validity"; }
};
/**
* The Guacamole server environment.
*/
@Inject
private Environment environment;
/**
* Returns a URI that should be used to authenticate users with SSL/TLS
* client authentication. The returned URI will consist of the configured
* client authentication URI with the wildcard portion ("*.") replaced with
* the given subdomain.
*
* @param subdomain
* The subdomain that should replace the wildcard portion of the
* configured client authentication URI.
*
* @return
* A URI that should be used to authenticate users with SSL/TLS
* client authentication.
*
* @throws GuacamoleException
* If the required property for configuring the client authentication
* URI is missing or cannot be parsed.
*/
public URI getClientAuthenticationURI(String subdomain) throws GuacamoleException {
URI authURI = environment.getRequiredProperty(SSL_CLIENT_AUTH_URI);
String baseHostname = authURI.getHost();
// Add provided subdomain to auth URI
return UriBuilder.fromUri(authURI)
.host(subdomain + "." + baseHostname)
.build();
}
/**
* Given a hostname that was used by a user for SSL/TLS client
* authentication, returns the subdomain at the beginning of that hostname.
* If the hostname does not match the pattern of hosts represented by the
* configured client authentication URI, null is returned.
*
* @param hostname
* The hostname to extract the subdomain from.
*
* @return
* The subdomain at the beginning of the provided hostname, if that
* hostname matches the pattern of hosts represented by the
* configured client authentication URI, or null otherwise.
*
* @throws GuacamoleException
* If the required property for configuring the client authentication
* URI is missing or cannot be parsed.
*/
public String getClientAuthenticationSubdomain(String hostname) throws GuacamoleException {
URI authURI = environment.getRequiredProperty(SSL_CLIENT_AUTH_URI);
String baseHostname = authURI.getHost();
// Verify the first domain component is at least one character in
// length
int firstPeriod = hostname.indexOf('.');
if (firstPeriod <= 0)
return null;
// Verify domain matches the configured auth URI except for the leading
// subdomain
if (!hostname.regionMatches(true, firstPeriod + 1, baseHostname, 0, baseHostname.length()))
return null;
// Extract subdomain
return hostname.substring(0, firstPeriod);
}
/**
* Returns the URI of this instance without SSL/TLS client authentication
* required.
*
* @return
* The URI of this instance without SSL/TLS client authentication
* required.
*
* @throws GuacamoleException
* If the required property for configuring the redirect URI is missing
* or cannot be parsed.
*/
public URI getRedirectURI() throws GuacamoleException {
return environment.getRequiredProperty(SSL_REDIRECT_URI);
}
/**
* Returns the name of the header to use to retrieve the URL-encoded client
* certificate from an HTTP request received from an SSL termination
* service providing SSL/TLS client authentication.
*
* @return
* The name of the header to use to retrieve the URL-encoded client
* certificate from an HTTP request received from an SSL termination
* service providing SSL/TLS client authentication.
*
* @throws GuacamoleException
* If the property for configuring the client certificate header cannot
* be parsed.
*/
public String getClientCertificateHeader() throws GuacamoleException {
return environment.getProperty(SSL_CLIENT_CERTIFICATE_HEADER, DEFAULT_CLIENT_CERTIFICATE_HEADER);
}
/**
* Returns the name of the header to use to retrieve the verification
* status of the certificate an HTTP request received from an SSL
* termination service providing SSL/TLS client authentication.
*
* @return
* The name of the header to use to retrieve the verification
* status of the certificate an HTTP request received from an SSL
* termination service providing SSL/TLS client authentication.
*
* @throws GuacamoleException
* If the property for configuring the client verification header
* cannot be parsed.
*/
public String getClientVerifiedHeader() throws GuacamoleException {
return environment.getProperty(SSL_CLIENT_VERIFIED_HEADER, DEFAULT_CLIENT_VERIFIED_HEADER);
}
/**
* Returns the maximum amount of time that the token generated by the
* Guacamole server representing current SSL authentication state should
* remain valid, in minutes. This imposes an upper limit on the amount of
* time any particular authentication request can result in successful
* authentication within Guacamole when SSL/TLS client authentication is
* configured. By default, this will be 5.
*
* @return
* The maximum amount of time that an SSL authentication token
* generated by the Guacamole server should remain valid, in minutes.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public int getMaxTokenValidity() throws GuacamoleException {
return environment.getProperty(SSL_MAX_TOKEN_VALIDITY, DEFAULT_MAX_TOKEN_VALIDITY);
}
/**
* Returns the maximum amount of time that a unique client authentication
* subdomain generated by the Guacamole server should remain valid, in
* minutes. This imposes an upper limit on the amount of time any
* particular authentication request can result in successful
* authentication within Guacamole when SSL/TLS client authentication is
* configured. By default, this will be 5.
*
* @return
* The maximum amount of time that a unique client authentication
* subdomain generated by the Guacamole server should remain valid, in
* minutes.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public int getMaxDomainValidity() throws GuacamoleException {
return environment.getProperty(SSL_MAX_DOMAIN_VALIDITY, DEFAULT_MAX_DOMAIN_VALIDITY);
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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.ssl.conf;
import java.net.URI;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.properties.URIGuacamoleProperty;
/**
* A GuacamoleProperty whose value is a wildcard URI. The behavior of this
* property is identical to URIGuacamoleProperty except that it verifies a
* wildcard hostname prefix ("*.") is present and strips that prefix from the
* parsed URI.
*/
public abstract class WildcardURIGuacamoleProperty extends URIGuacamoleProperty {
/**
* Regular expression that broadly matches URIs that contain wildcards in
* their hostname. This regular expression is NOT strict and will match
* invalid URIs. It is only strict enough to recognize a wildcard hostname
* prefix.
*/
private static final Pattern WILDCARD_URI_PATTERN = Pattern.compile("([^:]+://(?:[^@]+@)?)\\*\\.(.*)");
@Override
public URI parseValue(String value) throws GuacamoleException {
// Verify wildcard prefix is present
Matcher matcher = WILDCARD_URI_PATTERN.matcher(value);
if (matcher.matches()) {
// Strip wildcard prefix from URI and verify a valid hostname is
// still present
URI uri = super.parseValue(matcher.group(1) + matcher.group(2));
if (uri.getHost() != null)
return uri;
}
// All other values are not valid wildcard URIs
throw new GuacamoleServerException("Value \"" + value
+ "\" is not a valid wildcard URI.");
}
}

View File

@@ -0,0 +1,33 @@
{
"guacamoleVersion" : "1.5.0",
"name" : "SSL Authentication Extension",
"namespace" : "ssl",
"authProviders" : [
"org.apache.guacamole.auth.ssl.SSLAuthenticationProvider"
],
"css" : [
"styles/sso-providers.css"
],
"html" : [
"html/sso-providers.html",
"html/sso-provider-ssl.html"
],
"translations" : [
"translations/ca.json",
"translations/de.json",
"translations/en.json",
"translations/fr.json",
"translations/ja.json",
"translations/ko.json",
"translations/pt.json",
"translations/ru.json",
"translations/zh.json"
]
}

View File

@@ -0,0 +1,4 @@
<meta name="after-children" content=".login-ui .sso-provider-list:last-child">
<li class="sso-provider sso-provider-ssl"><a href="api/ext/ssl/login">{{
'LOGIN.NAME_IDP_SSL' | translate
}}</a></li>

View File

@@ -49,6 +49,7 @@
<module>modules/guacamole-auth-sso-cas</module> <module>modules/guacamole-auth-sso-cas</module>
<module>modules/guacamole-auth-sso-openid</module> <module>modules/guacamole-auth-sso-openid</module>
<module>modules/guacamole-auth-sso-saml</module> <module>modules/guacamole-auth-sso-saml</module>
<module>modules/guacamole-auth-sso-ssl</module>
</modules> </modules>