From 1015df6b47279f34c0dc53de8a6e73288cd19997 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 17 Jan 2023 10:42:53 -0800 Subject: [PATCH 01/16] GUACAMOLE-839: Move nonce-generation service to internal common SSO library. --- .../main/java/org/apache/guacamole/auth/sso}/NonceService.java | 2 +- .../guacamole/auth/openid/AuthenticationProviderService.java | 2 +- .../auth/openid/OpenIDAuthenticationProviderModule.java | 2 +- .../guacamole/auth/openid/token/TokenValidationService.java | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) rename extensions/guacamole-auth-sso/modules/{guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token => guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso}/NonceService.java (98%) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/NonceService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/NonceService.java similarity index 98% rename from extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/NonceService.java rename to extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/NonceService.java index 778112a76..7c41dd9d6 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/NonceService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/NonceService.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.guacamole.auth.openid.token; +package org.apache.guacamole.auth.sso; import com.google.inject.Singleton; import java.math.BigInteger; diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java index 23ac815dc..e83ca092e 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java @@ -29,9 +29,9 @@ import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.UriBuilder; import org.apache.guacamole.auth.openid.conf.ConfigurationService; -import org.apache.guacamole.auth.openid.token.NonceService; import org.apache.guacamole.auth.openid.token.TokenValidationService; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.auth.sso.NonceService; import org.apache.guacamole.auth.sso.SSOAuthenticationProviderService; import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser; import org.apache.guacamole.form.Field; diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/OpenIDAuthenticationProviderModule.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/OpenIDAuthenticationProviderModule.java index dde4ef2a6..ba1bf74b3 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/OpenIDAuthenticationProviderModule.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/OpenIDAuthenticationProviderModule.java @@ -21,7 +21,7 @@ package org.apache.guacamole.auth.openid; import com.google.inject.AbstractModule; import org.apache.guacamole.auth.openid.conf.ConfigurationService; -import org.apache.guacamole.auth.openid.token.NonceService; +import org.apache.guacamole.auth.sso.NonceService; import org.apache.guacamole.auth.openid.token.TokenValidationService; /** diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java index 72200df3c..b9c2add2a 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Set; import org.apache.guacamole.auth.openid.conf.ConfigurationService; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.auth.sso.NonceService; import org.jose4j.jwk.HttpsJwks; import org.jose4j.jwt.JwtClaims; import org.jose4j.jwt.MalformedClaimException; From f2c7d746ea75a6cf4985090bbb324fa40d055873 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 26 Jan 2023 09:43:49 -0800 Subject: [PATCH 02/16] GUACAMOLE-839: Move authentication session management service to internal common SSO library. --- .../auth/sso/AuthenticationSession.java | 58 ++++++++++++++++++ .../sso}/AuthenticationSessionManager.java | 56 +++++------------- .../auth/sso}/IdentifierGenerator.java | 25 ++++++-- .../guacamole/auth/sso/NonceService.java | 18 +++--- .../saml/AuthenticationProviderService.java | 4 +- .../SAMLAuthenticationProviderModule.java | 6 +- .../acs/AssertionConsumerServiceResource.java | 4 +- ...on.java => SAMLAuthenticationSession.java} | 28 ++++----- .../acs/SAMLAuthenticationSessionManager.java | 59 +++++++++++++++++++ .../guacamole/auth/saml/acs/SAMLService.java | 14 ++--- 10 files changed, 188 insertions(+), 84 deletions(-) create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSession.java rename extensions/guacamole-auth-sso/modules/{guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs => guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso}/AuthenticationSessionManager.java (72%) rename extensions/guacamole-auth-sso/modules/{guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs => guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso}/IdentifierGenerator.java (66%) rename extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/{AuthenticationSession.java => SAMLAuthenticationSession.java} (77%) create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/SAMLAuthenticationSessionManager.java diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSession.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSession.java new file mode 100644 index 000000000..89d75df70 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSession.java @@ -0,0 +1,58 @@ +/* + * 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.sso; + +/** + * Representation of an in-progress authentication attempt. + */ +public class AuthenticationSession { + + /** + * The absolute point in time after which this authentication session is + * invalid. This value is a UNIX epoch timestamp, as may be returned by + * {@link System#currentTimeMillis()}. + */ + private final long expirationTimestamp; + + /** + * Creates a new AuthenticationSession representing an in-progress + * authentication attempt. + * + * @param expires + * The number of milliseconds that may elapse before this session must + * be considered invalid. + */ + public AuthenticationSession(long expires) { + this.expirationTimestamp = System.currentTimeMillis() + expires; + } + + /** + * Returns whether this authentication session is still valid (has not yet + * expired). + * + * @return + * true if this authentication session is still valid, false if it has + * expired. + */ + public boolean isValid() { + return System.currentTimeMillis() < expirationTimestamp; + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/AuthenticationSessionManager.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSessionManager.java similarity index 72% rename from extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/AuthenticationSessionManager.java rename to extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSessionManager.java index 2e55d2cfc..7050c98be 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/AuthenticationSessionManager.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSessionManager.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.guacamole.auth.saml.acs; +package org.apache.guacamole.auth.sso; import com.google.common.base.Predicates; import com.google.inject.Inject; @@ -29,14 +29,17 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** - * Manager service that temporarily stores SAML authentication attempts while + * Manager service that temporarily stores a user's authentication status while * the authentication flow is underway. Authentication attempts are represented * as temporary authentication sessions, allowing authentication attempts to - * span multiple requests and redirects. Invalid or stale authentication + * span multiple requests, redirects, etc. Invalid or stale authentication * sessions are automatically purged from storage. + * + * @param + * The type of sessions managed by this session manager. */ @Singleton -public class AuthenticationSessionManager { +public class AuthenticationSessionManager { /** * Generator of arbitrary, unique, unpredictable identifiers. @@ -48,8 +51,7 @@ public class AuthenticationSessionManager { * Map of authentication session identifiers to their associated * {@link AuthenticationSession}. */ - private final ConcurrentMap sessions = - new ConcurrentHashMap<>(); + private final ConcurrentMap sessions = new ConcurrentHashMap<>(); /** * Executor service which runs the periodic cleanup task @@ -59,7 +61,7 @@ public class AuthenticationSessionManager { /** * Creates a new AuthenticationSessionManager that manages in-progress - * SAML authentication attempts. Invalid, stale sessions are automatically + * authentication attempts. Invalid, stale sessions are automatically * cleaned up. */ public AuthenticationSessionManager() { @@ -82,10 +84,10 @@ public class AuthenticationSessionManager { * was invoked, or null if the session is no longer valid or no such * value was returned by defer(). */ - public AuthenticationSession resume(String identifier) { + public T resume(String identifier) { if (identifier != null) { - AuthenticationSession session = sessions.remove(identifier); + T session = sessions.remove(identifier); if (session != null && session.isValid()) return session; } @@ -94,32 +96,6 @@ public class AuthenticationSessionManager { } - /** - * Returns the identity finally asserted by the SAML IdP at the end of the - * authentication process represented by the authentication session with - * the given identifier. If there is no such authentication session, or no - * valid identity has been asserted by the SAML IdP for that session, null - * is returned. - * - * @param identifier - * The unique string returned by the call to defer(). For convenience, - * this value may safely be null. - * - * @return - * The identity finally asserted by the SAML IdP at the end of the - * authentication process represented by the authentication session - * with the given identifier, or null if there is no such identity. - */ - public AssertedIdentity getIdentity(String identifier) { - - AuthenticationSession session = resume(identifier); - if (session != null) - return session.getIdentity(); - - return null; - - } - /** * Defers the Guacamole side of authentication for the user having the * given authentication session such that it may be later resumed through a @@ -129,14 +105,14 @@ public class AuthenticationSessionManager { * This method will automatically generate a new identifier. * * @param session - * The {@link AuthenticationSession} representing the in-progress SAML + * The {@link AuthenticationSession} representing the in-progress * authentication attempt. * * @return * A unique and unpredictable string that may be used to represent the * given session when calling resume(). */ - public String defer(AuthenticationSession session) { + public String defer(T session) { String identifier = idGenerator.generateIdentifier(); sessions.put(identifier, session); return identifier; @@ -152,20 +128,20 @@ public class AuthenticationSessionManager { * or similar unique identifier. * * @param session - * The {@link AuthenticationSession} representing the in-progress SAML + * The {@link AuthenticationSession} representing the in-progress * authentication attempt. * * @param identifier * A unique and unpredictable string that may be used to represent the * given session when calling resume(). */ - public void defer(AuthenticationSession session, String identifier) { + public void defer(T session, String identifier) { sessions.put(identifier, session); } /** * Shuts down the executor service that periodically removes all invalid - * authentication sessions. This must be invoked when the SAML extension is + * authentication sessions. This must be invoked when the auth extension is * shut down in order to avoid resource leaks. */ public void shutdown() { diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/IdentifierGenerator.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/IdentifierGenerator.java similarity index 66% rename from extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/IdentifierGenerator.java rename to extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/IdentifierGenerator.java index a2a3aae6a..799b31b13 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/IdentifierGenerator.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/IdentifierGenerator.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.guacamole.auth.saml.acs; +package org.apache.guacamole.auth.sso; import com.google.common.io.BaseEncoding; import com.google.inject.Singleton; @@ -26,7 +26,7 @@ import java.security.SecureRandom; /** * Generator of unique and unpredictable identifiers. Each generated identifier * is an arbitrary, random string produced using a cryptographically-secure - * random number generator and consists of at least 256 bits. + * random number generator. */ @Singleton public class IdentifierGenerator { @@ -43,10 +43,27 @@ public class IdentifierGenerator { * number generator. * * @return - * A unique and unpredictable identifier. + * A unique and unpredictable identifier with at least 256 bits of + * entropy. */ public String generateIdentifier() { - byte[] bytes = new byte[33]; + return generateIdentifier(256); + } + + /** + * Generates a unique and unpredictable identifier having at least the + * given number of bits of entropy. The resulting identifier may have more + * than the number of bits required. + * + * @param minBits + * The number of bits of entropy that the identifier should contain. + * + * @return + * A unique and unpredictable identifier with at least the given number + * of bits of entropy. + */ + public String generateIdentifier(int minBits) { + byte[] bytes = new byte[(minBits + 23) / 24 * 3]; // Round up to nearest multiple of 3 bytes, as base64 encodes blocks of 3 bytes at a time secureRandom.nextBytes(bytes); return BaseEncoding.base64().encode(bytes); } diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/NonceService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/NonceService.java index 7c41dd9d6..88fff881b 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/NonceService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/NonceService.java @@ -19,9 +19,8 @@ package org.apache.guacamole.auth.sso; +import com.google.inject.Inject; import com.google.inject.Singleton; -import java.math.BigInteger; -import java.security.SecureRandom; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -33,23 +32,28 @@ import java.util.concurrent.ConcurrentHashMap; public class NonceService { /** - * Cryptographically-secure random number generator for generating the - * required nonce. + * Generator of arbitrary, unique, unpredictable identifiers. */ - private final SecureRandom random = new SecureRandom(); + @Inject + private IdentifierGenerator idGenerator; /** * Map of all generated nonces to their corresponding expiration timestamps. * This Map must be periodically swept of expired nonces to avoid growing * without bound. */ - private final Map nonces = new ConcurrentHashMap(); + private final Map nonces = new ConcurrentHashMap<>(); /** * The timestamp of the last expired nonce sweep. */ private long lastSweep = System.currentTimeMillis(); + /** + * The minimum number of bits of entropy to include in each nonce. + */ + private static final int NONCE_BITS = 128; + /** * The minimum amount of time to wait between sweeping expired nonces from * the Map. @@ -102,7 +106,7 @@ public class NonceService { sweepExpiredNonces(); // Generate and store nonce, along with expiration timestamp - String nonce = new BigInteger(130, random).toString(32); + String nonce = idGenerator.generateIdentifier(NONCE_BITS); nonces.put(nonce, System.currentTimeMillis() + maxAge); return nonce; diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/AuthenticationProviderService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/AuthenticationProviderService.java index cdd53dec6..982028f16 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/AuthenticationProviderService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/AuthenticationProviderService.java @@ -28,7 +28,7 @@ import javax.servlet.http.HttpServletRequest; import org.apache.guacamole.auth.saml.user.SAMLAuthenticatedUser; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.auth.saml.acs.AssertedIdentity; -import org.apache.guacamole.auth.saml.acs.AuthenticationSessionManager; +import org.apache.guacamole.auth.saml.acs.SAMLAuthenticationSessionManager; import org.apache.guacamole.auth.saml.acs.SAMLService; import org.apache.guacamole.auth.sso.SSOAuthenticationProviderService; import org.apache.guacamole.form.Field; @@ -61,7 +61,7 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS * Manager of active SAML authentication attempts. */ @Inject - private AuthenticationSessionManager sessionManager; + private SAMLAuthenticationSessionManager sessionManager; /** * Service for processing SAML requests/responses. diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/SAMLAuthenticationProviderModule.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/SAMLAuthenticationProviderModule.java index 3c7300baa..7c7dd49ed 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/SAMLAuthenticationProviderModule.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/SAMLAuthenticationProviderModule.java @@ -22,8 +22,7 @@ package org.apache.guacamole.auth.saml; import com.google.inject.AbstractModule; import org.apache.guacamole.auth.saml.conf.ConfigurationService; import org.apache.guacamole.auth.saml.acs.AssertionConsumerServiceResource; -import org.apache.guacamole.auth.saml.acs.AuthenticationSessionManager; -import org.apache.guacamole.auth.saml.acs.IdentifierGenerator; +import org.apache.guacamole.auth.saml.acs.SAMLAuthenticationSessionManager; import org.apache.guacamole.auth.saml.acs.SAMLService; /** @@ -34,9 +33,8 @@ public class SAMLAuthenticationProviderModule extends AbstractModule { @Override protected void configure() { bind(AssertionConsumerServiceResource.class); - bind(AuthenticationSessionManager.class); bind(ConfigurationService.class); - bind(IdentifierGenerator.class); + bind(SAMLAuthenticationSessionManager.class); bind(SAMLService.class); } diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/AssertionConsumerServiceResource.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/AssertionConsumerServiceResource.java index e316b3868..7aadc73f5 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/AssertionConsumerServiceResource.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/AssertionConsumerServiceResource.java @@ -56,7 +56,7 @@ public class AssertionConsumerServiceResource extends SSOResource { * Manager of active SAML authentication attempts. */ @Inject - private AuthenticationSessionManager sessionManager; + private SAMLAuthenticationSessionManager sessionManager; /** * Service for processing SAML requests/responses. @@ -107,7 +107,7 @@ public class AssertionConsumerServiceResource extends SSOResource { try { // Validate and parse identity asserted by SAML IdP - AuthenticationSession session = saml.processResponse( + SAMLAuthenticationSession session = saml.processResponse( consumedRequest.getRequestURL().toString(), relayState, samlResponse); diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/AuthenticationSession.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/SAMLAuthenticationSession.java similarity index 77% rename from extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/AuthenticationSession.java rename to extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/SAMLAuthenticationSession.java index b73bc7adb..bbd74e2a9 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/AuthenticationSession.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/SAMLAuthenticationSession.java @@ -19,17 +19,12 @@ package org.apache.guacamole.auth.saml.acs; +import org.apache.guacamole.auth.sso.AuthenticationSession; + /** * Representation of an in-progress SAML authentication attempt. */ -public class AuthenticationSession { - - /** - * The absolute point in time after which this authentication session is - * invalid. This value is a UNIX epoch timestamp, as may be returned by - * {@link System#currentTimeMillis()}. - */ - private final long expirationTimestamp; +public class SAMLAuthenticationSession extends AuthenticationSession { /** * The request ID of the SAML request associated with the authentication @@ -55,24 +50,21 @@ public class AuthenticationSession { * The number of milliseconds that may elapse before this session must * be considered invalid. */ - public AuthenticationSession(String requestId, long expires) { - this.expirationTimestamp = System.currentTimeMillis() + expires; + public SAMLAuthenticationSession(String requestId, long expires) { + super(expires); this.requestId = requestId; } /** - * Returns whether this authentication session is still valid (has not yet - * expired). If an identity has been asserted by the SAML IdP, this + * {@inheritDoc} + * + *

If an identity has been asserted by the SAML IdP, this * considers also whether the SAML response asserting that identity has * expired. - * - * @return - * true if this authentication session is still valid, false if it has - * expired. */ + @Override public boolean isValid() { - return System.currentTimeMillis() < expirationTimestamp - && (identity == null || identity.isValid()); + return super.isValid() && (identity == null || identity.isValid()); } /** diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/SAMLAuthenticationSessionManager.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/SAMLAuthenticationSessionManager.java new file mode 100644 index 000000000..4adf82f2c --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/SAMLAuthenticationSessionManager.java @@ -0,0 +1,59 @@ +/* + * 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.saml.acs; + +import com.google.inject.Singleton; +import org.apache.guacamole.auth.sso.AuthenticationSessionManager; + +/** + * Manager service that temporarily stores SAML authentication attempts while + * the authentication flow is underway. + */ +@Singleton +public class SAMLAuthenticationSessionManager + extends AuthenticationSessionManager { + + /** + * Returns the identity finally asserted by the SAML IdP at the end of the + * authentication process represented by the authentication session with + * the given identifier. If there is no such authentication session, or no + * valid identity has been asserted by the SAML IdP for that session, null + * is returned. + * + * @param identifier + * The unique string returned by the call to defer(). For convenience, + * this value may safely be null. + * + * @return + * The identity finally asserted by the SAML IdP at the end of the + * authentication process represented by the authentication session + * with the given identifier, or null if there is no such identity. + */ + public AssertedIdentity getIdentity(String identifier) { + + SAMLAuthenticationSession session = resume(identifier); + if (session != null) + return session.getIdentity(); + + return null; + + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/SAMLService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/SAMLService.java index c3357eefb..2fe6da4c8 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/SAMLService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/SAMLService.java @@ -22,7 +22,6 @@ package org.apache.guacamole.auth.saml.acs; import com.google.inject.Inject; import com.google.inject.Singleton; import com.onelogin.saml2.Auth; -import com.onelogin.saml2.authn.AuthnRequest; import com.onelogin.saml2.authn.AuthnRequestParams; import com.onelogin.saml2.authn.SamlResponse; import com.onelogin.saml2.exception.SettingsException; @@ -37,6 +36,7 @@ import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleSecurityException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.auth.saml.conf.ConfigurationService; +import org.apache.guacamole.auth.sso.IdentifierGenerator; import org.xml.sax.SAXException; /** @@ -56,7 +56,7 @@ public class SAMLService { * Manager of active SAML authentication attempts. */ @Inject - private AuthenticationSessionManager sessionManager; + private SAMLAuthenticationSessionManager sessionManager; /** * Generator of arbitrary, unique, unpredictable identifiers. @@ -99,7 +99,7 @@ public class SAMLService { // Create a new authentication session to represent this attempt while // it is in progress, using the request ID that was just issued - AuthenticationSession session = new AuthenticationSession( + SAMLAuthenticationSession session = new SAMLAuthenticationSession( auth.getLastRequestId(), confService.getAuthenticationTimeout() * 60000L); @@ -127,7 +127,7 @@ public class SAMLService { /** * Processes the given SAML response, as received by the SAML ACS endpoint - * at the given URL, producing an {@link AuthenticationSession} that now + * at the given URL, producing an {@link SAMLAuthenticationSession} that now * includes a valid assertion of the user's identity. If the SAML response * is invalid in any way, an exception is thrown. * @@ -148,7 +148,7 @@ public class SAMLService { * given URL. * * @return - * The {@link AuthenticationSession} associated with the in-progress + * The {@link SAMLAuthenticationSession} associated with the in-progress * authentication attempt, now associated with the {@link AssertedIdentity} * representing the identity of the user asserted by the SAML IdP. * @@ -157,14 +157,14 @@ public class SAMLService { * information required to validate or decrypt the response cannot be * read. */ - public AuthenticationSession processResponse(String url, String relayState, + public SAMLAuthenticationSession processResponse(String url, String relayState, String encodedResponse) throws GuacamoleException { if (relayState == null) throw new GuacamoleSecurityException("\"RelayState\" value " + "is missing from SAML response."); - AuthenticationSession session = sessionManager.resume(relayState); + SAMLAuthenticationSession session = sessionManager.resume(relayState); if (session == null) throw new GuacamoleSecurityException("\"RelayState\" value " + "included with SAML response is not valid."); From 841190df5aa1b081abc30670c2b66c5f0c58972c Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 26 Jan 2023 10:24:08 -0800 Subject: [PATCH 03/16] GUACAMOLE-839: Generate case-insensitive nonce values that can safely be used in domains. --- .../auth/sso/IdentifierGenerator.java | 45 ++++++++++++++++--- .../guacamole/auth/sso/NonceService.java | 11 +++-- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/IdentifierGenerator.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/IdentifierGenerator.java index 799b31b13..82538c6a9 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/IdentifierGenerator.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/IdentifierGenerator.java @@ -21,6 +21,7 @@ package org.apache.guacamole.auth.sso; import com.google.common.io.BaseEncoding; import com.google.inject.Singleton; +import java.math.BigInteger; import java.security.SecureRandom; /** @@ -40,7 +41,8 @@ public class IdentifierGenerator { /** * Generates a unique and unpredictable identifier. Each identifier is at * least 256-bit and produced using a cryptographically-secure random - * number generator. + * number generator. The identifier may contain characters that differ only + * in case. * * @return * A unique and unpredictable identifier with at least 256 bits of @@ -53,7 +55,8 @@ public class IdentifierGenerator { /** * Generates a unique and unpredictable identifier having at least the * given number of bits of entropy. The resulting identifier may have more - * than the number of bits required. + * than the number of bits required. The identifier may contain characters + * that differ only in case. * * @param minBits * The number of bits of entropy that the identifier should contain. @@ -63,9 +66,41 @@ public class IdentifierGenerator { * of bits of entropy. */ public String generateIdentifier(int minBits) { - byte[] bytes = new byte[(minBits + 23) / 24 * 3]; // Round up to nearest multiple of 3 bytes, as base64 encodes blocks of 3 bytes at a time - secureRandom.nextBytes(bytes); - return BaseEncoding.base64().encode(bytes); + return generateIdentifier(minBits, true); + } + + /** + * Generates a unique and unpredictable identifier having at least the + * given number of bits of entropy. The resulting identifier may have more + * than the number of bits required. The identifier may contain characters + * that differ only in case. + * + * @param minBits + * The number of bits of entropy that the identifier should contain. + * + * @param caseSensitive + * Whether identifiers are permitted to contain characters that vary + * by case. If false, all characters that may vary by case will be + * lowercase, and the generated identifier will be longer. + * + * @return + * A unique and unpredictable identifier with at least the given number + * of bits of entropy. + */ + public String generateIdentifier(int minBits, boolean caseSensitive) { + + // Generate a base64 identifier if we're allowed to vary by case + if (caseSensitive) { + int minBytes = (minBits + 23) / 24 * 3; // Round up to nearest multiple of 3 bytes, as base64 encodes blocks of 3 bytes at a time + byte[] bytes = new byte[minBytes]; + secureRandom.nextBytes(bytes); + return BaseEncoding.base64().encode(bytes); + } + + // Generate base32 identifiers if we cannot vary by case + minBits = (minBits + 4) / 5 * 5; // Round up to nearest multiple of 5 bits, as base32 encodes 5 bits at a time + return new BigInteger(minBits, secureRandom).toString(32); + } } diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/NonceService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/NonceService.java index 88fff881b..a06340653 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/NonceService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/NonceService.java @@ -22,11 +22,13 @@ package org.apache.guacamole.auth.sso; import com.google.inject.Inject; import com.google.inject.Singleton; import java.util.Iterator; +import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Service for generating and validating single-use random tokens (nonces). + * Each generated nonce is at least 128 bits and case-insensitive. */ @Singleton public class NonceService { @@ -98,7 +100,8 @@ public class NonceService { * valid, in milliseconds. * * @return - * A cryptographically-secure nonce value. + * A cryptographically-secure nonce value. Generated nonces are at + * least 128-bit and are case-insensitive. */ public String generate(long maxAge) { @@ -106,7 +109,7 @@ public class NonceService { sweepExpiredNonces(); // Generate and store nonce, along with expiration timestamp - String nonce = idGenerator.generateIdentifier(NONCE_BITS); + String nonce = idGenerator.generateIdentifier(NONCE_BITS, false); nonces.put(nonce, System.currentTimeMillis() + maxAge); return nonce; @@ -119,7 +122,7 @@ public class NonceService { * invalidates that nonce. * * @param nonce - * The nonce value to test. + * The nonce value to test. Comparisons are case-insensitive. * * @return * true if the provided nonce is valid, false otherwise. @@ -127,7 +130,7 @@ public class NonceService { public boolean isValid(String nonce) { // Remove nonce, verifying whether it was present at all - Long expires = nonces.remove(nonce); + Long expires = nonces.remove(nonce.toLowerCase(Locale.US)); if (expires == null) return false; From e51d548995ba0ecee9cff4e4fe2ce1368d1fd5b5 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 26 Jan 2023 11:30:51 -0800 Subject: [PATCH 04/16] GUACAMOLE-839: Ensure each NonceService instance has its own context (will not validate the nonces of other contexts). --- .../main/java/org/apache/guacamole/auth/sso/NonceService.java | 2 -- .../auth/openid/OpenIDAuthenticationProviderModule.java | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/NonceService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/NonceService.java index a06340653..5eb1e4b5d 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/NonceService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/NonceService.java @@ -20,7 +20,6 @@ package org.apache.guacamole.auth.sso; import com.google.inject.Inject; -import com.google.inject.Singleton; import java.util.Iterator; import java.util.Locale; import java.util.Map; @@ -30,7 +29,6 @@ import java.util.concurrent.ConcurrentHashMap; * Service for generating and validating single-use random tokens (nonces). * Each generated nonce is at least 128 bits and case-insensitive. */ -@Singleton public class NonceService { /** diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/OpenIDAuthenticationProviderModule.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/OpenIDAuthenticationProviderModule.java index ba1bf74b3..2fce2a719 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/OpenIDAuthenticationProviderModule.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/OpenIDAuthenticationProviderModule.java @@ -20,6 +20,7 @@ package org.apache.guacamole.auth.openid; import com.google.inject.AbstractModule; +import com.google.inject.Scopes; import org.apache.guacamole.auth.openid.conf.ConfigurationService; import org.apache.guacamole.auth.sso.NonceService; import org.apache.guacamole.auth.openid.token.TokenValidationService; @@ -32,7 +33,7 @@ public class OpenIDAuthenticationProviderModule extends AbstractModule { @Override protected void configure() { bind(ConfigurationService.class); - bind(NonceService.class); + bind(NonceService.class).in(Scopes.SINGLETON); bind(TokenValidationService.class); } From 2e8cf82234e7cd4741229387ecdc36dc79e58e38 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 26 Jan 2023 11:31:55 -0800 Subject: [PATCH 05/16] GUACAMOLE-839: As with NonceService, ensure each auth session manager instance has its own context and will not validate the sessions of other session managers. --- .../apache/guacamole/auth/sso/AuthenticationSessionManager.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSessionManager.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSessionManager.java index 7050c98be..da530f61b 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSessionManager.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSessionManager.java @@ -21,7 +21,6 @@ package org.apache.guacamole.auth.sso; import com.google.common.base.Predicates; import com.google.inject.Inject; -import com.google.inject.Singleton; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executors; @@ -38,7 +37,6 @@ import java.util.concurrent.TimeUnit; * @param * The type of sessions managed by this session manager. */ -@Singleton public class AuthenticationSessionManager { /** From 6bf0b8cf631bded9def46084b9113a4f5e2d232c Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 26 Jan 2023 11:35:44 -0800 Subject: [PATCH 06/16] GUACAMOLE-839: Allow testing of null nonce values. --- .../java/org/apache/guacamole/auth/sso/NonceService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/NonceService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/NonceService.java index 5eb1e4b5d..5717794fd 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/NonceService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/NonceService.java @@ -120,13 +120,18 @@ public class NonceService { * invalidates that nonce. * * @param nonce - * The nonce value to test. Comparisons are case-insensitive. + * The nonce value to test. This value may be null, which will be + * considered an invalid nonce. Comparisons are case-insensitive. * * @return * true if the provided nonce is valid, false otherwise. */ public boolean isValid(String nonce) { + // All null nonces are invalid. + if (nonce == null) + return false; + // Remove nonce, verifying whether it was present at all Long expires = nonces.remove(nonce.toLowerCase(Locale.US)); if (expires == null) From e2a6947ff6413fd0e165000687ca6802e31372a0 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 26 Jan 2023 17:18:12 -0800 Subject: [PATCH 07/16] GUACAMOLE-839: Implement base support for SSO using SSL/TLS authentication (certificates / smart cards). --- .../src/main/resources/translations/en.json | 5 + .../modules/guacamole-auth-sso-ssl/.gitignore | 3 + .../modules/guacamole-auth-sso-ssl/.ratignore | 1 + .../modules/guacamole-auth-sso-ssl/pom.xml | 74 ++++ .../ssl/AuthenticationProviderService.java | 413 ++++++++++++++++++ .../auth/ssl/SSLAuthenticationProvider.java | 49 +++ .../ssl/SSLAuthenticationProviderModule.java | 39 ++ .../auth/ssl/SSLAuthenticationSession.java | 65 +++ .../ssl/SSLAuthenticationSessionManager.java | 61 +++ .../auth/ssl/conf/ConfigurationService.java | 325 ++++++++++++++ .../conf/WildcardURIGuacamoleProperty.java | 66 +++ .../src/main/resources/guac-manifest.json | 33 ++ .../main/resources/html/sso-provider-ssl.html | 4 + extensions/guacamole-auth-sso/pom.xml | 1 + 14 files changed, 1139 insertions(+) create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/.gitignore create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/.ratignore create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProvider.java create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProviderModule.java create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSession.java create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSessionManager.java create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/WildcardURIGuacamoleProperty.java create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/guac-manifest.json create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/en.json b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/en.json index 859301568..902b7aff1 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/en.json +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/en.json @@ -12,6 +12,10 @@ "NAME" : "SAML SSO Backend" }, + "DATA_SOURCE_SSL" : { + "NAME" : "SSL/TLS SSO Backend" + }, + "LOGIN" : { "FIELD_HEADER_ID_TOKEN" : "", "FIELD_HEADER_STATE" : "", @@ -20,6 +24,7 @@ "NAME_IDP_CAS" : "CAS", "NAME_IDP_OPENID" : "OpenID", "NAME_IDP_SAML" : "SAML", + "NAME_IDP_SSL" : "Certificate / Smart Card", "SECTION_HEADER_SSO_OPTIONS" : "Sign in with:" } diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/.gitignore b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/.gitignore new file mode 100644 index 000000000..30eb48707 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/.gitignore @@ -0,0 +1,3 @@ +*~ +target/ +src/main/resources/generated/ diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/.ratignore b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/.ratignore new file mode 100644 index 000000000..da318d12f --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/.ratignore @@ -0,0 +1 @@ +src/main/resources/html/*.html diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml new file mode 100644 index 000000000..a107e6b7f --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml @@ -0,0 +1,74 @@ + + + + + 4.0.0 + org.apache.guacamole + guacamole-auth-sso-ssl + jar + 1.5.0 + guacamole-auth-sso-ssl + http://guacamole.apache.org/ + + + org.apache.guacamole + guacamole-auth-sso + 1.5.0 + ../../ + + + + + + + org.apache.guacamole + guacamole-ext + + + + + org.apache.guacamole + guacamole-auth-sso-base + + + + + com.google.inject + guice + + + + + javax.servlet + servlet-api + + + + + javax.ws.rs + jsr311-api + + + + + diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java new file mode 100644 index 000000000..16414688e --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java @@ -0,0 +1,413 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.auth.ssl; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.UriBuilder; +import org.apache.guacamole.GuacamoleClientException; +import org.apache.guacamole.auth.ssl.conf.ConfigurationService; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; +import org.apache.guacamole.auth.sso.NonceService; +import org.apache.guacamole.auth.sso.SSOAuthenticationProviderService; +import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser; +import org.apache.guacamole.form.Field; +import org.apache.guacamole.form.RedirectField; +import org.apache.guacamole.language.TranslatableMessage; +import org.apache.guacamole.net.auth.Credentials; +import org.apache.guacamole.net.auth.credentials.CredentialsInfo; +import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; +import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; + +/** + * Service that authenticates Guacamole users using SSL/TLS authentication + * provided by an external SSL termination service. + */ +@Singleton +public class AuthenticationProviderService implements SSOAuthenticationProviderService { + + /** + * Service for retrieving configuration information. + */ + @Inject + private ConfigurationService confService; + + /** + * Service for validating and generating unique nonce values. Here, these + * nonces are used specifically for generating unique domains. + */ + @Inject + private NonceService subdomainNonceService; + + /** + * Session manager for generating and maintaining unique tokens to + * represent the authentication flow of a user who has only partially + * authenticated. Here, these tokens represent a user that has been + * validated by SSL termination and allow the Guacamole instance that + * doesn't require SSL/TLS authentication to retrieve the user's identity + * and complete the authentication process. + */ + @Inject + private SSLAuthenticationSessionManager sessionManager; + + /** + * Provider for AuthenticatedUser objects. + */ + @Inject + private Provider authenticatedUserProvider; + + /** + * The string value that the SSL termination service uses for its client + * verification header to represent that the client certificate has been + * verified. + */ + private static final String CLIENT_VERIFIED_HEADER_SUCCESS_VALUE = "SUCCESS"; + + /** + * The name of the query parameter containing the temporary session token + * representing the current state of an in-progress authentication attempt. + */ + private static final String AUTH_SESSION_PARAMETER_NAME = "state"; + + /** + * Decodes the provided URL-encoded string as UTF-8, returning the result. + * + * @param value + * The URL-encoded string to decode. + * + * @return + * The decoded string. + * + * @throws GuacamoleException + * If the provided value is not a value URL-encoded string. + */ + private byte[] decode(String value) throws GuacamoleException { + try { + return URLDecoder.decode(value, StandardCharsets.UTF_8.name()) + .getBytes(StandardCharsets.UTF_8); + } + catch (IllegalArgumentException e) { + throw new GuacamoleClientException("Invalid URL-encoded value.", e); + } + catch (UnsupportedEncodingException e) { + // This should never happen, as UTF-8 is a standard charset that + // the JVM is required to support + throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e); + } + } + + /** + * Authenticates a user using HTTP headers containing that user's verified + * X.509 certificate. It is assumed that this certificate is being passed + * to Guacamole from an SSL termination service that has already verified + * that this certificate is valid and authorized for access to that + * Guacamole instance. + * + * @param credentials + * The credentials received by Guacamole in the authentication request. + * + * @param certificate + * The raw bytes of the X.509 certificate retrieved from the request. + * + * @return + * A new SSOAuthenticatedUser representing the identity of the user + * asserted by the SSL termination service via that user's X.509 + * certificate. + * + * @throws GuacamoleException + * If the provided X.509 certificate is not valid or cannot be parsed. + * It is expected that the SSL termination service will already have + * validated the certificate; this function validates only the + * certificate timestamps. + */ + private SSOAuthenticatedUser authenticateUser(Credentials credentials, + byte[] certificate) throws GuacamoleException { + + // Parse and re-verify certificate is valid with respect to timestamps + X509Certificate cert; + try (InputStream input = new ByteArrayInputStream(certificate)) { + + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + cert = (X509Certificate) certFactory.generateCertificate(input); + + // Verify certificate is valid (it should be given pre-validation + // from SSL termination, but it's worth rechecking for sanity) + cert.checkValidity(); + + } + catch (CertificateException e) { + throw new GuacamoleClientException("The X.509 certificate " + + "presented is not valid.", e); + } + catch (IOException e) { + throw new GuacamoleServerException("Provided X.509 certificate " + + "could not be read.", e); + } + + // Extract user's DN from their X.509 certificate + LdapName dn; + try { + Principal principal = cert.getSubjectX500Principal(); + dn = new LdapName(principal.getName()); + } + catch (InvalidNameException e) { + throw new GuacamoleClientException("The X.509 certificate " + + "presented does not contain a valid subject DN.", e); + } + + // Verify DN actually contains components + int numComponents = dn.size(); + if (numComponents < 1) + throw new GuacamoleClientException("The X.509 certificate " + + "presented contains an empty subject DN."); + + // Simply use first component of DN as username (TODO: Enforce + // requirements on the attribute providing the username and the base DN, + // and consider using components following the username to determine + // group memberships) + String username = dn.getRdn(numComponents - 1).getValue().toString(); + + SSOAuthenticatedUser authenticatedUser = authenticatedUserProvider.get(); + authenticatedUser.init(username, credentials, + Collections.emptySet(), Collections.emptyMap()); + return authenticatedUser; + + } + + /** + * Processes the given HTTP request, returning the identity represented by + * the auth session token present in that request. If no such token is + * present, or the token does not represent a valid identity, null is + * returned. + * + * @param request + * The HTTP request to process. + * + * @return + * The identity represented by the auth session token in the request, + * or null if there is no such token or the token does not represent a + * valid identity. + */ + private SSOAuthenticatedUser processIdentity(HttpServletRequest request) { + String state = request.getParameter(AUTH_SESSION_PARAMETER_NAME); + return sessionManager.getIdentity(state); + } + + /** + * Processes the X.509 certificate in the headers of the given HTTP + * request, returning an authentication session token representing the + * identity in that certificate. If the certificate is invalid or not + * present, null is returned. + * + * @param credentials + * The credentials submitted in the HTTP request being processed. + * + * @param request + * The HTTP request to process. + * + * @return + * An authentication session token representing the identity in the + * certificate in the given HTTP request, or null if the request does + * not contain a valid certificate. + * + * @throws GuacamoleException + * If any configuration parameters related to retrieving certificates + * from HTTP request cannot be parsed. + */ + private String processCertificate(Credentials credentials, + HttpServletRequest request) throws GuacamoleException { + + // Verify that SSL termination has already verified the certificate + String verified = request.getHeader(confService.getClientVerifiedHeader()); + if (!CLIENT_VERIFIED_HEADER_SUCCESS_VALUE.equals(verified)) + return null; + + String certificate = request.getHeader(confService.getClientCertificateHeader()); + if (certificate == null) + return null; + + SSOAuthenticatedUser authenticatedUser = authenticateUser(credentials, decode(certificate)); + long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxTokenValidity()); + return sessionManager.defer(new SSLAuthenticationSession(authenticatedUser, validityDuration)); + + } + + /** + * Redirects the current user back to the main URL of the Guacamole + * instance to continue the authentication process after having identified + * themselves using SSL/TLS client authentication. + * + * @param token + * The authentication session token generated for the current user's + * identity. + * + * @throws GuacamoleException + * To redirect the user to the main URL of the Guacamole instance. + */ + private void resumeAuthenticationAtRedirectURI(String token) + throws GuacamoleException { + + URI redirectURI = UriBuilder.fromUri(confService.getRedirectURI()) + .queryParam(AUTH_SESSION_PARAMETER_NAME, token) + .build(); + + // Request that the provided credentials, now tokenized, be + // resubmitted in that tokenized form to the original host for + // authentication + throw new GuacamoleInsufficientCredentialsException("Please " + + "resubmit your tokenized credentials using the " + + "following URI.", + new CredentialsInfo(Arrays.asList(new Field[] { + new RedirectField(AUTH_SESSION_PARAMETER_NAME, redirectURI, + new TranslatableMessage("LOGIN.INFO_IDP_REDIRECT_PENDING")) + })) + ); + + } + + @Override + public SSOAuthenticatedUser authenticateUser(Credentials credentials) + throws GuacamoleException { + + // + // Overall flow: + // + // 1) Unauthenticated user is given a temporary auth session token + // and redirected to the SSL termination instance that provides + // SSL client auth. This redirect uses a unique and temporary + // subdomain to ensure each SSL client auth attempt is fresh and + // does not use cached auth details. + // + // 2) Unauthenticated user with a temporary auth session token + // is validated by SSL termination, with that SSL termination + // adding HTTP headers containing the validated certificate to the + // user's HTTP request. + // + // 3) If valid, the user is assigned a temporary token and redirected + // back to the original URL. That temporary token is accepted by + // this extension at the original URL as proof of the user's + // identity. + // + // NOTE: All SSL termination endpoints in front of Guacamole MUST + // be configured to drop these headers from any inbound requests + // or users may be able to assert arbitrary identities, since this + // extension does not validate anything but the certificate timestamps. + // It relies purely on SSL termination to validate that the certificate + // was signed by the expected CA. + // + + // We can't authenticate using SSL/TLS client auth unless there's an + // associated HTTP request + HttpServletRequest request = credentials.getRequest(); + if (request == null) + return null; + + // We MUST have the domain associated with the request to ensure we + // always get fresh SSL sessions when validating client certificates + String host = request.getHeader("Host"); + if (host == null) + return null; + + // + // Handle only auth session tokens at the main redirect URI, using the + // pre-verified information from those tokens to determine user + // identity. + // + + String redirectHost = confService.getRedirectURI().getHost(); + if (host.equals(redirectHost)) { + + SSOAuthenticatedUser user = processIdentity(request); + if (user != null) + return user; + + // Redirect unauthenticated requests to the endpoint requiring + // SSL client auth to request identity verification + throw new GuacamoleInvalidCredentialsException("Invalid login.", + new CredentialsInfo(Arrays.asList(new Field[] { + new RedirectField(AUTH_SESSION_PARAMETER_NAME, getLoginURI(), // <-- Each call to getLoginURI() produces a unique subdomain that is valid only for ONE use (see below) + new TranslatableMessage("LOGIN.INFO_IDP_REDIRECT_PENDING")) + })) + ); + + } + + // + // Process certificates only at valid single-use subdomains dedicated + // to client authentication, redirecting back to the main redirect URI + // for final authentication if that processing is successful. + // + // NOTE: This is CRITICAL. If unique subdomains are not generated and + // tied to strictly one authentication attempt, then those subdomains + // could be reused by a user on a shared machine to assume the cached + // credentials of another user that used that machine earlier. The + // browser and/or OS may cache the certificate so that it can be reused + // for future SSL sessions to that same domain. Here, we ensure each + // generated domain is unique and only valid for certificate processing + // ONCE. The domain may still be valid with DNS, but will no longer be + // usable for certificate authentication. + // + + else if (subdomainNonceService.isValid(confService.getClientAuthenticationSubdomain(host))) { + String token = processCertificate(credentials, request); + if (token != null) + resumeAuthenticationAtRedirectURI(token); + } + + // All other requests are not allowed - refuse to authenticate + throw new GuacamoleClientException("Direct authentication against " + + "this endpoint is not valid without first requesting to " + + "authenticate at the primary URL of this Guacamole " + + "instance."); + + } + + @Override + public URI getLoginURI() throws GuacamoleException { + long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxDomainValidity()); + String uniqueSubdomain = subdomainNonceService.generate(validityDuration); + return confService.getClientAuthenticationURI(uniqueSubdomain); + } + + @Override + public void shutdown() { + sessionManager.shutdown(); + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProvider.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProvider.java new file mode 100644 index 000000000..6b01858ef --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProvider.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.auth.ssl; + +import org.apache.guacamole.auth.sso.SSOAuthenticationProvider; +import org.apache.guacamole.auth.sso.SSOResource; + +/** + * Guacamole authentication backend which authenticates users using SSL/TLS + * client authentication provided by some external SSL termination system. This + * SSL termination system must be configured to provide access to this same + * instance of Guacamole and must have both a wildcard certificate and wildcard + * DNS. No storage for connections is provided - only authentication. Storage + * must be provided by some other extension. + */ +public class SSLAuthenticationProvider extends SSOAuthenticationProvider { + + /** + * Creates a new SSLAuthenticationProvider that authenticates users against + * an external SSL termination system using SSL/TLS client authentication. + */ + public SSLAuthenticationProvider() { + super(AuthenticationProviderService.class, SSOResource.class, + new SSLAuthenticationProviderModule()); + } + + @Override + public String getIdentifier() { + return "ssl"; + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProviderModule.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProviderModule.java new file mode 100644 index 000000000..56e50bd75 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProviderModule.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.auth.ssl; + +import com.google.inject.AbstractModule; +import org.apache.guacamole.auth.ssl.conf.ConfigurationService; +import org.apache.guacamole.auth.sso.NonceService; + +/** + * Guice module which configures injections specific to SSO using SSL/TLS + * client authentication. + */ +public class SSLAuthenticationProviderModule extends AbstractModule { + + @Override + protected void configure() { + bind(ConfigurationService.class); + bind(NonceService.class); + bind(SSLAuthenticationSessionManager.class); + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSession.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSession.java new file mode 100644 index 000000000..e2e0d4c43 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSession.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.auth.ssl; + +import org.apache.guacamole.auth.sso.AuthenticationSession; +import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser; + +/** + * Representation of an in-progress SSL/TLS authentication attempt. + */ +public class SSLAuthenticationSession extends AuthenticationSession { + + /** + * The identity asserted by the external SSL termination service. + */ + private final SSOAuthenticatedUser identity; + + /** + * Creates a new AuthenticationSession representing an in-progress SSL/TLS + * authentication attempt. + * + * @param identity + * The identity asserted by the external SSL termination service. This + * MAY NOT be null. + * + * @param expires + * The number of milliseconds that may elapse before this session must + * be considered invalid. + */ + public SSLAuthenticationSession(SSOAuthenticatedUser identity, long expires) { + super(expires); + this.identity = identity; + } + + /** + * Returns the identity asserted by the external SSL termination service. + * As authentication will have completed with respect to the SSL + * termination service by the time this session is created, this will + * always be non-null. + * + * @return + * The identity asserted by the external SSL termination service. + */ + public SSOAuthenticatedUser getIdentity() { + return identity; + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSessionManager.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSessionManager.java new file mode 100644 index 000000000..3200cae3d --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSessionManager.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.auth.ssl; + +import com.google.inject.Singleton; +import org.apache.guacamole.auth.sso.AuthenticationSessionManager; +import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser; + +/** + * Manager service that temporarily stores SSL/TLS authentication attempts + * while the authentication flow is underway. + */ +@Singleton +public class SSLAuthenticationSessionManager + extends AuthenticationSessionManager { + + /** + * Returns the identity asserted by the external SSL termination service at + * the end of the authentication process represented by the authentication + * session with the given identifier. If there is no such authentication + * session, or no valid identity has been asserted for that session, null + * is returned. + * + * @param identifier + * The unique string returned by the call to defer(). For convenience, + * this value may safely be null. + * + * @return + * The identity asserted by the external SSL termination service at the + * end of the authentication process represented by the authentication + * session with the given identifier, or null if there is no such + * identity. + */ + public SSOAuthenticatedUser getIdentity(String identifier) { + + SSLAuthenticationSession session = resume(identifier); + if (session != null) + return session.getIdentity(); + + return null; + + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java new file mode 100644 index 000000000..074ee093b --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java @@ -0,0 +1,325 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.auth.ssl.conf; + +import com.google.inject.Inject; +import java.net.URI; +import javax.ws.rs.core.UriBuilder; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.environment.Environment; +import org.apache.guacamole.properties.IntegerGuacamoleProperty; +import org.apache.guacamole.properties.StringGuacamoleProperty; +import org.apache.guacamole.properties.URIGuacamoleProperty; + +/** + * Service for retrieving configuration information regarding SSO using SSL/TLS + * authentication. + */ +public class ConfigurationService { + + /** + * The default name of the header to use to retrieve the URL-encoded client + * certificate from an HTTP request received from an SSL termination + * service providing SSL/TLS client authentication. + */ + private static String DEFAULT_CLIENT_CERTIFICATE_HEADER = "X-Client-Certificate"; + + /** + * The default name of the header to use to retrieve the verification + * status of the certificate an HTTP request received from an SSL + * termination service providing SSL/TLS client authentication. + */ + private static String DEFAULT_CLIENT_VERIFIED_HEADER = "X-Client-Verified"; + + /** + * The default amount of time that a temporary authentication token for + * SSL/TLS authentication may remain valid, in minutes. + */ + private static int DEFAULT_MAX_TOKEN_VALIDITY = 5; + + /** + * The default amount of time that the temporary, unique subdomain + * generated for SSL/TLS authentication may remain valid, in minutes. + */ + private static int DEFAULT_MAX_DOMAIN_VALIDITY = 5; + + /** + * The property representing the URI that should be used to authenticate + * users with SSL/TLS client authentication. This must be a URI that points + * to THIS instance of Guacamole, but behind SSL termination that requires + * SSL/TLS client authentication. + */ + private static final WildcardURIGuacamoleProperty SSL_CLIENT_AUTH_URI = + new WildcardURIGuacamoleProperty() { + + @Override + public String getName() { return "ssl-client-auth-uri"; } + + }; + + /** + * The property representing the URI of this instance without SSL/TLS + * client authentication required. This must be a URI that points + * to THIS instance of Guacamole, but behind SSL termination that DOES NOT + * require or request SSL/TLS client authentication. + */ + private static final URIGuacamoleProperty SSL_REDIRECT_URI = + new URIGuacamoleProperty() { + + @Override + public String getName() { return "ssl-redirect-uri"; } + + }; + + /** + * The property representing the name of the header to use to retrieve the + * URL-encoded client certificate from an HTTP request received from an + * SSL termination service providing SSL/TLS client authentication. + */ + private static final StringGuacamoleProperty SSL_CLIENT_CERTIFICATE_HEADER = + new StringGuacamoleProperty() { + + @Override + public String getName() { return "ssl-client-certificate-header"; } + + }; + + /** + * The property representing the name of the header to use to retrieve the + * verification status of the certificate an HTTP request received from an + * SSL termination service providing SSL/TLS client authentication. This + * value of this header must be "SUCCESS" (all uppercase) if the + * certificate was successfully verified. + */ + private static final StringGuacamoleProperty SSL_CLIENT_VERIFIED_HEADER = + new StringGuacamoleProperty() { + + @Override + public String getName() { return "ssl-client-verified-header"; } + + }; + + /** + * The property representing the amount of time that a temporary + * authentication token for SSL/TLS authentication may remain valid, in + * minutes. This token is used to represent the user's asserted identity + * after it has been verified by the SSL termination service. This interval + * must be long enough to allow for network delays in redirecting the user + * back to the main Guacamole URL, but short enough that unused tokens do + * not consume unnecessary server resources and cannot potentially be + * guessed while the token is still valid. These tokens are 256-bit secure + * random values. + */ + private static final IntegerGuacamoleProperty SSL_MAX_TOKEN_VALIDITY = + new IntegerGuacamoleProperty() { + + @Override + public String getName() { return "ssl-max-token-validity"; } + + }; + + /** + * The property representing the amount of time that the temporary, unique + * subdomain generated for SSL/TLS authentication may remain valid, in + * minutes. This subdomain is used to ensure each SSL/TLS authentication + * attempt is fresh and does not potentially reuse a previous + * authentication attempt that was cached by the browser or OS. This + * interval must be long enough to allow for network delays in redirecting + * the user to the SSL termination service enforcing SSL/TLS + * authentication, but short enough that an unused domain does not consume + * unnecessary server resources and cannot potentially be guessed while + * that subdomain is still valid. These subdomains are 128-bit secure + * random values. + */ + private static final IntegerGuacamoleProperty SSL_MAX_DOMAIN_VALIDITY = + new IntegerGuacamoleProperty() { + + @Override + public String getName() { return "ssl-max-domain-validity"; } + + }; + + /** + * The Guacamole server environment. + */ + @Inject + private Environment environment; + + /** + * Returns a URI that should be used to authenticate users with SSL/TLS + * client authentication. The returned URI will consist of the configured + * client authentication URI with the wildcard portion ("*.") replaced with + * the given subdomain. + * + * @param subdomain + * The subdomain that should replace the wildcard portion of the + * configured client authentication URI. + * + * @return + * A URI that should be used to authenticate users with SSL/TLS + * client authentication. + * + * @throws GuacamoleException + * If the required property for configuring the client authentication + * URI is missing or cannot be parsed. + */ + public URI getClientAuthenticationURI(String subdomain) throws GuacamoleException { + + URI authURI = environment.getRequiredProperty(SSL_CLIENT_AUTH_URI); + String baseHostname = authURI.getHost(); + + // Add provided subdomain to auth URI + return UriBuilder.fromUri(authURI) + .host(subdomain + "." + baseHostname) + .build(); + + } + + /** + * Given a hostname that was used by a user for SSL/TLS client + * authentication, returns the subdomain at the beginning of that hostname. + * If the hostname does not match the pattern of hosts represented by the + * configured client authentication URI, null is returned. + * + * @param hostname + * The hostname to extract the subdomain from. + * + * @return + * The subdomain at the beginning of the provided hostname, if that + * hostname matches the pattern of hosts represented by the + * configured client authentication URI, or null otherwise. + * + * @throws GuacamoleException + * If the required property for configuring the client authentication + * URI is missing or cannot be parsed. + */ + public String getClientAuthenticationSubdomain(String hostname) throws GuacamoleException { + + URI authURI = environment.getRequiredProperty(SSL_CLIENT_AUTH_URI); + String baseHostname = authURI.getHost(); + + // Verify the first domain component is at least one character in + // length + int firstPeriod = hostname.indexOf('.'); + if (firstPeriod <= 0) + return null; + + // Verify domain matches the configured auth URI except for the leading + // subdomain + if (!hostname.regionMatches(true, firstPeriod + 1, baseHostname, 0, baseHostname.length())) + return null; + + // Extract subdomain + return hostname.substring(0, firstPeriod); + + } + + /** + * Returns the URI of this instance without SSL/TLS client authentication + * required. + * + * @return + * The URI of this instance without SSL/TLS client authentication + * required. + * + * @throws GuacamoleException + * If the required property for configuring the redirect URI is missing + * or cannot be parsed. + */ + public URI getRedirectURI() throws GuacamoleException { + return environment.getRequiredProperty(SSL_REDIRECT_URI); + } + + /** + * Returns the name of the header to use to retrieve the URL-encoded client + * certificate from an HTTP request received from an SSL termination + * service providing SSL/TLS client authentication. + * + * @return + * The name of the header to use to retrieve the URL-encoded client + * certificate from an HTTP request received from an SSL termination + * service providing SSL/TLS client authentication. + * + * @throws GuacamoleException + * If the property for configuring the client certificate header cannot + * be parsed. + */ + public String getClientCertificateHeader() throws GuacamoleException { + return environment.getProperty(SSL_CLIENT_CERTIFICATE_HEADER, DEFAULT_CLIENT_CERTIFICATE_HEADER); + } + + /** + * Returns the name of the header to use to retrieve the verification + * status of the certificate an HTTP request received from an SSL + * termination service providing SSL/TLS client authentication. + * + * @return + * The name of the header to use to retrieve the verification + * status of the certificate an HTTP request received from an SSL + * termination service providing SSL/TLS client authentication. + * + * @throws GuacamoleException + * If the property for configuring the client verification header + * cannot be parsed. + */ + public String getClientVerifiedHeader() throws GuacamoleException { + return environment.getProperty(SSL_CLIENT_VERIFIED_HEADER, DEFAULT_CLIENT_VERIFIED_HEADER); + } + + /** + * Returns the maximum amount of time that the token generated by the + * Guacamole server representing current SSL authentication state should + * remain valid, in minutes. This imposes an upper limit on the amount of + * time any particular authentication request can result in successful + * authentication within Guacamole when SSL/TLS client authentication is + * configured. By default, this will be 5. + * + * @return + * The maximum amount of time that an SSL authentication token + * generated by the Guacamole server should remain valid, in minutes. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public int getMaxTokenValidity() throws GuacamoleException { + return environment.getProperty(SSL_MAX_TOKEN_VALIDITY, DEFAULT_MAX_TOKEN_VALIDITY); + } + + /** + * Returns the maximum amount of time that a unique client authentication + * subdomain generated by the Guacamole server should remain valid, in + * minutes. This imposes an upper limit on the amount of time any + * particular authentication request can result in successful + * authentication within Guacamole when SSL/TLS client authentication is + * configured. By default, this will be 5. + * + * @return + * The maximum amount of time that a unique client authentication + * subdomain generated by the Guacamole server should remain valid, in + * minutes. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public int getMaxDomainValidity() throws GuacamoleException { + return environment.getProperty(SSL_MAX_DOMAIN_VALIDITY, DEFAULT_MAX_DOMAIN_VALIDITY); + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/WildcardURIGuacamoleProperty.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/WildcardURIGuacamoleProperty.java new file mode 100644 index 000000000..d237d8031 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/WildcardURIGuacamoleProperty.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.auth.ssl.conf; + +import java.net.URI; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; +import org.apache.guacamole.properties.URIGuacamoleProperty; + +/** + * A GuacamoleProperty whose value is a wildcard URI. The behavior of this + * property is identical to URIGuacamoleProperty except that it verifies a + * wildcard hostname prefix ("*.") is present and strips that prefix from the + * parsed URI. + */ +public abstract class WildcardURIGuacamoleProperty extends URIGuacamoleProperty { + + /** + * Regular expression that broadly matches URIs that contain wildcards in + * their hostname. This regular expression is NOT strict and will match + * invalid URIs. It is only strict enough to recognize a wildcard hostname + * prefix. + */ + private static final Pattern WILDCARD_URI_PATTERN = Pattern.compile("([^:]+://(?:[^@]+@)?)\\*\\.(.*)"); + + @Override + public URI parseValue(String value) throws GuacamoleException { + + // Verify wildcard prefix is present + Matcher matcher = WILDCARD_URI_PATTERN.matcher(value); + if (matcher.matches()) { + + // Strip wildcard prefix from URI and verify a valid hostname is + // still present + URI uri = super.parseValue(matcher.group(1) + matcher.group(2)); + if (uri.getHost() != null) + return uri; + + } + + // All other values are not valid wildcard URIs + throw new GuacamoleServerException("Value \"" + value + + "\" is not a valid wildcard URI."); + + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/guac-manifest.json new file mode 100644 index 000000000..30c8a1c99 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/guac-manifest.json @@ -0,0 +1,33 @@ +{ + + "guacamoleVersion" : "1.5.0", + + "name" : "SSL Authentication Extension", + "namespace" : "ssl", + + "authProviders" : [ + "org.apache.guacamole.auth.ssl.SSLAuthenticationProvider" + ], + + "css" : [ + "styles/sso-providers.css" + ], + + "html" : [ + "html/sso-providers.html", + "html/sso-provider-ssl.html" + ], + + "translations" : [ + "translations/ca.json", + "translations/de.json", + "translations/en.json", + "translations/fr.json", + "translations/ja.json", + "translations/ko.json", + "translations/pt.json", + "translations/ru.json", + "translations/zh.json" + ] + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html new file mode 100644 index 000000000..b6c510931 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html @@ -0,0 +1,4 @@ + +

  • {{ + 'LOGIN.NAME_IDP_SSL' | translate +}}
  • diff --git a/extensions/guacamole-auth-sso/pom.xml b/extensions/guacamole-auth-sso/pom.xml index 04ffe842f..99632e4c3 100644 --- a/extensions/guacamole-auth-sso/pom.xml +++ b/extensions/guacamole-auth-sso/pom.xml @@ -49,6 +49,7 @@ modules/guacamole-auth-sso-cas modules/guacamole-auth-sso-openid modules/guacamole-auth-sso-saml + modules/guacamole-auth-sso-ssl From 9177cac3cc641d561f4c817b721348355943e812 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 27 Jan 2023 12:40:02 -0800 Subject: [PATCH 08/16] GUACAMOLE-839: Allow authenticationService to affect login screen even if not invoked by login screen. --- .../app/auth/service/authenticationService.js | 71 ++++++++++----- .../src/app/login/directives/login.js | 88 +++++++++---------- 2 files changed, 94 insertions(+), 65 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js b/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js index 8a867cea5..a2faa90ff 100644 --- a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js +++ b/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js @@ -18,25 +18,45 @@ */ /** - * A service for authenticating a user against the REST API. + * A service for authenticating a user against the REST API. Invoking the + * authenticate() or login() functions of this service will automatically + * affect the login dialog, if visible. * - * This service broadcasts two events on $rootScope depending on the result of - * authentication operations: 'guacLogin' if authentication was successful and - * a new token was created, and 'guacLogout' if an existing token is being - * destroyed or replaced. Both events will be passed the related token as their - * sole parameter. + * This service broadcasts events on $rootScope depending on the status and + * result of authentication operations: * - * If a login attempt results in an existing token being replaced, 'guacLogout' - * will be broadcast first for the token being replaced, followed by - * 'guacLogin' for the new token. - * - * Failed logins may also result in guacInsufficientCredentials or - * guacInvalidCredentials events, if the provided credentials were rejected for - * being insufficient or invalid respectively. Both events will be provided - * the set of parameters originally given to authenticate() and the error that - * rejected the credentials. The Error object provided will contain set of - * expected credentials returned by the REST endpoint. This set of credentials - * will be in the form of a Field array. + * "guacLoginPending" + * An authentication request was submitted and we are awaiting the result. + * This event receives an object containing the HTTP parameters submitted + * as its sole parameter. + * + * "guacLogin" + * Authentication was successful and a new token was created. This event + * receives the authentication token as its sole parameter. + * + * "guacLogout" + * An existing token is being destroyed. This event receives the + * authentication token as its sole parameter. If the existing token for + * the current session is being replaced without destroying that session, + * this event is not fired. + * + * "guacLoginFailed" + * An authentication request has failed for any reason. This event is + * broadcast before any other events that are specific to the nature of + * the failure, and may be used to detect login failures in lieu of those + * events. This event receives two parameters: the HTTP parameters + * submitted and the Error object received from the REST endpoint. + * + * "guacInsufficientCredentials" + * An authentication request failed because additional credentials are + * needed before the request can be processed. This event receives two + * parameters: the HTTP parameters submitted and the Error object received + * from the REST endpoint. + * + * "guacInvalidCredentials" + * An authentication request failed because the credentials provided are + * invalid. This event receives two parameters: the HTTP parameters + * submitted and the Error object received from the REST endpoint. */ angular.module('auth').factory('authenticationService', ['$injector', function authenticationService($injector) { @@ -141,7 +161,8 @@ angular.module('auth').factory('authenticationService', ['$injector', * and given arbitrary parameters, returning a promise that succeeds only * if the authentication operation was successful. The resulting * authentication data can be retrieved later via getCurrentToken() or - * getCurrentUsername(). + * getCurrentUsername(). Invoking this function will affect the UI, + * including the login screen if visible. * * The provided parameters can be virtually any object, as each property * will be sent as an HTTP parameter in the authentication request. @@ -159,6 +180,9 @@ angular.module('auth').factory('authenticationService', ['$injector', */ service.authenticate = function authenticate(parameters) { + // Notify that a fresh authentication request is being submitted + $rootScope.$broadcast('guacLoginPending', parameters); + // Attempt authentication return requestService({ method: 'POST', @@ -202,6 +226,10 @@ angular.module('auth').factory('authenticationService', ['$injector', // If authentication fails, propogate failure to returned promise ['catch'](requestService.createErrorCallback(function authenticationFailed(error) { + // Notify of generic login failure, for any event consumers that + // wish to handle all types of failures at once + $rootScope.$broadcast('guacLoginFailed', parameters, error); + // Request credentials if provided credentials were invalid if (error.type === Error.Type.INVALID_CREDENTIALS) { $rootScope.$broadcast('guacInvalidCredentials', parameters, error); @@ -321,7 +349,8 @@ angular.module('auth').factory('authenticationService', ['$injector', * with a username and password, ignoring any currently-stored token, * returning a promise that succeeds only if the login operation was * successful. The resulting authentication data can be retrieved later - * via getCurrentToken() or getCurrentUsername(). + * via getCurrentToken() or getCurrentUsername(). Invoking this function + * will affect the UI, including the login screen if visible. * * @param {String} username * The username to log in with. @@ -342,7 +371,9 @@ angular.module('auth').factory('authenticationService', ['$injector', /** * Makes a request to logout a user using the token REST API endpoint, * returning a promise that succeeds only if the logout operation was - * successful. + * successful. Invoking this function will affect the UI, causing the + * visible components of the application to be replaced with a status + * message noting that the user has been logged out. * * @returns {Promise} * A promise which succeeds only if the logout operation was diff --git a/guacamole/src/main/frontend/src/app/login/directives/login.js b/guacamole/src/main/frontend/src/app/login/directives/login.js index b7967d798..80ce00fe4 100644 --- a/guacamole/src/main/frontend/src/app/login/directives/login.js +++ b/guacamole/src/main/frontend/src/app/login/directives/login.js @@ -177,51 +177,7 @@ angular.module('login').directive('guacLogin', [function guacLogin() { * authentication service, redirecting to the main view if successful. */ $scope.login = function login() { - - // Authentication is now in progress - $scope.submitted = true; - - // Start with cleared status - $scope.loginError = null; - - // Attempt login once existing session is destroyed - authenticationService.authenticate($scope.enteredValues) - - // Retry route upon success (entered values will be cleared only - // after route change has succeeded as this can take time) - .then(function loginSuccessful() { - $route.reload(); - }) - - // Reset upon failure - ['catch'](requestService.createErrorCallback(function loginFailed(error) { - - // Initial submission is complete and has failed - $scope.submitted = false; - - // Clear out passwords if the credentials were rejected for any reason - if (error.type !== Error.Type.INSUFFICIENT_CREDENTIALS) { - - // Flag generic error for invalid login - if (error.type === Error.Type.INVALID_CREDENTIALS) - $scope.loginError = { - 'key' : 'LOGIN.ERROR_INVALID_LOGIN' - }; - - // Display error if anything else goes wrong - else - $scope.loginError = error.translatableMessage; - - // Reset all remaining fields to default values, but - // preserve any usernames - angular.forEach($scope.remainingFields, function clearEnteredValueIfPassword(field) { - if (field.type !== Field.Type.USERNAME && field.name in $scope.enteredValues) - $scope.enteredValues[field.name] = DEFAULT_FIELD_VALUE; - }); - } - - })); - + authenticationService.authenticate($scope.enteredValues)['catch'](requestService.IGNORE); }; /** @@ -244,6 +200,48 @@ angular.module('login').directive('guacLogin', [function guacLogin() { }; + // Update UI to reflect in-progress auth status (clear any previous + // errors, flag as pending) + $rootScope.$on('guacLoginPending', function loginSuccessful() { + $scope.submitted = true; + $scope.loginError = null; + }); + + // Retry route upon success (entered values will be cleared only + // after route change has succeeded as this can take time) + $rootScope.$on('guacLogin', function loginSuccessful() { + $route.reload(); + }); + + // Reset upon failure + $rootScope.$on('guacLoginFailed', function loginFailed(event, parameters, error) { + + // Initial submission is complete and has failed + $scope.submitted = false; + + // Clear out passwords if the credentials were rejected for any reason + if (error.type !== Error.Type.INSUFFICIENT_CREDENTIALS) { + + // Flag generic error for invalid login + if (error.type === Error.Type.INVALID_CREDENTIALS) + $scope.loginError = { + 'key' : 'LOGIN.ERROR_INVALID_LOGIN' + }; + + // Display error if anything else goes wrong + else + $scope.loginError = error.translatableMessage; + + // Reset all remaining fields to default values, but + // preserve any usernames + angular.forEach($scope.remainingFields, function clearEnteredValueIfPassword(field) { + if (field.type !== Field.Type.USERNAME && field.name in $scope.enteredValues) + $scope.enteredValues[field.name] = DEFAULT_FIELD_VALUE; + }); + } + + }); + // Reset state after authentication and routing have succeeded $rootScope.$on('$routeChangeSuccess', function routeChanged() { $scope.enteredValues = {}; From b6ce4776258ba5366a852b6dc950fb55306c28b1 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 27 Jan 2023 13:49:18 -0800 Subject: [PATCH 09/16] GUACAMOLE-839: Support deferred retrieval of authentication parameters. --- .../app/auth/service/authenticationService.js | 86 +++++++++++-------- 1 file changed, 50 insertions(+), 36 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js b/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js index a2faa90ff..88143a740 100644 --- a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js +++ b/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js @@ -26,9 +26,11 @@ * result of authentication operations: * * "guacLoginPending" - * An authentication request was submitted and we are awaiting the result. - * This event receives an object containing the HTTP parameters submitted - * as its sole parameter. + * An authentication request is being submitted and we are awaiting the + * result. The request may not yet have been submitted if the parameters + * for that request are not ready. This event receives a promise that + * resolves with the HTTP parameters that were ultimately submitted as its + * sole parameter. * * "guacLogin" * Authentication was successful and a new token was created. This event @@ -66,6 +68,7 @@ angular.module('auth').factory('authenticationService', ['$injector', var Error = $injector.get('Error'); // Required services + var $q = $injector.get('$q'); var $rootScope = $injector.get('$rootScope'); var localStorageService = $injector.get('localStorageService'); var requestService = $injector.get('requestService'); @@ -172,58 +175,69 @@ angular.module('auth').factory('authenticationService', ['$injector', * * If a token is provided, it will be reused if possible. * - * @param {Object} parameters - * Arbitrary parameters to authenticate with. + * @param {Object|Promise} parameters + * Arbitrary parameters to authenticate with. If a Promise is provided, + * that Promise must resolve with the parameters to be submitted when + * those parameters are available, and any error will be handled as if + * from the authentication endpoint of the REST API itself. * * @returns {Promise} * A promise which succeeds only if the login operation was successful. */ service.authenticate = function authenticate(parameters) { - // Notify that a fresh authentication request is being submitted + // Coerce received parameters object into a Promise, if it isn't + // already a Promise + parameters = $q.resolve(parameters); + + // Notify that a fresh authentication request is underway $rootScope.$broadcast('guacLoginPending', parameters); - // Attempt authentication - return requestService({ - method: 'POST', - url: 'api/tokens', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - data: $.param(parameters) - }) + // Attempt authentication after auth parameters are available ... + return parameters.then(function requestParametersReady(requestParams) { - // If authentication succeeds, handle received auth data - .then(function authenticationSuccessful(data) { + return requestService({ + method: 'POST', + url: 'api/tokens', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data: $.param(requestParams) + }) - var currentToken = service.getCurrentToken(); + // ... if authentication succeeds, handle received auth data ... + .then(function authenticationSuccessful(data) { - // If a new token was received, ensure the old token is invalidated, - // if any, and notify listeners of the new token - if (data.authToken !== currentToken) { + var currentToken = service.getCurrentToken(); + + // If a new token was received, ensure the old token is invalidated, + // if any, and notify listeners of the new token + if (data.authToken !== currentToken) { + + // If an old token existed, request that the token be revoked + if (currentToken) { + service.revokeToken(currentToken).catch(angular.noop); + } + + // Notify of login and new token + setAuthenticationResult(new AuthenticationResult(data)); + $rootScope.$broadcast('guacLogin', data.authToken); - // If an old token existed, request that the token be revoked - if (currentToken) { - service.revokeToken(currentToken).catch(angular.noop); } - // Notify of login and new token - setAuthenticationResult(new AuthenticationResult(data)); - $rootScope.$broadcast('guacLogin', data.authToken); + // Update cached authentication result, even if the token remains + // the same + else + setAuthenticationResult(new AuthenticationResult(data)); - } + // Authentication was successful + return data; - // Update cached authentication result, even if the token remains - // the same - else - setAuthenticationResult(new AuthenticationResult(data)); - - // Authentication was successful - return data; + }); }) - // If authentication fails, propogate failure to returned promise + // ... if authentication fails, propogate failure to returned promise ['catch'](requestService.createErrorCallback(function authenticationFailed(error) { // Notify of generic login failure, for any event consumers that From 38f1360dec7e00ba4fededfa40001d0a4b02e86a Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 27 Jan 2023 16:38:45 -0800 Subject: [PATCH 10/16] GUACAMOLE-839: Ensure SSL/TLS client auth failures are reflected in the Guacamole UI. --- .../sso/AuthenticationSessionManager.java | 14 + .../modules/guacamole-auth-sso-ssl/pom.xml | 50 +++ .../ssl/AuthenticationProviderService.java | 296 ++-------------- .../auth/ssl/OpaqueAuthenticationResult.java | 65 ++++ .../auth/ssl/SSLAuthenticationProvider.java | 3 +- .../ssl/SSLAuthenticationProviderModule.java | 3 +- .../auth/ssl/SSLAuthenticationSession.java | 7 +- .../ssl/SSLAuthenticationSessionManager.java | 3 +- .../ssl/SSLClientAuthenticationResource.java | 332 ++++++++++++++++++ .../auth/ssl/conf/ConfigurationService.java | 84 ++++- .../main/resources/directives/guacSslAuth.js | 68 ++++ .../src/main/resources/guac-manifest.json | 2 + .../main/resources/html/sso-provider-ssl.html | 2 +- .../src/main/resources/license.txt | 18 + .../src/main/resources/sslModule.js | 29 ++ 15 files changed, 681 insertions(+), 295 deletions(-) create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/OpaqueAuthenticationResult.java create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/license.txt create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/sslModule.js diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSessionManager.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSessionManager.java index da530f61b..3261228b8 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSessionManager.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSessionManager.java @@ -68,6 +68,20 @@ public class AuthenticationSessionManager { }, 1, 1, TimeUnit.MINUTES); } + /** + * Generates a cryptographically-secure value identical in form to the + * session tokens generated by {@link #defer(org.apache.guacamole.auth.sso.AuthenticationSession)} + * but invalid. The returned value is indistinguishable from a valid token, + * but is not a valid token. + * + * @return + * An invalid token value that is indistinguishable from a valid + * token. + */ + public String generateInvalid() { + return idGenerator.generateIdentifier(); + } + /** * Resumes the Guacamole side of the authentication process that was * previously deferred through a call to defer(). Once invoked, the diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml index a107e6b7f..c6542ae56 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml @@ -37,6 +37,56 @@ ../../ + + + + + + com.github.buckelieg + minify-maven-plugin + + + default-cli + + UTF-8 + + ${basedir}/src/main/resources + ${project.build.directory}/classes + + / + / + ssl.js + + + license.txt + + + + **/*.js + + + + + **/*.test.js + + CLOSURE + + + + OFF + OFF + + + + + minify + + + + + + + diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java index 16414688e..bc311de47 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java @@ -22,38 +22,16 @@ package org.apache.guacamole.auth.ssl; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; import java.net.URI; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.security.Principal; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.Arrays; import java.util.Collections; -import java.util.concurrent.TimeUnit; -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.core.UriBuilder; import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.auth.ssl.conf.ConfigurationService; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.GuacamoleServerException; -import org.apache.guacamole.auth.sso.NonceService; +import org.apache.guacamole.GuacamoleResourceNotFoundException; import org.apache.guacamole.auth.sso.SSOAuthenticationProviderService; import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser; -import org.apache.guacamole.form.Field; -import org.apache.guacamole.form.RedirectField; -import org.apache.guacamole.language.TranslatableMessage; import org.apache.guacamole.net.auth.Credentials; -import org.apache.guacamole.net.auth.credentials.CredentialsInfo; -import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; -import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; /** * Service that authenticates Guacamole users using SSL/TLS authentication @@ -68,13 +46,6 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS @Inject private ConfigurationService confService; - /** - * Service for validating and generating unique nonce values. Here, these - * nonces are used specifically for generating unique domains. - */ - @Inject - private NonceService subdomainNonceService; - /** * Session manager for generating and maintaining unique tokens to * represent the authentication flow of a user who has only partially @@ -92,124 +63,12 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS @Inject private Provider authenticatedUserProvider; - /** - * The string value that the SSL termination service uses for its client - * verification header to represent that the client certificate has been - * verified. - */ - private static final String CLIENT_VERIFIED_HEADER_SUCCESS_VALUE = "SUCCESS"; - /** * The name of the query parameter containing the temporary session token * representing the current state of an in-progress authentication attempt. */ private static final String AUTH_SESSION_PARAMETER_NAME = "state"; - /** - * Decodes the provided URL-encoded string as UTF-8, returning the result. - * - * @param value - * The URL-encoded string to decode. - * - * @return - * The decoded string. - * - * @throws GuacamoleException - * If the provided value is not a value URL-encoded string. - */ - private byte[] decode(String value) throws GuacamoleException { - try { - return URLDecoder.decode(value, StandardCharsets.UTF_8.name()) - .getBytes(StandardCharsets.UTF_8); - } - catch (IllegalArgumentException e) { - throw new GuacamoleClientException("Invalid URL-encoded value.", e); - } - catch (UnsupportedEncodingException e) { - // This should never happen, as UTF-8 is a standard charset that - // the JVM is required to support - throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e); - } - } - - /** - * Authenticates a user using HTTP headers containing that user's verified - * X.509 certificate. It is assumed that this certificate is being passed - * to Guacamole from an SSL termination service that has already verified - * that this certificate is valid and authorized for access to that - * Guacamole instance. - * - * @param credentials - * The credentials received by Guacamole in the authentication request. - * - * @param certificate - * The raw bytes of the X.509 certificate retrieved from the request. - * - * @return - * A new SSOAuthenticatedUser representing the identity of the user - * asserted by the SSL termination service via that user's X.509 - * certificate. - * - * @throws GuacamoleException - * If the provided X.509 certificate is not valid or cannot be parsed. - * It is expected that the SSL termination service will already have - * validated the certificate; this function validates only the - * certificate timestamps. - */ - private SSOAuthenticatedUser authenticateUser(Credentials credentials, - byte[] certificate) throws GuacamoleException { - - // Parse and re-verify certificate is valid with respect to timestamps - X509Certificate cert; - try (InputStream input = new ByteArrayInputStream(certificate)) { - - CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); - cert = (X509Certificate) certFactory.generateCertificate(input); - - // Verify certificate is valid (it should be given pre-validation - // from SSL termination, but it's worth rechecking for sanity) - cert.checkValidity(); - - } - catch (CertificateException e) { - throw new GuacamoleClientException("The X.509 certificate " - + "presented is not valid.", e); - } - catch (IOException e) { - throw new GuacamoleServerException("Provided X.509 certificate " - + "could not be read.", e); - } - - // Extract user's DN from their X.509 certificate - LdapName dn; - try { - Principal principal = cert.getSubjectX500Principal(); - dn = new LdapName(principal.getName()); - } - catch (InvalidNameException e) { - throw new GuacamoleClientException("The X.509 certificate " - + "presented does not contain a valid subject DN.", e); - } - - // Verify DN actually contains components - int numComponents = dn.size(); - if (numComponents < 1) - throw new GuacamoleClientException("The X.509 certificate " - + "presented contains an empty subject DN."); - - // Simply use first component of DN as username (TODO: Enforce - // requirements on the attribute providing the username and the base DN, - // and consider using components following the username to determine - // group memberships) - String username = dn.getRdn(numComponents - 1).getValue().toString(); - - SSOAuthenticatedUser authenticatedUser = authenticatedUserProvider.get(); - authenticatedUser.init(username, credentials, - Collections.emptySet(), Collections.emptyMap()); - return authenticatedUser; - - } - /** * Processes the given HTTP request, returning the identity represented by * the auth session token present in that request. If no such token is @@ -224,80 +83,17 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS * or null if there is no such token or the token does not represent a * valid identity. */ - private SSOAuthenticatedUser processIdentity(HttpServletRequest request) { + private SSOAuthenticatedUser processIdentity(Credentials credentials, HttpServletRequest request) { + String state = request.getParameter(AUTH_SESSION_PARAMETER_NAME); - return sessionManager.getIdentity(state); - } - - /** - * Processes the X.509 certificate in the headers of the given HTTP - * request, returning an authentication session token representing the - * identity in that certificate. If the certificate is invalid or not - * present, null is returned. - * - * @param credentials - * The credentials submitted in the HTTP request being processed. - * - * @param request - * The HTTP request to process. - * - * @return - * An authentication session token representing the identity in the - * certificate in the given HTTP request, or null if the request does - * not contain a valid certificate. - * - * @throws GuacamoleException - * If any configuration parameters related to retrieving certificates - * from HTTP request cannot be parsed. - */ - private String processCertificate(Credentials credentials, - HttpServletRequest request) throws GuacamoleException { - - // Verify that SSL termination has already verified the certificate - String verified = request.getHeader(confService.getClientVerifiedHeader()); - if (!CLIENT_VERIFIED_HEADER_SUCCESS_VALUE.equals(verified)) + String username = sessionManager.getIdentity(state); + if (username == null) return null; - String certificate = request.getHeader(confService.getClientCertificateHeader()); - if (certificate == null) - return null; - - SSOAuthenticatedUser authenticatedUser = authenticateUser(credentials, decode(certificate)); - long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxTokenValidity()); - return sessionManager.defer(new SSLAuthenticationSession(authenticatedUser, validityDuration)); - - } - - /** - * Redirects the current user back to the main URL of the Guacamole - * instance to continue the authentication process after having identified - * themselves using SSL/TLS client authentication. - * - * @param token - * The authentication session token generated for the current user's - * identity. - * - * @throws GuacamoleException - * To redirect the user to the main URL of the Guacamole instance. - */ - private void resumeAuthenticationAtRedirectURI(String token) - throws GuacamoleException { - - URI redirectURI = UriBuilder.fromUri(confService.getRedirectURI()) - .queryParam(AUTH_SESSION_PARAMETER_NAME, token) - .build(); - - // Request that the provided credentials, now tokenized, be - // resubmitted in that tokenized form to the original host for - // authentication - throw new GuacamoleInsufficientCredentialsException("Please " - + "resubmit your tokenized credentials using the " - + "following URI.", - new CredentialsInfo(Arrays.asList(new Field[] { - new RedirectField(AUTH_SESSION_PARAMETER_NAME, redirectURI, - new TranslatableMessage("LOGIN.INFO_IDP_REDIRECT_PENDING")) - })) - ); + SSOAuthenticatedUser authenticatedUser = authenticatedUserProvider.get(); + authenticatedUser.init(username, credentials, + Collections.emptySet(), Collections.emptyMap()); + return authenticatedUser; } @@ -308,21 +104,22 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS // // Overall flow: // - // 1) Unauthenticated user is given a temporary auth session token - // and redirected to the SSL termination instance that provides - // SSL client auth. This redirect uses a unique and temporary - // subdomain to ensure each SSL client auth attempt is fresh and - // does not use cached auth details. + // 1) An unauthenticated user makes a GET request to + // ".../api/ext/ssl/identity". After a series of redirects + // intended to prevent that identity from being inadvertently + // cached and inherited by future authentication attempts on the + // same client machine, an external SSL termination service requests + // and validates the user's certificate, those details are passed + // back to Guacamole via HTTP headers, and Guacamole produces a JSON + // response containing an opaque state value. // - // 2) Unauthenticated user with a temporary auth session token - // is validated by SSL termination, with that SSL termination - // adding HTTP headers containing the validated certificate to the - // user's HTTP request. + // 2) The user (still unauthenticated) resubmits the opaque state + // value from the received JSON as the "state" parameter of a + // standard Guacamole authentication request (".../api/tokens"). // - // 3) If valid, the user is assigned a temporary token and redirected - // back to the original URL. That temporary token is accepted by - // this extension at the original URL as proof of the user's - // identity. + // 3) If the certificate received was valid, the user is authenticated + // according to the identity asserted by that certificate. If not, + // authentication is refused. // // NOTE: All SSL termination endpoints in front of Guacamole MUST // be configured to drop these headers from any inbound requests @@ -345,50 +142,13 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS return null; // - // Handle only auth session tokens at the main redirect URI, using the + // Handle only auth session tokens at the primary URI, using the // pre-verified information from those tokens to determine user // identity. // - String redirectHost = confService.getRedirectURI().getHost(); - if (host.equals(redirectHost)) { - - SSOAuthenticatedUser user = processIdentity(request); - if (user != null) - return user; - - // Redirect unauthenticated requests to the endpoint requiring - // SSL client auth to request identity verification - throw new GuacamoleInvalidCredentialsException("Invalid login.", - new CredentialsInfo(Arrays.asList(new Field[] { - new RedirectField(AUTH_SESSION_PARAMETER_NAME, getLoginURI(), // <-- Each call to getLoginURI() produces a unique subdomain that is valid only for ONE use (see below) - new TranslatableMessage("LOGIN.INFO_IDP_REDIRECT_PENDING")) - })) - ); - - } - - // - // Process certificates only at valid single-use subdomains dedicated - // to client authentication, redirecting back to the main redirect URI - // for final authentication if that processing is successful. - // - // NOTE: This is CRITICAL. If unique subdomains are not generated and - // tied to strictly one authentication attempt, then those subdomains - // could be reused by a user on a shared machine to assume the cached - // credentials of another user that used that machine earlier. The - // browser and/or OS may cache the certificate so that it can be reused - // for future SSL sessions to that same domain. Here, we ensure each - // generated domain is unique and only valid for certificate processing - // ONCE. The domain may still be valid with DNS, but will no longer be - // usable for certificate authentication. - // - - else if (subdomainNonceService.isValid(confService.getClientAuthenticationSubdomain(host))) { - String token = processCertificate(credentials, request); - if (token != null) - resumeAuthenticationAtRedirectURI(token); - } + if (confService.isPrimaryHostname(host)) + return processIdentity(credentials, request); // All other requests are not allowed - refuse to authenticate throw new GuacamoleClientException("Direct authentication against " @@ -400,9 +160,7 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS @Override public URI getLoginURI() throws GuacamoleException { - long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxDomainValidity()); - String uniqueSubdomain = subdomainNonceService.generate(validityDuration); - return confService.getClientAuthenticationURI(uniqueSubdomain); + throw new GuacamoleResourceNotFoundException("No such resource."); } @Override diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/OpaqueAuthenticationResult.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/OpaqueAuthenticationResult.java new file mode 100644 index 000000000..66020f20a --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/OpaqueAuthenticationResult.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.guacamole.auth.ssl; + +/** + * REST API response that reports the result of attempting to authenticate the + * user using SSL/TLS client authentication. The information within this + * result is intentionally opaque and must be resubmitted in a separate + * authentication request for authentication to finally succeed or fail. + */ +public class OpaqueAuthenticationResult { + + /** + * An arbitrary value representing the result of authenticating the + * current user. + */ + private final String state; + + /** + * Creates a new OpaqueAuthenticationResult containing the given opaque + * state value. Successful authentication results must be indistinguishable + * from unsuccessful results with respect to this value. Only using this + * value within ANOTHER authentication attempt can determine whether + * authentication is successful. + * + * @param state + * An arbitrary value representing the result of authenticating the + * current user. + */ + public OpaqueAuthenticationResult(String state) { + this.state = state; + } + + /** + * Returns an arbitrary value representing the result of authenticating the + * current user. This value may be resubmitted as the "state" parameter of + * an authentication request beneath the primary URI of the web application + * to finalize the authentication procedure and determine whether the + * operation has succeeded or failed. + * + * @return + * An arbitrary value representing the result of authenticating the + * current user. + */ + public String getState() { + return state; + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProvider.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProvider.java index 6b01858ef..2458c1a44 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProvider.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProvider.java @@ -20,7 +20,6 @@ package org.apache.guacamole.auth.ssl; import org.apache.guacamole.auth.sso.SSOAuthenticationProvider; -import org.apache.guacamole.auth.sso.SSOResource; /** * Guacamole authentication backend which authenticates users using SSL/TLS @@ -37,7 +36,7 @@ public class SSLAuthenticationProvider extends SSOAuthenticationProvider { * an external SSL termination system using SSL/TLS client authentication. */ public SSLAuthenticationProvider() { - super(AuthenticationProviderService.class, SSOResource.class, + super(AuthenticationProviderService.class, SSLClientAuthenticationResource.class, new SSLAuthenticationProviderModule()); } diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProviderModule.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProviderModule.java index 56e50bd75..46eeaa94f 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProviderModule.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProviderModule.java @@ -20,6 +20,7 @@ package org.apache.guacamole.auth.ssl; import com.google.inject.AbstractModule; +import com.google.inject.Scopes; import org.apache.guacamole.auth.ssl.conf.ConfigurationService; import org.apache.guacamole.auth.sso.NonceService; @@ -32,7 +33,7 @@ public class SSLAuthenticationProviderModule extends AbstractModule { @Override protected void configure() { bind(ConfigurationService.class); - bind(NonceService.class); + bind(NonceService.class).in(Scopes.SINGLETON); bind(SSLAuthenticationSessionManager.class); } diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSession.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSession.java index e2e0d4c43..4a4c9ce8f 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSession.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSession.java @@ -20,7 +20,6 @@ package org.apache.guacamole.auth.ssl; import org.apache.guacamole.auth.sso.AuthenticationSession; -import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser; /** * Representation of an in-progress SSL/TLS authentication attempt. @@ -30,7 +29,7 @@ public class SSLAuthenticationSession extends AuthenticationSession { /** * The identity asserted by the external SSL termination service. */ - private final SSOAuthenticatedUser identity; + private final String identity; /** * Creates a new AuthenticationSession representing an in-progress SSL/TLS @@ -44,7 +43,7 @@ public class SSLAuthenticationSession extends AuthenticationSession { * The number of milliseconds that may elapse before this session must * be considered invalid. */ - public SSLAuthenticationSession(SSOAuthenticatedUser identity, long expires) { + public SSLAuthenticationSession(String identity, long expires) { super(expires); this.identity = identity; } @@ -58,7 +57,7 @@ public class SSLAuthenticationSession extends AuthenticationSession { * @return * The identity asserted by the external SSL termination service. */ - public SSOAuthenticatedUser getIdentity() { + public String getIdentity() { return identity; } diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSessionManager.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSessionManager.java index 3200cae3d..fc1b0842f 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSessionManager.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSessionManager.java @@ -21,7 +21,6 @@ package org.apache.guacamole.auth.ssl; import com.google.inject.Singleton; import org.apache.guacamole.auth.sso.AuthenticationSessionManager; -import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser; /** * Manager service that temporarily stores SSL/TLS authentication attempts @@ -48,7 +47,7 @@ public class SSLAuthenticationSessionManager * session with the given identifier, or null if there is no such * identity. */ - public SSOAuthenticatedUser getIdentity(String identifier) { + public String getIdentity(String identifier) { SSLAuthenticationSession session = resume(identifier); if (session != null) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java new file mode 100644 index 000000000..4c99394f5 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java @@ -0,0 +1,332 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.guacamole.auth.ssl; + +import com.google.inject.Inject; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.ws.rs.GET; +import javax.ws.rs.core.Response; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriBuilder; +import org.apache.guacamole.GuacamoleClientException; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleResourceNotFoundException; +import org.apache.guacamole.GuacamoleServerException; +import org.apache.guacamole.auth.ssl.conf.ConfigurationService; +import org.apache.guacamole.auth.sso.NonceService; +import org.apache.guacamole.auth.sso.SSOResource; + +/** + * REST API resource that allows the user to retrieve an opaque state value + * representing their identity as determined by SSL/TLS client authentication. + * The opaque value may represent a valid identity or an authentication + * failure, and must be resubmitted within a normal Guacamole authentication + * request to finalize the authentication process. + */ +public class SSLClientAuthenticationResource extends SSOResource { + + /** + * The string value that the SSL termination service uses for its client + * verification header to represent that the client certificate has been + * verified. + */ + private static final String CLIENT_VERIFIED_HEADER_SUCCESS_VALUE = "SUCCESS"; + + /** + * Service for retrieving configuration information. + */ + @Inject + private ConfigurationService confService; + + /** + * Session manager for generating and maintaining unique tokens to + * represent the authentication flow of a user who has only partially + * authenticated. Here, these tokens represent a user that has been + * validated by SSL termination and allow the Guacamole instance that + * doesn't require SSL/TLS authentication to retrieve the user's identity + * and complete the authentication process. + */ + @Inject + private SSLAuthenticationSessionManager sessionManager; + + /** + * Service for validating and generating unique nonce values. Here, these + * nonces are used specifically for generating unique domains. + */ + @Inject + private NonceService subdomainNonceService; + + /** + * Retrieves a single value from the HTTP header having the given name. If + * there are multiple HTTP headers present with this name, the first + * matching header in the request is used. If there are no such headers in + * the request, null is returned. + * + * @param headers + * The HTTP headers present in the request. + * + * @param name + * The name of the header to retrieve. + * + * @return + * The first value of the HTTP header having the given name, or null if + * there is no such header. + */ + private String getHeader(HttpHeaders headers, String name) { + + List values = headers.getRequestHeader(name); + if (values.isEmpty()) + return null; + + return values.get(0); + + } + + /** + * Decodes the provided URL-encoded string as UTF-8, returning the result. + * + * @param value + * The URL-encoded string to decode. + * + * @return + * The decoded string. + * + * @throws GuacamoleException + * If the provided value is not a value URL-encoded string. + */ + private byte[] decode(String value) throws GuacamoleException { + try { + return URLDecoder.decode(value, StandardCharsets.UTF_8.name()) + .getBytes(StandardCharsets.UTF_8); + } + catch (IllegalArgumentException e) { + throw new GuacamoleClientException("Invalid URL-encoded value.", e); + } + catch (UnsupportedEncodingException e) { + // This should never happen, as UTF-8 is a standard charset that + // the JVM is required to support + throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e); + } + } + + /** + * Authenticates a user using HTTP headers containing that user's verified + * X.509 certificate. It is assumed that this certificate is being passed + * to Guacamole from an SSL termination service that has already verified + * that this certificate is valid and authorized for access to that + * Guacamole instance. + * + * @param certificate + * The raw bytes of the X.509 certificate retrieved from the request. + * + * @return + * A new SSOAuthenticatedUser representing the identity of the user + * asserted by the SSL termination service via that user's X.509 + * certificate. + * + * @throws GuacamoleException + * If the provided X.509 certificate is not valid or cannot be parsed. + * It is expected that the SSL termination service will already have + * validated the certificate; this function validates only the + * certificate timestamps. + */ + public String getUsername(byte[] certificate) throws GuacamoleException { + + // Parse and re-verify certificate is valid with respect to timestamps + X509Certificate cert; + try (InputStream input = new ByteArrayInputStream(certificate)) { + + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + cert = (X509Certificate) certFactory.generateCertificate(input); + + // Verify certificate is valid (it should be given pre-validation + // from SSL termination, but it's worth rechecking for sanity) + cert.checkValidity(); + + } + catch (CertificateException e) { + throw new GuacamoleClientException("The X.509 certificate " + + "presented is not valid.", e); + } + catch (IOException e) { + throw new GuacamoleServerException("Provided X.509 certificate " + + "could not be read.", e); + } + + // Extract user's DN from their X.509 certificate + LdapName dn; + try { + Principal principal = cert.getSubjectX500Principal(); + dn = new LdapName(principal.getName()); + } + catch (InvalidNameException e) { + throw new GuacamoleClientException("The X.509 certificate " + + "presented does not contain a valid subject DN.", e); + } + + // Verify DN actually contains components + int numComponents = dn.size(); + if (numComponents < 1) + throw new GuacamoleClientException("The X.509 certificate " + + "presented contains an empty subject DN."); + + // Simply use first component of DN as username (TODO: Enforce + // requirements on the attribute providing the username and the base DN, + // and consider using components following the username to determine + // group memberships) + return dn.getRdn(numComponents - 1).getValue().toString(); + + } + + /** + * Processes the X.509 certificate in the headers of the given HTTP + * request, returning an authentication session token representing the + * identity in that certificate. If the certificate is invalid or not + * present, null is returned. + * + * @param credentials + * The credentials submitted in the HTTP request being processed. + * + * @param request + * The HTTP request to process. + * + * @return + * An authentication session token representing the identity in the + * certificate in the given HTTP request, or null if the request does + * not contain a valid certificate. + * + * @throws GuacamoleException + * If any configuration parameters related to retrieving certificates + * from HTTP request cannot be parsed. + */ + private String processCertificate(HttpHeaders headers) throws GuacamoleException { + + // + // NOTE: A result with an associated state is ALWAYS returned by + // processCertificate(), even if the request does not actually contain + // a valid certificate. This is by design and ensures that the nature + // of a certificate (valid vs. invalid) cannot be determined except + // via Guacamole's authentication endpoint, thus allowing auth failure + // hooks to consider attempts to use invalid certificates as auth + // failures. + // + + // Verify that SSL termination has already verified the certificate + String verified = getHeader(headers, confService.getClientVerifiedHeader()); + if (!CLIENT_VERIFIED_HEADER_SUCCESS_VALUE.equals(verified)) + return sessionManager.generateInvalid(); + + String certificate = getHeader(headers, confService.getClientCertificateHeader()); + if (certificate == null) + return sessionManager.generateInvalid(); + + String username = getUsername(decode(certificate)); + long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxTokenValidity()); + return sessionManager.defer(new SSLAuthenticationSession(username, validityDuration)); + + } + + /** + * Attempts to authenticate the current user using SSL/TLS client + * authentication, returning an opaque value that represents their + * authenticated status. If necessary, the user is first redirected to a + * unique endpoint that supports SSL/TLS client authentication. + * + * @param headers + * All HTTP headers submitted in the user's authentication request. + * + * @param host + * The hostname that the user specified in their HTTP request. + * + * @return + * A Response containing an opaque value representing the user's + * authenticated status, or a Response redirecting the user to a + * unique endpoint that can provide this. + * + * @throws GuacamoleException + * If any required configuration information is missing or cannot be + * parsed, or if the request was not received at a valid subdomain. + */ + @GET + @Path("identity") + public Response authenticateClient(@Context HttpHeaders headers, + @HeaderParam("Host") String host) throws GuacamoleException { + + // Redirect any requests to the domain that does NOT require SSL/TLS + // client authentication to the same endpoint at a domain that does + // require SSL/TLS authentication + String subdomain = confService.getClientAuthenticationSubdomain(host); + if (subdomain == null) { + + long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxDomainValidity()); + String uniqueSubdomain = subdomainNonceService.generate(validityDuration); + + URI clientAuthURI = UriBuilder.fromUri(confService.getClientAuthenticationURI(uniqueSubdomain)) + .path("api/ext/ssl/identity") + .build(); + + return Response.seeOther(clientAuthURI).build(); + + } + + // + // Process certificates only at valid single-use subdomains dedicated + // to client authentication, redirecting back to the main redirect URI + // for final authentication if that processing is successful. + // + // NOTE: This is CRITICAL. If unique subdomains are not generated and + // tied to strictly one authentication attempt, then those subdomains + // could be reused by a user on a shared machine to assume the cached + // credentials of another user that used that machine earlier. The + // browser and/or OS may cache the certificate so that it can be reused + // for future SSL sessions to that same domain. Here, we ensure each + // generated domain is unique and only valid for certificate processing + // ONCE. The domain may still be valid with DNS, but will no longer be + // usable for certificate authentication. + // + + if (subdomainNonceService.isValid(subdomain)) + return Response.ok(new OpaqueAuthenticationResult(processCertificate(headers))) + .header("Access-Control-Allow-Origin", confService.getPrimaryOrigin().toString()) + .type(MediaType.APPLICATION_JSON) + .build(); + + throw new GuacamoleResourceNotFoundException("No such resource."); + + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java index 074ee093b..3f57bd864 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java @@ -21,8 +21,10 @@ package org.apache.guacamole.auth.ssl.conf; import com.google.inject.Inject; import java.net.URI; +import java.net.URISyntaxException; import javax.ws.rs.core.UriBuilder; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.environment.Environment; import org.apache.guacamole.properties.IntegerGuacamoleProperty; import org.apache.guacamole.properties.StringGuacamoleProperty; @@ -80,11 +82,11 @@ public class ConfigurationService { * to THIS instance of Guacamole, but behind SSL termination that DOES NOT * require or request SSL/TLS client authentication. */ - private static final URIGuacamoleProperty SSL_REDIRECT_URI = + private static final URIGuacamoleProperty SSL_PRIMARY_URI = new URIGuacamoleProperty() { @Override - public String getName() { return "ssl-redirect-uri"; } + public String getName() { return "ssl-primary-uri"; } }; @@ -121,11 +123,10 @@ public class ConfigurationService { * authentication token for SSL/TLS authentication may remain valid, in * minutes. This token is used to represent the user's asserted identity * after it has been verified by the SSL termination service. This interval - * must be long enough to allow for network delays in redirecting the user - * back to the main Guacamole URL, but short enough that unused tokens do - * not consume unnecessary server resources and cannot potentially be - * guessed while the token is still valid. These tokens are 256-bit secure - * random values. + * must be long enough to allow for network delays in receiving the token, + * but short enough that unused tokens do not consume unnecessary server + * resources and cannot potentially be guessed while the token is still + * valid. These tokens are 256-bit secure random values. */ private static final IntegerGuacamoleProperty SSL_MAX_TOKEN_VALIDITY = new IntegerGuacamoleProperty() { @@ -141,12 +142,12 @@ public class ConfigurationService { * minutes. This subdomain is used to ensure each SSL/TLS authentication * attempt is fresh and does not potentially reuse a previous * authentication attempt that was cached by the browser or OS. This - * interval must be long enough to allow for network delays in redirecting - * the user to the SSL termination service enforcing SSL/TLS - * authentication, but short enough that an unused domain does not consume - * unnecessary server resources and cannot potentially be guessed while - * that subdomain is still valid. These subdomains are 128-bit secure - * random values. + * interval must be long enough to allow for network delays in + * authenticating the user with the SSL termination service that enforces + * SSL/TLS client authentication, but short enough that an unused domain + * does not consume unnecessary server resources and cannot potentially be + * guessed while that subdomain is still valid. These subdomains are + * 128-bit secure random values. */ private static final IntegerGuacamoleProperty SSL_MAX_DOMAIN_VALIDITY = new IntegerGuacamoleProperty() { @@ -212,6 +213,11 @@ public class ConfigurationService { */ public String getClientAuthenticationSubdomain(String hostname) throws GuacamoleException { + // Any hostname that matches the explicitly-specific primary URI is not + // a client auth subdomain + if (isPrimaryHostname(hostname)) + return null; + URI authURI = environment.getRequiredProperty(SSL_CLIENT_AUTH_URI); String baseHostname = authURI.getHost(); @@ -240,11 +246,57 @@ public class ConfigurationService { * required. * * @throws GuacamoleException - * If the required property for configuring the redirect URI is missing + * If the required property for configuring the primary URI is missing * or cannot be parsed. */ - public URI getRedirectURI() throws GuacamoleException { - return environment.getRequiredProperty(SSL_REDIRECT_URI); + public URI getPrimaryURI() throws GuacamoleException { + return environment.getRequiredProperty(SSL_PRIMARY_URI); + } + + /** + * Returns the HTTP request origin for requests originating from this + * instance via the primary URI (as returned by {@link #getPrimaryURI()}. + * This value is essentially the same as the primary URI but with only the + * scheme, host, and port present. + * + * @return + * The HTTP request origin for requests originating from this instance + * via the primary URI. + * + * @throws GuacamoleException + * If the required property for configuring the primary URI is missing + * or cannot be parsed. + */ + public URI getPrimaryOrigin() throws GuacamoleException { + URI primaryURI = getPrimaryURI(); + try { + return new URI(primaryURI.getScheme(), null, primaryURI.getHost(), primaryURI.getPort(), null, null, null); + } + catch (URISyntaxException e) { + throw new GuacamoleServerException("Request origin could not be " + + "derived from the configured primary URI.", e); + } + } + + /** + * Returns whether the given hostname is the same as the hostname in the + * primary URI (as returned by {@link #getPrimaryURI()}. Hostnames are + * case-insensitive. + * + * @param hostname + * The hostname to test. + * + * @return + * true if the hostname is the same as the hostname in the primary URI, + * false otherwise. + * + * @throws GuacamoleException + * If the required property for configuring the primary URI is missing + * or cannot be parsed. + */ + public boolean isPrimaryHostname(String hostname) throws GuacamoleException { + URI primaryURI = getPrimaryURI(); + return hostname.equalsIgnoreCase(primaryURI.getHost()); } /** diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js new file mode 100644 index 000000000..82e5c8b6d --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * A directive which automatically attempts to log the current user in using + * SSL/TLS client authentication when the associated element is clicked. + */ +angular.module('element').directive('guacSslAuth', ['$injector', function guacSslAuth($injector) { + + // Required services + var requestService = $injector.get('requestService'); + var authenticationService = $injector.get('authenticationService'); + + var directive = { + restrict: 'A' + }; + + directive.link = function linkGuacSslAuth($scope, $element) { + + /** + * The element which will register the click. + * + * @type Element + */ + const element = $element[0]; + + // Attempt SSL/TLS client authentication upon click + element.addEventListener('click', function elementClicked() { + + // Transform SSL/TLS identity into an opaque "state" value and + // attempt authentication using that value + authenticationService.authenticate( + requestService({ + method: 'GET', + headers : { + 'Cache-Control' : undefined, // Avoid sending headers that would result in a pre-flight OPTIONS request for CORS + 'Pragma' : undefined + }, + url: 'api/ext/ssl/identity' + }) + .then(function identityRetrieved(data) { + return { 'state' : data.state || '' }; + }) + )['catch'](requestService.IGNORE); + + }); + + }; + + return directive; + +}]); diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/guac-manifest.json index 30c8a1c99..7b8099427 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/guac-manifest.json +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/guac-manifest.json @@ -13,6 +13,8 @@ "styles/sso-providers.css" ], + "js" : [ "ssl.min.js" ], + "html" : [ "html/sso-providers.html", "html/sso-provider-ssl.html" diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html index b6c510931..8798eed2d 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html @@ -1,4 +1,4 @@ -
  • {{ +
  • {{ 'LOGIN.NAME_IDP_SSL' | translate }}
  • diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/license.txt b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/license.txt new file mode 100644 index 000000000..042f3ce1f --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/license.txt @@ -0,0 +1,18 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/sslModule.js b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/sslModule.js new file mode 100644 index 000000000..2e3f844ef --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/sslModule.js @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * The module for code implementing SSO using SSL/TLS client authentication. + */ +angular.module('guacSsoSsl', [ + 'auth', + 'rest' +]); + +// Ensure the guacSsoSsl module is loaded along with the rest of the app +angular.module('index').requires.push('guacSsoSsl'); From 82553265121ff4df4f52b1eb71f850893b7a22b2 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 30 Jan 2023 16:29:04 -0800 Subject: [PATCH 11/16] GUACAMOLE-839: Move SSL/TLS client auth logic to separate service. --- .../main/resources/directives/guacSslAuth.js | 21 +------ .../resources/services/clientAuthService.js | 58 +++++++++++++++++++ 2 files changed, 60 insertions(+), 19 deletions(-) create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/services/clientAuthService.js diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js index 82e5c8b6d..49347127a 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js @@ -24,8 +24,7 @@ angular.module('element').directive('guacSslAuth', ['$injector', function guacSslAuth($injector) { // Required services - var requestService = $injector.get('requestService'); - var authenticationService = $injector.get('authenticationService'); + var clientAuthService = $injector.get('clientAuthService'); var directive = { restrict: 'A' @@ -42,23 +41,7 @@ angular.module('element').directive('guacSslAuth', ['$injector', function guacSs // 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); - + clientAuthService.authenticate(); }); }; diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/services/clientAuthService.js b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/services/clientAuthService.js new file mode 100644 index 000000000..3bf9d9e86 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/services/clientAuthService.js @@ -0,0 +1,58 @@ +/* + * 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. + */ + +/** + * Service for authenticating a user using SSL/TLS client authentication. + */ +angular.module('guacSsoSsl').factory('clientAuthService', ['$injector', + function clientAuthServiceProvider($injector) { + + // Required services + var requestService = $injector.get('requestService'); + var authenticationService = $injector.get('authenticationService'); + + var service = {}; + + /** + * Attempt to authenticate using a unique token obtained through SSL/TLS + * client authentication. + */ + service.authenticate = function authenticate() { + + // 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 service; + +}]); From 0b5b82cc486029e3759a33e9224e5ccc3923a039 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 21 Feb 2023 17:06:37 -0800 Subject: [PATCH 12/16] GUACAMOLE-839: Allow accepted subject DNs to be restricted via configuration. --- .../ssl/SSLClientAuthenticationResource.java | 185 +++++++++++++----- .../auth/ssl/conf/ConfigurationService.java | 63 ++++++ .../ssl/conf/LdapNameGuacamoleProperty.java | 50 +++++ 3 files changed, 247 insertions(+), 51 deletions(-) create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/LdapNameGuacamoleProperty.java diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java index 4c99394f5..21a2e0c25 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java @@ -34,6 +34,7 @@ import java.util.List; import java.util.concurrent.TimeUnit; import javax.naming.InvalidNameException; import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; import javax.ws.rs.GET; import javax.ws.rs.core.Response; import javax.ws.rs.HeaderParam; @@ -45,10 +46,11 @@ 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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * REST API resource that allows the user to retrieve an opaque state value @@ -66,6 +68,25 @@ public class SSLClientAuthenticationResource extends SSOResource { */ private static final String CLIENT_VERIFIED_HEADER_SUCCESS_VALUE = "SUCCESS"; + /** + * The string value that the SSL termination service uses for its client + * verification header to represent that the client certificate is absent. + */ + private static final String CLIENT_VERIFIED_HEADER_NONE_VALUE = "NONE"; + + /** + * The string prefix that the SSL termination service uses for its client + * verification header to represent that the client certificate has failed + * validation. The error message describing the nature of the failure is + * provided by the SSL termination service after this prefix. + */ + private static final String CLIENT_VERIFIED_HEADER_FAILED_PREFIX = "FAILED:"; + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(SSLClientAuthenticationResource.class); + /** * Service for retrieving configuration information. */ @@ -143,6 +164,67 @@ public class SSLClientAuthenticationResource extends SSOResource { } } + /** + * Extracts a user's username from the X.509 subject name, which should be + * in LDAP DN format. If specific username attributes are configured, only + * those username attributes are used to determine the name. If a specific + * base DN is configured, only subject names that are formatted as LDAP DNs + * within that base DN will be accepted. + * + * @param name + * The subject name to extract the username from. + * + * @return + * The username of the user represented by the given subject name. + * + * @throws GuacamoleException + * If any configuration parameters related to retrieving certificates + * from HTTP request cannot be parsed, or if the provided subject name + * cannot be parsed or is not acceptable (wrong base DN or wrong + * username attribute). + */ + public String getUsername(String name) throws GuacamoleException { + + // Extract user's DN from their X.509 certificate + LdapName dn; + try { + dn = new LdapName(name); + } + catch (InvalidNameException e) { + throw new GuacamoleClientException("Subject \"" + name + "\" is " + + "not a valid DN: " + e.getMessage(), e); + } + + // Verify DN actually contains components + int numComponents = dn.size(); + if (numComponents < 1) + throw new GuacamoleClientException("Subject DN is empty."); + + // Verify DN is within configured base DN (if any) + LdapName baseDN = confService.getSubjectBaseDN(); + if (baseDN != null && !(numComponents > baseDN.size() && dn.startsWith(baseDN))) + throw new GuacamoleClientException("Subject DN \"" + dn + "\" is " + + "not within the configured base DN."); + + // Retrieve the least significant attribute from the parsed DN - this + // will be the username + Rdn nameRdn = dn.getRdn(numComponents - 1); + + // Verify that the username is specified with one of the allowed + // attributes + List usernameAttributes = confService.getSubjectUsernameAttributes(); + if (usernameAttributes != null && !usernameAttributes.stream().anyMatch(nameRdn.getType()::equalsIgnoreCase)) + throw new GuacamoleClientException("Subject DN \"" + dn + "\" " + + "does not contain an acceptable username attribute."); + + // The DN is valid - extract the username from the least significant + // component + String username = nameRdn.getValue().toString(); + logger.debug("Username \"{}\" extracted from subject DN \"{}\".", username, dn); + return username; + + } + /** * Authenticates a user using HTTP headers containing that user's verified * X.509 certificate. It is assumed that this certificate is being passed @@ -154,15 +236,13 @@ public class SSLClientAuthenticationResource extends SSOResource { * 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. + * The username 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. + * If any configuration parameters related to retrieving certificates + * from HTTP request cannot be parsed, or if the certificate is not + * valid/present. */ public String getUsername(byte[] certificate) throws GuacamoleException { @@ -179,36 +259,15 @@ public class SSLClientAuthenticationResource extends SSOResource { } catch (CertificateException e) { - throw new GuacamoleClientException("The X.509 certificate " - + "presented is not valid.", e); + throw new GuacamoleClientException("Certificate is not valid: " + e.getMessage(), e); } catch (IOException e) { - throw new GuacamoleServerException("Provided X.509 certificate " - + "could not be read.", e); + throw new GuacamoleClientException("Certificate could not be read: " + e.getMessage(), 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(); + Principal principal = cert.getSubjectX500Principal(); + return getUsername(principal.getName()); } @@ -216,7 +275,7 @@ public class SSLClientAuthenticationResource extends SSOResource { * 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. + * present, an invalid session token is returned. * * @param credentials * The credentials submitted in the HTTP request being processed. @@ -226,14 +285,10 @@ public class SSLClientAuthenticationResource extends SSOResource { * * @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. + * certificate in the given HTTP request, or an invalid session token + * if no valid identity was asserted. */ - private String processCertificate(HttpHeaders headers) throws GuacamoleException { + private String processCertificate(HttpHeaders headers) { // // NOTE: A result with an associated state is ALWAYS returned by @@ -245,18 +300,46 @@ public class SSLClientAuthenticationResource extends SSOResource { // 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(); + try { - String certificate = getHeader(headers, confService.getClientCertificateHeader()); - if (certificate == null) - return sessionManager.generateInvalid(); + // Verify that SSL termination has already verified the certificate + String verified = getHeader(headers, confService.getClientVerifiedHeader()); + if (verified != null && verified.startsWith(CLIENT_VERIFIED_HEADER_FAILED_PREFIX)) { + String message = verified.substring(CLIENT_VERIFIED_HEADER_FAILED_PREFIX.length()); + throw new GuacamoleClientException("Client certificate did " + + "not pass validation. SSL termination reports the " + + "following failure: \"" + message + "\""); + } + else if (CLIENT_VERIFIED_HEADER_NONE_VALUE.equals(verified)) { + throw new GuacamoleClientException("No client certificate was presented."); + } + else if (!CLIENT_VERIFIED_HEADER_SUCCESS_VALUE.equals(verified)) { + throw new GuacamoleClientException("Client certificate did not pass validation."); + } - String username = getUsername(decode(certificate)); - long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxTokenValidity()); - return sessionManager.defer(new SSLAuthenticationSession(username, validityDuration)); + String certificate = getHeader(headers, confService.getClientCertificateHeader()); + if (certificate == null) + throw new GuacamoleClientException("Client certificate missing from request."); + + String username = getUsername(decode(certificate)); + long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxTokenValidity()); + return sessionManager.defer(new SSLAuthenticationSession(username, validityDuration)); + + } + catch (GuacamoleClientException e) { + logger.warn("SSL/TLS client authentication attempt rejected: {}", e.getMessage()); + logger.debug("SSL/TLS client authentication failed.", e); + } + catch (GuacamoleException e) { + logger.error("SSL/TLS client authentication attempt could not be processed: {}", e.getMessage()); + logger.debug("SSL/TLS client authentication failed.", e); + } + catch (RuntimeException | Error e) { + logger.error("SSL/TLS client authentication attempt failed internally: {}", e.getMessage()); + logger.debug("Internal failure processing SSL/TLS client authentication attempt.", e); + } + + return sessionManager.generateInvalid(); } diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java index 3f57bd864..48fc86b65 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java @@ -22,12 +22,15 @@ package org.apache.guacamole.auth.ssl.conf; import com.google.inject.Inject; import java.net.URI; import java.net.URISyntaxException; +import java.util.List; +import javax.naming.ldap.LdapName; 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; +import org.apache.guacamole.properties.StringListProperty; import org.apache.guacamole.properties.URIGuacamoleProperty; /** @@ -136,6 +139,34 @@ public class ConfigurationService { }; + /** + * The property defining the LDAP attribute or attributes that may be used + * to represent a username within the subject DN of a user's X.509 + * certificate. If the least-significant attribute of the subject DN is not + * one of these attributes, the certificate will be rejected. By default, + * any attribute is accepted. + */ + private static final StringListProperty SSL_SUBJECT_USERNAME_ATTRIBUTE = + new StringListProperty () { + + @Override + public String getName() { return "ssl-subject-username-attribute"; } + + }; + + /** + * The property defining the base DN containing all valid subject DNs. If + * specified, only certificates asserting subject DNs beneath this base DN + * will be accepted. By default, all DNs are accepted. + */ + private static final LdapNameGuacamoleProperty SSL_SUBJECT_BASE_DN = + new LdapNameGuacamoleProperty () { + + @Override + public String getName() { return "ssl-subject-base-dn"; } + + }; + /** * The property representing the amount of time that the temporary, unique * subdomain generated for SSL/TLS authentication may remain valid, in @@ -374,4 +405,36 @@ public class ConfigurationService { return environment.getProperty(SSL_MAX_DOMAIN_VALIDITY, DEFAULT_MAX_DOMAIN_VALIDITY); } + /** + * Returns the base DN that contains all valid subject DNs. If there is no + * such base DN (and all subject DNs are valid), null is returned. + * + * @return + * The base DN that contains all valid subject DNs, or null if all + * subject DNs are valid. + * + * @throws GuacamoleException + * If the configured base DN cannot be read or is not a valid LDAP DN. + */ + public LdapName getSubjectBaseDN() throws GuacamoleException { + return environment.getProperty(SSL_SUBJECT_BASE_DN); + } + + /** + * Returns a list of all attributes that may be used to represent a user's + * username within their subject DN. If all attributes may be accepted, + * null is returned. + * + * @return + * A list of all attributes that may be used to represent a user's + * username within their subject DN, or null if any attribute may be + * used. + * + * @throws GuacamoleException + * If the configured set of username attributes cannot be read. + */ + public List getSubjectUsernameAttributes() throws GuacamoleException { + return environment.getProperty(SSL_SUBJECT_USERNAME_ATTRIBUTE); + } + } diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/LdapNameGuacamoleProperty.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/LdapNameGuacamoleProperty.java new file mode 100644 index 000000000..0299d0163 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/LdapNameGuacamoleProperty.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.auth.ssl.conf; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; +import org.apache.guacamole.properties.GuacamoleProperty; + +/** + * A GuacamoleProperty whose value is an LDAP name, such as a distinguished + * name. + */ +public abstract class LdapNameGuacamoleProperty implements GuacamoleProperty { + + @Override + public LdapName parseValue(String value) throws GuacamoleException { + + if (value == null) + return null; + + try { + return new LdapName(value); + } + catch (InvalidNameException e) { + throw new GuacamoleServerException("Value \"" + value + + "\" is not a valid LDAP name.", e); + } + + } + +} From b9958fa3318423f4b98cec66f734bd10f31ac2c3 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 5 Mar 2023 21:36:54 -0800 Subject: [PATCH 13/16] GUACAMOLE-839: Include SSL/TLS auth support within SSO dist archive. --- .../modules/guacamole-auth-sso-dist/pom.xml | 7 +++++++ .../guacamole-auth-sso-dist/src/main/assembly/dist.xml | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-dist/pom.xml b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-dist/pom.xml index f167b8c9b..7bbbd1f70 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-dist/pom.xml +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-dist/pom.xml @@ -59,6 +59,13 @@ 1.5.0 + + + org.apache.guacamole + guacamole-auth-sso-ssl + 1.5.0 + +
    diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-dist/src/main/assembly/dist.xml b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-dist/src/main/assembly/dist.xml index f122c8d0a..066da2956 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-dist/src/main/assembly/dist.xml +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-dist/src/main/assembly/dist.xml @@ -60,6 +60,15 @@ + + + ssl + false + + org.apache.guacamole:guacamole-auth-sso-ssl + + + From d0574f8d82c47edc2cfe21225839deeceb52b8a3 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 7 Mar 2023 16:34:38 -0800 Subject: [PATCH 14/16] GUACAMOLE-839: Use BouncyCastle for retrieval of certificate details. Java's build-in support for reading X.509 certificates does not deal well with PIV certificates containing the username as a "serialNumber" attribute. Rather than exposing the string value of that attribute, the Java implementation exposes a byte array that does not fully match the string value shown by a tool like OpenSSL. BouncyCastle, on the other hand, _does_ match the output of OpenSSL, and provides a predictable means of decoding the certificate. --- .../bouncycastle-pkix-fips-1.0.7/LICENSE | 20 +++++++++++ .../bouncycastle-pkix-fips-1.0.7/README | 8 +++++ .../dep-coordinates.txt | 1 + .../modules/guacamole-auth-sso-ssl/pom.xml | 7 ++++ .../ssl/SSLClientAuthenticationResource.java | 36 +++++++++---------- 5 files changed, 54 insertions(+), 18 deletions(-) create mode 100644 doc/licenses/bouncycastle-pkix-fips-1.0.7/LICENSE create mode 100644 doc/licenses/bouncycastle-pkix-fips-1.0.7/README create mode 100644 doc/licenses/bouncycastle-pkix-fips-1.0.7/dep-coordinates.txt diff --git a/doc/licenses/bouncycastle-pkix-fips-1.0.7/LICENSE b/doc/licenses/bouncycastle-pkix-fips-1.0.7/LICENSE new file mode 100644 index 000000000..a02bc176b --- /dev/null +++ b/doc/licenses/bouncycastle-pkix-fips-1.0.7/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2000 - 2021 The Legion of the Bouncy Castle Inc. +(https://www.bouncycastle.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/doc/licenses/bouncycastle-pkix-fips-1.0.7/README b/doc/licenses/bouncycastle-pkix-fips-1.0.7/README new file mode 100644 index 000000000..b362257b8 --- /dev/null +++ b/doc/licenses/bouncycastle-pkix-fips-1.0.7/README @@ -0,0 +1,8 @@ +BouncyCastle PKIX APIs, FIPS Distribution (https://www.bouncycastle.org/fips-java) +----------------------------------------------------------------------- + + Version: 1.0.7 + From: 'The Legion of Bouncy Castle' (https://www.bouncycastle.org) + License(s): + MIT (bundled/bouncycastle-pkix-fips-1.0.7/LICENSE) + diff --git a/doc/licenses/bouncycastle-pkix-fips-1.0.7/dep-coordinates.txt b/doc/licenses/bouncycastle-pkix-fips-1.0.7/dep-coordinates.txt new file mode 100644 index 000000000..23ca14e38 --- /dev/null +++ b/doc/licenses/bouncycastle-pkix-fips-1.0.7/dep-coordinates.txt @@ -0,0 +1 @@ +org.bouncycastle:bcpkix-fips:jar:1.0.7 diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml index c6542ae56..7ece6b40e 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml @@ -119,6 +119,13 @@ jsr311-api + + + org.bouncycastle + bcpkix-fips + 1.0.7 + + diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java index 21a2e0c25..75a190d35 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java @@ -19,17 +19,14 @@ 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.Reader; +import java.io.StringReader; 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.Date; import java.util.List; import java.util.concurrent.TimeUnit; import javax.naming.InvalidNameException; @@ -46,9 +43,14 @@ 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; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.RFC4519Style; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.openssl.PEMParser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -247,27 +249,25 @@ public class SSLClientAuthenticationResource extends SSOResource { 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)) { + X509CertificateHolder cert; + try (Reader reader = new StringReader(new String(certificate, StandardCharsets.UTF_8))) { - CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); - cert = (X509Certificate) certFactory.generateCertificate(input); + PEMParser parser = new PEMParser(reader); + cert = (X509CertificateHolder) parser.readObject(); // Verify certificate is valid (it should be given pre-validation // from SSL termination, but it's worth rechecking for sanity) - cert.checkValidity(); + if (!cert.isValidOn(new Date())) + throw new GuacamoleClientException("Certificate has expired."); } - catch (CertificateException e) { - throw new GuacamoleClientException("Certificate is not valid: " + e.getMessage(), e); - } catch (IOException e) { - throw new GuacamoleClientException("Certificate could not be read: " + e.getMessage(), e); + throw new GuacamoleServerException("Certificate could not be read: " + e.getMessage(), e); } - // Extract user's DN from their X.509 certificate - Principal principal = cert.getSubjectX500Principal(); - return getUsername(principal.getName()); + // Extract user's DN from their X.509 certificate in LDAP (RFC 4919) format + X500Name subject = X500Name.getInstance(RFC4519Style.INSTANCE, cert.getSubject()); + return getUsername(subject.toString()); } From 9f8bb71b0ef7cb41b7042456f2571c7882bbb03e Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 7 Mar 2023 16:37:00 -0800 Subject: [PATCH 15/16] GUACAMOLE-839: Correct JavaDoc for parameters of getUsername(). --- .../auth/ssl/SSLClientAuthenticationResource.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java index 75a190d35..583f2caa9 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java @@ -272,16 +272,13 @@ public class SSLClientAuthenticationResource extends SSOResource { } /** - * Processes the X.509 certificate in the headers of the given HTTP - * request, returning an authentication session token representing the + * Processes the X.509 certificate in the given set of HTTP request + * headers, returning an authentication session token representing the * identity in that certificate. If the certificate is invalid or not * present, an invalid session token is returned. * - * @param credentials - * The credentials submitted in the HTTP request being processed. - * - * @param request - * The HTTP request to process. + * @param headers + * The headers of the HTTP request to process. * * @return * An authentication session token representing the identity in the From 82073a5976715933a5d1085b8a6a9b057936efd9 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 7 Mar 2023 16:38:51 -0800 Subject: [PATCH 16/16] GUACAMOLE-839: Correct typo in JavaDoc of decode() - "valid", not "value". --- .../guacamole/auth/ssl/SSLClientAuthenticationResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java index 583f2caa9..787bf3e52 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java @@ -149,7 +149,7 @@ public class SSLClientAuthenticationResource extends SSOResource { * The decoded string. * * @throws GuacamoleException - * If the provided value is not a value URL-encoded string. + * If the provided value is not a valid URL-encoded string. */ private byte[] decode(String value) throws GuacamoleException { try {