From 38f1360dec7e00ba4fededfa40001d0a4b02e86a Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 27 Jan 2023 16:38:45 -0800 Subject: [PATCH] GUACAMOLE-839: Ensure SSL/TLS client auth failures are reflected in the Guacamole UI. --- .../sso/AuthenticationSessionManager.java | 14 + .../modules/guacamole-auth-sso-ssl/pom.xml | 50 +++ .../ssl/AuthenticationProviderService.java | 296 ++-------------- .../auth/ssl/OpaqueAuthenticationResult.java | 65 ++++ .../auth/ssl/SSLAuthenticationProvider.java | 3 +- .../ssl/SSLAuthenticationProviderModule.java | 3 +- .../auth/ssl/SSLAuthenticationSession.java | 7 +- .../ssl/SSLAuthenticationSessionManager.java | 3 +- .../ssl/SSLClientAuthenticationResource.java | 332 ++++++++++++++++++ .../auth/ssl/conf/ConfigurationService.java | 84 ++++- .../main/resources/directives/guacSslAuth.js | 68 ++++ .../src/main/resources/guac-manifest.json | 2 + .../main/resources/html/sso-provider-ssl.html | 2 +- .../src/main/resources/license.txt | 18 + .../src/main/resources/sslModule.js | 29 ++ 15 files changed, 681 insertions(+), 295 deletions(-) create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/OpaqueAuthenticationResult.java create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/license.txt create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/sslModule.js diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSessionManager.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSessionManager.java index da530f61b..3261228b8 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSessionManager.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSessionManager.java @@ -68,6 +68,20 @@ public class AuthenticationSessionManager { }, 1, 1, TimeUnit.MINUTES); } + /** + * Generates a cryptographically-secure value identical in form to the + * session tokens generated by {@link #defer(org.apache.guacamole.auth.sso.AuthenticationSession)} + * but invalid. The returned value is indistinguishable from a valid token, + * but is not a valid token. + * + * @return + * An invalid token value that is indistinguishable from a valid + * token. + */ + public String generateInvalid() { + return idGenerator.generateIdentifier(); + } + /** * Resumes the Guacamole side of the authentication process that was * previously deferred through a call to defer(). Once invoked, the 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 index a107e6b7f..c6542ae56 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml @@ -37,6 +37,56 @@ ../../ + + + + + + com.github.buckelieg + minify-maven-plugin + + + default-cli + + UTF-8 + + ${basedir}/src/main/resources + ${project.build.directory}/classes + + / + / + ssl.js + + + license.txt + + + + **/*.js + + + + + **/*.test.js + + CLOSURE + + + + OFF + OFF + + + + + minify + + + + + + + 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 index 16414688e..bc311de47 100644 --- 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 @@ -22,38 +22,16 @@ 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.GuacamoleResourceNotFoundException; 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 @@ -68,13 +46,6 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS @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 @@ -92,124 +63,12 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS @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 @@ -224,80 +83,17 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS * or null if there is no such token or the token does not represent a * valid identity. */ - private SSOAuthenticatedUser processIdentity(HttpServletRequest request) { + private SSOAuthenticatedUser processIdentity(Credentials credentials, 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)) + String username = sessionManager.getIdentity(state); + if (username == null) 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")) - })) - ); + SSOAuthenticatedUser authenticatedUser = authenticatedUserProvider.get(); + authenticatedUser.init(username, credentials, + Collections.emptySet(), Collections.emptyMap()); + return authenticatedUser; } @@ -308,21 +104,22 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS // // 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. + // 1) An unauthenticated user makes a GET request to + // ".../api/ext/ssl/identity". After a series of redirects + // intended to prevent that identity from being inadvertently + // cached and inherited by future authentication attempts on the + // same client machine, an external SSL termination service requests + // and validates the user's certificate, those details are passed + // back to Guacamole via HTTP headers, and Guacamole produces a JSON + // response containing an opaque state value. // - // 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. + // 2) The user (still unauthenticated) resubmits the opaque state + // value from the received JSON as the "state" parameter of a + // standard Guacamole authentication request (".../api/tokens"). // - // 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. + // 3) If the certificate received was valid, the user is authenticated + // according to the identity asserted by that certificate. If not, + // authentication is refused. // // NOTE: All SSL termination endpoints in front of Guacamole MUST // be configured to drop these headers from any inbound requests @@ -345,50 +142,13 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS return null; // - // Handle only auth session tokens at the main redirect URI, using the + // Handle only auth session tokens at the primary 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); - } + if (confService.isPrimaryHostname(host)) + return processIdentity(credentials, request); // All other requests are not allowed - refuse to authenticate throw new GuacamoleClientException("Direct authentication against " @@ -400,9 +160,7 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS @Override public URI getLoginURI() throws GuacamoleException { - long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxDomainValidity()); - String uniqueSubdomain = subdomainNonceService.generate(validityDuration); - return confService.getClientAuthenticationURI(uniqueSubdomain); + throw new GuacamoleResourceNotFoundException("No such resource."); } @Override diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/OpaqueAuthenticationResult.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/OpaqueAuthenticationResult.java new file mode 100644 index 000000000..66020f20a --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/OpaqueAuthenticationResult.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; + +/** + * REST API response that reports the result of attempting to authenticate the + * user using SSL/TLS client authentication. The information within this + * result is intentionally opaque and must be resubmitted in a separate + * authentication request for authentication to finally succeed or fail. + */ +public class OpaqueAuthenticationResult { + + /** + * An arbitrary value representing the result of authenticating the + * current user. + */ + private final String state; + + /** + * Creates a new OpaqueAuthenticationResult containing the given opaque + * state value. Successful authentication results must be indistinguishable + * from unsuccessful results with respect to this value. Only using this + * value within ANOTHER authentication attempt can determine whether + * authentication is successful. + * + * @param state + * An arbitrary value representing the result of authenticating the + * current user. + */ + public OpaqueAuthenticationResult(String state) { + this.state = state; + } + + /** + * Returns an arbitrary value representing the result of authenticating the + * current user. This value may be resubmitted as the "state" parameter of + * an authentication request beneath the primary URI of the web application + * to finalize the authentication procedure and determine whether the + * operation has succeeded or failed. + * + * @return + * An arbitrary value representing the result of authenticating the + * current user. + */ + public String getState() { + return state; + } + +} 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 index 6b01858ef..2458c1a44 100644 --- 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 @@ -20,7 +20,6 @@ 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 @@ -37,7 +36,7 @@ public class SSLAuthenticationProvider extends SSOAuthenticationProvider { * an external SSL termination system using SSL/TLS client authentication. */ public SSLAuthenticationProvider() { - super(AuthenticationProviderService.class, SSOResource.class, + super(AuthenticationProviderService.class, SSLClientAuthenticationResource.class, new SSLAuthenticationProviderModule()); } 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 index 56e50bd75..46eeaa94f 100644 --- 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 @@ -20,6 +20,7 @@ package org.apache.guacamole.auth.ssl; import com.google.inject.AbstractModule; +import com.google.inject.Scopes; import org.apache.guacamole.auth.ssl.conf.ConfigurationService; import org.apache.guacamole.auth.sso.NonceService; @@ -32,7 +33,7 @@ public class SSLAuthenticationProviderModule extends AbstractModule { @Override protected void configure() { bind(ConfigurationService.class); - bind(NonceService.class); + bind(NonceService.class).in(Scopes.SINGLETON); 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 index e2e0d4c43..4a4c9ce8f 100644 --- 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 @@ -20,7 +20,6 @@ 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. @@ -30,7 +29,7 @@ public class SSLAuthenticationSession extends AuthenticationSession { /** * The identity asserted by the external SSL termination service. */ - private final SSOAuthenticatedUser identity; + private final String identity; /** * Creates a new AuthenticationSession representing an in-progress SSL/TLS @@ -44,7 +43,7 @@ public class SSLAuthenticationSession extends AuthenticationSession { * The number of milliseconds that may elapse before this session must * be considered invalid. */ - public SSLAuthenticationSession(SSOAuthenticatedUser identity, long expires) { + public SSLAuthenticationSession(String identity, long expires) { super(expires); this.identity = identity; } @@ -58,7 +57,7 @@ public class SSLAuthenticationSession extends AuthenticationSession { * @return * The identity asserted by the external SSL termination service. */ - public SSOAuthenticatedUser getIdentity() { + public String 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 index 3200cae3d..fc1b0842f 100644 --- 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 @@ -21,7 +21,6 @@ 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 @@ -48,7 +47,7 @@ public class SSLAuthenticationSessionManager * session with the given identifier, or null if there is no such * identity. */ - public SSOAuthenticatedUser getIdentity(String identifier) { + public String getIdentity(String identifier) { SSLAuthenticationSession session = resume(identifier); if (session != null) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java new file mode 100644 index 000000000..4c99394f5 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java @@ -0,0 +1,332 @@ +/* + * 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 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.List; +import java.util.concurrent.TimeUnit; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.ws.rs.GET; +import javax.ws.rs.core.Response; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriBuilder; +import org.apache.guacamole.GuacamoleClientException; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleResourceNotFoundException; +import org.apache.guacamole.GuacamoleServerException; +import org.apache.guacamole.auth.ssl.conf.ConfigurationService; +import org.apache.guacamole.auth.sso.NonceService; +import org.apache.guacamole.auth.sso.SSOResource; + +/** + * REST API resource that allows the user to retrieve an opaque state value + * representing their identity as determined by SSL/TLS client authentication. + * The opaque value may represent a valid identity or an authentication + * failure, and must be resubmitted within a normal Guacamole authentication + * request to finalize the authentication process. + */ +public class SSLClientAuthenticationResource extends SSOResource { + + /** + * 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"; + + /** + * Service for retrieving configuration information. + */ + @Inject + private ConfigurationService confService; + + /** + * 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; + + /** + * Service for validating and generating unique nonce values. Here, these + * nonces are used specifically for generating unique domains. + */ + @Inject + private NonceService subdomainNonceService; + + /** + * Retrieves a single value from the HTTP header having the given name. If + * there are multiple HTTP headers present with this name, the first + * matching header in the request is used. If there are no such headers in + * the request, null is returned. + * + * @param headers + * The HTTP headers present in the request. + * + * @param name + * The name of the header to retrieve. + * + * @return + * The first value of the HTTP header having the given name, or null if + * there is no such header. + */ + private String getHeader(HttpHeaders headers, String name) { + + List values = headers.getRequestHeader(name); + if (values.isEmpty()) + return null; + + return values.get(0); + + } + + /** + * 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 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. + */ + public String getUsername(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) + return dn.getRdn(numComponents - 1).getValue().toString(); + + } + + /** + * 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(HttpHeaders headers) throws GuacamoleException { + + // + // NOTE: A result with an associated state is ALWAYS returned by + // processCertificate(), even if the request does not actually contain + // a valid certificate. This is by design and ensures that the nature + // of a certificate (valid vs. invalid) cannot be determined except + // via Guacamole's authentication endpoint, thus allowing auth failure + // hooks to consider attempts to use invalid certificates as auth + // failures. + // + + // Verify that SSL termination has already verified the certificate + String verified = getHeader(headers, confService.getClientVerifiedHeader()); + if (!CLIENT_VERIFIED_HEADER_SUCCESS_VALUE.equals(verified)) + return sessionManager.generateInvalid(); + + String certificate = getHeader(headers, confService.getClientCertificateHeader()); + if (certificate == null) + return sessionManager.generateInvalid(); + + String username = getUsername(decode(certificate)); + long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxTokenValidity()); + return sessionManager.defer(new SSLAuthenticationSession(username, validityDuration)); + + } + + /** + * Attempts to authenticate the current user using SSL/TLS client + * authentication, returning an opaque value that represents their + * authenticated status. If necessary, the user is first redirected to a + * unique endpoint that supports SSL/TLS client authentication. + * + * @param headers + * All HTTP headers submitted in the user's authentication request. + * + * @param host + * The hostname that the user specified in their HTTP request. + * + * @return + * A Response containing an opaque value representing the user's + * authenticated status, or a Response redirecting the user to a + * unique endpoint that can provide this. + * + * @throws GuacamoleException + * If any required configuration information is missing or cannot be + * parsed, or if the request was not received at a valid subdomain. + */ + @GET + @Path("identity") + public Response authenticateClient(@Context HttpHeaders headers, + @HeaderParam("Host") String host) throws GuacamoleException { + + // Redirect any requests to the domain that does NOT require SSL/TLS + // client authentication to the same endpoint at a domain that does + // require SSL/TLS authentication + String subdomain = confService.getClientAuthenticationSubdomain(host); + if (subdomain == null) { + + long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxDomainValidity()); + String uniqueSubdomain = subdomainNonceService.generate(validityDuration); + + URI clientAuthURI = UriBuilder.fromUri(confService.getClientAuthenticationURI(uniqueSubdomain)) + .path("api/ext/ssl/identity") + .build(); + + return Response.seeOther(clientAuthURI).build(); + + } + + // + // 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. + // + + if (subdomainNonceService.isValid(subdomain)) + return Response.ok(new OpaqueAuthenticationResult(processCertificate(headers))) + .header("Access-Control-Allow-Origin", confService.getPrimaryOrigin().toString()) + .type(MediaType.APPLICATION_JSON) + .build(); + + throw new GuacamoleResourceNotFoundException("No such resource."); + + } + +} 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 index 074ee093b..3f57bd864 100644 --- 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 @@ -21,8 +21,10 @@ package org.apache.guacamole.auth.ssl.conf; import com.google.inject.Inject; import java.net.URI; +import java.net.URISyntaxException; import javax.ws.rs.core.UriBuilder; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.environment.Environment; import org.apache.guacamole.properties.IntegerGuacamoleProperty; import org.apache.guacamole.properties.StringGuacamoleProperty; @@ -80,11 +82,11 @@ public class ConfigurationService { * 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 = + private static final URIGuacamoleProperty SSL_PRIMARY_URI = new URIGuacamoleProperty() { @Override - public String getName() { return "ssl-redirect-uri"; } + public String getName() { return "ssl-primary-uri"; } }; @@ -121,11 +123,10 @@ public class ConfigurationService { * 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. + * must be long enough to allow for network delays in receiving the token, + * 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() { @@ -141,12 +142,12 @@ public class ConfigurationService { * 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. + * interval must be long enough to allow for network delays in + * authenticating the user with the SSL termination service that enforces + * SSL/TLS client 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() { @@ -212,6 +213,11 @@ public class ConfigurationService { */ public String getClientAuthenticationSubdomain(String hostname) throws GuacamoleException { + // Any hostname that matches the explicitly-specific primary URI is not + // a client auth subdomain + if (isPrimaryHostname(hostname)) + return null; + URI authURI = environment.getRequiredProperty(SSL_CLIENT_AUTH_URI); String baseHostname = authURI.getHost(); @@ -240,11 +246,57 @@ public class ConfigurationService { * required. * * @throws GuacamoleException - * If the required property for configuring the redirect URI is missing + * If the required property for configuring the primary URI is missing * or cannot be parsed. */ - public URI getRedirectURI() throws GuacamoleException { - return environment.getRequiredProperty(SSL_REDIRECT_URI); + public URI getPrimaryURI() throws GuacamoleException { + return environment.getRequiredProperty(SSL_PRIMARY_URI); + } + + /** + * Returns the HTTP request origin for requests originating from this + * instance via the primary URI (as returned by {@link #getPrimaryURI()}. + * This value is essentially the same as the primary URI but with only the + * scheme, host, and port present. + * + * @return + * The HTTP request origin for requests originating from this instance + * via the primary URI. + * + * @throws GuacamoleException + * If the required property for configuring the primary URI is missing + * or cannot be parsed. + */ + public URI getPrimaryOrigin() throws GuacamoleException { + URI primaryURI = getPrimaryURI(); + try { + return new URI(primaryURI.getScheme(), null, primaryURI.getHost(), primaryURI.getPort(), null, null, null); + } + catch (URISyntaxException e) { + throw new GuacamoleServerException("Request origin could not be " + + "derived from the configured primary URI.", e); + } + } + + /** + * Returns whether the given hostname is the same as the hostname in the + * primary URI (as returned by {@link #getPrimaryURI()}. Hostnames are + * case-insensitive. + * + * @param hostname + * The hostname to test. + * + * @return + * true if the hostname is the same as the hostname in the primary URI, + * false otherwise. + * + * @throws GuacamoleException + * If the required property for configuring the primary URI is missing + * or cannot be parsed. + */ + public boolean isPrimaryHostname(String hostname) throws GuacamoleException { + URI primaryURI = getPrimaryURI(); + return hostname.equalsIgnoreCase(primaryURI.getHost()); } /** diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js new file mode 100644 index 000000000..82e5c8b6d --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js @@ -0,0 +1,68 @@ +/* + * 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. + */ + +/** + * A directive which automatically attempts to log the current user in using + * SSL/TLS client authentication when the associated element is clicked. + */ +angular.module('element').directive('guacSslAuth', ['$injector', function guacSslAuth($injector) { + + // Required services + var requestService = $injector.get('requestService'); + var authenticationService = $injector.get('authenticationService'); + + var directive = { + restrict: 'A' + }; + + directive.link = function linkGuacSslAuth($scope, $element) { + + /** + * The element which will register the click. + * + * @type Element + */ + const element = $element[0]; + + // Attempt SSL/TLS client authentication upon click + element.addEventListener('click', function elementClicked() { + + // Transform SSL/TLS identity into an opaque "state" value and + // attempt authentication using that value + authenticationService.authenticate( + requestService({ + method: 'GET', + headers : { + 'Cache-Control' : undefined, // Avoid sending headers that would result in a pre-flight OPTIONS request for CORS + 'Pragma' : undefined + }, + url: 'api/ext/ssl/identity' + }) + .then(function identityRetrieved(data) { + return { 'state' : data.state || '' }; + }) + )['catch'](requestService.IGNORE); + + }); + + }; + + return directive; + +}]); 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 index 30c8a1c99..7b8099427 100644 --- 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 @@ -13,6 +13,8 @@ "styles/sso-providers.css" ], + "js" : [ "ssl.min.js" ], + "html" : [ "html/sso-providers.html", "html/sso-provider-ssl.html" 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 index b6c510931..8798eed2d 100644 --- 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 @@ -1,4 +1,4 @@ -
  • {{ +
  • {{ 'LOGIN.NAME_IDP_SSL' | translate }}
  • diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/license.txt b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/license.txt new file mode 100644 index 000000000..042f3ce1f --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/license.txt @@ -0,0 +1,18 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/sslModule.js b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/sslModule.js new file mode 100644 index 000000000..2e3f844ef --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/sslModule.js @@ -0,0 +1,29 @@ +/* + * 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. + */ + +/** + * The module for code implementing SSO using SSL/TLS client authentication. + */ +angular.module('guacSsoSsl', [ + 'auth', + 'rest' +]); + +// Ensure the guacSsoSsl module is loaded along with the rest of the app +angular.module('index').requires.push('guacSsoSsl');