GUACAMOLE-839: Merge add webapp SSO support for certificates / smart cards.

This commit is contained in:
Virtually Nick
2023-03-07 20:59:57 -05:00
committed by GitHub
42 changed files with 2241 additions and 242 deletions

View File

@@ -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.

View File

@@ -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)

View File

@@ -0,0 +1 @@
org.bouncycastle:bcpkix-fips:jar:1.0.7

View File

@@ -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;
}
}

View File

@@ -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 <T>
* The type of sessions managed by this session manager.
*/
@Singleton
public class AuthenticationSessionManager {
public class AuthenticationSessionManager<T extends AuthenticationSession> {
/**
* 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<String, AuthenticationSession> sessions =
new ConcurrentHashMap<>();
private final ConcurrentMap<String, T> 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() {

View File

@@ -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);
}
}

View File

@@ -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<String, Long> nonces = new ConcurrentHashMap<String, Long>();
private final Map<String, Long> 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;

View File

@@ -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:"
}

View File

@@ -59,6 +59,13 @@
<version>1.5.0</version>
</dependency>
<!-- SSL Authentication Extension -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso-ssl</artifactId>
<version>1.5.0</version>
</dependency>
</dependencies>
<build>

View File

@@ -60,6 +60,15 @@
</includes>
</dependencySet>
<!-- SSL extension .jar -->
<dependencySet>
<outputDirectory>ssl</outputDirectory>
<useProjectArtifact>false</useProjectArtifact>
<includes>
<include>org.apache.guacamole:guacamole-auth-sso-ssl</include>
</includes>
</dependencySet>
</dependencySets>
<!-- Include extension licenses -->

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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.

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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}
*
* <p>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());
}
/**

View File

@@ -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<SAMLAuthenticationSession> {
/**
* 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;
}
}

View File

@@ -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.");

View File

@@ -0,0 +1,3 @@
*~
target/
src/main/resources/generated/

View File

@@ -0,0 +1 @@
src/main/resources/html/*.html

View File

@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso-ssl</artifactId>
<packaging>jar</packaging>
<version>1.5.0</version>
<name>guacamole-auth-sso-ssl</name>
<url>http://guacamole.apache.org/</url>
<parent>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso</artifactId>
<version>1.5.0</version>
<relativePath>../../</relativePath>
</parent>
<build>
<plugins>
<!-- JS/CSS Minification Plugin -->
<plugin>
<groupId>com.github.buckelieg</groupId>
<artifactId>minify-maven-plugin</artifactId>
<executions>
<execution>
<id>default-cli</id>
<configuration>
<charset>UTF-8</charset>
<webappSourceDir>${basedir}/src/main/resources</webappSourceDir>
<webappTargetDir>${project.build.directory}/classes</webappTargetDir>
<jsSourceDir>/</jsSourceDir>
<jsTargetDir>/</jsTargetDir>
<jsFinalFile>ssl.js</jsFinalFile>
<jsSourceFiles>
<jsSourceFile>license.txt</jsSourceFile>
</jsSourceFiles>
<jsSourceIncludes>
<jsSourceInclude>**/*.js</jsSourceInclude>
</jsSourceIncludes>
<!-- Do not minify and include tests -->
<jsSourceExcludes>
<jsSourceExclude>**/*.test.js</jsSourceExclude>
</jsSourceExcludes>
<jsEngine>CLOSURE</jsEngine>
<!-- Disable warnings for JSDoc annotations -->
<closureWarningLevels>
<misplacedTypeAnnotation>OFF</misplacedTypeAnnotation>
<nonStandardJsDocs>OFF</nonStandardJsDocs>
</closureWarningLevels>
</configuration>
<goals>
<goal>minify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<!-- Guacamole Extension API -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-ext</artifactId>
</dependency>
<!-- Core SSO support -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-sso-base</artifactId>
</dependency>
<!-- Guice -->
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
</dependency>
<!-- Java servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</dependency>
<!-- JAX-RS Annotations -->
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
</dependency>
<!-- Use FIPS variant of Bouncy Castle crypto library -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-fips</artifactId>
<version>1.0.7</version>
</dependency>
</dependencies>
</project>

View File

@@ -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<SSOAuthenticatedUser> 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();
}
}

View File

@@ -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;
}
}

View File

@@ -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";
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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<SSLAuthenticationSession> {
/**
* 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;
}
}

View File

@@ -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<String> values = headers.getRequestHeader(name);
if (values.isEmpty())
return null;
return values.get(0);
}
/**
* Decodes the provided URL-encoded string as UTF-8, returning the result.
*
* @param value
* The URL-encoded string to decode.
*
* @return
* The decoded string.
*
* @throws GuacamoleException
* If the provided value is not a 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<String> 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.");
}
}

View File

@@ -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<String> getSubjectUsernameAttributes() throws GuacamoleException {
return environment.getProperty(SSL_SUBJECT_USERNAME_ATTRIBUTE);
}
}

View File

@@ -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<LdapName> {
@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);
}
}
}

View File

@@ -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.");
}
}

View File

@@ -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;
}]);

View File

@@ -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"
]
}

View File

@@ -0,0 +1,4 @@
<meta name="after-children" content=".login-ui .sso-provider-list:last-child">
<li class="sso-provider sso-provider-ssl"><a guac-ssl-auth href="">{{
'LOGIN.NAME_IDP_SSL' | translate
}}</a></li>

View File

@@ -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.
*/

View File

@@ -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;
}]);

View File

@@ -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');

View File

@@ -49,6 +49,7 @@
<module>modules/guacamole-auth-sso-cas</module>
<module>modules/guacamole-auth-sso-openid</module>
<module>modules/guacamole-auth-sso-saml</module>
<module>modules/guacamole-auth-sso-ssl</module>
</modules>

View File

@@ -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

View File

@@ -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 = {};