mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 13:17:41 +00:00
GUACAMOLE-839: Ensure SSL/TLS client auth failures are reflected in the Guacamole UI.
This commit is contained in:
@@ -68,6 +68,20 @@ public class AuthenticationSessionManager<T extends AuthenticationSession> {
|
|||||||
}, 1, 1, TimeUnit.MINUTES);
|
}, 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
|
* Resumes the Guacamole side of the authentication process that was
|
||||||
* previously deferred through a call to defer(). Once invoked, the
|
* previously deferred through a call to defer(). Once invoked, the
|
||||||
|
@@ -37,6 +37,56 @@
|
|||||||
<relativePath>../../</relativePath>
|
<relativePath>../../</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
|
||||||
|
<!-- JS/CSS Minification Plugin -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>com.github.buckelieg</groupId>
|
||||||
|
<artifactId>minify-maven-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>default-cli</id>
|
||||||
|
<configuration>
|
||||||
|
<charset>UTF-8</charset>
|
||||||
|
|
||||||
|
<webappSourceDir>${basedir}/src/main/resources</webappSourceDir>
|
||||||
|
<webappTargetDir>${project.build.directory}/classes</webappTargetDir>
|
||||||
|
|
||||||
|
<jsSourceDir>/</jsSourceDir>
|
||||||
|
<jsTargetDir>/</jsTargetDir>
|
||||||
|
<jsFinalFile>ssl.js</jsFinalFile>
|
||||||
|
|
||||||
|
<jsSourceFiles>
|
||||||
|
<jsSourceFile>license.txt</jsSourceFile>
|
||||||
|
</jsSourceFiles>
|
||||||
|
|
||||||
|
<jsSourceIncludes>
|
||||||
|
<jsSourceInclude>**/*.js</jsSourceInclude>
|
||||||
|
</jsSourceIncludes>
|
||||||
|
|
||||||
|
<!-- Do not minify and include tests -->
|
||||||
|
<jsSourceExcludes>
|
||||||
|
<jsSourceExclude>**/*.test.js</jsSourceExclude>
|
||||||
|
</jsSourceExcludes>
|
||||||
|
<jsEngine>CLOSURE</jsEngine>
|
||||||
|
|
||||||
|
<!-- Disable warnings for JSDoc annotations -->
|
||||||
|
<closureWarningLevels>
|
||||||
|
<misplacedTypeAnnotation>OFF</misplacedTypeAnnotation>
|
||||||
|
<nonStandardJsDocs>OFF</nonStandardJsDocs>
|
||||||
|
</closureWarningLevels>
|
||||||
|
|
||||||
|
</configuration>
|
||||||
|
<goals>
|
||||||
|
<goal>minify</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
||||||
<!-- Guacamole Extension API -->
|
<!-- Guacamole Extension API -->
|
||||||
|
@@ -22,38 +22,16 @@ package org.apache.guacamole.auth.ssl;
|
|||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
import com.google.inject.Singleton;
|
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.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.Collections;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import javax.naming.InvalidNameException;
|
|
||||||
import javax.naming.ldap.LdapName;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
|
||||||
import org.apache.guacamole.GuacamoleClientException;
|
import org.apache.guacamole.GuacamoleClientException;
|
||||||
import org.apache.guacamole.auth.ssl.conf.ConfigurationService;
|
import org.apache.guacamole.auth.ssl.conf.ConfigurationService;
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
import org.apache.guacamole.GuacamoleServerException;
|
import org.apache.guacamole.GuacamoleResourceNotFoundException;
|
||||||
import org.apache.guacamole.auth.sso.NonceService;
|
|
||||||
import org.apache.guacamole.auth.sso.SSOAuthenticationProviderService;
|
import org.apache.guacamole.auth.sso.SSOAuthenticationProviderService;
|
||||||
import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser;
|
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;
|
||||||
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
|
* Service that authenticates Guacamole users using SSL/TLS authentication
|
||||||
@@ -68,13 +46,6 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS
|
|||||||
@Inject
|
@Inject
|
||||||
private ConfigurationService confService;
|
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
|
* Session manager for generating and maintaining unique tokens to
|
||||||
* represent the authentication flow of a user who has only partially
|
* represent the authentication flow of a user who has only partially
|
||||||
@@ -92,124 +63,12 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS
|
|||||||
@Inject
|
@Inject
|
||||||
private Provider<SSOAuthenticatedUser> authenticatedUserProvider;
|
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
|
* The name of the query parameter containing the temporary session token
|
||||||
* representing the current state of an in-progress authentication attempt.
|
* representing the current state of an in-progress authentication attempt.
|
||||||
*/
|
*/
|
||||||
private static final String AUTH_SESSION_PARAMETER_NAME = "state";
|
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
|
* Processes the given HTTP request, returning the identity represented by
|
||||||
* the auth session token present in that request. If no such token is
|
* 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
|
* or null if there is no such token or the token does not represent a
|
||||||
* valid identity.
|
* valid identity.
|
||||||
*/
|
*/
|
||||||
private SSOAuthenticatedUser processIdentity(HttpServletRequest request) {
|
private SSOAuthenticatedUser processIdentity(Credentials credentials, HttpServletRequest request) {
|
||||||
|
|
||||||
String state = request.getParameter(AUTH_SESSION_PARAMETER_NAME);
|
String state = request.getParameter(AUTH_SESSION_PARAMETER_NAME);
|
||||||
return sessionManager.getIdentity(state);
|
String username = sessionManager.getIdentity(state);
|
||||||
}
|
if (username == null)
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes the X.509 certificate in the headers of the given HTTP
|
|
||||||
* request, returning an authentication session token representing the
|
|
||||||
* identity in that certificate. If the certificate is invalid or not
|
|
||||||
* present, null is returned.
|
|
||||||
*
|
|
||||||
* @param credentials
|
|
||||||
* The credentials submitted in the HTTP request being processed.
|
|
||||||
*
|
|
||||||
* @param request
|
|
||||||
* The HTTP request to process.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
* An authentication session token representing the identity in the
|
|
||||||
* certificate in the given HTTP request, or null if the request does
|
|
||||||
* not contain a valid certificate.
|
|
||||||
*
|
|
||||||
* @throws GuacamoleException
|
|
||||||
* If any configuration parameters related to retrieving certificates
|
|
||||||
* from HTTP request cannot be parsed.
|
|
||||||
*/
|
|
||||||
private String processCertificate(Credentials credentials,
|
|
||||||
HttpServletRequest request) throws GuacamoleException {
|
|
||||||
|
|
||||||
// Verify that SSL termination has already verified the certificate
|
|
||||||
String verified = request.getHeader(confService.getClientVerifiedHeader());
|
|
||||||
if (!CLIENT_VERIFIED_HEADER_SUCCESS_VALUE.equals(verified))
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
String certificate = request.getHeader(confService.getClientCertificateHeader());
|
SSOAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
|
||||||
if (certificate == null)
|
authenticatedUser.init(username, credentials,
|
||||||
return null;
|
Collections.emptySet(), Collections.emptyMap());
|
||||||
|
return authenticatedUser;
|
||||||
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"))
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,21 +104,22 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS
|
|||||||
//
|
//
|
||||||
// Overall flow:
|
// Overall flow:
|
||||||
//
|
//
|
||||||
// 1) Unauthenticated user is given a temporary auth session token
|
// 1) An unauthenticated user makes a GET request to
|
||||||
// and redirected to the SSL termination instance that provides
|
// ".../api/ext/ssl/identity". After a series of redirects
|
||||||
// SSL client auth. This redirect uses a unique and temporary
|
// intended to prevent that identity from being inadvertently
|
||||||
// subdomain to ensure each SSL client auth attempt is fresh and
|
// cached and inherited by future authentication attempts on the
|
||||||
// does not use cached auth details.
|
// 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
|
// 2) The user (still unauthenticated) resubmits the opaque state
|
||||||
// is validated by SSL termination, with that SSL termination
|
// value from the received JSON as the "state" parameter of a
|
||||||
// adding HTTP headers containing the validated certificate to the
|
// standard Guacamole authentication request (".../api/tokens").
|
||||||
// user's HTTP request.
|
|
||||||
//
|
//
|
||||||
// 3) If valid, the user is assigned a temporary token and redirected
|
// 3) If the certificate received was valid, the user is authenticated
|
||||||
// back to the original URL. That temporary token is accepted by
|
// according to the identity asserted by that certificate. If not,
|
||||||
// this extension at the original URL as proof of the user's
|
// authentication is refused.
|
||||||
// identity.
|
|
||||||
//
|
//
|
||||||
// NOTE: All SSL termination endpoints in front of Guacamole MUST
|
// NOTE: All SSL termination endpoints in front of Guacamole MUST
|
||||||
// be configured to drop these headers from any inbound requests
|
// be configured to drop these headers from any inbound requests
|
||||||
@@ -345,50 +142,13 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS
|
|||||||
return null;
|
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
|
// pre-verified information from those tokens to determine user
|
||||||
// identity.
|
// identity.
|
||||||
//
|
//
|
||||||
|
|
||||||
String redirectHost = confService.getRedirectURI().getHost();
|
if (confService.isPrimaryHostname(host))
|
||||||
if (host.equals(redirectHost)) {
|
return processIdentity(credentials, request);
|
||||||
|
|
||||||
SSOAuthenticatedUser user = processIdentity(request);
|
|
||||||
if (user != null)
|
|
||||||
return user;
|
|
||||||
|
|
||||||
// Redirect unauthenticated requests to the endpoint requiring
|
|
||||||
// SSL client auth to request identity verification
|
|
||||||
throw new GuacamoleInvalidCredentialsException("Invalid login.",
|
|
||||||
new CredentialsInfo(Arrays.asList(new Field[] {
|
|
||||||
new RedirectField(AUTH_SESSION_PARAMETER_NAME, getLoginURI(), // <-- Each call to getLoginURI() produces a unique subdomain that is valid only for ONE use (see below)
|
|
||||||
new TranslatableMessage("LOGIN.INFO_IDP_REDIRECT_PENDING"))
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Process certificates only at valid single-use subdomains dedicated
|
|
||||||
// to client authentication, redirecting back to the main redirect URI
|
|
||||||
// for final authentication if that processing is successful.
|
|
||||||
//
|
|
||||||
// NOTE: This is CRITICAL. If unique subdomains are not generated and
|
|
||||||
// tied to strictly one authentication attempt, then those subdomains
|
|
||||||
// could be reused by a user on a shared machine to assume the cached
|
|
||||||
// credentials of another user that used that machine earlier. The
|
|
||||||
// browser and/or OS may cache the certificate so that it can be reused
|
|
||||||
// for future SSL sessions to that same domain. Here, we ensure each
|
|
||||||
// generated domain is unique and only valid for certificate processing
|
|
||||||
// ONCE. The domain may still be valid with DNS, but will no longer be
|
|
||||||
// usable for certificate authentication.
|
|
||||||
//
|
|
||||||
|
|
||||||
else if (subdomainNonceService.isValid(confService.getClientAuthenticationSubdomain(host))) {
|
|
||||||
String token = processCertificate(credentials, request);
|
|
||||||
if (token != null)
|
|
||||||
resumeAuthenticationAtRedirectURI(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
// All other requests are not allowed - refuse to authenticate
|
// All other requests are not allowed - refuse to authenticate
|
||||||
throw new GuacamoleClientException("Direct authentication against "
|
throw new GuacamoleClientException("Direct authentication against "
|
||||||
@@ -400,9 +160,7 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public URI getLoginURI() throws GuacamoleException {
|
public URI getLoginURI() throws GuacamoleException {
|
||||||
long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxDomainValidity());
|
throw new GuacamoleResourceNotFoundException("No such resource.");
|
||||||
String uniqueSubdomain = subdomainNonceService.generate(validityDuration);
|
|
||||||
return confService.getClientAuthenticationURI(uniqueSubdomain);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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;
|
package org.apache.guacamole.auth.ssl;
|
||||||
|
|
||||||
import org.apache.guacamole.auth.sso.SSOAuthenticationProvider;
|
import org.apache.guacamole.auth.sso.SSOAuthenticationProvider;
|
||||||
import org.apache.guacamole.auth.sso.SSOResource;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Guacamole authentication backend which authenticates users using SSL/TLS
|
* 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.
|
* an external SSL termination system using SSL/TLS client authentication.
|
||||||
*/
|
*/
|
||||||
public SSLAuthenticationProvider() {
|
public SSLAuthenticationProvider() {
|
||||||
super(AuthenticationProviderService.class, SSOResource.class,
|
super(AuthenticationProviderService.class, SSLClientAuthenticationResource.class,
|
||||||
new SSLAuthenticationProviderModule());
|
new SSLAuthenticationProviderModule());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -20,6 +20,7 @@
|
|||||||
package org.apache.guacamole.auth.ssl;
|
package org.apache.guacamole.auth.ssl;
|
||||||
|
|
||||||
import com.google.inject.AbstractModule;
|
import com.google.inject.AbstractModule;
|
||||||
|
import com.google.inject.Scopes;
|
||||||
import org.apache.guacamole.auth.ssl.conf.ConfigurationService;
|
import org.apache.guacamole.auth.ssl.conf.ConfigurationService;
|
||||||
import org.apache.guacamole.auth.sso.NonceService;
|
import org.apache.guacamole.auth.sso.NonceService;
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ public class SSLAuthenticationProviderModule extends AbstractModule {
|
|||||||
@Override
|
@Override
|
||||||
protected void configure() {
|
protected void configure() {
|
||||||
bind(ConfigurationService.class);
|
bind(ConfigurationService.class);
|
||||||
bind(NonceService.class);
|
bind(NonceService.class).in(Scopes.SINGLETON);
|
||||||
bind(SSLAuthenticationSessionManager.class);
|
bind(SSLAuthenticationSessionManager.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -20,7 +20,6 @@
|
|||||||
package org.apache.guacamole.auth.ssl;
|
package org.apache.guacamole.auth.ssl;
|
||||||
|
|
||||||
import org.apache.guacamole.auth.sso.AuthenticationSession;
|
import org.apache.guacamole.auth.sso.AuthenticationSession;
|
||||||
import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Representation of an in-progress SSL/TLS authentication attempt.
|
* 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.
|
* 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
|
* 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
|
* The number of milliseconds that may elapse before this session must
|
||||||
* be considered invalid.
|
* be considered invalid.
|
||||||
*/
|
*/
|
||||||
public SSLAuthenticationSession(SSOAuthenticatedUser identity, long expires) {
|
public SSLAuthenticationSession(String identity, long expires) {
|
||||||
super(expires);
|
super(expires);
|
||||||
this.identity = identity;
|
this.identity = identity;
|
||||||
}
|
}
|
||||||
@@ -58,7 +57,7 @@ public class SSLAuthenticationSession extends AuthenticationSession {
|
|||||||
* @return
|
* @return
|
||||||
* The identity asserted by the external SSL termination service.
|
* The identity asserted by the external SSL termination service.
|
||||||
*/
|
*/
|
||||||
public SSOAuthenticatedUser getIdentity() {
|
public String getIdentity() {
|
||||||
return identity;
|
return identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -21,7 +21,6 @@ package org.apache.guacamole.auth.ssl;
|
|||||||
|
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import org.apache.guacamole.auth.sso.AuthenticationSessionManager;
|
import org.apache.guacamole.auth.sso.AuthenticationSessionManager;
|
||||||
import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manager service that temporarily stores SSL/TLS authentication attempts
|
* 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
|
* session with the given identifier, or null if there is no such
|
||||||
* identity.
|
* identity.
|
||||||
*/
|
*/
|
||||||
public SSOAuthenticatedUser getIdentity(String identifier) {
|
public String getIdentity(String identifier) {
|
||||||
|
|
||||||
SSLAuthenticationSession session = resume(identifier);
|
SSLAuthenticationSession session = resume(identifier);
|
||||||
if (session != null)
|
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 com.google.inject.Inject;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.GuacamoleServerException;
|
||||||
import org.apache.guacamole.environment.Environment;
|
import org.apache.guacamole.environment.Environment;
|
||||||
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
|
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
|
||||||
import org.apache.guacamole.properties.StringGuacamoleProperty;
|
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
|
* to THIS instance of Guacamole, but behind SSL termination that DOES NOT
|
||||||
* require or request SSL/TLS client authentication.
|
* require or request SSL/TLS client authentication.
|
||||||
*/
|
*/
|
||||||
private static final URIGuacamoleProperty SSL_REDIRECT_URI =
|
private static final URIGuacamoleProperty SSL_PRIMARY_URI =
|
||||||
new URIGuacamoleProperty() {
|
new URIGuacamoleProperty() {
|
||||||
|
|
||||||
@Override
|
@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
|
* authentication token for SSL/TLS authentication may remain valid, in
|
||||||
* minutes. This token is used to represent the user's asserted identity
|
* minutes. This token is used to represent the user's asserted identity
|
||||||
* after it has been verified by the SSL termination service. This interval
|
* 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
|
* must be long enough to allow for network delays in receiving the token,
|
||||||
* back to the main Guacamole URL, but short enough that unused tokens do
|
* but short enough that unused tokens do not consume unnecessary server
|
||||||
* not consume unnecessary server resources and cannot potentially be
|
* resources and cannot potentially be guessed while the token is still
|
||||||
* guessed while the token is still valid. These tokens are 256-bit secure
|
* valid. These tokens are 256-bit secure random values.
|
||||||
* random values.
|
|
||||||
*/
|
*/
|
||||||
private static final IntegerGuacamoleProperty SSL_MAX_TOKEN_VALIDITY =
|
private static final IntegerGuacamoleProperty SSL_MAX_TOKEN_VALIDITY =
|
||||||
new IntegerGuacamoleProperty() {
|
new IntegerGuacamoleProperty() {
|
||||||
@@ -141,12 +142,12 @@ public class ConfigurationService {
|
|||||||
* minutes. This subdomain is used to ensure each SSL/TLS authentication
|
* minutes. This subdomain is used to ensure each SSL/TLS authentication
|
||||||
* attempt is fresh and does not potentially reuse a previous
|
* attempt is fresh and does not potentially reuse a previous
|
||||||
* authentication attempt that was cached by the browser or OS. This
|
* authentication attempt that was cached by the browser or OS. This
|
||||||
* interval must be long enough to allow for network delays in redirecting
|
* interval must be long enough to allow for network delays in
|
||||||
* the user to the SSL termination service enforcing SSL/TLS
|
* authenticating the user with the SSL termination service that enforces
|
||||||
* authentication, but short enough that an unused domain does not consume
|
* SSL/TLS client authentication, but short enough that an unused domain
|
||||||
* unnecessary server resources and cannot potentially be guessed while
|
* does not consume unnecessary server resources and cannot potentially be
|
||||||
* that subdomain is still valid. These subdomains are 128-bit secure
|
* guessed while that subdomain is still valid. These subdomains are
|
||||||
* random values.
|
* 128-bit secure random values.
|
||||||
*/
|
*/
|
||||||
private static final IntegerGuacamoleProperty SSL_MAX_DOMAIN_VALIDITY =
|
private static final IntegerGuacamoleProperty SSL_MAX_DOMAIN_VALIDITY =
|
||||||
new IntegerGuacamoleProperty() {
|
new IntegerGuacamoleProperty() {
|
||||||
@@ -212,6 +213,11 @@ public class ConfigurationService {
|
|||||||
*/
|
*/
|
||||||
public String getClientAuthenticationSubdomain(String hostname) throws GuacamoleException {
|
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);
|
URI authURI = environment.getRequiredProperty(SSL_CLIENT_AUTH_URI);
|
||||||
String baseHostname = authURI.getHost();
|
String baseHostname = authURI.getHost();
|
||||||
|
|
||||||
@@ -240,11 +246,57 @@ public class ConfigurationService {
|
|||||||
* required.
|
* required.
|
||||||
*
|
*
|
||||||
* @throws GuacamoleException
|
* @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.
|
* or cannot be parsed.
|
||||||
*/
|
*/
|
||||||
public URI getRedirectURI() throws GuacamoleException {
|
public URI getPrimaryURI() throws GuacamoleException {
|
||||||
return environment.getRequiredProperty(SSL_REDIRECT_URI);
|
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"
|
"styles/sso-providers.css"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
"js" : [ "ssl.min.js" ],
|
||||||
|
|
||||||
"html" : [
|
"html" : [
|
||||||
"html/sso-providers.html",
|
"html/sso-providers.html",
|
||||||
"html/sso-provider-ssl.html"
|
"html/sso-provider-ssl.html"
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<meta name="after-children" content=".login-ui .sso-provider-list:last-child">
|
<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
|
'LOGIN.NAME_IDP_SSL' | translate
|
||||||
}}</a></li>
|
}}</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