mirror of
				https://github.com/gyurix1968/guacamole-client.git
				synced 2025-10-31 09:03:21 +00:00 
			
		
		
		
	GUACAMOLE-839: Ensure SSL/TLS client auth failures are reflected in the Guacamole UI.
This commit is contained in:
		| @@ -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<SSOAuthenticatedUser> authenticatedUserProvider; | ||||
|  | ||||
|     /** | ||||
|      * The string value that the SSL termination service uses for its client | ||||
|      * verification header to represent that the client certificate has been | ||||
|      * verified. | ||||
|      */ | ||||
|     private static final String CLIENT_VERIFIED_HEADER_SUCCESS_VALUE = "SUCCESS"; | ||||
|  | ||||
|     /** | ||||
|      * The name of the query parameter containing the temporary session token | ||||
|      * representing the current state of an in-progress authentication attempt. | ||||
|      */ | ||||
|     private static final String AUTH_SESSION_PARAMETER_NAME = "state"; | ||||
|  | ||||
|     /** | ||||
|      * Decodes the provided URL-encoded string as UTF-8, returning the result. | ||||
|      * | ||||
|      * @param value | ||||
|      *     The URL-encoded string to decode. | ||||
|      * | ||||
|      * @return | ||||
|      *     The decoded string. | ||||
|      * | ||||
|      * @throws GuacamoleException | ||||
|      *     If the provided value is not a value URL-encoded string. | ||||
|      */ | ||||
|     private byte[] decode(String value) throws GuacamoleException { | ||||
|         try { | ||||
|             return URLDecoder.decode(value, StandardCharsets.UTF_8.name()) | ||||
|                     .getBytes(StandardCharsets.UTF_8); | ||||
|         } | ||||
|         catch (IllegalArgumentException e) { | ||||
|             throw new GuacamoleClientException("Invalid URL-encoded value.", e); | ||||
|         } | ||||
|         catch (UnsupportedEncodingException e) { | ||||
|             // This should never happen, as UTF-8 is a standard charset that | ||||
|             // the JVM is required to support | ||||
|             throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Authenticates a user using HTTP headers containing that user's verified | ||||
|      * X.509 certificate. It is assumed that this certificate is being passed | ||||
|      * to Guacamole from an SSL termination service that has already verified | ||||
|      * that this certificate is valid and authorized for access to that | ||||
|      * Guacamole instance. | ||||
|      * | ||||
|      * @param credentials | ||||
|      *     The credentials received by Guacamole in the authentication request. | ||||
|      * | ||||
|      * @param certificate | ||||
|      *     The raw bytes of the X.509 certificate retrieved from the request. | ||||
|      * | ||||
|      * @return | ||||
|      *     A new SSOAuthenticatedUser representing the identity of the user | ||||
|      *     asserted by the SSL termination service via that user's X.509 | ||||
|      *     certificate. | ||||
|      * | ||||
|      * @throws GuacamoleException | ||||
|      *     If the provided X.509 certificate is not valid or cannot be parsed. | ||||
|      *     It is expected that the SSL termination service will already have | ||||
|      *     validated the certificate; this function validates only the | ||||
|      *     certificate timestamps. | ||||
|      */ | ||||
|     private SSOAuthenticatedUser authenticateUser(Credentials credentials, | ||||
|             byte[] certificate) throws GuacamoleException { | ||||
|  | ||||
|         // Parse and re-verify certificate is valid with respect to timestamps | ||||
|         X509Certificate cert; | ||||
|         try (InputStream input = new ByteArrayInputStream(certificate)) { | ||||
|  | ||||
|             CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); | ||||
|             cert = (X509Certificate) certFactory.generateCertificate(input); | ||||
|  | ||||
|             // Verify certificate is valid (it should be given pre-validation | ||||
|             // from SSL termination, but it's worth rechecking for sanity) | ||||
|             cert.checkValidity(); | ||||
|  | ||||
|         } | ||||
|         catch (CertificateException e) { | ||||
|             throw new GuacamoleClientException("The X.509 certificate " | ||||
|                     + "presented is not valid.", e); | ||||
|         } | ||||
|         catch (IOException e) { | ||||
|             throw new GuacamoleServerException("Provided X.509 certificate " | ||||
|                     + "could not be read.", e); | ||||
|         } | ||||
|  | ||||
|         // Extract user's DN from their X.509 certificate | ||||
|         LdapName dn; | ||||
|         try { | ||||
|             Principal principal = cert.getSubjectX500Principal(); | ||||
|             dn = new LdapName(principal.getName()); | ||||
|         } | ||||
|         catch (InvalidNameException e) { | ||||
|             throw new GuacamoleClientException("The X.509 certificate " | ||||
|                     + "presented does not contain a valid subject DN.", e); | ||||
|         } | ||||
|  | ||||
|         // Verify DN actually contains components | ||||
|         int numComponents = dn.size(); | ||||
|         if (numComponents < 1) | ||||
|             throw new GuacamoleClientException("The X.509 certificate " | ||||
|                     + "presented contains an empty subject DN."); | ||||
|  | ||||
|         // Simply use first component of DN as username (TODO: Enforce | ||||
|         // requirements on the attribute providing the username and the base DN, | ||||
|         // and consider using components following the username to determine | ||||
|         // group memberships) | ||||
|         String username = dn.getRdn(numComponents - 1).getValue().toString(); | ||||
|  | ||||
|         SSOAuthenticatedUser authenticatedUser = authenticatedUserProvider.get(); | ||||
|         authenticatedUser.init(username, credentials, | ||||
|                 Collections.emptySet(), Collections.emptyMap()); | ||||
|         return authenticatedUser; | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Processes the given HTTP request, returning the identity represented by | ||||
|      * the auth session token present in that request. If no such token is | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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()); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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<String> 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."); | ||||
|  | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -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; | ||||
|  | ||||
| }]); | ||||
| @@ -13,6 +13,8 @@ | ||||
|         "styles/sso-providers.css" | ||||
|     ], | ||||
|  | ||||
|     "js" : [ "ssl.min.js" ], | ||||
|  | ||||
|     "html" : [ | ||||
|         "html/sso-providers.html", | ||||
|         "html/sso-provider-ssl.html" | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| <meta name="after-children" content=".login-ui .sso-provider-list:last-child"> | ||||
| <li class="sso-provider sso-provider-ssl"><a href="api/ext/ssl/login">{{ | ||||
| <li class="sso-provider sso-provider-ssl"><a guac-ssl-auth href="">{{ | ||||
|     'LOGIN.NAME_IDP_SSL' | translate | ||||
| }}</a></li> | ||||
|   | ||||
| @@ -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. | ||||
|  */ | ||||
| @@ -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'); | ||||
		Reference in New Issue
	
	Block a user