diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/en.json b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/en.json
index 859301568..902b7aff1 100644
--- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/en.json
+++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/en.json
@@ -12,6 +12,10 @@
"NAME" : "SAML SSO Backend"
},
+ "DATA_SOURCE_SSL" : {
+ "NAME" : "SSL/TLS SSO Backend"
+ },
+
"LOGIN" : {
"FIELD_HEADER_ID_TOKEN" : "",
"FIELD_HEADER_STATE" : "",
@@ -20,6 +24,7 @@
"NAME_IDP_CAS" : "CAS",
"NAME_IDP_OPENID" : "OpenID",
"NAME_IDP_SAML" : "SAML",
+ "NAME_IDP_SSL" : "Certificate / Smart Card",
"SECTION_HEADER_SSO_OPTIONS" : "Sign in with:"
}
diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/.gitignore b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/.gitignore
new file mode 100644
index 000000000..30eb48707
--- /dev/null
+++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/.gitignore
@@ -0,0 +1,3 @@
+*~
+target/
+src/main/resources/generated/
diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/.ratignore b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/.ratignore
new file mode 100644
index 000000000..da318d12f
--- /dev/null
+++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/.ratignore
@@ -0,0 +1 @@
+src/main/resources/html/*.html
diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml
new file mode 100644
index 000000000..a107e6b7f
--- /dev/null
+++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml
@@ -0,0 +1,74 @@
+
+
+
+
+ 4.0.0
+ org.apache.guacamole
+ guacamole-auth-sso-ssl
+ jar
+ 1.5.0
+ guacamole-auth-sso-ssl
+ http://guacamole.apache.org/
+
+
+ org.apache.guacamole
+ guacamole-auth-sso
+ 1.5.0
+ ../../
+
+
+
+
+
+
+ org.apache.guacamole
+ guacamole-ext
+
+
+
+
+ org.apache.guacamole
+ guacamole-auth-sso-base
+
+
+
+
+ com.google.inject
+ guice
+
+
+
+
+ javax.servlet
+ servlet-api
+
+
+
+
+ javax.ws.rs
+ jsr311-api
+
+
+
+
+
diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java
new file mode 100644
index 000000000..16414688e
--- /dev/null
+++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java
@@ -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 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();
+ }
+
+}
diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProvider.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProvider.java
new file mode 100644
index 000000000..6b01858ef
--- /dev/null
+++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProvider.java
@@ -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";
+ }
+
+}
diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProviderModule.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProviderModule.java
new file mode 100644
index 000000000..56e50bd75
--- /dev/null
+++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProviderModule.java
@@ -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);
+ }
+
+}
diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSession.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSession.java
new file mode 100644
index 000000000..e2e0d4c43
--- /dev/null
+++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSession.java
@@ -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;
+ }
+
+}
diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSessionManager.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSessionManager.java
new file mode 100644
index 000000000..3200cae3d
--- /dev/null
+++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSessionManager.java
@@ -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 {
+
+ /**
+ * 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;
+
+ }
+
+}
diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java
new file mode 100644
index 000000000..074ee093b
--- /dev/null
+++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java
@@ -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);
+ }
+
+}
diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/WildcardURIGuacamoleProperty.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/WildcardURIGuacamoleProperty.java
new file mode 100644
index 000000000..d237d8031
--- /dev/null
+++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/WildcardURIGuacamoleProperty.java
@@ -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.");
+
+ }
+
+}
diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/guac-manifest.json
new file mode 100644
index 000000000..30c8a1c99
--- /dev/null
+++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/guac-manifest.json
@@ -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"
+ ]
+
+}
diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html
new file mode 100644
index 000000000..b6c510931
--- /dev/null
+++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html
@@ -0,0 +1,4 @@
+
+{{
+ 'LOGIN.NAME_IDP_SSL' | translate
+}}
diff --git a/extensions/guacamole-auth-sso/pom.xml b/extensions/guacamole-auth-sso/pom.xml
index 04ffe842f..99632e4c3 100644
--- a/extensions/guacamole-auth-sso/pom.xml
+++ b/extensions/guacamole-auth-sso/pom.xml
@@ -49,6 +49,7 @@
modules/guacamole-auth-sso-cas
modules/guacamole-auth-sso-openid
modules/guacamole-auth-sso-saml
+ modules/guacamole-auth-sso-ssl