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