mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 05:07:41 +00:00
GUACAMOLE-839: Merge add webapp SSO support for certificates / smart cards.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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() {
|
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
|
@@ -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:"
|
||||
}
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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 -->
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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.
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -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.");
|
||||
|
3
extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/.gitignore
vendored
Normal file
3
extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*~
|
||||
target/
|
||||
src/main/resources/generated/
|
@@ -0,0 +1 @@
|
||||
src/main/resources/html/*.html
|
@@ -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>
|
@@ -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();
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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";
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -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.");
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -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.");
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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"
|
||||
]
|
||||
|
||||
}
|
@@ -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>
|
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
@@ -0,0 +1,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;
|
||||
|
||||
}]);
|
@@ -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');
|
@@ -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>
|
||||
|
||||
|
Reference in New Issue
Block a user