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

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