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-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..3261228b8 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,11 +17,10 @@ * 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; -import com.google.inject.Singleton; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executors; @@ -29,14 +28,16 @@ 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 +49,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 +59,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() { @@ -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 @@ -82,10 +96,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 +108,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 +117,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 +140,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-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 new file mode 100644 index 000000000..82538c6a9 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/IdentifierGenerator.java @@ -0,0 +1,106 @@ +/* + * 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; + +import com.google.common.io.BaseEncoding; +import com.google.inject.Singleton; +import java.math.BigInteger; +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. + */ +@Singleton +public class IdentifierGenerator { + + /** + * Cryptographically-secure random number generator for generating unique + * identifiers. + */ + private final SecureRandom secureRandom = new SecureRandom(); + + /** + * Generates a unique and unpredictable identifier. Each identifier is at + * least 256-bit and produced using a cryptographically-secure random + * number generator. The identifier may contain characters that differ only + * in case. + * + * @return + * A unique and unpredictable identifier with at least 256 bits of + * entropy. + */ + public String generateIdentifier() { + 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. The identifier may contain characters + * that differ only in case. + * + * @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) { + 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-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 80% 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..5717794fd 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,39 +17,43 @@ * 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; -import java.security.SecureRandom; +import com.google.inject.Inject; 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 { /** - * 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. @@ -94,7 +98,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) { @@ -102,7 +107,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, false); nonces.put(nonce, System.currentTimeMillis() + maxAge); return nonce; @@ -115,15 +120,20 @@ public class NonceService { * invalidates that nonce. * * @param nonce - * The nonce value to test. + * 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); + Long expires = nonces.remove(nonce.toLowerCase(Locale.US)); if (expires == null) return false; 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-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 + + + 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..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,8 +20,9 @@ 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.openid.token.NonceService; +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); } 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; 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/IdentifierGenerator.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/IdentifierGenerator.java deleted file mode 100644 index a2a3aae6a..000000000 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/IdentifierGenerator.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.common.io.BaseEncoding; -import com.google.inject.Singleton; -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. - */ -@Singleton -public class IdentifierGenerator { - - /** - * Cryptographically-secure random number generator for generating unique - * identifiers. - */ - private final SecureRandom secureRandom = new SecureRandom(); - - /** - * Generates a unique and unpredictable identifier. Each identifier is at - * least 256-bit and produced using a cryptographically-secure random - * number generator. - * - * @return - * A unique and unpredictable identifier. - */ - public String generateIdentifier() { - byte[] bytes = new byte[33]; - secureRandom.nextBytes(bytes); - return BaseEncoding.base64().encode(bytes); - } - -} 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."); 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..7ece6b40e --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml @@ -0,0 +1,131 @@ + + + + + 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 + ../../ + + + + + + + + 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 + + + + + + + + + + + + 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 + + + + + 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/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..bc311de47 --- /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,171 @@ +/* + * 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.net.URI; +import java.util.Collections; +import javax.servlet.http.HttpServletRequest; +import org.apache.guacamole.GuacamoleClientException; +import org.apache.guacamole.auth.ssl.conf.ConfigurationService; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleResourceNotFoundException; +import org.apache.guacamole.auth.sso.SSOAuthenticationProviderService; +import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser; +import org.apache.guacamole.net.auth.Credentials; + +/** + * 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; + + /** + * 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 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"; + + /** + * 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(Credentials credentials, HttpServletRequest request) { + + String state = request.getParameter(AUTH_SESSION_PARAMETER_NAME); + String username = sessionManager.getIdentity(state); + if (username == null) + return null; + + SSOAuthenticatedUser authenticatedUser = authenticatedUserProvider.get(); + authenticatedUser.init(username, credentials, + Collections.emptySet(), Collections.emptyMap()); + return authenticatedUser; + + } + + @Override + public SSOAuthenticatedUser authenticateUser(Credentials credentials) + throws GuacamoleException { + + // + // Overall flow: + // + // 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) 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 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 + // 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 primary URI, using the + // pre-verified information from those tokens to determine user + // identity. + // + + if (confService.isPrimaryHostname(host)) + return processIdentity(credentials, request); + + // 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 { + throw new GuacamoleResourceNotFoundException("No such resource."); + } + + @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/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 new file mode 100644 index 000000000..2458c1a44 --- /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,48 @@ +/* + * 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; + +/** + * 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, SSLClientAuthenticationResource.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..46eeaa94f --- /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,40 @@ +/* + * 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 com.google.inject.Scopes; +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).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 new file mode 100644 index 000000000..4a4c9ce8f --- /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,64 @@ +/* + * 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; + +/** + * 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 String 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(String 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 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 new file mode 100644 index 000000000..fc1b0842f --- /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,60 @@ +/* + * 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; + +/** + * 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 String 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/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..787bf3e52 --- /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,412 @@ +/* + * 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.IOException; +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.util.Date; +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; +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; +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; + +/** + * 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"; + + /** + * 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. + */ + @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 valid 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); + } + } + + /** + * 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 + * 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 + * The username of the user asserted by the SSL termination service via + * that user's X.509 certificate. + * + * @throws GuacamoleException + * 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 { + + // Parse and re-verify certificate is valid with respect to timestamps + X509CertificateHolder cert; + try (Reader reader = new StringReader(new String(certificate, StandardCharsets.UTF_8))) { + + 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) + if (!cert.isValidOn(new Date())) + throw new GuacamoleClientException("Certificate has expired."); + + } + catch (IOException e) { + throw new GuacamoleServerException("Certificate could not be read: " + e.getMessage(), e); + } + + // 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()); + + } + + /** + * 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 headers + * The headers of the HTTP request to process. + * + * @return + * An authentication session token representing the identity in the + * certificate in the given HTTP request, or an invalid session token + * if no valid identity was asserted. + */ + private String processCertificate(HttpHeaders headers) { + + // + // 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. + // + + try { + + // 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 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(); + + } + + /** + * 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 new file mode 100644 index 000000000..48fc86b65 --- /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,440 @@ +/* + * 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 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; + +/** + * 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_PRIMARY_URI = + new URIGuacamoleProperty() { + + @Override + public String getName() { return "ssl-primary-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 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() { + + @Override + public String getName() { return "ssl-max-token-validity"; } + + }; + + /** + * 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 + * 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 + * 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() { + + @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 { + + // 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(); + + // 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 primary URI is missing + * or cannot be parsed. + */ + 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()); + } + + /** + * 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); + } + + /** + * 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); + } + + } + +} 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/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..49347127a --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js @@ -0,0 +1,51 @@ +/* + * 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 clientAuthService = $injector.get('clientAuthService'); + + 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() { + clientAuthService.authenticate(); + }); + + }; + + 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 new file mode 100644 index 000000000..7b8099427 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/guac-manifest.json @@ -0,0 +1,35 @@ +{ + + "guacamoleVersion" : "1.5.0", + + "name" : "SSL Authentication Extension", + "namespace" : "ssl", + + "authProviders" : [ + "org.apache.guacamole.auth.ssl.SSLAuthenticationProvider" + ], + + "css" : [ + "styles/sso-providers.css" + ], + + "js" : [ "ssl.min.js" ], + + "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..8798eed2d --- /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/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/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; + +}]); 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'); 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 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..88143a740 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,47 @@ */ /** - * 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 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 + * 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) { @@ -46,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'); @@ -141,7 +164,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. @@ -151,57 +175,75 @@ 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) { - // Attempt authentication - return requestService({ - method: 'POST', - url: 'api/tokens', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - data: $.param(parameters) - }) + // Coerce received parameters object into a Promise, if it isn't + // already a Promise + parameters = $q.resolve(parameters); - // If authentication succeeds, handle received auth data - .then(function authenticationSuccessful(data) { + // Notify that a fresh authentication request is underway + $rootScope.$broadcast('guacLoginPending', parameters); - var currentToken = service.getCurrentToken(); + // Attempt authentication after auth parameters are available ... + return parameters.then(function requestParametersReady(requestParams) { - // 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) { + return requestService({ + method: 'POST', + url: 'api/tokens', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data: $.param(requestParams) + }) + + // ... if authentication succeeds, handle received auth data ... + .then(function authenticationSuccessful(data) { + + 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 + // 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 +363,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 +385,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 = {};