Add .gitignore and .ratignore files for various directories
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
2
extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/.gitignore
vendored
Normal file
2
extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
target/
|
||||
*~
|
@@ -0,0 +1 @@
|
||||
src/main/resources/html/*.html
|
@@ -0,0 +1,61 @@
|
||||
<?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-base</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>guacamole-auth-sso-base</name>
|
||||
<url>http://guacamole.apache.org/</url>
|
||||
|
||||
<parent>
|
||||
<groupId>org.apache.guacamole</groupId>
|
||||
<artifactId>guacamole-auth-sso</artifactId>
|
||||
<version>1.6.0</version>
|
||||
<relativePath>../../</relativePath>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- Guacamole Extension API -->
|
||||
<dependency>
|
||||
<groupId>org.apache.guacamole</groupId>
|
||||
<artifactId>guacamole-ext</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Guice -->
|
||||
<dependency>
|
||||
<groupId>com.google.inject</groupId>
|
||||
<artifactId>guice</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JAX-RS Annotations -->
|
||||
<dependency>
|
||||
<groupId>javax.ws.rs</groupId>
|
||||
<artifactId>jsr311-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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 java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.apache.guacamole.net.auth.IdentifierGenerator;
|
||||
|
||||
/**
|
||||
* Service for generating and validating single-use random tokens (nonces).
|
||||
* Each generated nonce is at least 128 bits and case-insensitive.
|
||||
*/
|
||||
public class NonceService {
|
||||
|
||||
/**
|
||||
* 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<>();
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private static final long SWEEP_INTERVAL = 60000;
|
||||
|
||||
/**
|
||||
* Iterates through the entire Map of generated nonces, removing any nonce
|
||||
* that has exceeded its expiration timestamp. If insufficient time has
|
||||
* elapsed since the last sweep, as dictated by SWEEP_INTERVAL, this
|
||||
* function has no effect.
|
||||
*/
|
||||
private void sweepExpiredNonces() {
|
||||
|
||||
// Do not sweep until enough time has elapsed since the last sweep
|
||||
long currentTime = System.currentTimeMillis();
|
||||
if (currentTime - lastSweep < SWEEP_INTERVAL)
|
||||
return;
|
||||
|
||||
// Record time of sweep
|
||||
lastSweep = currentTime;
|
||||
|
||||
// For each stored nonce
|
||||
Iterator<Map.Entry<String, Long>> entries = nonces.entrySet().iterator();
|
||||
while (entries.hasNext()) {
|
||||
|
||||
// Remove all entries which have expired
|
||||
Map.Entry<String, Long> current = entries.next();
|
||||
if (current.getValue() <= System.currentTimeMillis())
|
||||
entries.remove();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a cryptographically-secure nonce value. The nonce is intended
|
||||
* to be used to prevent replay attacks.
|
||||
*
|
||||
* @param maxAge
|
||||
* The maximum amount of time that the generated nonce should remain
|
||||
* valid, in milliseconds.
|
||||
*
|
||||
* @return
|
||||
* A cryptographically-secure nonce value. Generated nonces are at
|
||||
* least 128-bit and are case-insensitive.
|
||||
*/
|
||||
public String generate(long maxAge) {
|
||||
|
||||
// Sweep expired nonces if enough time has passed
|
||||
sweepExpiredNonces();
|
||||
|
||||
// Generate and store nonce, along with expiration timestamp
|
||||
String nonce = IdentifierGenerator.generateIdentifier(NONCE_BITS, false);
|
||||
nonces.put(nonce, System.currentTimeMillis() + maxAge);
|
||||
return nonce;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the give nonce value is valid. A nonce is valid if and
|
||||
* only if it was generated by this instance of the NonceService. Testing
|
||||
* nonce validity through this function immediately and permanently
|
||||
* invalidates that nonce.
|
||||
*
|
||||
* @param nonce
|
||||
* The nonce value to test. This value may be null, which will be
|
||||
* considered an invalid nonce. Comparisons are case-insensitive.
|
||||
*
|
||||
* @return
|
||||
* true if the provided nonce is valid, false otherwise.
|
||||
*/
|
||||
public boolean isValid(String nonce) {
|
||||
|
||||
// All null nonces are invalid.
|
||||
if (nonce == null)
|
||||
return false;
|
||||
|
||||
// Remove nonce, verifying whether it was present at all
|
||||
Long expires = nonces.remove(nonce.toLowerCase(Locale.US));
|
||||
if (expires == null)
|
||||
return false;
|
||||
|
||||
// Nonce is only valid if it hasn't expired
|
||||
return expires > System.currentTimeMillis();
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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 org.apache.guacamole.GuacamoleClientException;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.GuacamoleSecurityException;
|
||||
import org.apache.guacamole.net.auth.Credentials;
|
||||
import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
|
||||
import org.apache.guacamole.net.event.AuthenticationFailureEvent;
|
||||
import org.apache.guacamole.net.event.AuthenticationRequestReceivedEvent;
|
||||
import org.apache.guacamole.net.event.CredentialEvent;
|
||||
import org.apache.guacamole.net.event.listener.Listener;
|
||||
|
||||
/**
|
||||
* A Listener that will reactivate or invalidate SSO auth sessions depending on
|
||||
* overall auth success or failure.
|
||||
*/
|
||||
public abstract class SSOAuthenticationEventListener implements Listener {
|
||||
|
||||
@Override
|
||||
public void handleEvent(Object event) throws GuacamoleException {
|
||||
|
||||
// If the authentication attempt is incomplete or credentials cannot be
|
||||
// extracted, there's nothing to do
|
||||
if (event instanceof AuthenticationRequestReceivedEvent
|
||||
|| !(event instanceof CredentialEvent))
|
||||
return;
|
||||
|
||||
// Look for a session identifier associated with these credentials
|
||||
String sessionIdentifier = getSessionIdentifier(
|
||||
((CredentialEvent) event).getCredentials());
|
||||
|
||||
// If no session is associated with these credentials, there's
|
||||
// nothing to do
|
||||
if (sessionIdentifier == null)
|
||||
return;
|
||||
|
||||
// If the SSO auth succeeded, but other auth providers failed to
|
||||
// authenticate the user associated with the credentials in this
|
||||
// failure event, they may wish to make another login attempt. To
|
||||
// avoid an infinite login attempt loop, re-enable the session
|
||||
// associated with these credentials, allowing the auth attempt to be
|
||||
// resumed without requiring another round trip to the SSO service.
|
||||
if (event instanceof AuthenticationFailureEvent) {
|
||||
Throwable failure = ((AuthenticationFailureEvent) event).getFailure();
|
||||
|
||||
// If and only if the failure was associated with missing or
|
||||
// credentials, or a non-security related request issue,
|
||||
// reactivate the session
|
||||
if (failure instanceof GuacamoleInsufficientCredentialsException
|
||||
|| ((failure instanceof GuacamoleClientException)
|
||||
&& !(failure instanceof GuacamoleSecurityException))) {
|
||||
|
||||
reactivateSession(sessionIdentifier);
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Invalidate the session in all other cases
|
||||
invalidateSession(sessionIdentifier);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the session identifier associated with the provided credentials,
|
||||
* if any. If no session is associated with the credentials, null will
|
||||
* be returned.
|
||||
*
|
||||
* @param credentials
|
||||
* The credentials assoociated with the deferred SSO authentication
|
||||
* session to reactivate.
|
||||
*
|
||||
* @return
|
||||
* The session identifier associated with the provided credentials,
|
||||
* or null if no session is found.
|
||||
*/
|
||||
protected abstract String getSessionIdentifier(Credentials credentials);
|
||||
|
||||
/**
|
||||
* Reactivate the session identified by the provided identifier, if any.
|
||||
*
|
||||
* @param sessionIdentifier
|
||||
* The identifier of the session to reactivate.
|
||||
*/
|
||||
protected abstract void reactivateSession(String sessionIdentifier);
|
||||
|
||||
/**
|
||||
* Invalidate the session identified by the provided identifier, if any.
|
||||
*
|
||||
* @param sessionIdentifier
|
||||
* The identifier of the session to invalidate.
|
||||
*/
|
||||
protected abstract void invalidateSession(String sessionIdentifier);
|
||||
|
||||
}
|
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* 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.collect.Iterables;
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Guice;
|
||||
import com.google.inject.Injector;
|
||||
import com.google.inject.Module;
|
||||
import com.google.inject.binder.LinkedBindingBuilder;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser;
|
||||
import org.apache.guacamole.environment.Environment;
|
||||
import org.apache.guacamole.environment.LocalEnvironment;
|
||||
import org.apache.guacamole.net.auth.AbstractAuthenticationProvider;
|
||||
import org.apache.guacamole.net.auth.AuthenticatedUser;
|
||||
import org.apache.guacamole.net.auth.AuthenticationProvider;
|
||||
import org.apache.guacamole.net.auth.Credentials;
|
||||
import org.apache.guacamole.net.auth.TokenInjectingUserContext;
|
||||
import org.apache.guacamole.net.auth.UserContext;
|
||||
|
||||
/**
|
||||
* An AuthenticationProvider which authenticates users against an arbitrary
|
||||
* SSO system. Guice dependency injection is automatically configured via
|
||||
* modules provided by the implementation. Implementations will typically
|
||||
* provide no storage for connections, instead relying on other installed
|
||||
* extensions.
|
||||
*/
|
||||
public abstract class SSOAuthenticationProvider extends AbstractAuthenticationProvider {
|
||||
|
||||
/**
|
||||
* The Guice injector.
|
||||
*/
|
||||
private final Injector injector;
|
||||
|
||||
/**
|
||||
* Creates a new SSOAuthenticationProvider that authenticates users against
|
||||
* an arbitrary SSO system. Guice dependency injection is automatically
|
||||
* configured, with the resulting injector available to implementations via
|
||||
* {@link #getInjector()}. Core authentication functions are provided by
|
||||
* the given SSOAuthenticationProviderService implementation, and
|
||||
* additional implementation-specific services, providers, etc. may be
|
||||
* bound by specifying additional Guice modules.
|
||||
*
|
||||
* @param authService
|
||||
* The SSOAuthenticationProviderService implementation that should be
|
||||
* used for core authentication functions.
|
||||
*
|
||||
* @param ssoResource
|
||||
* The SSOResource that should be used to manually redirect the user to
|
||||
* the IdP, as well as to provide any implementation-specific REST
|
||||
* endpoints.
|
||||
*
|
||||
* @param modules
|
||||
* Any additional modules that should be used when creating the Guice
|
||||
* injector.
|
||||
*/
|
||||
public SSOAuthenticationProvider(
|
||||
Class<? extends SSOAuthenticationProviderService> authService,
|
||||
Class<? extends SSOResource> ssoResource,
|
||||
Module... modules) {
|
||||
this(authService, ssoResource, Arrays.asList(modules));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new SSOAuthenticationProvider that authenticates users against
|
||||
* an arbitrary SSO system. Guice dependency injection is automatically
|
||||
* configured, with the resulting injector available to implementations via
|
||||
* {@link #getInjector()}. Core authentication functions are provided by
|
||||
* the given SSOAuthenticationProviderService implementation, and
|
||||
* additional may be provided by specifying additional Guice modules.
|
||||
*
|
||||
* @param authService
|
||||
* The SSOAuthenticationProviderService implementation that should be
|
||||
* used for core authentication functions.
|
||||
*
|
||||
* @param ssoResource
|
||||
* The SSOResource that should be used to manually redirect the user to
|
||||
* the IdP, as well as to provide any implementation-specific REST
|
||||
* endpoints.
|
||||
*
|
||||
* @param modules
|
||||
* Any additional modules that should be used when creating the Guice
|
||||
* injector.
|
||||
*/
|
||||
public SSOAuthenticationProvider(
|
||||
Class<? extends SSOAuthenticationProviderService> authService,
|
||||
Class<? extends SSOResource> ssoResource,
|
||||
Iterable<? extends Module> modules) {
|
||||
injector = Guice.createInjector(Iterables.concat(Collections.singletonList(new AbstractModule() {
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
|
||||
bind(AuthenticationProvider.class).toInstance(SSOAuthenticationProvider.this);
|
||||
bind(SSOAuthenticationProviderService.class).to(authService);
|
||||
|
||||
// Bind custom SSOResource implementation if different from
|
||||
// core implementation (explicitly binding SSOResource as
|
||||
// SSOResource results in a runtime error from Guice otherwise)
|
||||
LinkedBindingBuilder<SSOResource> resourceBinding = bind(SSOResource.class);
|
||||
if (ssoResource != SSOResource.class)
|
||||
resourceBinding.to(ssoResource);
|
||||
|
||||
}
|
||||
|
||||
}), modules));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Guice injector available for use by this implementation of
|
||||
* SSOAuthenticationProvider. The returned injector has already been
|
||||
* configured with all modules supplied at the time this
|
||||
* SSOAuthenticationProvider was created.
|
||||
*
|
||||
* @return
|
||||
* The Guice injector available for use by this implementation of
|
||||
* SSOAuthenticationProvider.
|
||||
*/
|
||||
protected final Injector getInjector() {
|
||||
return injector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticatedUser authenticateUser(Credentials credentials)
|
||||
throws GuacamoleException {
|
||||
|
||||
// Attempt to authenticate user with given credentials
|
||||
SSOAuthenticationProviderService authProviderService =
|
||||
injector.getInstance(SSOAuthenticationProviderService.class);
|
||||
|
||||
return authProviderService.authenticateUser(credentials);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserContext decorate(UserContext context,
|
||||
AuthenticatedUser authenticatedUser, Credentials credentials)
|
||||
throws GuacamoleException {
|
||||
|
||||
// Only inject tokens for users authenticated by this extension
|
||||
if (authenticatedUser.getAuthenticationProvider() != this)
|
||||
return context;
|
||||
|
||||
return new TokenInjectingUserContext(context,
|
||||
((SSOAuthenticatedUser) authenticatedUser).getTokens());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public SSOResource getResource() {
|
||||
return getInjector().getInstance(SSOResource.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
injector.getInstance(SSOAuthenticationProviderService.class).shutdown();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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 java.net.URI;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser;
|
||||
import org.apache.guacamole.net.auth.Credentials;
|
||||
|
||||
/**
|
||||
* Service that authenticates Guacamole users by leveraging an arbitrary SSO
|
||||
* service.
|
||||
*/
|
||||
public interface SSOAuthenticationProviderService {
|
||||
|
||||
/**
|
||||
* Returns an SSOAuthenticatedUser representing the user authenticated by
|
||||
* the given credentials. Tokens associated with the returned
|
||||
* SSOAuthenticatedUser will automatically be injected into any connections
|
||||
* used by that user during their session.
|
||||
*
|
||||
* @param credentials
|
||||
* The credentials to use for authentication.
|
||||
*
|
||||
* @return
|
||||
* An SSOAuthenticatedUser representing the user authenticated by the
|
||||
* given credentials.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If an error occurs while authenticating the user, or if access is
|
||||
* denied.
|
||||
*/
|
||||
SSOAuthenticatedUser authenticateUser(Credentials credentials)
|
||||
throws GuacamoleException;
|
||||
|
||||
/**
|
||||
* Returns the full URI of the login endpoint to which a user must be
|
||||
* redirected in order to authenticate with the SSO identity provider.
|
||||
*
|
||||
* @return
|
||||
* The full URI of the SSO login endpoint.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If configuration information required for generating the login URI
|
||||
* cannot be read.
|
||||
*/
|
||||
URI getLoginURI() throws GuacamoleException;
|
||||
|
||||
/**
|
||||
* Frees all resources associated with the relevant
|
||||
* SSOAuthenticationProvider implementation. This function is automatically
|
||||
* invoked when an implementation of SSOAuthenticationProvider is shut
|
||||
* down.
|
||||
*/
|
||||
void shutdown();
|
||||
|
||||
}
|
@@ -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;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
|
||||
/**
|
||||
* REST API resource that provides allows the user to be manually redirected to
|
||||
* the applicable identity provider. Implementations may also provide
|
||||
* additional resources and endpoints beneath this resource as needed.
|
||||
*/
|
||||
public class SSOResource {
|
||||
|
||||
/**
|
||||
* Service for authenticating users using CAS.
|
||||
*/
|
||||
@Inject
|
||||
private SSOAuthenticationProviderService authService;
|
||||
|
||||
/**
|
||||
* Redirects the user to the relevant identity provider. If the SSO
|
||||
* extension defining this resource is not the primary extension, and thus
|
||||
* the user will not be automatically redirected to the IdP, this endpoint
|
||||
* allows that redirect to occur manually upon a link/button click.
|
||||
*
|
||||
* @return
|
||||
* An HTTP Response that will redirect the user to the IdP.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If an error occurs preventing the redirect from being created.
|
||||
*/
|
||||
@GET
|
||||
@Path("login")
|
||||
public Response redirectToIdentityProvider() throws GuacamoleException {
|
||||
return Response.seeOther(authService.getLoginURI()).build();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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.user;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.apache.guacamole.net.auth.AbstractAuthenticatedUser;
|
||||
import org.apache.guacamole.net.auth.AuthenticationProvider;
|
||||
import org.apache.guacamole.net.auth.Credentials;
|
||||
|
||||
/**
|
||||
* An AuthenticatedUser whose identity has been supplied by an arbitrary SSO
|
||||
* service. An SSOAuthenticatedUser may additionally be associated with a set
|
||||
* of user-specific parameter tokens to be injected into any connections used
|
||||
* by that user.
|
||||
*/
|
||||
public class SSOAuthenticatedUser extends AbstractAuthenticatedUser {
|
||||
|
||||
/**
|
||||
* Reference to the authentication provider associated with this
|
||||
* authenticated user.
|
||||
*/
|
||||
@Inject
|
||||
private AuthenticationProvider authProvider;
|
||||
|
||||
/**
|
||||
* The credentials provided when this user was authenticated.
|
||||
*/
|
||||
private Credentials credentials;
|
||||
|
||||
/**
|
||||
* The groups that this user belongs to.
|
||||
*/
|
||||
private Set<String> effectiveGroups;
|
||||
|
||||
/**
|
||||
* Parameter tokens to be automatically injected for any connections used
|
||||
* by this user.
|
||||
*/
|
||||
private Map<String, String> tokens;
|
||||
|
||||
/**
|
||||
* Initializes this SSOAuthenticatedUser, associating it with the given
|
||||
* username, credentials, groups, and parameter tokens. The contents of the
|
||||
* given credentials are automatically updated to match the provided
|
||||
* username. This function must be invoked for every SSOAuthenticatedUser
|
||||
* created.
|
||||
*
|
||||
* @param username
|
||||
* The username of the user that was authenticated.
|
||||
*
|
||||
* @param credentials
|
||||
* The credentials provided when this user was authenticated. These
|
||||
* credentials will be updated to match the provided username.
|
||||
*
|
||||
* @param effectiveGroups
|
||||
* The groups that the authenticated user belongs to.
|
||||
*
|
||||
* @param tokens
|
||||
* A map of all the name/value pairs that should be available
|
||||
* as tokens when connections are established by this user.
|
||||
*/
|
||||
public void init(String username, Credentials credentials,
|
||||
Set<String> effectiveGroups, Map<String, String> tokens) {
|
||||
|
||||
this.credentials = credentials;
|
||||
this.effectiveGroups = Collections.unmodifiableSet(effectiveGroups);
|
||||
this.tokens = Collections.unmodifiableMap(tokens);
|
||||
setIdentifier(username);
|
||||
|
||||
// Update credentials with username provided via SSO for sake of
|
||||
// ${GUAC_USERNAME} token
|
||||
credentials.setUsername(username);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Map of the parameter tokens that should be automatically
|
||||
* injected into connections used by this user during their session. If
|
||||
* there are no parameter tokens applicable to the SSO implementation, this
|
||||
* may simply be an empty map.
|
||||
*
|
||||
* @return
|
||||
* A map of the parameter token name/value pairs that should be
|
||||
* automatically injected into connections used by this user.
|
||||
*/
|
||||
public Map<String, String> getTokens() {
|
||||
return tokens;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationProvider getAuthenticationProvider() {
|
||||
return authProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Credentials getCredentials() {
|
||||
return credentials;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getEffectiveUserGroups() {
|
||||
return effectiveGroups;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
<meta name="after" content=".login-ui .login-dialog-middle:not(:has(~ .sso-providers))">
|
||||
<div class="sso-providers">
|
||||
{{ 'LOGIN.SECTION_HEADER_SSO_OPTIONS' | translate }}
|
||||
<ul class="sso-provider-list"></ul>
|
||||
</div>
|
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.login-ui .sso-providers {
|
||||
|
||||
padding: 0.25em 0.5em;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
}
|
||||
|
||||
.sso-providers ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sso-providers ul, .sso-providers li {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sso-providers li::before {
|
||||
content: ' / ';
|
||||
}
|
||||
|
||||
.sso-providers li:first-child::before {
|
||||
display: none;
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
{
|
||||
|
||||
"DATA_SOURCE_CAS" : {
|
||||
"NAME" : "Backend d'inici de sessió unificat (SSO) CAS"
|
||||
},
|
||||
|
||||
"DATA_SOURCE_SAML" : {
|
||||
"NAME" : "Extensión de autenticación SAML"
|
||||
},
|
||||
|
||||
"LOGIN" : {
|
||||
"INFO_IDP_REDIRECT_PENDING" : "Espereu, redirigint al proveïdor d'identitat ..."
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
{
|
||||
|
||||
"LOGIN" : {
|
||||
"INFO_IDP_REDIRECT_PENDING" : "Bitte warten, Sie werden zum Identitätsprovider weitergeleitet..."
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
{
|
||||
|
||||
"DATA_SOURCE_CAS" : {
|
||||
"NAME" : "CAS SSO Backend"
|
||||
},
|
||||
|
||||
"DATA_SOURCE_OPENID" : {
|
||||
"NAME" : "OpenID SSO Backend"
|
||||
},
|
||||
|
||||
"DATA_SOURCE_SAML" : {
|
||||
"NAME" : "SAML SSO Backend"
|
||||
},
|
||||
|
||||
"DATA_SOURCE_SSL" : {
|
||||
"NAME" : "SSL/TLS SSO Backend"
|
||||
},
|
||||
|
||||
"LOGIN" : {
|
||||
"FIELD_HEADER_ID_TOKEN" : "",
|
||||
"FIELD_HEADER_STATE" : "",
|
||||
"FIELD_HEADER_TICKET" : "",
|
||||
"INFO_IDP_REDIRECT_PENDING" : "Please wait, redirecting to identity provider...",
|
||||
"INFO_REDIRECT_PENDING" : "Please wait while you are redirected...",
|
||||
"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:"
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
{
|
||||
|
||||
"DATA_SOURCE_CAS" : {
|
||||
"NAME" : "Backend CAS SSO"
|
||||
},
|
||||
|
||||
"DATA_SOURCE_OPENID" : {
|
||||
"NAME" : "Backend OpenID SSO"
|
||||
},
|
||||
|
||||
"DATA_SOURCE_SAML" : {
|
||||
"NAME" : "Backend SAML SSO"
|
||||
},
|
||||
|
||||
"DATA_SOURCE_SSL" : {
|
||||
"NAME" : "Backend SSL/TLS SSO"
|
||||
},
|
||||
|
||||
"LOGIN" : {
|
||||
"INFO_IDP_REDIRECT_PENDING" : "Veuillez patienter, redirection vers le fournisseur d'identité...",
|
||||
"INFO_REDIRECT_PENDING" : "Veuillez patienter pendant que vous êtes redirigé...",
|
||||
"NAME_IDP_CAS" : "CAS",
|
||||
"NAME_IDP_OPENID" : "OpenID",
|
||||
"NAME_IDP_SAML" : "SAML",
|
||||
"NAME_IDP_SSL" : "Certificat / Carte à puce",
|
||||
"SECTION_HEADER_SSO_OPTIONS" : "Connectez-vous avec:"
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
{
|
||||
|
||||
"LOGIN" : {
|
||||
"INFO_IDP_REDIRECT_PENDING" : "Attendi, reindirizzamento al provider di identità...",
|
||||
"NAME_IDP_CAS" : "CAS",
|
||||
"NAME_IDP_OPENID" : "OpenID",
|
||||
"NAME_IDP_SAML" : "SAML",
|
||||
"SECTION_HEADER_SSO_OPTIONS" : "Accedi con:"
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
{
|
||||
|
||||
"LOGIN" : {
|
||||
"INFO_IDP_REDIRECT_PENDING" : "IDプロバイダへリダイレクトしています。"
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
{
|
||||
|
||||
"DATA_SOURCE_SAML" : {
|
||||
"NAME" : "SAML 인증 확장 프로그램"
|
||||
},
|
||||
|
||||
"LOGIN" : {
|
||||
"INFO_IDP_REDIRECT_PENDING" : "잠시만 기다려주십시오. ID 제공자로 리디렉션 중..."
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
{
|
||||
|
||||
"DATA_SOURCE_CAS" : {
|
||||
"NAME" : "CAS SSO Backend"
|
||||
},
|
||||
|
||||
"DATA_SOURCE_OPENID" : {
|
||||
"NAME" : "OpenID SSO Backend"
|
||||
},
|
||||
|
||||
"DATA_SOURCE_SAML" : {
|
||||
"NAME" : "SAML SSO Backend"
|
||||
},
|
||||
|
||||
"LOGIN" : {
|
||||
"FIELD_HEADER_ID_TOKEN" : "",
|
||||
"FIELD_HEADER_STATE" : "",
|
||||
"FIELD_HEADER_TICKET" : "",
|
||||
"INFO_IDP_REDIRECT_PENDING" : "Proszę czekać, przekierowanie do dostawcy tożsamości...",
|
||||
"NAME_IDP_CAS" : "CAS",
|
||||
"NAME_IDP_OPENID" : "OpenID",
|
||||
"NAME_IDP_SAML" : "SAML",
|
||||
"SECTION_HEADER_SSO_OPTIONS" : "Zaloguj z:"
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
{
|
||||
|
||||
"LOGIN" : {
|
||||
"INFO_IDP_REDIRECT_PENDING" : "Por favor aguarde, redirecionando ao provedor de indentidade..."
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
{
|
||||
|
||||
"DATA_SOURCE_CAS" : {
|
||||
"NAME" : "Бэкенд CAS SSO"
|
||||
},
|
||||
|
||||
"DATA_SOURCE_OPENID" : {
|
||||
"NAME" : "Бэкенд OpenID SSO"
|
||||
},
|
||||
|
||||
"LOGIN" : {
|
||||
"INFO_IDP_REDIRECT_PENDING" : "Пожалуйста, подождите. Переадресую на страницу аутентификации..."
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
{
|
||||
|
||||
"DATA_SOURCE_CAS" : {
|
||||
"NAME" : "CAS SSO后端"
|
||||
},
|
||||
|
||||
"DATA_SOURCE_OPENID" : {
|
||||
"NAME" : "OpenID SSO后端"
|
||||
},
|
||||
|
||||
"LOGIN" : {
|
||||
"INFO_IDP_REDIRECT_PENDING" : "请稍候,正在重定向到身份提供者..."
|
||||
}
|
||||
|
||||
}
|
3
extensions/guacamole-auth-sso/modules/guacamole-auth-sso-cas/.gitignore
vendored
Normal file
3
extensions/guacamole-auth-sso/modules/guacamole-auth-sso-cas/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*~
|
||||
target/
|
||||
src/main/resources/generated/
|
@@ -0,0 +1 @@
|
||||
src/main/resources/html/*.html
|
@@ -0,0 +1,160 @@
|
||||
<?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-cas</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>1.6.0</version>
|
||||
<name>guacamole-auth-sso-cas</name>
|
||||
<url>http://guacamole.apache.org/</url>
|
||||
|
||||
<parent>
|
||||
<groupId>org.apache.guacamole</groupId>
|
||||
<artifactId>guacamole-auth-sso</artifactId>
|
||||
<version>1.6.0</version>
|
||||
<relativePath>../../</relativePath>
|
||||
</parent>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- Apereo CAS Client API -->
|
||||
<dependency>
|
||||
<groupId>org.jasig.cas.client</groupId>
|
||||
<artifactId>cas-client-core</artifactId>
|
||||
<version>3.6.4</version>
|
||||
<exclusions>
|
||||
|
||||
<!-- Resolve version conflict (see below - transitive
|
||||
dependencies of cas-client-core disagree on 2.3.0.1 vs.
|
||||
2.3.1) -->
|
||||
<exclusion>
|
||||
<groupId>org.glassfish.jaxb</groupId>
|
||||
<artifactId>jaxb-core</artifactId>
|
||||
</exclusion>
|
||||
|
||||
<!--
|
||||
Replace slightly older commons-codec (1.15) with newer
|
||||
and identical version to that used by Apache Directory API
|
||||
for LDAP (1.16.0) so that we don't need two copies of the
|
||||
same license information.
|
||||
-->
|
||||
<exclusion>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
</exclusion>
|
||||
|
||||
<!--
|
||||
Replace older BouncyCastle (1.70) with newer, compatible
|
||||
version (1.80), which has since been renamed from
|
||||
"bcpkix-jdk15on" to "bcpkix-jdk15to18".
|
||||
-->
|
||||
<exclusion>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk15on</artifactId>
|
||||
</exclusion>
|
||||
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- BouncyCastle for Java 5 and above (formerly "bcpkix-jdk15on", a
|
||||
transitive dependency of cas-client-core that we are overriding to
|
||||
force an update) -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk15to18</artifactId>
|
||||
<version>1.80</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Guava - Utility Library -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</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>
|
||||
|
||||
<!-- JUnit -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-params</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Force use of version 2.3.1 (transitive dependencies of
|
||||
cas-client-core disagree on 2.3.0. vs. 2.3.1) -->
|
||||
<dependency>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
<version>2.3.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache Commons Codec (see exclusions for cas-client-core) -->
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>1.17.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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.cas;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import org.apache.guacamole.form.Field;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.net.auth.Credentials;
|
||||
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
|
||||
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
|
||||
import org.apache.guacamole.auth.cas.conf.ConfigurationService;
|
||||
import org.apache.guacamole.auth.cas.ticket.TicketValidationService;
|
||||
import org.apache.guacamole.auth.sso.SSOAuthenticationProviderService;
|
||||
import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser;
|
||||
import org.apache.guacamole.form.RedirectField;
|
||||
import org.apache.guacamole.language.TranslatableMessage;
|
||||
|
||||
/**
|
||||
* Service that authenticates Guacamole users by processing CAS tickets.
|
||||
*/
|
||||
@Singleton
|
||||
public class AuthenticationProviderService implements SSOAuthenticationProviderService {
|
||||
|
||||
/**
|
||||
* The parameter that will be present upon successful CAS authentication.
|
||||
*/
|
||||
public static final String TICKET_PARAMETER_NAME = "ticket";
|
||||
|
||||
/**
|
||||
* The standard URI name for the CAS login resource.
|
||||
*/
|
||||
private static final String CAS_LOGIN_URI = "login";
|
||||
|
||||
/**
|
||||
* Service for retrieving CAS configuration information.
|
||||
*/
|
||||
@Inject
|
||||
private ConfigurationService confService;
|
||||
|
||||
/**
|
||||
* Service for validating received ID tickets.
|
||||
*/
|
||||
@Inject
|
||||
private TicketValidationService ticketService;
|
||||
|
||||
@Override
|
||||
public SSOAuthenticatedUser authenticateUser(Credentials credentials)
|
||||
throws GuacamoleException {
|
||||
|
||||
// Pull CAS ticket from request if present
|
||||
String ticket = credentials.getParameter(TICKET_PARAMETER_NAME);
|
||||
if (ticket != null) {
|
||||
return ticketService.validateTicket(ticket, credentials);
|
||||
}
|
||||
|
||||
// Request CAS ticket (will automatically redirect the user to the
|
||||
// CAS authorization page via JavaScript)
|
||||
throw new GuacamoleInvalidCredentialsException("Invalid login.",
|
||||
new CredentialsInfo(Arrays.asList(new Field[] {
|
||||
new RedirectField(TICKET_PARAMETER_NAME, getLoginURI(),
|
||||
new TranslatableMessage("LOGIN.INFO_IDP_REDIRECT_PENDING"))
|
||||
|
||||
}))
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getLoginURI() throws GuacamoleException {
|
||||
return UriBuilder.fromUri(confService.getAuthorizationEndpoint())
|
||||
.path(CAS_LOGIN_URI)
|
||||
.queryParam("service", confService.getRedirectURI())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
// Nothing to clean up
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.cas;
|
||||
|
||||
import org.apache.guacamole.auth.sso.SSOAuthenticationProvider;
|
||||
import org.apache.guacamole.auth.sso.SSOResource;
|
||||
|
||||
/**
|
||||
* Guacamole authentication backend which authenticates users using an
|
||||
* arbitrary external system implementing CAS. No storage for connections is
|
||||
* provided - only authentication. Storage must be provided by some other
|
||||
* extension.
|
||||
*/
|
||||
public class CASAuthenticationProvider extends SSOAuthenticationProvider {
|
||||
|
||||
/**
|
||||
* Creates a new CASAuthenticationProvider that authenticates users
|
||||
* against an CAS service
|
||||
*/
|
||||
public CASAuthenticationProvider() {
|
||||
super(AuthenticationProviderService.class,
|
||||
SSOResource.class, new CASAuthenticationProviderModule());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIdentifier() {
|
||||
return "cas";
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.cas;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import org.apache.guacamole.auth.cas.conf.CASEnvironment;
|
||||
import org.apache.guacamole.auth.cas.conf.ConfigurationService;
|
||||
import org.apache.guacamole.auth.cas.ticket.TicketValidationService;
|
||||
import org.apache.guacamole.environment.Environment;
|
||||
|
||||
/**
|
||||
* Guice module which configures CAS-specific injections.
|
||||
*/
|
||||
public class CASAuthenticationProviderModule extends AbstractModule {
|
||||
|
||||
/**
|
||||
* The configuration environment for this server and extension.
|
||||
*/
|
||||
private final Environment environment = new CASEnvironment();
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(ConfigurationService.class);
|
||||
bind(TicketValidationService.class);
|
||||
|
||||
bind(Environment.class).toInstance(environment);
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.guacamole.auth.cas.conf;
|
||||
|
||||
import org.apache.guacamole.environment.DelegatingEnvironment;
|
||||
import org.apache.guacamole.environment.LocalEnvironment;
|
||||
|
||||
/**
|
||||
* An environment for retrieving CAS-related properties from the Guacamole
|
||||
* configuration.
|
||||
*/
|
||||
public class CASEnvironment extends DelegatingEnvironment {
|
||||
|
||||
/**
|
||||
* Create a new instance of the configuration environment for the
|
||||
* CAS SSO module, pulling the default instance of the LocalEnvironment.
|
||||
*/
|
||||
public CASEnvironment() {
|
||||
super(LocalEnvironment.getInstance());
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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.cas.conf;
|
||||
|
||||
import org.apache.guacamole.auth.cas.group.GroupFormat;
|
||||
import org.apache.guacamole.properties.EnumGuacamoleProperty;
|
||||
import org.apache.guacamole.properties.URIGuacamoleProperty;
|
||||
import org.apache.guacamole.properties.StringGuacamoleProperty;
|
||||
|
||||
/**
|
||||
* Provides properties required for use of the CAS authentication provider.
|
||||
* These properties will be read from guacamole.properties when the CAS
|
||||
* authentication provider is used.
|
||||
*/
|
||||
public class CASGuacamoleProperties {
|
||||
|
||||
/**
|
||||
* This class should not be instantiated.
|
||||
*/
|
||||
private CASGuacamoleProperties() {}
|
||||
|
||||
/**
|
||||
* The authorization endpoint (URI) of the CAS service.
|
||||
*/
|
||||
public static final URIGuacamoleProperty CAS_AUTHORIZATION_ENDPOINT =
|
||||
new URIGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "cas-authorization-endpoint"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The URI that the CAS service should redirect to after the
|
||||
* authentication process is complete. This must be the full URL that a
|
||||
* user would enter into their browser to access Guacamole.
|
||||
*/
|
||||
public static final URIGuacamoleProperty CAS_REDIRECT_URI =
|
||||
new URIGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "cas-redirect-uri"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The location of the private key file used to retrieve the
|
||||
* password if CAS is configured to support ClearPass.
|
||||
*/
|
||||
public static final PrivateKeyGuacamoleProperty CAS_CLEARPASS_KEY =
|
||||
new PrivateKeyGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "cas-clearpass-key"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The name of the CAS attribute used for group membership, such as
|
||||
* "memberOf". This attribute is case sensitive.
|
||||
*/
|
||||
public static final StringGuacamoleProperty CAS_GROUP_ATTRIBUTE =
|
||||
new StringGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "cas-group-attribute"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The format used by CAS to represent group names. Possible formats are
|
||||
* "plain" (simple text names) or "ldap" (fully-qualified LDAP DNs).
|
||||
*/
|
||||
public static final EnumGuacamoleProperty<GroupFormat> CAS_GROUP_FORMAT =
|
||||
new EnumGuacamoleProperty<GroupFormat>(GroupFormat.class) {
|
||||
|
||||
@Override
|
||||
public String getName() { return "cas-group-format"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The LDAP base DN to require for all CAS groups.
|
||||
*/
|
||||
public static final LdapNameGuacamoleProperty CAS_GROUP_LDAP_BASE_DN =
|
||||
new LdapNameGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "cas-group-ldap-base-dn"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The LDAP attribute to require for the names of CAS groups.
|
||||
*/
|
||||
public static final StringGuacamoleProperty CAS_GROUP_LDAP_ATTRIBUTE =
|
||||
new StringGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "cas-group-ldap-attribute"; }
|
||||
|
||||
};
|
||||
|
||||
}
|
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* 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.cas.conf;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import java.net.URI;
|
||||
import java.security.PrivateKey;
|
||||
import javax.naming.ldap.LdapName;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.GuacamoleServerException;
|
||||
import org.apache.guacamole.auth.cas.group.GroupFormat;
|
||||
import org.apache.guacamole.environment.Environment;
|
||||
import org.apache.guacamole.auth.cas.group.GroupParser;
|
||||
import org.apache.guacamole.auth.cas.group.LDAPGroupParser;
|
||||
import org.apache.guacamole.auth.cas.group.PlainGroupParser;
|
||||
|
||||
/**
|
||||
* Service for retrieving configuration information regarding the CAS service.
|
||||
*/
|
||||
public class ConfigurationService {
|
||||
|
||||
/**
|
||||
* The Guacamole server environment.
|
||||
*/
|
||||
@Inject
|
||||
private Environment environment;
|
||||
|
||||
/**
|
||||
* Returns the authorization endpoint (URI) of the CAS service as
|
||||
* configured with guacamole.properties.
|
||||
*
|
||||
* @return
|
||||
* The authorization endpoint of the CAS service, as configured with
|
||||
* guacamole.properties.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed, or if the authorization
|
||||
* endpoint property is missing.
|
||||
*/
|
||||
public URI getAuthorizationEndpoint() throws GuacamoleException {
|
||||
return environment.getRequiredProperty(CASGuacamoleProperties.CAS_AUTHORIZATION_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URI that the CAS service should redirect to after
|
||||
* the authentication process is complete, as configured with
|
||||
* guacamole.properties. This must be the full URL that a user would enter
|
||||
* into their browser to access Guacamole.
|
||||
*
|
||||
* @return
|
||||
* The URI to redirect the client back to after authentication
|
||||
* is completed, as configured in guacamole.properties.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed, or if the redirect URI
|
||||
* property is missing.
|
||||
*/
|
||||
public URI getRedirectURI() throws GuacamoleException {
|
||||
return environment.getRequiredProperty(CASGuacamoleProperties.CAS_REDIRECT_URI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the PrivateKey used to decrypt the credential object
|
||||
* sent encrypted by CAS, or null if no key is defined.
|
||||
*
|
||||
* @return
|
||||
* The PrivateKey used to decrypt the ClearPass
|
||||
* credential returned by CAS.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed.
|
||||
*/
|
||||
public PrivateKey getClearpassKey() throws GuacamoleException {
|
||||
return environment.getProperty(CASGuacamoleProperties.CAS_CLEARPASS_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the CAS attribute that should be used to determine group
|
||||
* memberships in CAS, such as "memberOf". If no attribute has been
|
||||
* specified, null is returned.
|
||||
*
|
||||
* @return
|
||||
* The attribute name used to determine group memberships in CAS,
|
||||
* null if not defined.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed.
|
||||
*/
|
||||
public String getGroupAttribute() throws GuacamoleException {
|
||||
return environment.getProperty(CASGuacamoleProperties.CAS_GROUP_ATTRIBUTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the format that CAS is expected to use for its group names, such
|
||||
* as {@link GroupFormat#PLAIN} (simple plain-text names) or
|
||||
* {@link GroupFormat#LDAP} (fully-qualified LDAP DNs). If not specified,
|
||||
* PLAIN is used by default.
|
||||
*
|
||||
* @return
|
||||
* The format that CAS is expected to use for its group names.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If the format specified within guacamole.properties is not valid.
|
||||
*/
|
||||
public GroupFormat getGroupFormat() throws GuacamoleException {
|
||||
return environment.getProperty(CASGuacamoleProperties.CAS_GROUP_FORMAT, GroupFormat.PLAIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base DN that all LDAP-formatted CAS groups must reside
|
||||
* beneath. Any groups that are not beneath this base DN should be ignored.
|
||||
* If no such base DN is provided, the tree structure of the ancestors of
|
||||
* LDAP-formatted CAS groups should not be considered.
|
||||
*
|
||||
* @return
|
||||
* The base DN that all LDAP-formatted CAS groups must reside beneath,
|
||||
* or null if the tree structure of the ancestors of LDAP-formatted
|
||||
* CAS groups should not be considered.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If the provided base DN is not a valid LDAP DN.
|
||||
*/
|
||||
public LdapName getGroupLDAPBaseDN() throws GuacamoleException {
|
||||
return environment.getProperty(CASGuacamoleProperties.CAS_GROUP_LDAP_BASE_DN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the LDAP attribute that should be required for all LDAP-formatted
|
||||
* CAS groups. Any groups that do not use this attribute as the last
|
||||
* (leftmost) attribute of their DN should be ignored. If no such LDAP
|
||||
* attribute is provided, the last (leftmost) attribute should still be
|
||||
* used to determine the group name, but the specific attribute involved
|
||||
* should not be considered.
|
||||
*
|
||||
* @return
|
||||
* The LDAP attribute that should be required for all LDAP-formatted
|
||||
* CAS groups, or null if any attribute should be allowed.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed.
|
||||
*/
|
||||
public String getGroupLDAPAttribute() throws GuacamoleException {
|
||||
return environment.getProperty(CASGuacamoleProperties.CAS_GROUP_LDAP_ATTRIBUTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a GroupParser instance that can be used to parse CAS group
|
||||
* names. The parser returned will take into account the configured CAS
|
||||
* group format, as well as any configured LDAP-specific restrictions.
|
||||
*
|
||||
* @return
|
||||
* A GroupParser instance that can be used to parse CAS group names as
|
||||
* configured in guacamole.properties.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed.
|
||||
*/
|
||||
public GroupParser getGroupParser() throws GuacamoleException {
|
||||
switch (getGroupFormat()) {
|
||||
|
||||
// Simple, plain-text groups
|
||||
case PLAIN:
|
||||
return new PlainGroupParser();
|
||||
|
||||
// LDAP DNs
|
||||
case LDAP:
|
||||
return new LDAPGroupParser(getGroupLDAPAttribute(), getGroupLDAPBaseDN());
|
||||
|
||||
default:
|
||||
throw new GuacamoleServerException("Unsupported CAS group format: " + getGroupFormat());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.guacamole.auth.cas.conf;
|
||||
|
||||
import javax.naming.InvalidNameException;
|
||||
import javax.naming.ldap.LdapName;
|
||||
import org.apache.guacamole.properties.GuacamoleProperty;
|
||||
import org.apache.guacamole.GuacamoleServerException;
|
||||
|
||||
/**
|
||||
* A GuacamoleProperty whose value is an LDAP DN.
|
||||
*/
|
||||
public abstract class LdapNameGuacamoleProperty implements GuacamoleProperty<LdapName> {
|
||||
|
||||
@Override
|
||||
public LdapName parseValue(String value) throws GuacamoleServerException {
|
||||
|
||||
// Consider null/empty values to be empty
|
||||
if (value == null || value.isEmpty())
|
||||
return null;
|
||||
|
||||
// Parse provided value as an LDAP DN
|
||||
try {
|
||||
return new LdapName(value);
|
||||
}
|
||||
catch (InvalidNameException e) {
|
||||
throw new GuacamoleServerException("Invalid LDAP distinguished name.", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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.cas.conf;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import org.apache.guacamole.properties.GuacamoleProperty;
|
||||
import org.apache.guacamole.GuacamoleServerException;
|
||||
|
||||
/**
|
||||
* A GuacamoleProperty whose value is derived from a private key file.
|
||||
*/
|
||||
public abstract class PrivateKeyGuacamoleProperty implements GuacamoleProperty<PrivateKey> {
|
||||
|
||||
@Override
|
||||
public PrivateKey parseValue(String value) throws GuacamoleServerException {
|
||||
|
||||
if (value == null || value.isEmpty())
|
||||
return null;
|
||||
|
||||
FileInputStream keyStreamIn = null;
|
||||
|
||||
try {
|
||||
try {
|
||||
|
||||
// Open and read the file specified in the configuration.
|
||||
File keyFile = new File(value);
|
||||
keyStreamIn = new FileInputStream(keyFile);
|
||||
ByteArrayOutputStream keyStreamOut = new ByteArrayOutputStream();
|
||||
byte[] keyBuffer = new byte[1024];
|
||||
|
||||
for (int readBytes; (readBytes = keyStreamIn.read(keyBuffer)) != -1;)
|
||||
keyStreamOut.write(keyBuffer, 0, readBytes);
|
||||
|
||||
final byte[] keyBytes = keyStreamOut.toByteArray();
|
||||
|
||||
// Set up decryption infrastructure
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
KeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
|
||||
return keyFactory.generatePrivate(keySpec);
|
||||
|
||||
}
|
||||
catch (FileNotFoundException e) {
|
||||
throw new GuacamoleServerException("Could not find the specified key file.", e);
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
throw new GuacamoleServerException("RSA algorithm is not available.", e);
|
||||
}
|
||||
catch (InvalidKeySpecException e) {
|
||||
throw new GuacamoleServerException("Key is not in expected PKCS8 encoding.", e);
|
||||
}
|
||||
finally {
|
||||
if (keyStreamIn != null)
|
||||
keyStreamIn.close();
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new GuacamoleServerException("Could not read in the specified key file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.cas.group;
|
||||
|
||||
import org.apache.guacamole.properties.EnumGuacamoleProperty.PropertyValue;
|
||||
|
||||
/**
|
||||
* Possible formats of group names received from CAS.
|
||||
*/
|
||||
public enum GroupFormat {
|
||||
|
||||
/**
|
||||
* Simple, plain-text group names.
|
||||
*/
|
||||
@PropertyValue("plain")
|
||||
PLAIN,
|
||||
|
||||
/**
|
||||
* Group names formatted as LDAP DNs.
|
||||
*/
|
||||
@PropertyValue("ldap")
|
||||
LDAP
|
||||
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.cas.group;
|
||||
|
||||
/**
|
||||
* Parser which converts the group names returned by CAS into names usable by
|
||||
* Guacamole. The format of a CAS group name may vary by the underlying
|
||||
* authentication backend. For example, a CAS deployment backed by LDAP may
|
||||
* provide group names as LDAP DNs, which must be transformed into normal group
|
||||
* names to be usable within Guacamole.
|
||||
*
|
||||
* @see LDAPGroupParser
|
||||
*/
|
||||
public interface GroupParser {
|
||||
|
||||
/**
|
||||
* Parses the given CAS group name into a group name usable by Guacamole.
|
||||
*
|
||||
* @param casGroup
|
||||
* The group name retrieved from CAS.
|
||||
*
|
||||
* @return
|
||||
* A group name usable by Guacamole, or null if the group is not valid.
|
||||
*/
|
||||
String parse(String casGroup);
|
||||
|
||||
}
|
@@ -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.cas.group;
|
||||
|
||||
import javax.naming.InvalidNameException;
|
||||
import javax.naming.ldap.LdapName;
|
||||
import javax.naming.ldap.Rdn;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* GroupParser that converts group names from LDAP DNs into normal group names,
|
||||
* using the last (leftmost) attribute of the DN as the name. Groups may
|
||||
* optionally be restricted to only those beneath a specific base DN, or only
|
||||
* those using a specific attribute as their last (leftmost) attribute.
|
||||
*/
|
||||
public class LDAPGroupParser implements GroupParser {
|
||||
|
||||
/**
|
||||
* Logger for this class.
|
||||
*/
|
||||
private static final Logger logger = LoggerFactory.getLogger(LDAPGroupParser.class);
|
||||
|
||||
/**
|
||||
* The LDAP attribute to require for all accepted group names. If null, any
|
||||
* LDAP attribute will be allowed.
|
||||
*/
|
||||
private final String nameAttribute;
|
||||
|
||||
/**
|
||||
* The base DN to require for all accepted group names. If null, ancestor
|
||||
* tree structure will not be considered in accepting/rejecting a group.
|
||||
*/
|
||||
private final LdapName baseDn;
|
||||
|
||||
/**
|
||||
* Creates a new LDAPGroupParser which applies the given restrictions on
|
||||
* any provided group names.
|
||||
*
|
||||
* @param nameAttribute
|
||||
* The LDAP attribute to require for all accepted group names. This
|
||||
* restriction applies to the last (leftmost) attribute only, which is
|
||||
* always used to determine the name of the group. If null, any LDAP
|
||||
* attribute will be allowed in the last (leftmost) position.
|
||||
*
|
||||
* @param baseDn
|
||||
* The base DN to require for all accepted group names. If null,
|
||||
* ancestor tree structure will not be considered in
|
||||
* accepting/rejecting a group.
|
||||
*/
|
||||
public LDAPGroupParser(String nameAttribute, LdapName baseDn) {
|
||||
this.nameAttribute = nameAttribute;
|
||||
this.baseDn = baseDn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parse(String casGroup) {
|
||||
|
||||
// Reject null/empty group names
|
||||
if (casGroup == null || casGroup.isEmpty())
|
||||
return null;
|
||||
|
||||
// Parse group as an LDAP DN
|
||||
LdapName group;
|
||||
try {
|
||||
group = new LdapName(casGroup);
|
||||
}
|
||||
catch (InvalidNameException e) {
|
||||
logger.debug("CAS group \"{}\" has been rejected as it is not a "
|
||||
+ "valid LDAP DN.", casGroup, e);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Reject any group that is not beneath the base DN
|
||||
if (baseDn != null && !group.startsWith(baseDn))
|
||||
return null;
|
||||
|
||||
// If a specific name attribute is defined, restrict to groups that
|
||||
// use that attribute to distinguish themselves
|
||||
Rdn last = group.getRdn(group.size() - 1);
|
||||
if (nameAttribute != null && !nameAttribute.equalsIgnoreCase(last.getType()))
|
||||
return null;
|
||||
|
||||
// The group name is the string value of the final attribute in the DN
|
||||
return last.getValue().toString();
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.cas.group;
|
||||
|
||||
/**
|
||||
* GroupParser which simply passes through all CAS group names untouched.
|
||||
*/
|
||||
public class PlainGroupParser implements GroupParser {
|
||||
|
||||
@Override
|
||||
public String parse(String casGroup) {
|
||||
return casGroup;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* 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.cas.ticket;
|
||||
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import java.net.URI;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.GuacamoleSecurityException;
|
||||
import org.apache.guacamole.GuacamoleServerException;
|
||||
import org.apache.guacamole.auth.cas.conf.ConfigurationService;
|
||||
import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser;
|
||||
import org.apache.guacamole.net.auth.Credentials;
|
||||
import org.apache.guacamole.token.TokenName;
|
||||
import org.jasig.cas.client.authentication.AttributePrincipal;
|
||||
import org.jasig.cas.client.validation.Assertion;
|
||||
import org.jasig.cas.client.validation.Cas20ProxyTicketValidator;
|
||||
import org.jasig.cas.client.validation.TicketValidationException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Service for validating ID tickets forwarded to us by the client, verifying
|
||||
* that they did indeed come from the CAS service.
|
||||
*/
|
||||
public class TicketValidationService {
|
||||
|
||||
/**
|
||||
* Logger for this class.
|
||||
*/
|
||||
private static final Logger logger = LoggerFactory.getLogger(TicketValidationService.class);
|
||||
|
||||
/**
|
||||
* The prefix to use when generating token names.
|
||||
*/
|
||||
public static final String CAS_ATTRIBUTE_TOKEN_PREFIX = "CAS_";
|
||||
|
||||
/**
|
||||
* Service for retrieving CAS configuration information.
|
||||
*/
|
||||
@Inject
|
||||
private ConfigurationService confService;
|
||||
|
||||
/**
|
||||
* Provider for AuthenticatedUser objects.
|
||||
*/
|
||||
@Inject
|
||||
private Provider<SSOAuthenticatedUser> authenticatedUserProvider;
|
||||
|
||||
/**
|
||||
* Converts the given CAS attribute value object (whose type is variable)
|
||||
* to a Set of String values. If the value is already a Collection of some
|
||||
* kind, its values are converted to Strings and returned as the members of
|
||||
* the Set. If the value is not already a Collection, it is assumed to be a
|
||||
* single value, converted to a String, and used as the sole member of the
|
||||
* set.
|
||||
*
|
||||
* @param obj
|
||||
* The CAS attribute value to convert to a Set of Strings.
|
||||
*
|
||||
* @return
|
||||
* A Set of all String values contained within the given CAS attribute
|
||||
* value.
|
||||
*/
|
||||
private Set<String> toStringSet(Object obj) {
|
||||
|
||||
// Consider null to represent no provided values
|
||||
if (obj == null)
|
||||
return Collections.emptySet();
|
||||
|
||||
// If the provided object is already a Collection, produce a Collection
|
||||
// where we know for certain that all values are Strings
|
||||
if (obj instanceof Collection) {
|
||||
return ((Collection<?>) obj).stream()
|
||||
.map(Object::toString)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
// Otherwise, assume we have only a single value
|
||||
return Collections.singleton(obj.toString());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and parses the given ID ticket, returning a map of all
|
||||
* available tokens for the given user based on attributes provided by the
|
||||
* CAS server. If the ticket is invalid an exception is thrown.
|
||||
*
|
||||
* @param ticket
|
||||
* The ID ticket to validate and parse.
|
||||
*
|
||||
* @param credentials
|
||||
* The Credentials object to store retrieved username and
|
||||
* password values in.
|
||||
*
|
||||
* @return
|
||||
* A CASAuthenticatedUser instance containing the ticket data returned by the CAS server.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If the ID ticket is not valid or guacamole.properties could
|
||||
* not be parsed.
|
||||
*/
|
||||
public SSOAuthenticatedUser validateTicket(String ticket,
|
||||
Credentials credentials) throws GuacamoleException {
|
||||
|
||||
// Create a ticket validator that uses the configured CAS URL
|
||||
URI casServerUrl = confService.getAuthorizationEndpoint();
|
||||
Cas20ProxyTicketValidator validator = new Cas20ProxyTicketValidator(casServerUrl.toString());
|
||||
validator.setAcceptAnyProxy(true);
|
||||
validator.setEncoding("UTF-8");
|
||||
|
||||
// Attempt to validate the supplied ticket
|
||||
Assertion assertion;
|
||||
try {
|
||||
URI confRedirectURI = confService.getRedirectURI();
|
||||
assertion = validator.validate(ticket, confRedirectURI.toString());
|
||||
}
|
||||
catch (TicketValidationException e) {
|
||||
throw new GuacamoleException("Ticket validation failed.", e);
|
||||
}
|
||||
|
||||
// Pull user principal and associated attributes
|
||||
AttributePrincipal principal = assertion.getPrincipal();
|
||||
Map<String, Object> ticketAttrs = new HashMap<>(principal.getAttributes());
|
||||
|
||||
// Retrieve user identity from principal
|
||||
String username = principal.getName();
|
||||
if (username == null)
|
||||
throw new GuacamoleSecurityException("No username provided by CAS.");
|
||||
|
||||
// Canonicalize username as lowercase
|
||||
username = username.toLowerCase();
|
||||
|
||||
// Retrieve password, attempt decryption, and set credentials.
|
||||
Object credObj = ticketAttrs.remove("credential");
|
||||
if (credObj != null) {
|
||||
String clearPass = decryptPassword(credObj.toString());
|
||||
if (clearPass != null && !clearPass.isEmpty())
|
||||
credentials.setPassword(clearPass);
|
||||
}
|
||||
|
||||
Set<String> effectiveGroups;
|
||||
|
||||
// Parse effective groups from principal attributes if a specific
|
||||
// group attribute has been configured
|
||||
String groupAttribute = confService.getGroupAttribute();
|
||||
if (groupAttribute != null) {
|
||||
effectiveGroups = toStringSet(ticketAttrs.get(groupAttribute)).stream()
|
||||
.map(confService.getGroupParser()::parse)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
// Otherwise, assume no effective groups
|
||||
else
|
||||
effectiveGroups = Collections.emptySet();
|
||||
|
||||
// Convert remaining attributes that have values to Strings
|
||||
Map<String, String> tokens = new HashMap<>(ticketAttrs.size());
|
||||
ticketAttrs.forEach((key, value) -> {
|
||||
if (value != null) {
|
||||
String tokenName = TokenName.canonicalize(key, CAS_ATTRIBUTE_TOKEN_PREFIX);
|
||||
tokens.put(tokenName, value.toString());
|
||||
}
|
||||
});
|
||||
|
||||
SSOAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
|
||||
authenticatedUser.init(username, credentials, effectiveGroups, tokens);
|
||||
return authenticatedUser;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an encrypted string representing a password provided by
|
||||
* the CAS ClearPass service and decrypts it using the private
|
||||
* key configured for this extension. Returns null if it is
|
||||
* unable to decrypt the password.
|
||||
*
|
||||
* @param encryptedPassword
|
||||
* A string with the encrypted password provided by the
|
||||
* CAS service.
|
||||
*
|
||||
* @return
|
||||
* The decrypted password, or null if it is unable to
|
||||
* decrypt the password.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If unable to get Guacamole configuration data
|
||||
*/
|
||||
private final String decryptPassword(String encryptedPassword)
|
||||
throws GuacamoleException {
|
||||
|
||||
// If we get nothing, we return nothing.
|
||||
if (encryptedPassword == null || encryptedPassword.isEmpty()) {
|
||||
logger.warn("No or empty encrypted password, no password will be available.");
|
||||
return null;
|
||||
}
|
||||
|
||||
final PrivateKey clearpassKey = confService.getClearpassKey();
|
||||
if (clearpassKey == null) {
|
||||
logger.debug("No private key available to decrypt password.");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
final Cipher cipher = Cipher.getInstance(clearpassKey.getAlgorithm());
|
||||
|
||||
if (cipher == null)
|
||||
throw new GuacamoleServerException("Failed to initialize cipher object with private key.");
|
||||
|
||||
// Initialize the Cipher in decrypt mode.
|
||||
cipher.init(Cipher.DECRYPT_MODE, clearpassKey);
|
||||
|
||||
// Decode and decrypt, and return a new string.
|
||||
final byte[] pass64 = BaseEncoding.base64().decode(encryptedPassword);
|
||||
final byte[] cipherData = cipher.doFinal(pass64);
|
||||
return new String(cipherData, Charset.forName("UTF-8"));
|
||||
|
||||
}
|
||||
catch (BadPaddingException e) {
|
||||
throw new GuacamoleServerException("Bad padding when decrypting cipher data.", e);
|
||||
}
|
||||
catch (IllegalBlockSizeException e) {
|
||||
throw new GuacamoleServerException("Illegal block size while opening private key.", e);
|
||||
}
|
||||
catch (InvalidKeyException e) {
|
||||
throw new GuacamoleServerException("Specified private key for ClearPass decryption is invalid.", e);
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
throw new GuacamoleServerException("Unexpected algorithm for the private key.", e);
|
||||
}
|
||||
catch (NoSuchPaddingException e) {
|
||||
throw new GuacamoleServerException("No such padding trying to initialize cipher with private key.", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
{
|
||||
|
||||
"guacamoleVersion" : "1.6.0",
|
||||
|
||||
"name" : "CAS Authentication Extension",
|
||||
"namespace" : "cas",
|
||||
|
||||
"authProviders" : [
|
||||
"org.apache.guacamole.auth.cas.CASAuthenticationProvider"
|
||||
],
|
||||
|
||||
"css" : [
|
||||
"styles/sso-providers.css"
|
||||
],
|
||||
|
||||
"html" : [
|
||||
"html/sso-providers.html",
|
||||
"html/sso-provider-cas.html"
|
||||
],
|
||||
|
||||
"translations" : [
|
||||
"translations/ca.json",
|
||||
"translations/de.json",
|
||||
"translations/en.json",
|
||||
"translations/fr.json",
|
||||
"translations/ja.json",
|
||||
"translations/ko.json",
|
||||
"translations/pl.json",
|
||||
"translations/pt.json",
|
||||
"translations/ru.json",
|
||||
"translations/zh.json"
|
||||
]
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
<meta name="after-children" content=".login-ui .sso-provider-list">
|
||||
<li class="sso-provider sso-provider-cas"><a href="api/ext/cas/login">{{
|
||||
'LOGIN.NAME_IDP_CAS' | 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,164 @@
|
||||
/*
|
||||
* 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.cas.group;
|
||||
|
||||
import javax.naming.InvalidNameException;
|
||||
import javax.naming.ldap.LdapName;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Test which confirms that the LDAPGroupParser implementation of GroupParser
|
||||
* parses CAS groups correctly.
|
||||
*/
|
||||
public class LDAPGroupParserTest {
|
||||
|
||||
/**
|
||||
* LdapName instance representing the LDAP DN: "dc=example,dc=net".
|
||||
*/
|
||||
private final LdapName exampleBaseDn;
|
||||
|
||||
/**
|
||||
* Creates a new LDAPGroupParserTest that verifies the functionality of
|
||||
* LDAPGroupParser.
|
||||
*
|
||||
* @throws InvalidNameException
|
||||
* If the static string LDAP DN of any test instance of LdapName is
|
||||
* unexpectedly invalid.
|
||||
*/
|
||||
public LDAPGroupParserTest() throws InvalidNameException {
|
||||
exampleBaseDn = new LdapName("dc=example,dc=net");
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that LDAPGroupParser correctly parses LDAP-based CAS groups
|
||||
* when no restrictions are enforced on LDAP attributes or the base DN.
|
||||
*/
|
||||
@Test
|
||||
public void testParseRestrictNothing() {
|
||||
|
||||
GroupParser parser = new LDAPGroupParser(null, null);
|
||||
|
||||
// null input should be rejected as null
|
||||
assertNull(parser.parse(null));
|
||||
|
||||
// Invalid DNs should be rejected as null
|
||||
assertNull(parser.parse(""));
|
||||
assertNull(parser.parse("foo"));
|
||||
|
||||
// Valid DNs should be accepted
|
||||
assertEquals("bar", parser.parse("foo=bar"));
|
||||
assertEquals("baz", parser.parse("CN=baz,dc=example,dc=com"));
|
||||
assertEquals("baz", parser.parse("ou=baz,dc=example,dc=net"));
|
||||
assertEquals("foo", parser.parse("ou=foo,cn=baz,dc=example,dc=net"));
|
||||
assertEquals("foo", parser.parse("cn=foo,DC=example,dc=net"));
|
||||
assertEquals("bar", parser.parse("CN=bar,OU=groups,dc=example,Dc=net"));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that LDAPGroupParser correctly parses LDAP-based CAS groups
|
||||
* when restrictions are enforced on LDAP attributes only.
|
||||
*/
|
||||
@Test
|
||||
public void testParseRestrictAttribute() {
|
||||
|
||||
GroupParser parser = new LDAPGroupParser("cn", null);
|
||||
|
||||
// null input should be rejected as null
|
||||
assertNull(parser.parse(null));
|
||||
|
||||
// Invalid DNs should be rejected as null
|
||||
assertNull(parser.parse(""));
|
||||
assertNull(parser.parse("foo"));
|
||||
|
||||
// Valid DNs not using the correct attribute should be rejected as null
|
||||
assertNull(parser.parse("foo=bar"));
|
||||
assertNull(parser.parse("ou=baz,dc=example,dc=com"));
|
||||
assertNull(parser.parse("ou=foo,cn=baz,dc=example,dc=com"));
|
||||
|
||||
// Valid DNs using the correct attribute should be accepted
|
||||
assertEquals("foo", parser.parse("cn=foo,DC=example,dc=net"));
|
||||
assertEquals("bar", parser.parse("CN=bar,OU=groups,dc=example,Dc=net"));
|
||||
assertEquals("baz", parser.parse("CN=baz,dc=example,dc=com"));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that LDAPGroupParser correctly parses LDAP-based CAS groups
|
||||
* when restrictions are enforced on the LDAP base DN only.
|
||||
*/
|
||||
@Test
|
||||
public void testParseRestrictBaseDN() {
|
||||
|
||||
GroupParser parser = new LDAPGroupParser(null, exampleBaseDn);
|
||||
|
||||
// null input should be rejected as null
|
||||
assertNull(parser.parse(null));
|
||||
|
||||
// Invalid DNs should be rejected as null
|
||||
assertNull(parser.parse(""));
|
||||
assertNull(parser.parse("foo"));
|
||||
|
||||
// Valid DNs outside the base DN should be rejected as null
|
||||
assertNull(parser.parse("foo=bar"));
|
||||
assertNull(parser.parse("CN=baz,dc=example,dc=com"));
|
||||
|
||||
// Valid DNs beneath the base DN should be accepted
|
||||
assertEquals("foo", parser.parse("cn=foo,DC=example,dc=net"));
|
||||
assertEquals("bar", parser.parse("CN=bar,OU=groups,dc=example,Dc=net"));
|
||||
assertEquals("baz", parser.parse("ou=baz,dc=example,dc=net"));
|
||||
assertEquals("foo", parser.parse("ou=foo,cn=baz,dc=example,dc=net"));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that LDAPGroupParser correctly parses LDAP-based CAS groups
|
||||
* when restrictions are enforced on both LDAP attributes and the base DN.
|
||||
*/
|
||||
@Test
|
||||
public void testParseRestrictAll() {
|
||||
|
||||
GroupParser parser = new LDAPGroupParser("cn", exampleBaseDn);
|
||||
|
||||
// null input should be rejected as null
|
||||
assertNull(parser.parse(null));
|
||||
|
||||
// Invalid DNs should be rejected as null
|
||||
assertNull(parser.parse(""));
|
||||
assertNull(parser.parse("foo"));
|
||||
|
||||
// Valid DNs outside the base DN should be rejected as null
|
||||
assertNull(parser.parse("foo=bar"));
|
||||
assertNull(parser.parse("CN=baz,dc=example,dc=com"));
|
||||
|
||||
// Valid DNs beneath the base DN but not using the correct attribute
|
||||
// should be rejected as null
|
||||
assertNull(parser.parse("ou=baz,dc=example,dc=net"));
|
||||
assertNull(parser.parse("ou=foo,cn=baz,dc=example,dc=net"));
|
||||
|
||||
// Valid DNs beneath the base DN and using the correct attribute should
|
||||
// be accepted
|
||||
assertEquals("foo", parser.parse("cn=foo,DC=example,dc=net"));
|
||||
assertEquals("bar", parser.parse("CN=bar,OU=groups,dc=example,Dc=net"));
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,79 @@
|
||||
<?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-dist</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<name>guacamole-auth-sso-dist</name>
|
||||
<url>http://guacamole.apache.org/</url>
|
||||
|
||||
<parent>
|
||||
<groupId>org.apache.guacamole</groupId>
|
||||
<artifactId>guacamole-auth-sso</artifactId>
|
||||
<version>1.6.0</version>
|
||||
<relativePath>../../</relativePath>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- CAS Authentication Extension -->
|
||||
<dependency>
|
||||
<groupId>org.apache.guacamole</groupId>
|
||||
<artifactId>guacamole-auth-sso-cas</artifactId>
|
||||
<version>1.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- OpenID Authentication Extension -->
|
||||
<dependency>
|
||||
<groupId>org.apache.guacamole</groupId>
|
||||
<artifactId>guacamole-auth-sso-openid</artifactId>
|
||||
<version>1.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- SAML Authentication Extension -->
|
||||
<dependency>
|
||||
<groupId>org.apache.guacamole</groupId>
|
||||
<artifactId>guacamole-auth-sso-saml</artifactId>
|
||||
<version>1.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- SSL Authentication Extension -->
|
||||
<dependency>
|
||||
<groupId>org.apache.guacamole</groupId>
|
||||
<artifactId>guacamole-auth-sso-ssl</artifactId>
|
||||
<version>1.6.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
<!-- Dist .tar.gz for guacamole-auth-sso should be named after the
|
||||
parent guacamole-auth-sso project, not after guacamole-auth-sso-dist -->
|
||||
<finalName>${project.parent.artifactId}-${project.parent.version}</finalName>
|
||||
|
||||
</build>
|
||||
|
||||
</project>
|
@@ -0,0 +1,82 @@
|
||||
<?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.
|
||||
-->
|
||||
<assembly
|
||||
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
|
||||
|
||||
<id>dist</id>
|
||||
<baseDirectory>${project.parent.artifactId}-${project.parent.version}</baseDirectory>
|
||||
|
||||
<!-- Output .tar.gz -->
|
||||
<formats>
|
||||
<format>tar.gz</format>
|
||||
</formats>
|
||||
|
||||
<!-- Include extension .jars -->
|
||||
<dependencySets>
|
||||
|
||||
<!-- CAS extension .jar -->
|
||||
<dependencySet>
|
||||
<outputDirectory>cas</outputDirectory>
|
||||
<useProjectArtifact>false</useProjectArtifact>
|
||||
<includes>
|
||||
<include>org.apache.guacamole:guacamole-auth-sso-cas</include>
|
||||
</includes>
|
||||
</dependencySet>
|
||||
|
||||
<!-- OpenID extension .jar -->
|
||||
<dependencySet>
|
||||
<outputDirectory>openid</outputDirectory>
|
||||
<useProjectArtifact>false</useProjectArtifact>
|
||||
<includes>
|
||||
<include>org.apache.guacamole:guacamole-auth-sso-openid</include>
|
||||
</includes>
|
||||
</dependencySet>
|
||||
|
||||
<!-- SAML extension .jar -->
|
||||
<dependencySet>
|
||||
<outputDirectory>saml</outputDirectory>
|
||||
<useProjectArtifact>false</useProjectArtifact>
|
||||
<includes>
|
||||
<include>org.apache.guacamole:guacamole-auth-sso-saml</include>
|
||||
</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 -->
|
||||
<fileSets>
|
||||
<fileSet>
|
||||
<outputDirectory></outputDirectory>
|
||||
<directory>target/licenses</directory>
|
||||
</fileSet>
|
||||
</fileSets>
|
||||
|
||||
</assembly>
|
3
extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/.gitignore
vendored
Normal file
3
extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*~
|
||||
target/
|
||||
src/main/resources/generated/
|
@@ -0,0 +1 @@
|
||||
src/main/resources/html/*.html
|
@@ -0,0 +1,132 @@
|
||||
<?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-openid</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>1.6.0</version>
|
||||
<name>guacamole-auth-sso-openid</name>
|
||||
<url>http://guacamole.apache.org/</url>
|
||||
|
||||
<parent>
|
||||
<groupId>org.apache.guacamole</groupId>
|
||||
<artifactId>guacamole-auth-sso</artifactId>
|
||||
<version>1.6.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>openid.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>
|
||||
|
||||
<!-- Java implementation of JOSE (jose.4.j) -->
|
||||
<dependency>
|
||||
<groupId>org.bitbucket.b_c</groupId>
|
||||
<artifactId>jose4j</artifactId>
|
||||
<version>0.9.6</version>
|
||||
</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>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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.openid;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import org.apache.guacamole.auth.openid.conf.ConfigurationService;
|
||||
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;
|
||||
import org.apache.guacamole.form.RedirectField;
|
||||
import org.apache.guacamole.language.TranslatableMessage;
|
||||
import org.apache.guacamole.net.auth.Credentials;
|
||||
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
|
||||
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
|
||||
import org.jose4j.jwt.JwtClaims;
|
||||
|
||||
/**
|
||||
* Service that authenticates Guacamole users by processing OpenID tokens.
|
||||
*/
|
||||
@Singleton
|
||||
public class AuthenticationProviderService implements SSOAuthenticationProviderService {
|
||||
|
||||
/**
|
||||
* The standard HTTP parameter which will be included within the URL by all
|
||||
* OpenID services upon successful authentication and redirect.
|
||||
*/
|
||||
public static final String TOKEN_PARAMETER_NAME = "id_token";
|
||||
|
||||
/**
|
||||
* Service for retrieving OpenID configuration information.
|
||||
*/
|
||||
@Inject
|
||||
private ConfigurationService confService;
|
||||
|
||||
/**
|
||||
* Service for validating and generating unique nonce values.
|
||||
*/
|
||||
@Inject
|
||||
private NonceService nonceService;
|
||||
|
||||
/**
|
||||
* Service for validating received ID tokens.
|
||||
*/
|
||||
@Inject
|
||||
private TokenValidationService tokenService;
|
||||
|
||||
/**
|
||||
* Provider for AuthenticatedUser objects.
|
||||
*/
|
||||
@Inject
|
||||
private Provider<SSOAuthenticatedUser> authenticatedUserProvider;
|
||||
|
||||
@Override
|
||||
public SSOAuthenticatedUser authenticateUser(Credentials credentials)
|
||||
throws GuacamoleException {
|
||||
|
||||
String username = null;
|
||||
Set<String> groups = null;
|
||||
Map<String,String> tokens = Collections.emptyMap();
|
||||
|
||||
// Validate OpenID token in request, if present, and derive username
|
||||
String token = credentials.getParameter(TOKEN_PARAMETER_NAME);
|
||||
if (token != null) {
|
||||
JwtClaims claims = tokenService.validateToken(token);
|
||||
if (claims != null) {
|
||||
username = tokenService.processUsername(claims);
|
||||
groups = tokenService.processGroups(claims);
|
||||
tokens = tokenService.processAttributes(claims);
|
||||
}
|
||||
}
|
||||
|
||||
// If the username was successfully retrieved from the token, produce
|
||||
// authenticated user
|
||||
if (username != null) {
|
||||
|
||||
// Create corresponding authenticated user
|
||||
SSOAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
|
||||
authenticatedUser.init(username, credentials, groups, tokens);
|
||||
return authenticatedUser;
|
||||
|
||||
}
|
||||
|
||||
// Request OpenID token (will automatically redirect the user to the
|
||||
// OpenID authorization page via JavaScript)
|
||||
throw new GuacamoleInvalidCredentialsException("Invalid login.",
|
||||
new CredentialsInfo(Arrays.asList(new Field[] {
|
||||
new RedirectField(TOKEN_PARAMETER_NAME, getLoginURI(),
|
||||
new TranslatableMessage("LOGIN.INFO_IDP_REDIRECT_PENDING"))
|
||||
}))
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getLoginURI() throws GuacamoleException {
|
||||
return UriBuilder.fromUri(confService.getAuthorizationEndpoint())
|
||||
.queryParam("scope", confService.getScope())
|
||||
.queryParam("response_type", "id_token")
|
||||
.queryParam("client_id", confService.getClientID())
|
||||
.queryParam("redirect_uri", confService.getRedirectURI())
|
||||
.queryParam("nonce", nonceService.generate(confService.getMaxNonceValidity() * 60000L))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
// Nothing to clean up
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.openid;
|
||||
|
||||
import org.apache.guacamole.auth.sso.SSOAuthenticationProvider;
|
||||
import org.apache.guacamole.auth.sso.SSOResource;
|
||||
|
||||
/**
|
||||
* Guacamole authentication backend which authenticates users using an
|
||||
* arbitrary external system implementing OpenID. No storage for connections is
|
||||
* provided - only authentication. Storage must be provided by some other
|
||||
* extension.
|
||||
*/
|
||||
public class OpenIDAuthenticationProvider extends SSOAuthenticationProvider {
|
||||
|
||||
/**
|
||||
* Creates a new OpenIDAuthenticationProvider that authenticates users
|
||||
* against an OpenID service.
|
||||
*/
|
||||
public OpenIDAuthenticationProvider() {
|
||||
super(AuthenticationProviderService.class, SSOResource.class,
|
||||
new OpenIDAuthenticationProviderModule());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIdentifier() {
|
||||
return "openid";
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.guacamole.auth.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.conf.OpenIDEnvironment;
|
||||
import org.apache.guacamole.auth.sso.NonceService;
|
||||
import org.apache.guacamole.auth.openid.token.TokenValidationService;
|
||||
import org.apache.guacamole.environment.Environment;
|
||||
|
||||
/**
|
||||
* Guice module which configures OpenID-specific injections.
|
||||
*/
|
||||
public class OpenIDAuthenticationProviderModule extends AbstractModule {
|
||||
|
||||
/**
|
||||
* The configuration environment for this server and extension.
|
||||
*/
|
||||
private final Environment environment = new OpenIDEnvironment();
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(ConfigurationService.class);
|
||||
bind(NonceService.class).in(Scopes.SINGLETON);
|
||||
bind(TokenValidationService.class);
|
||||
|
||||
bind(Environment.class).toInstance(environment);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,432 @@
|
||||
/*
|
||||
* 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.openid.conf;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import java.net.URI;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.environment.Environment;
|
||||
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
|
||||
import org.apache.guacamole.properties.StringGuacamoleProperty;
|
||||
import org.apache.guacamole.properties.URIGuacamoleProperty;
|
||||
|
||||
/**
|
||||
* Service for retrieving configuration information regarding the OpenID
|
||||
* service.
|
||||
*/
|
||||
public class ConfigurationService {
|
||||
|
||||
/**
|
||||
* The default claim type to use to retrieve an authenticated user's
|
||||
* username.
|
||||
*/
|
||||
private static final String DEFAULT_USERNAME_CLAIM_TYPE = "email";
|
||||
|
||||
/**
|
||||
* The default claim type to use to retrieve an authenticated user's
|
||||
* groups.
|
||||
*/
|
||||
private static final String DEFAULT_GROUPS_CLAIM_TYPE = "groups";
|
||||
|
||||
/**
|
||||
* The default JWT claims list to map to tokens.
|
||||
*/
|
||||
private static final List<String> DEFAULT_ATTRIBUTES_CLAIM_TYPE = Collections.emptyList();
|
||||
|
||||
/**
|
||||
* The default space-separated list of OpenID scopes to request.
|
||||
*/
|
||||
private static final String DEFAULT_SCOPE = "openid email profile";
|
||||
|
||||
/**
|
||||
* The default amount of clock skew tolerated for timestamp comparisons
|
||||
* between the Guacamole server and OpenID service clocks, in seconds.
|
||||
*/
|
||||
private static final int DEFAULT_ALLOWED_CLOCK_SKEW = 30;
|
||||
|
||||
/**
|
||||
* The default maximum amount of time that an OpenID token should remain
|
||||
* valid, in minutes.
|
||||
*/
|
||||
private static final int DEFAULT_MAX_TOKEN_VALIDITY = 300;
|
||||
|
||||
/**
|
||||
* The default maximum amount of time that a nonce generated by the
|
||||
* Guacamole server should remain valid, in minutes.
|
||||
*/
|
||||
private static final int DEFAULT_MAX_NONCE_VALIDITY = 10;
|
||||
|
||||
/**
|
||||
* The authorization endpoint (URI) of the OpenID service.
|
||||
*/
|
||||
private static final URIGuacamoleProperty OPENID_AUTHORIZATION_ENDPOINT =
|
||||
new URIGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "openid-authorization-endpoint"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The endpoint (URI) of the JWKS service which defines how received ID
|
||||
* tokens (JWTs) shall be validated.
|
||||
*/
|
||||
private static final URIGuacamoleProperty OPENID_JWKS_ENDPOINT =
|
||||
new URIGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "openid-jwks-endpoint"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The issuer to expect for all received ID tokens.
|
||||
*/
|
||||
private static final StringGuacamoleProperty OPENID_ISSUER =
|
||||
new StringGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "openid-issuer"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The claim type which contains the authenticated user's username within
|
||||
* any valid JWT.
|
||||
*/
|
||||
private static final StringGuacamoleProperty OPENID_USERNAME_CLAIM_TYPE =
|
||||
new StringGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "openid-username-claim-type"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The claim type which contains the authenticated user's groups within
|
||||
* any valid JWT.
|
||||
*/
|
||||
private static final StringGuacamoleProperty OPENID_GROUPS_CLAIM_TYPE =
|
||||
new StringGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "openid-groups-claim-type"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The claims within any valid JWT that should be mapped to
|
||||
* the authenticated user's tokens, as configured with guacamole.properties.
|
||||
*/
|
||||
private static final StringGuacamoleProperty OPENID_ATTRIBUTES_CLAIM_TYPE =
|
||||
new StringGuacamoleProperty() {
|
||||
@Override
|
||||
public String getName() { return "openid-attributes-claim-type"; }
|
||||
};
|
||||
|
||||
/**
|
||||
* The space-separated list of OpenID scopes to request.
|
||||
*/
|
||||
private static final StringGuacamoleProperty OPENID_SCOPE =
|
||||
new StringGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "openid-scope"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The amount of clock skew tolerated for timestamp comparisons between the
|
||||
* Guacamole server and OpenID service clocks, in seconds.
|
||||
*/
|
||||
private static final IntegerGuacamoleProperty OPENID_ALLOWED_CLOCK_SKEW =
|
||||
new IntegerGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "openid-allowed-clock-skew"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The maximum amount of time that an OpenID token should remain valid, in
|
||||
* minutes.
|
||||
*/
|
||||
private static final IntegerGuacamoleProperty OPENID_MAX_TOKEN_VALIDITY =
|
||||
new IntegerGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "openid-max-token-validity"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The maximum amount of time that a nonce generated by the Guacamole server
|
||||
* should remain valid, in minutes. As each OpenID request has a unique
|
||||
* nonce value, this imposes an upper limit on the amount of time any
|
||||
* particular OpenID request can result in successful authentication within
|
||||
* Guacamole.
|
||||
*/
|
||||
private static final IntegerGuacamoleProperty OPENID_MAX_NONCE_VALIDITY =
|
||||
new IntegerGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "openid-max-nonce-validity"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* OpenID client ID which should be submitted to the OpenID service when
|
||||
* necessary. This value is typically provided by the OpenID service when
|
||||
* OpenID credentials are generated for your application.
|
||||
*/
|
||||
private static final StringGuacamoleProperty OPENID_CLIENT_ID =
|
||||
new StringGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "openid-client-id"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The URI that the OpenID service should redirect to after the
|
||||
* authentication process is complete. This must be the full URL that a
|
||||
* user would enter into their browser to access Guacamole.
|
||||
*/
|
||||
private static final URIGuacamoleProperty OPENID_REDIRECT_URI =
|
||||
new URIGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "openid-redirect-uri"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The Guacamole server environment.
|
||||
*/
|
||||
@Inject
|
||||
private Environment environment;
|
||||
|
||||
/**
|
||||
* Returns the authorization endpoint (URI) of the OpenID service as
|
||||
* configured with guacamole.properties.
|
||||
*
|
||||
* @return
|
||||
* The authorization endpoint of the OpenID service, as configured with
|
||||
* guacamole.properties.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed, or if the authorization
|
||||
* endpoint property is missing.
|
||||
*/
|
||||
public URI getAuthorizationEndpoint() throws GuacamoleException {
|
||||
return environment.getRequiredProperty(OPENID_AUTHORIZATION_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OpenID client ID which should be submitted to the OpenID
|
||||
* service when necessary, as configured with guacamole.properties. This
|
||||
* value is typically provided by the OpenID service when OpenID credentials
|
||||
* are generated for your application.
|
||||
*
|
||||
* @return
|
||||
* The client ID to use when communicating with the OpenID service,
|
||||
* as configured with guacamole.properties.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed, or if the client ID
|
||||
* property is missing.
|
||||
*/
|
||||
public String getClientID() throws GuacamoleException {
|
||||
return environment.getRequiredProperty(OPENID_CLIENT_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URI that the OpenID service should redirect to after
|
||||
* the authentication process is complete, as configured with
|
||||
* guacamole.properties. This must be the full URL that a user would enter
|
||||
* into their browser to access Guacamole.
|
||||
*
|
||||
* @return
|
||||
* The client secret to use when communicating with the OpenID service,
|
||||
* as configured with guacamole.properties.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed, or if the redirect URI
|
||||
* property is missing.
|
||||
*/
|
||||
public URI getRedirectURI() throws GuacamoleException {
|
||||
return environment.getRequiredProperty(OPENID_REDIRECT_URI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the issuer to expect for all received ID tokens, as configured
|
||||
* with guacamole.properties.
|
||||
*
|
||||
* @return
|
||||
* The issuer to expect for all received ID tokens, as configured with
|
||||
* guacamole.properties.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed, or if the issuer property
|
||||
* is missing.
|
||||
*/
|
||||
public String getIssuer() throws GuacamoleException {
|
||||
return environment.getRequiredProperty(OPENID_ISSUER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the endpoint (URI) of the JWKS service which defines how
|
||||
* received ID tokens (JWTs) shall be validated, as configured with
|
||||
* guacamole.properties.
|
||||
*
|
||||
* @return
|
||||
* The endpoint (URI) of the JWKS service which defines how received ID
|
||||
* tokens (JWTs) shall be validated, as configured with
|
||||
* guacamole.properties.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed, or if the JWKS endpoint
|
||||
* property is missing.
|
||||
*/
|
||||
public URI getJWKSEndpoint() throws GuacamoleException {
|
||||
return environment.getRequiredProperty(OPENID_JWKS_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the claim type which contains the authenticated user's username
|
||||
* within any valid JWT, as configured with guacamole.properties. By
|
||||
* default, this will be "email".
|
||||
*
|
||||
* @return
|
||||
* The claim type which contains the authenticated user's username
|
||||
* within any valid JWT, as configured with guacamole.properties.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed.
|
||||
*/
|
||||
public String getUsernameClaimType() throws GuacamoleException {
|
||||
return environment.getProperty(OPENID_USERNAME_CLAIM_TYPE, DEFAULT_USERNAME_CLAIM_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the claim type which contains the authenticated user's groups
|
||||
* within any valid JWT, as configured with guacamole.properties. By
|
||||
* default, this will be "groups".
|
||||
*
|
||||
* @return
|
||||
* The claim type which contains the authenticated user's groups
|
||||
* within any valid JWT, as configured with guacamole.properties.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed.
|
||||
*/
|
||||
public String getGroupsClaimType() throws GuacamoleException {
|
||||
return environment.getProperty(OPENID_GROUPS_CLAIM_TYPE, DEFAULT_GROUPS_CLAIM_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the claims list within any valid JWT that should be mapped to
|
||||
* the authenticated user's tokens, as configured with guacamole.properties.
|
||||
* Empty by default.
|
||||
*
|
||||
* @return
|
||||
* The claims list within any valid JWT that should be mapped to
|
||||
* the authenticated user's tokens, as configured with guacamole.properties.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed.
|
||||
*/
|
||||
public Collection<String> getAttributesClaimType() throws GuacamoleException {
|
||||
return environment.getPropertyCollection(OPENID_ATTRIBUTES_CLAIM_TYPE, DEFAULT_ATTRIBUTES_CLAIM_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the space-separated list of OpenID scopes to request. By default,
|
||||
* this will be "openid email profile". The OpenID scopes determine the
|
||||
* information returned within the OpenID token, and thus affect what
|
||||
* values can be used as an authenticated user's username.
|
||||
*
|
||||
* @return
|
||||
* The space-separated list of OpenID scopes to request when identifying
|
||||
* a user.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed.
|
||||
*/
|
||||
public String getScope() throws GuacamoleException {
|
||||
return environment.getProperty(OPENID_SCOPE, DEFAULT_SCOPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of clock skew tolerated for timestamp comparisons
|
||||
* between the Guacamole server and OpenID service clocks, in seconds. Too
|
||||
* much clock skew will affect token expiration calculations, possibly
|
||||
* allowing old tokens to be used. By default, this will be 30.
|
||||
*
|
||||
* @return
|
||||
* The amount of clock skew tolerated for timestamp comparisons, in
|
||||
* seconds.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed.
|
||||
*/
|
||||
public int getAllowedClockSkew() throws GuacamoleException {
|
||||
return environment.getProperty(OPENID_ALLOWED_CLOCK_SKEW, DEFAULT_ALLOWED_CLOCK_SKEW);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum amount of time that an OpenID token should remain
|
||||
* valid, in minutes. A token received from an OpenID service which is
|
||||
* older than this amount of time will be rejected, even if it is otherwise
|
||||
* valid. By default, this will be 300 (5 hours).
|
||||
*
|
||||
* @return
|
||||
* The maximum amount of time that an OpenID token should remain valid,
|
||||
* in minutes.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed.
|
||||
*/
|
||||
public int getMaxTokenValidity() throws GuacamoleException {
|
||||
return environment.getProperty(OPENID_MAX_TOKEN_VALIDITY, DEFAULT_MAX_TOKEN_VALIDITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum amount of time that a nonce generated by the
|
||||
* Guacamole server should remain valid, in minutes. As each OpenID request
|
||||
* has a unique nonce value, this imposes an upper limit on the amount of
|
||||
* time any particular OpenID request can result in successful
|
||||
* authentication within Guacamole. By default, this will be 10.
|
||||
*
|
||||
* @return
|
||||
* The maximum amount of time that a nonce generated by the Guacamole
|
||||
* server should remain valid, in minutes.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed.
|
||||
*/
|
||||
public int getMaxNonceValidity() throws GuacamoleException {
|
||||
return environment.getProperty(OPENID_MAX_NONCE_VALIDITY, DEFAULT_MAX_NONCE_VALIDITY);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.guacamole.auth.openid.conf;
|
||||
|
||||
import org.apache.guacamole.environment.DelegatingEnvironment;
|
||||
import org.apache.guacamole.environment.LocalEnvironment;
|
||||
|
||||
/**
|
||||
* An environment for retrieving OpenID-related properties from the Guacamole
|
||||
* configuration.
|
||||
*/
|
||||
public class OpenIDEnvironment extends DelegatingEnvironment {
|
||||
|
||||
/**
|
||||
* Create a new instance of the configuration environment for the
|
||||
* OpenID SSO module, pulling the default instance of the LocalEnvironment.
|
||||
*/
|
||||
public OpenIDEnvironment() {
|
||||
super(LocalEnvironment.getInstance());
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* 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.openid.token;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.auth.openid.conf.ConfigurationService;
|
||||
import org.apache.guacamole.auth.sso.NonceService;
|
||||
import org.apache.guacamole.token.TokenName;
|
||||
import org.jose4j.jwk.HttpsJwks;
|
||||
import org.jose4j.jwt.JwtClaims;
|
||||
import org.jose4j.jwt.MalformedClaimException;
|
||||
import org.jose4j.jwt.consumer.InvalidJwtException;
|
||||
import org.jose4j.jwt.consumer.JwtConsumer;
|
||||
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
|
||||
import org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Service for validating ID tokens forwarded to us by the client, verifying
|
||||
* that they did indeed come from the OpenID service.
|
||||
*/
|
||||
public class TokenValidationService {
|
||||
|
||||
/**
|
||||
* Logger for this class.
|
||||
*/
|
||||
private final Logger logger = LoggerFactory.getLogger(TokenValidationService.class);
|
||||
|
||||
/**
|
||||
* The prefix to use when generating token names.
|
||||
*/
|
||||
public static final String OIDC_ATTRIBUTE_TOKEN_PREFIX = "OIDC_";
|
||||
|
||||
/**
|
||||
* Service for retrieving OpenID configuration information.
|
||||
*/
|
||||
@Inject
|
||||
private ConfigurationService confService;
|
||||
|
||||
/**
|
||||
* Service for validating and generating unique nonce values.
|
||||
*/
|
||||
@Inject
|
||||
private NonceService nonceService;
|
||||
|
||||
/**
|
||||
* Validates the given ID token, returning the JwtClaims contained therein.
|
||||
* If the ID token is invalid, null is returned.
|
||||
*
|
||||
* @param token
|
||||
* The ID token to validate.
|
||||
*
|
||||
* @return
|
||||
* The JWT claims contained within the given ID token if it passes tests,
|
||||
* or null if the token is not valid.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties could not be parsed.
|
||||
*/
|
||||
public JwtClaims validateToken(String token) throws GuacamoleException {
|
||||
// Validating the token requires a JWKS key resolver
|
||||
HttpsJwks jwks = new HttpsJwks(confService.getJWKSEndpoint().toString());
|
||||
HttpsJwksVerificationKeyResolver resolver = new HttpsJwksVerificationKeyResolver(jwks);
|
||||
|
||||
// Create JWT consumer for validating received token
|
||||
JwtConsumer jwtConsumer = new JwtConsumerBuilder()
|
||||
.setRequireExpirationTime()
|
||||
.setMaxFutureValidityInMinutes(confService.getMaxTokenValidity())
|
||||
.setAllowedClockSkewInSeconds(confService.getAllowedClockSkew())
|
||||
.setRequireSubject()
|
||||
.setExpectedIssuer(confService.getIssuer())
|
||||
.setExpectedAudience(confService.getClientID())
|
||||
.setVerificationKeyResolver(resolver)
|
||||
.build();
|
||||
|
||||
try {
|
||||
// Validate JWT
|
||||
JwtClaims claims = jwtConsumer.processToClaims(token);
|
||||
|
||||
// Verify a nonce is present
|
||||
String nonce = claims.getStringClaimValue("nonce");
|
||||
if (nonce != null) {
|
||||
// Verify that we actually generated the nonce, and that it has not
|
||||
// already been used
|
||||
if (nonceService.isValid(nonce)) {
|
||||
// nonce is valid, consider claims valid
|
||||
return claims;
|
||||
}
|
||||
else {
|
||||
logger.info("Rejected OpenID token with invalid/old nonce.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.info("Rejected OpenID token without nonce.");
|
||||
}
|
||||
}
|
||||
// Log any failures to validate/parse the JWT
|
||||
catch (MalformedClaimException e) {
|
||||
logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage());
|
||||
logger.debug("Malformed claim within received JWT.", e);
|
||||
}
|
||||
catch (InvalidJwtException e) {
|
||||
logger.info("Rejected invalid OpenID token: {}", e.getMessage());
|
||||
logger.debug("Invalid JWT received.", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given JwtClaims, returning the username contained
|
||||
* therein, as defined by the username claim type given in
|
||||
* guacamole.properties. If the username claim type is missing or
|
||||
* is invalid, null is returned.
|
||||
*
|
||||
* @param claims
|
||||
* A valid JwtClaims to extract the username from.
|
||||
*
|
||||
* @return
|
||||
* The username contained within the given JwtClaims, or null if the
|
||||
* claim is not valid or the username claim type is missing,
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties could not be parsed.
|
||||
*/
|
||||
public String processUsername(JwtClaims claims) throws GuacamoleException {
|
||||
String usernameClaim = confService.getUsernameClaimType();
|
||||
|
||||
if (claims != null) {
|
||||
try {
|
||||
// Pull username from claims
|
||||
String username = claims.getStringClaimValue(usernameClaim);
|
||||
if (username != null)
|
||||
return username;
|
||||
}
|
||||
catch (MalformedClaimException e) {
|
||||
logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage());
|
||||
logger.debug("Malformed claim within received JWT.", e);
|
||||
}
|
||||
|
||||
// Warn if username was not present in token, as it likely means
|
||||
// the system is not set up correctly
|
||||
logger.warn("Username claim \"{}\" missing from token. Perhaps the "
|
||||
+ "OpenID scope and/or username claim type are "
|
||||
+ "misconfigured?", usernameClaim);
|
||||
}
|
||||
|
||||
// Could not retrieve username from JWT
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given JwtClaims, returning the groups contained
|
||||
* therein, as defined by the groups claim type given in
|
||||
* guacamole.properties. If the groups claim type is missing or
|
||||
* is invalid, an empty set is returned.
|
||||
*
|
||||
* @param claims
|
||||
* A valid JwtClaims to extract groups from.
|
||||
*
|
||||
* @return
|
||||
* A Set of String representing the groups the user is member of
|
||||
* from the OpenID provider point of view, or an empty Set if
|
||||
* claim is not valid or the groups claim type is missing,
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties could not be parsed.
|
||||
*/
|
||||
public Set<String> processGroups(JwtClaims claims) throws GuacamoleException {
|
||||
String groupsClaim = confService.getGroupsClaimType();
|
||||
|
||||
if (claims != null) {
|
||||
try {
|
||||
// Pull groups from claims
|
||||
List<String> oidcGroups = claims.getStringListClaimValue(groupsClaim);
|
||||
if (oidcGroups != null && !oidcGroups.isEmpty())
|
||||
return Collections.unmodifiableSet(new HashSet<>(oidcGroups));
|
||||
}
|
||||
catch (MalformedClaimException e) {
|
||||
logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage());
|
||||
logger.debug("Malformed claim within received JWT.", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Could not retrieve groups from JWT
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given JwtClaims, returning the attributes contained
|
||||
* therein, as defined by the attributes claim type given in
|
||||
* guacamole.properties. If the attributes claim type is missing or
|
||||
* is invalid, an empty set is returned.
|
||||
*
|
||||
* @param claims
|
||||
* A valid JwtClaims to extract attributes from.
|
||||
*
|
||||
* @return
|
||||
* A Map of String,String representing the attributes and values
|
||||
* from the OpenID provider point of view, or an empty Map if
|
||||
* claim is not valid or the attributes claim type is missing.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties could not be parsed.
|
||||
*/
|
||||
public Map<String, String> processAttributes(JwtClaims claims) throws GuacamoleException {
|
||||
Collection<String> attributesClaim = confService.getAttributesClaimType();
|
||||
|
||||
if (claims != null && !attributesClaim.isEmpty()) {
|
||||
try {
|
||||
logger.debug("Iterating over attributes claim list : {}", attributesClaim);
|
||||
|
||||
// We suppose all claims are resolved, so the hashmap is initialised to
|
||||
// the size of the configuration list
|
||||
Map<String, String> tokens = new HashMap<String, String>(attributesClaim.size());
|
||||
|
||||
// We iterate over the configured attributes
|
||||
for (String key: attributesClaim) {
|
||||
// Retrieve the corresponding claim
|
||||
String oidcAttr = claims.getStringClaimValue(key);
|
||||
|
||||
// We do have a matching claim and it is not empty
|
||||
if (oidcAttr != null && !oidcAttr.isEmpty()) {
|
||||
// append the prefixed claim value to the token map with its value
|
||||
String tokenName = TokenName.canonicalize(key, OIDC_ATTRIBUTE_TOKEN_PREFIX);
|
||||
tokens.put(tokenName, oidcAttr);
|
||||
logger.debug("Claim {} found and set to {}", key, tokenName);
|
||||
}
|
||||
else {
|
||||
// wanted attribute is not found in the claim
|
||||
logger.debug("Claim {} not found in JWT.", key);
|
||||
}
|
||||
}
|
||||
|
||||
// We did process all the expected claims
|
||||
return Collections.unmodifiableMap(tokens);
|
||||
}
|
||||
catch (MalformedClaimException e) {
|
||||
logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage());
|
||||
logger.debug("Malformed claim within received JWT.", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Could not retrieve attributes from JWT
|
||||
logger.debug("Attributes claim not defined. Returning empty map.");
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
{
|
||||
|
||||
"guacamoleVersion" : "1.6.0",
|
||||
|
||||
"name" : "OpenID Authentication Extension",
|
||||
"namespace" : "openid",
|
||||
|
||||
"authProviders" : [
|
||||
"org.apache.guacamole.auth.openid.OpenIDAuthenticationProvider"
|
||||
],
|
||||
|
||||
"css" : [
|
||||
"styles/sso-providers.css"
|
||||
],
|
||||
|
||||
"html" : [
|
||||
"html/sso-providers.html",
|
||||
"html/sso-provider-openid.html"
|
||||
],
|
||||
|
||||
"translations" : [
|
||||
"translations/ca.json",
|
||||
"translations/de.json",
|
||||
"translations/en.json",
|
||||
"translations/fr.json",
|
||||
"translations/ja.json",
|
||||
"translations/ko.json",
|
||||
"translations/pl.json",
|
||||
"translations/pt.json",
|
||||
"translations/ru.json",
|
||||
"translations/zh.json"
|
||||
],
|
||||
|
||||
"js" : [
|
||||
"openid.min.js"
|
||||
]
|
||||
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
<meta name="after-children" content=".login-ui .sso-provider-list">
|
||||
<li class="sso-provider sso-provider-openid"><a href="api/ext/openid/login">{{
|
||||
'LOGIN.NAME_IDP_OPENID' | 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,35 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Before AngularJS routing takes effect, reformat the URL fragment
|
||||
* from the format used by OpenID Connect ("#param1=value1¶m2=value2&...")
|
||||
* to the format used by AngularJS ("#/?param1=value1¶m2=value2&...") such
|
||||
* that the client side of Guacamole's authentication system will automatically
|
||||
* forward the "id_token" value for server-side validation.
|
||||
*
|
||||
* Note that not all OpenID identity providers will include the "id_token"
|
||||
* parameter in the first position; it may occur after several other parameters
|
||||
* within the fragment.
|
||||
*/
|
||||
|
||||
(function guacOpenIDTransformToken() {
|
||||
if (/^#(?![?\/])(.*&)?id_token=/.test(location.hash))
|
||||
location.hash = '/?' + location.hash.substring(1);
|
||||
})();
|
3
extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/.gitignore
vendored
Normal file
3
extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*~
|
||||
target/
|
||||
src/main/resources/generated/
|
@@ -0,0 +1 @@
|
||||
src/main/resources/html/*.html
|
@@ -0,0 +1,165 @@
|
||||
<?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-saml</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>1.6.0</version>
|
||||
<name>guacamole-auth-sso-saml</name>
|
||||
<url>http://guacamole.apache.org/</url>
|
||||
|
||||
<parent>
|
||||
<groupId>org.apache.guacamole</groupId>
|
||||
<artifactId>guacamole-auth-sso</artifactId>
|
||||
<version>1.6.0</version>
|
||||
<relativePath>../../</relativePath>
|
||||
</parent>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- OneLogin SAML Library -->
|
||||
<dependency>
|
||||
<groupId>com.onelogin</groupId>
|
||||
<artifactId>java-saml</artifactId>
|
||||
<version>2.9.0</version>
|
||||
<exclusions>
|
||||
|
||||
<!--
|
||||
Replace vulnerable version of Woodstox until upstream
|
||||
releases a version with fixed dependencies
|
||||
-->
|
||||
<exclusion>
|
||||
<groupId>com.fasterxml.woodstox</groupId>
|
||||
<artifactId>woodstox-core</artifactId>
|
||||
</exclusion>
|
||||
|
||||
<!--
|
||||
Replace vulnerable version of xmlsec until upstream
|
||||
releases a version with fixed dependencies
|
||||
-->
|
||||
<exclusion>
|
||||
<groupId>org.apache.santuario</groupId>
|
||||
<artifactId>xmlsec</artifactId>
|
||||
</exclusion>
|
||||
|
||||
<!--
|
||||
Replace slightly older commons-lang3 (3.12.0) with latest
|
||||
compatible version (3.17.0) so that we don't need two copies
|
||||
of the same license information.
|
||||
-->
|
||||
<exclusion>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</exclusion>
|
||||
|
||||
<!--
|
||||
Replace slightly older commons-codec (1.15) with newer
|
||||
and identical version to that used by Apache Directory API
|
||||
for LDAP (1.16.0) so that we don't need two copies of the
|
||||
same license information.
|
||||
-->
|
||||
<exclusion>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
</exclusion>
|
||||
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Woodstox (see exclusions for java-saml) -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.woodstox</groupId>
|
||||
<artifactId>woodstox-core</artifactId>
|
||||
<version>5.4.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache XML Security for Java (see exclusions for java-saml) -->
|
||||
<dependency>
|
||||
<groupId>org.apache.santuario</groupId>
|
||||
<artifactId>xmlsec</artifactId>
|
||||
<version>2.2.6</version>
|
||||
<exclusions>
|
||||
|
||||
<!--
|
||||
Replace slightly older commons-codec (1.15) with newer
|
||||
and identical version to that used by Apache Directory API
|
||||
for LDAP (1.17.1) so that we don't need two copies of the
|
||||
same license information.
|
||||
-->
|
||||
<exclusion>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
</exclusion>
|
||||
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache Commons Lang (see exclusions for java-saml) -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.17.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache Commons Codec (see exclusions for java-saml and xmlsec) -->
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>1.17.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
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.SAMLAuthenticationSessionManager;
|
||||
import org.apache.guacamole.auth.saml.acs.SAMLService;
|
||||
import org.apache.guacamole.auth.sso.SSOAuthenticationProviderService;
|
||||
import org.apache.guacamole.form.Field;
|
||||
import org.apache.guacamole.form.RedirectField;
|
||||
import org.apache.guacamole.language.TranslatableMessage;
|
||||
import org.apache.guacamole.net.auth.Credentials;
|
||||
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
|
||||
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
|
||||
|
||||
/**
|
||||
* Service that authenticates Guacamole users by processing the responses of
|
||||
* SAML identity providers.
|
||||
*/
|
||||
@Singleton
|
||||
public class AuthenticationProviderService implements SSOAuthenticationProviderService {
|
||||
|
||||
/**
|
||||
* The name of the query parameter that identifies an active authentication
|
||||
* session (in-progress SAML authentication attempt).
|
||||
*/
|
||||
public static final String AUTH_SESSION_QUERY_PARAM = "state";
|
||||
|
||||
/**
|
||||
* Provider for AuthenticatedUser objects.
|
||||
*/
|
||||
@Inject
|
||||
private Provider<SAMLAuthenticatedUser> authenticatedUserProvider;
|
||||
|
||||
/**
|
||||
* Manager of active SAML authentication attempts.
|
||||
*/
|
||||
@Inject
|
||||
private SAMLAuthenticationSessionManager sessionManager;
|
||||
|
||||
/**
|
||||
* Service for processing SAML requests/responses.
|
||||
*/
|
||||
@Inject
|
||||
private SAMLService saml;
|
||||
|
||||
/**
|
||||
* Return the value of the session identifier associated with the given
|
||||
* credentials, or null if no session identifier is found in the
|
||||
* credentials.
|
||||
*
|
||||
* @param credentials
|
||||
* The credentials from which to extract the session identifier.
|
||||
*
|
||||
* @return
|
||||
* The session identifier associated with the given credentials, or
|
||||
* null if no identifier is found.
|
||||
*/
|
||||
public static String getSessionIdentifier(Credentials credentials) {
|
||||
|
||||
// Return the session identifier from the request params, if set, or
|
||||
// null otherwise
|
||||
return credentials != null ? credentials.getParameter(AUTH_SESSION_QUERY_PARAM) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SAMLAuthenticatedUser authenticateUser(Credentials credentials)
|
||||
throws GuacamoleException {
|
||||
|
||||
// Use established SAML identity if already provided by the SAML IdP
|
||||
AssertedIdentity identity = sessionManager.getIdentity(
|
||||
getSessionIdentifier(credentials));
|
||||
|
||||
if (identity != null) {
|
||||
SAMLAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
|
||||
authenticatedUser.init(identity, credentials);
|
||||
return authenticatedUser;
|
||||
}
|
||||
|
||||
// Redirect to SAML IdP if no SAML identity is associated with the
|
||||
// Guacamole authentication request
|
||||
throw new GuacamoleInvalidCredentialsException("Redirecting to SAML IdP.",
|
||||
new CredentialsInfo(Arrays.asList(new Field[] {
|
||||
new RedirectField(AUTH_SESSION_QUERY_PARAM, getLoginURI(),
|
||||
new TranslatableMessage("LOGIN.INFO_IDP_REDIRECT_PENDING"))
|
||||
}))
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getLoginURI() throws GuacamoleException {
|
||||
return saml.createRequest();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
sessionManager.shutdown();
|
||||
}
|
||||
|
||||
}
|
@@ -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.saml;
|
||||
|
||||
import org.apache.guacamole.auth.saml.acs.SAMLAuthenticationSessionManager;
|
||||
import org.apache.guacamole.auth.sso.SSOAuthenticationEventListener;
|
||||
import org.apache.guacamole.net.auth.Credentials;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
|
||||
/**
|
||||
* A Listener that will reactivate or invalidate SAML auth sessions depending on
|
||||
* overall auth success or failure.
|
||||
*/
|
||||
public class SAMLAuthenticationEventListener extends SSOAuthenticationEventListener {
|
||||
|
||||
/**
|
||||
* Session manager for generating and maintaining unique tokens to
|
||||
* represent the authentication flow of a user who has only partially
|
||||
* authenticated.
|
||||
*
|
||||
* Requires static injection due to the fact that the webapp just calls the
|
||||
* constructor directly when creating new Listeners. The instances will not
|
||||
* be constructed by guice.
|
||||
*
|
||||
* Note that is possible to instead inject an AuthenticationSessionManager
|
||||
* instance directly into the base class, but this results in different
|
||||
* instances of the session manager injected here and in the rest of the
|
||||
* extension, regardless of singleton configuration for the service.
|
||||
*/
|
||||
@Inject
|
||||
protected static SAMLAuthenticationSessionManager sessionManager;
|
||||
|
||||
@Override
|
||||
protected String getSessionIdentifier(Credentials credentials) {
|
||||
return AuthenticationProviderService.getSessionIdentifier(credentials);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reactivateSession(String sessionIdentifier) {
|
||||
sessionManager.reactivateSession(sessionIdentifier);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidateSession(String sessionIdentifier) {
|
||||
sessionManager.invalidateSession(sessionIdentifier);
|
||||
}
|
||||
|
||||
}
|
@@ -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.saml;
|
||||
|
||||
import org.apache.guacamole.auth.saml.acs.AssertionConsumerServiceResource;
|
||||
import org.apache.guacamole.auth.sso.SSOAuthenticationProvider;
|
||||
|
||||
/**
|
||||
* AuthenticationProvider implementation that authenticates Guacamole users
|
||||
* against a SAML SSO Identity Provider (IdP). This module does not provide any
|
||||
* storage for connection information, and must be layered with other modules
|
||||
* for authenticated users to have access to Guacamole connections.
|
||||
*/
|
||||
public class SAMLAuthenticationProvider extends SSOAuthenticationProvider {
|
||||
|
||||
/**
|
||||
* Creates a new SAMLAuthenticationProvider that authenticates users
|
||||
* against a SAML IdP.
|
||||
*/
|
||||
public SAMLAuthenticationProvider() {
|
||||
super(AuthenticationProviderService.class,
|
||||
AssertionConsumerServiceResource.class,
|
||||
new SAMLAuthenticationProviderModule());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIdentifier() {
|
||||
return "saml";
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
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.SAMLAuthenticationSessionManager;
|
||||
import org.apache.guacamole.auth.saml.acs.SAMLService;
|
||||
import org.apache.guacamole.auth.saml.conf.SAMLEnvironment;
|
||||
import org.apache.guacamole.environment.Environment;
|
||||
|
||||
/**
|
||||
* Guice module which configures SAML-specific injections.
|
||||
*/
|
||||
public class SAMLAuthenticationProviderModule extends AbstractModule {
|
||||
|
||||
/**
|
||||
* The environment for this server and extension.
|
||||
*/
|
||||
private final Environment environment = new SAMLEnvironment();
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(AssertionConsumerServiceResource.class);
|
||||
bind(ConfigurationService.class);
|
||||
bind(SAMLAuthenticationSessionManager.class);
|
||||
bind(SAMLService.class);
|
||||
|
||||
bind(Environment.class).toInstance(environment);
|
||||
|
||||
requestStaticInjection(SAMLAuthenticationEventListener.class);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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.onelogin.saml2.authn.SamlResponse;
|
||||
import com.onelogin.saml2.exception.ValidationError;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.GuacamoleSecurityException;
|
||||
|
||||
/**
|
||||
* Representation of a user's identity as asserted by a SAML IdP.
|
||||
*/
|
||||
public class AssertedIdentity {
|
||||
|
||||
/**
|
||||
* The original SAML response received from the SAML IdP that asserted
|
||||
* the user's identity.
|
||||
*/
|
||||
private final SamlResponse response;
|
||||
|
||||
/**
|
||||
* The user's Guacamole username.
|
||||
*/
|
||||
private final String username;
|
||||
|
||||
/**
|
||||
* All attributes included in the original SAML response. Attributes may
|
||||
* possibly be associated with multiple values.
|
||||
*/
|
||||
private final Map<String, List<String>> attributes;
|
||||
|
||||
/**
|
||||
* Creates a new AssertedIdentity representing the identity asserted by the
|
||||
* SAML IdP in the given response.
|
||||
*
|
||||
* @param response
|
||||
* The response received from the SAML IdP.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If the given SAML response cannot be parsed or is missing required
|
||||
* information.
|
||||
*/
|
||||
public AssertedIdentity(SamlResponse response) throws GuacamoleException {
|
||||
|
||||
// Parse user identity from SAML response
|
||||
String nameId;
|
||||
try {
|
||||
nameId = response.getNameId();
|
||||
if (nameId == null)
|
||||
throw new GuacamoleSecurityException("SAML response did not "
|
||||
+ "include the relevant user's identity (no name ID).");
|
||||
}
|
||||
|
||||
// Unfortunately, getNameId() is declared as "throws Exception", so
|
||||
// this error handling has to be pretty generic
|
||||
catch (Exception e) {
|
||||
throw new GuacamoleSecurityException("User identity (name ID) "
|
||||
+ "could not be retrieved from the SAML response: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
// Retrieve any provided attributes
|
||||
Map<String, List<String>> responseAttributes;
|
||||
try {
|
||||
responseAttributes = Collections.unmodifiableMap(response.getAttributes());
|
||||
}
|
||||
catch (XPathExpressionException | ValidationError e) {
|
||||
throw new GuacamoleSecurityException("SAML attributes could not "
|
||||
+ "be parsed from the SAML response: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
this.response = response;
|
||||
this.username = nameId.toLowerCase(); // Canonicalize username as lowercase
|
||||
this.attributes = responseAttributes;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the username of the Guacamole user whose identity was asserted
|
||||
* by the SAML IdP.
|
||||
*
|
||||
* @return
|
||||
* The username of the Guacamole user whose identity was asserted by
|
||||
* the SAML IdP.
|
||||
*/
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Map containing all attributes included in the original SAML
|
||||
* response that asserted this user's identity. Attributes may possibly be
|
||||
* associated with multiple values.
|
||||
*
|
||||
* @return
|
||||
* A Map of all attributes included in the original SAML response.
|
||||
*/
|
||||
public Map<String, List<String>> getAttributes() {
|
||||
return attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the identity asserted by the original SAML response is
|
||||
* still valid. An asserted identity may cease to be valid after creation
|
||||
* if it has expired according to the timestamps included in the response.
|
||||
*
|
||||
* @return
|
||||
* true if the original SAML response is still valid, false otherwise.
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return response.isValid();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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.Inject;
|
||||
import java.net.URI;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.FormParam;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.auth.saml.AuthenticationProviderService;
|
||||
import org.apache.guacamole.auth.saml.conf.ConfigurationService;
|
||||
import org.apache.guacamole.auth.sso.SSOResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* REST API resource that provides a SAML assertion consumer service (ACS)
|
||||
* endpoint. SAML identity providers will issue an HTTP POST to this endpoint
|
||||
* asserting the user's identity when the user has successfully authenticated.
|
||||
*/
|
||||
public class AssertionConsumerServiceResource extends SSOResource {
|
||||
|
||||
/**
|
||||
* Logger for this class.
|
||||
*/
|
||||
private static final Logger logger = LoggerFactory.getLogger(AssertionConsumerServiceResource.class);
|
||||
|
||||
/**
|
||||
* The configuration service for this module.
|
||||
*/
|
||||
@Inject
|
||||
private ConfigurationService confService;
|
||||
|
||||
/**
|
||||
* Manager of active SAML authentication attempts.
|
||||
*/
|
||||
@Inject
|
||||
private SAMLAuthenticationSessionManager sessionManager;
|
||||
|
||||
/**
|
||||
* Service for processing SAML requests/responses.
|
||||
*/
|
||||
@Inject
|
||||
private SAMLService saml;
|
||||
|
||||
/**
|
||||
* Processes the SAML response submitted by the SAML IdP via an HTTP POST.
|
||||
* If SSO has been successful, the user is redirected back to Guacamole to
|
||||
* complete authentication. If SSO has failed, the user is redirected back
|
||||
* to Guacamole to re-attempt authentication.
|
||||
*
|
||||
* @param relayState
|
||||
* The "RelayState" value originally provided in the SAML request,
|
||||
* which in our case is the transient the session identifier of the
|
||||
* in-progress authentication attempt. The SAML standard requires that
|
||||
* the identity provider include the "RelayState" value it received
|
||||
* alongside its SAML response.
|
||||
*
|
||||
* @param samlResponse
|
||||
* The encoded response returned by the SAML IdP.
|
||||
*
|
||||
* @param consumedRequest
|
||||
* The HttpServletRequest associated with the SAML response. The
|
||||
* parameters of this request may not be accessible, as the request may
|
||||
* have been fully consumed by JAX-RS.
|
||||
*
|
||||
* @return
|
||||
* An HTTP Response that will redirect the user back to Guacamole,
|
||||
* with an internal state identifier included in the URL such that the
|
||||
* the Guacamole side of authentication can complete.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If configuration information required for processing SAML responses
|
||||
* cannot be read.
|
||||
*/
|
||||
@POST
|
||||
@Path("callback")
|
||||
public Response processSamlResponse(
|
||||
@FormParam("RelayState") String relayState,
|
||||
@FormParam("SAMLResponse") String samlResponse,
|
||||
@Context HttpServletRequest consumedRequest)
|
||||
throws GuacamoleException {
|
||||
|
||||
URI guacBase = confService.getCallbackUrl();
|
||||
|
||||
try {
|
||||
|
||||
// Validate and parse identity asserted by SAML IdP
|
||||
SAMLAuthenticationSession session = saml.processResponse(
|
||||
consumedRequest.getRequestURL().toString(),
|
||||
relayState, samlResponse);
|
||||
|
||||
// Store asserted identity for future retrieval via redirect
|
||||
String identifier = sessionManager.defer(session);
|
||||
|
||||
// Redirect user such that Guacamole's authentication system can
|
||||
// retrieve the relevant SAML identity (stored above)
|
||||
return Response.seeOther(UriBuilder.fromUri(guacBase)
|
||||
.queryParam(AuthenticationProviderService.AUTH_SESSION_QUERY_PARAM, identifier)
|
||||
.build()
|
||||
).build();
|
||||
|
||||
}
|
||||
|
||||
// If invalid, redirect back to main page to re-attempt authentication
|
||||
catch (GuacamoleException e) {
|
||||
logger.warn("Authentication attempted with an invalid SAML response: {}", e.getMessage());
|
||||
logger.debug("Received SAML response failed validation.", e);
|
||||
return Response.seeOther(guacBase).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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 org.apache.guacamole.net.auth.AuthenticationSession;
|
||||
|
||||
/**
|
||||
* Representation of an in-progress SAML authentication attempt.
|
||||
*/
|
||||
public class SAMLAuthenticationSession extends AuthenticationSession {
|
||||
|
||||
/**
|
||||
* The request ID of the SAML request associated with the authentication
|
||||
* attempt.
|
||||
*/
|
||||
private final String requestId;
|
||||
|
||||
/**
|
||||
* The identity asserted by the SAML IdP, or null if authentication has not
|
||||
* yet completed successfully.
|
||||
*/
|
||||
private AssertedIdentity identity = null;
|
||||
|
||||
/**
|
||||
* Creates a new AuthenticationSession representing an in-progress SAML
|
||||
* authentication attempt.
|
||||
*
|
||||
* @param requestId
|
||||
* The request ID of the SAML request associated with the
|
||||
* authentication attempt.
|
||||
*
|
||||
* @param expires
|
||||
* The number of milliseconds that may elapse before this session must
|
||||
* be considered invalid.
|
||||
*/
|
||||
public SAMLAuthenticationSession(String requestId, long expires) {
|
||||
super(expires);
|
||||
this.requestId = requestId;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* <p>If an identity has been asserted by the SAML IdP, this
|
||||
* considers also whether the SAML response asserting that identity has
|
||||
* expired.
|
||||
*/
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return super.isValid() && (identity == null || identity.isValid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request ID of the SAML request associated with the
|
||||
* authentication attempt.
|
||||
*
|
||||
* @return
|
||||
* The request ID of the SAML request associated with the
|
||||
* authentication attempt.
|
||||
*/
|
||||
public String getRequestID() {
|
||||
return requestId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks this authentication attempt as completed and successful, with the
|
||||
* user having been asserted as having the given identity by the SAML IdP.
|
||||
*
|
||||
* @param identity
|
||||
* The identity asserted by the SAML IdP.
|
||||
*/
|
||||
public void setIdentity(AssertedIdentity identity) {
|
||||
this.identity = identity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the identity asserted by the SAML IdP. If authentication has not
|
||||
* yet completed successfully, this will be null.
|
||||
*
|
||||
* @return
|
||||
* The identity asserted by the SAML IdP, or null if authentication has
|
||||
* not yet completed successfully.
|
||||
*/
|
||||
public AssertedIdentity getIdentity() {
|
||||
return identity;
|
||||
}
|
||||
|
||||
}
|
@@ -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.net.auth.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;
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* 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.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import com.onelogin.saml2.Auth;
|
||||
import com.onelogin.saml2.authn.AuthnRequestParams;
|
||||
import com.onelogin.saml2.authn.SamlResponse;
|
||||
import com.onelogin.saml2.exception.SettingsException;
|
||||
import com.onelogin.saml2.exception.ValidationError;
|
||||
import com.onelogin.saml2.settings.Saml2Settings;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
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.net.auth.IdentifierGenerator;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
/**
|
||||
* Service which abstracts the internals of handling SAML requests and
|
||||
* responses.
|
||||
*/
|
||||
@Singleton
|
||||
public class SAMLService {
|
||||
|
||||
/**
|
||||
* Service for retrieving SAML configuration information.
|
||||
*/
|
||||
@Inject
|
||||
private ConfigurationService confService;
|
||||
|
||||
/**
|
||||
* Manager of active SAML authentication attempts.
|
||||
*/
|
||||
@Inject
|
||||
private SAMLAuthenticationSessionManager sessionManager;
|
||||
|
||||
/**
|
||||
* Creates a new SAML request, beginning the overall authentication flow
|
||||
* that will ultimately result in an asserted user identity if the user is
|
||||
* successfully authenticated by the SAML IdP. The URI of the SSO endpoint
|
||||
* of the SAML IdP that the user must be redirected to for the
|
||||
* authentication process to continue is returned.
|
||||
*
|
||||
* @return
|
||||
* The URI of the SSO endpoint of the SAML IdP that the user must be
|
||||
* redirected to.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If an error prevents the SAML request and redirect URI from being
|
||||
* generated.
|
||||
*/
|
||||
public URI createRequest() throws GuacamoleException {
|
||||
|
||||
Saml2Settings samlSettings = confService.getSamlSettings();
|
||||
|
||||
// Produce redirect for continuing the authentication process with
|
||||
// the SAML IdP
|
||||
try {
|
||||
Auth auth = new Auth(samlSettings, null, null);
|
||||
|
||||
// Generate a unique ID to use for the relay state
|
||||
String identifier = IdentifierGenerator.generateIdentifier();
|
||||
|
||||
// Create the request URL for the SAML IdP
|
||||
String requestUrl = auth.login(
|
||||
identifier,
|
||||
new AuthnRequestParams(false, false, true),
|
||||
true);
|
||||
|
||||
// Create a new authentication session to represent this attempt while
|
||||
// it is in progress, using the request ID that was just issued
|
||||
SAMLAuthenticationSession session = new SAMLAuthenticationSession(
|
||||
auth.getLastRequestId(),
|
||||
confService.getAuthenticationTimeout() * 60000L);
|
||||
|
||||
// Save the session with the unique relay state ID
|
||||
sessionManager.defer(session, identifier);
|
||||
|
||||
return new URI(requestUrl);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new GuacamoleServerException("SAML authentication request "
|
||||
+ "could not be encoded: " + e.getMessage());
|
||||
}
|
||||
catch (URISyntaxException e) {
|
||||
throw new GuacamoleServerException("SAML IdP redirect could not "
|
||||
+ "be generated due to an error in the URI syntax: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
catch (SettingsException e) {
|
||||
throw new GuacamoleServerException("Error while attempting to sign "
|
||||
+ "request using provided private key / certificate: "
|
||||
+ e.getMessage(), e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the given SAML response, as received by the SAML ACS endpoint
|
||||
* 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.
|
||||
*
|
||||
* @param url
|
||||
* The URL of the ACS endpoint that received the SAML response. This
|
||||
* should be the URL pointing to the single POST-handling endpoint of
|
||||
* {@link AssertionConsumerServiceResource}.
|
||||
*
|
||||
* @param relayState
|
||||
* The "RelayState" value originally provided in the SAML request,
|
||||
* which in our case is the transient the session identifier of the
|
||||
* in-progress authentication attempt. The SAML standard requires that
|
||||
* the identity provider include the "RelayState" value it received
|
||||
* alongside its SAML response.
|
||||
*
|
||||
* @param encodedResponse
|
||||
* The response received from the SAML IdP via the ACS endpoint at the
|
||||
* given URL.
|
||||
*
|
||||
* @return
|
||||
* 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.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If the given SAML response is not valid, or if the configuration
|
||||
* information required to validate or decrypt the response cannot be
|
||||
* read.
|
||||
*/
|
||||
public SAMLAuthenticationSession processResponse(String url, String relayState,
|
||||
String encodedResponse) throws GuacamoleException {
|
||||
|
||||
if (relayState == null)
|
||||
throw new GuacamoleSecurityException("\"RelayState\" value "
|
||||
+ "is missing from SAML response.");
|
||||
|
||||
SAMLAuthenticationSession session = sessionManager.resume(relayState);
|
||||
if (session == null)
|
||||
throw new GuacamoleSecurityException("\"RelayState\" value "
|
||||
+ "included with SAML response is not valid.");
|
||||
|
||||
try {
|
||||
|
||||
// Decode received SAML response
|
||||
SamlResponse response = new SamlResponse(confService.getSamlSettings(),
|
||||
url, encodedResponse);
|
||||
|
||||
// Validate SAML response timestamp, signature, etc.
|
||||
if (!response.isValid(session.getRequestID())) {
|
||||
Exception validationException = response.getValidationException();
|
||||
throw new GuacamoleSecurityException("SAML response did not "
|
||||
+ "pass validation: " + validationException.getMessage(),
|
||||
validationException);
|
||||
}
|
||||
|
||||
// Parse identity asserted by SAML IdP
|
||||
session.setIdentity(new AssertedIdentity(response));
|
||||
return session;
|
||||
|
||||
}
|
||||
catch (ValidationError e) {
|
||||
throw new GuacamoleSecurityException("SAML response did not pass "
|
||||
+ "validation: " + e.getMessage(), e);
|
||||
}
|
||||
catch (SettingsException e) {
|
||||
throw new GuacamoleServerException("Current SAML settings are "
|
||||
+ "insufficient to decrypt/parse the received SAML "
|
||||
+ "response.", e);
|
||||
}
|
||||
catch (ParserConfigurationException | SAXException | XPathExpressionException e) {
|
||||
throw new GuacamoleServerException("XML contents of SAML "
|
||||
+ "response could not be parsed.", e);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new GuacamoleServerException("Contents of SAML response "
|
||||
+ "could not be decrypted/read.", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,507 @@
|
||||
/*
|
||||
* 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.conf;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.onelogin.saml2.settings.IdPMetadataParser;
|
||||
import com.onelogin.saml2.settings.Saml2Settings;
|
||||
import com.onelogin.saml2.settings.SettingsBuilder;
|
||||
import com.onelogin.saml2.util.Constants;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
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.BooleanGuacamoleProperty;
|
||||
import org.apache.guacamole.properties.FileGuacamoleProperty;
|
||||
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
|
||||
import org.apache.guacamole.properties.StringGuacamoleProperty;
|
||||
import org.apache.guacamole.properties.URIGuacamoleProperty;
|
||||
|
||||
/**
|
||||
* Service for retrieving configuration information regarding the SAML
|
||||
* authentication module.
|
||||
*/
|
||||
public class ConfigurationService {
|
||||
|
||||
/**
|
||||
* The URI of the file containing the XML Metadata associated with the
|
||||
* SAML IdP.
|
||||
*/
|
||||
private static final URIGuacamoleProperty SAML_IDP_METADATA_URL =
|
||||
new URIGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "saml-idp-metadata-url"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The URL of the SAML IdP.
|
||||
*/
|
||||
private static final URIGuacamoleProperty SAML_IDP_URL =
|
||||
new URIGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "saml-idp-url"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The URL identifier for this SAML client.
|
||||
*/
|
||||
private static final URIGuacamoleProperty SAML_ENTITY_ID =
|
||||
new URIGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "saml-entity-id"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The callback URL to use for SAML IdP, normally the base
|
||||
* of the Guacamole install. The SAML extensions callback
|
||||
* endpoint will be appended to this value.
|
||||
*/
|
||||
private static final URIGuacamoleProperty SAML_CALLBACK_URL =
|
||||
new URIGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "saml-callback-url"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether or not debugging should be enabled in the SAML library to help
|
||||
* track down errors.
|
||||
*/
|
||||
private static final BooleanGuacamoleProperty SAML_DEBUG =
|
||||
new BooleanGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "saml-debug"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether or not to enable compression for the SAML request.
|
||||
*/
|
||||
private static final BooleanGuacamoleProperty SAML_COMPRESS_REQUEST =
|
||||
new BooleanGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "saml-compress-request"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether or not to enable compression for the SAML response.
|
||||
*/
|
||||
private static final BooleanGuacamoleProperty SAML_COMPRESS_RESPONSE =
|
||||
new BooleanGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "saml-compress-response"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether or not to enforce strict SAML security during processing.
|
||||
*/
|
||||
private static final BooleanGuacamoleProperty SAML_STRICT =
|
||||
new BooleanGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "saml-strict"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The property that defines what attribute the SAML provider will return
|
||||
* that contains group membership for the authenticated user.
|
||||
*/
|
||||
private static final StringGuacamoleProperty SAML_GROUP_ATTRIBUTE =
|
||||
new StringGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "saml-group-attribute"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The maximum amount of time to allow for an in-progress SAML
|
||||
* authentication attempt to be completed, in minutes. A user that takes
|
||||
* longer than this amount of time to complete authentication with their
|
||||
* identity provider will be redirected back to the identity provider to
|
||||
* try again.
|
||||
*/
|
||||
private static final IntegerGuacamoleProperty SAML_AUTH_TIMEOUT =
|
||||
new IntegerGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "saml-auth-timeout"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The file containing the X.509 cert to use when signing or encrypting
|
||||
* requests to the SAML IdP.
|
||||
*/
|
||||
private static final FileGuacamoleProperty SAML_X509_CERT_PATH =
|
||||
new FileGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "saml-x509-cert-path"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The file containing the private key to use when signing or encrypting
|
||||
* requests to the SAML IdP.
|
||||
*/
|
||||
private static final FileGuacamoleProperty SAML_PRIVATE_KEY_PATH =
|
||||
new FileGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "saml-private-key-path"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The Guacamole server environment.
|
||||
*/
|
||||
@Inject
|
||||
private Environment environment;
|
||||
|
||||
/**
|
||||
* Returns the URL to be submitted as the client ID to the SAML IdP, as
|
||||
* configured in guacamole.properties.
|
||||
*
|
||||
* @return
|
||||
* The URL to send to the SAML IdP as the Client Identifier.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed.
|
||||
*/
|
||||
private URI getEntityId() throws GuacamoleException {
|
||||
return environment.getProperty(SAML_ENTITY_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* The URI that contains the metadata that the SAML client should
|
||||
* use to communicate with the SAML IdP. This can either be a remote
|
||||
* URL of a server that provides this, or can be a URI to a file on the
|
||||
* local filesystem. The metadata file is usually generated by the SAML IdP
|
||||
* and should be uploaded to the system where the Guacamole client is
|
||||
* running.
|
||||
*
|
||||
* @return
|
||||
* The URI of the file containing the metadata used by the SAML client
|
||||
* when it communicates with the SAML IdP.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed, or if the client
|
||||
* metadata is missing.
|
||||
*/
|
||||
private URI getIdpMetadata() throws GuacamoleException {
|
||||
return environment.getProperty(SAML_IDP_METADATA_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the URL used to log in to the SAML IdP.
|
||||
*
|
||||
* @return
|
||||
* The URL used to log in to the SAML IdP.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed.
|
||||
*/
|
||||
private URI getIdpUrl() throws GuacamoleException {
|
||||
return environment.getProperty(SAML_IDP_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback URL used for the SAML IdP to POST a response
|
||||
* to upon completion of authentication, normally the base
|
||||
* of the Guacamole install.
|
||||
*
|
||||
* @return
|
||||
* The callback URL to be sent to the SAML IdP that will
|
||||
* be POSTed to upon completion of SAML authentication.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed, or the property
|
||||
* is missing.
|
||||
*/
|
||||
public URI getCallbackUrl() throws GuacamoleException {
|
||||
return environment.getRequiredProperty(SAML_CALLBACK_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Boolean value that indicates whether SAML client debugging
|
||||
* will be enabled, as configured in guacamole.properties. The default is
|
||||
* false, and debug information will not be generated or logged.
|
||||
*
|
||||
* @return
|
||||
* True if debugging should be enabled in the SAML library, otherwise
|
||||
* false.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed.
|
||||
*/
|
||||
private boolean getDebug() throws GuacamoleException {
|
||||
return environment.getProperty(SAML_DEBUG, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Boolean value that indicates whether or not compression of
|
||||
* SAML requests to the IdP should be enabled or not, as configured in
|
||||
* guacamole.properties. The default is to enable compression.
|
||||
*
|
||||
* @return
|
||||
* True if compression should be enabled when sending the SAML request,
|
||||
* otherwise false.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed.
|
||||
*/
|
||||
private boolean getCompressRequest() throws GuacamoleException {
|
||||
return environment.getProperty(SAML_COMPRESS_REQUEST, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a Boolean value that indicates whether or not the SAML login
|
||||
* should enforce strict security controls, as configured in
|
||||
* guacamole.properties. By default this is true, and should be set to
|
||||
* true in any production environment.
|
||||
*
|
||||
* @return
|
||||
* True if the SAML login should enforce strict security checks,
|
||||
* otherwise false.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed.
|
||||
*/
|
||||
private boolean getStrict() throws GuacamoleException {
|
||||
return environment.getProperty(SAML_STRICT, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a Boolean value that indicates whether or not compression should
|
||||
* be requested from the server when the SAML response is returned, as
|
||||
* configured in guacamole.properties. The default is to request that the
|
||||
* response be compressed.
|
||||
*
|
||||
* @return
|
||||
* True if compression should be requested from the server for the SAML
|
||||
* response.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed.
|
||||
*/
|
||||
private boolean getCompressResponse() throws GuacamoleException {
|
||||
return environment.getProperty(SAML_COMPRESS_RESPONSE, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of the attribute that will be supplied by the identity
|
||||
* provider that contains the groups of which this user is a member.
|
||||
*
|
||||
* @return
|
||||
* The name of the attribute that contains the user groups.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed.
|
||||
*/
|
||||
public String getGroupAttribute() throws GuacamoleException {
|
||||
return environment.getProperty(SAML_GROUP_ATTRIBUTE, "groups");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum amount of time to allow for an in-progress SAML
|
||||
* authentication attempt to be completed, in minutes. A user that takes
|
||||
* longer than this amount of time to complete authentication with their
|
||||
* identity provider will be redirected back to the identity provider to
|
||||
* try again.
|
||||
*
|
||||
* @return
|
||||
* The maximum amount of time to allow for an in-progress SAML
|
||||
* authentication attempt to be completed, in minutes.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If the authentication timeout cannot be parsed.
|
||||
*/
|
||||
public int getAuthenticationTimeout() throws GuacamoleException {
|
||||
return environment.getProperty(SAML_AUTH_TIMEOUT, 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file containing the X.509 certificate to use when signing
|
||||
* requests to the SAML IdP. If the property is not set, null will be
|
||||
* returned.
|
||||
*
|
||||
* @return
|
||||
* The file containing the X.509 certificate to use when signing
|
||||
* requests to the SAML IdP, or null if not defined.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If the X.509 certificate cannot be parsed.
|
||||
*/
|
||||
public File getCertificateFile() throws GuacamoleException {
|
||||
return environment.getProperty(SAML_X509_CERT_PATH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file containing the private key to use when signing
|
||||
* requests to the SAML IdP. If the property is not set, null will be
|
||||
* returned.
|
||||
*
|
||||
* @return
|
||||
* The file containing the private key to use when signing
|
||||
* requests to the SAML IdP, or null if not defined.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If the private key file cannot be parsed.
|
||||
*/
|
||||
public File getPrivateKeyFile() throws GuacamoleException {
|
||||
return environment.getProperty(SAML_PRIVATE_KEY_PATH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contents of a small file, such as a private key or certificate into
|
||||
* a String. If the file does not exist, or cannot be read for any reason, an exception
|
||||
* will be thrown with the details of the failure.
|
||||
*
|
||||
* @param file
|
||||
* The file to read into a string.
|
||||
*
|
||||
* @param name
|
||||
* A human-readable name for the file, to be used when formatting log messages.
|
||||
*
|
||||
* @return
|
||||
* The contents of the file having the given path.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If the provided file does not exist, or cannot be read for any reason.
|
||||
*/
|
||||
private String readFileContentsIntoString(File file, String name) throws GuacamoleException {
|
||||
|
||||
// Attempt to read the file directly into a String
|
||||
try {
|
||||
return new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
// If the file cannot be read, log a warning and treat it as if it does not exist
|
||||
catch (IOException e) {
|
||||
throw new GuacamoleServerException(
|
||||
name + " at \"" + file.getAbsolutePath() + "\" could not be read.", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the collection of SAML settings used to initialize the client.
|
||||
*
|
||||
* @return
|
||||
* The collection of SAML settings used to initialize the SAML client.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If guacamole.properties cannot be parsed or if required parameters
|
||||
* are missing.
|
||||
*/
|
||||
public Saml2Settings getSamlSettings() throws GuacamoleException {
|
||||
|
||||
// Try to get the XML file, first.
|
||||
URI idpMetadata = getIdpMetadata();
|
||||
Map<String, Object> samlMap;
|
||||
if (idpMetadata != null) {
|
||||
try {
|
||||
samlMap = IdPMetadataParser.parseRemoteXML(idpMetadata.toURL());
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new GuacamoleServerException(
|
||||
"Could not parse SAML IdP Metadata file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
// If no XML metadata is provided, fall-back to individual values.
|
||||
else {
|
||||
samlMap = new HashMap<>();
|
||||
samlMap.put(SettingsBuilder.IDP_ENTITYID_PROPERTY_KEY,
|
||||
getIdpUrl().toString());
|
||||
samlMap.put(SettingsBuilder.IDP_SINGLE_SIGN_ON_SERVICE_URL_PROPERTY_KEY,
|
||||
getIdpUrl().toString());
|
||||
samlMap.put(SettingsBuilder.IDP_SINGLE_SIGN_ON_SERVICE_BINDING_PROPERTY_KEY,
|
||||
Constants.BINDING_HTTP_REDIRECT);
|
||||
}
|
||||
|
||||
// Read entity ID from properties if not provided within metadata XML
|
||||
if (!samlMap.containsKey(SettingsBuilder.SP_ENTITYID_PROPERTY_KEY)) {
|
||||
URI entityId = getEntityId();
|
||||
if (entityId == null)
|
||||
throw new GuacamoleServerException("SAML Entity ID was not found"
|
||||
+ " in either the metadata XML file or guacamole.properties");
|
||||
samlMap.put(SettingsBuilder.SP_ENTITYID_PROPERTY_KEY, entityId.toString());
|
||||
}
|
||||
|
||||
// Derive ACS URL from properties if not provided within metadata XML
|
||||
if (!samlMap.containsKey(SettingsBuilder.SP_ASSERTION_CONSUMER_SERVICE_URL_PROPERTY_KEY)) {
|
||||
samlMap.put(SettingsBuilder.SP_ASSERTION_CONSUMER_SERVICE_URL_PROPERTY_KEY,
|
||||
UriBuilder.fromUri(getCallbackUrl()).path("api/ext/saml/callback").build().toString());
|
||||
}
|
||||
|
||||
// If a private key file is set, load the value into the builder now
|
||||
File privateKeyFile = getPrivateKeyFile();
|
||||
if (privateKeyFile != null)
|
||||
samlMap.put(SettingsBuilder.SP_PRIVATEKEY_PROPERTY_KEY,
|
||||
readFileContentsIntoString(privateKeyFile, "Private Key"));
|
||||
|
||||
// If a certificate file is set, load the value into the builder now
|
||||
File certificateFile = getCertificateFile();
|
||||
if (certificateFile != null)
|
||||
samlMap.put(SettingsBuilder.SP_X509CERT_PROPERTY_KEY,
|
||||
readFileContentsIntoString(certificateFile, "X.509 Certificate"));
|
||||
|
||||
SettingsBuilder samlBuilder = new SettingsBuilder();
|
||||
Saml2Settings samlSettings = samlBuilder.fromValues(samlMap).build();
|
||||
samlSettings.setStrict(getStrict());
|
||||
samlSettings.setDebug(getDebug());
|
||||
samlSettings.setCompressRequest(getCompressRequest());
|
||||
samlSettings.setCompressResponse(getCompressResponse());
|
||||
|
||||
// Request that the SAML library sign everything that it can, if
|
||||
// both private key and certificate are specified
|
||||
if (privateKeyFile != null && certificateFile != null) {
|
||||
samlSettings.setAuthnRequestsSigned(true);
|
||||
samlSettings.setLogoutRequestSigned(true);
|
||||
samlSettings.setLogoutResponseSigned(true);
|
||||
}
|
||||
|
||||
return samlSettings;
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.guacamole.auth.saml.conf;
|
||||
|
||||
import org.apache.guacamole.environment.DelegatingEnvironment;
|
||||
import org.apache.guacamole.environment.LocalEnvironment;
|
||||
|
||||
/**
|
||||
* An environment for retrieving SAML-related properties from the Guacamole
|
||||
* configuration.
|
||||
*/
|
||||
public class SAMLEnvironment extends DelegatingEnvironment {
|
||||
|
||||
/**
|
||||
* Create a new instance of the configuration environment for the
|
||||
* SAML SSO module, pulling the default instance of the LocalEnvironment.
|
||||
*/
|
||||
public SAMLEnvironment() {
|
||||
super(LocalEnvironment.getInstance());
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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.user;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.auth.saml.acs.AssertedIdentity;
|
||||
import org.apache.guacamole.auth.saml.conf.ConfigurationService;
|
||||
import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser;
|
||||
import org.apache.guacamole.net.auth.Credentials;
|
||||
import org.apache.guacamole.token.TokenName;
|
||||
|
||||
/**
|
||||
* A SAML-specific implementation of AuthenticatedUser, associating a SAML
|
||||
* identity and particular set of credentials with the SAML authentication
|
||||
* provider.
|
||||
*/
|
||||
public class SAMLAuthenticatedUser extends SSOAuthenticatedUser {
|
||||
|
||||
/**
|
||||
* The prefix that should be prepended to all parameter tokens generated
|
||||
* from SAML attributes.
|
||||
*/
|
||||
private static final String SAML_ATTRIBUTE_TOKEN_PREFIX = "SAML_";
|
||||
|
||||
/**
|
||||
* Service for retrieving SAML configuration information.
|
||||
*/
|
||||
@Inject
|
||||
private ConfigurationService confService;
|
||||
|
||||
/**
|
||||
* Returns a Map of all parameter tokens that should be made available for
|
||||
* substitution based on the given {@link AssertedIdentity}. The resulting
|
||||
* Map will contain one parameter token for each SAML attribute in the
|
||||
* SAML response that originally asserted the user's identity. Attributes
|
||||
* that have multiple values will be reduced to a single value, taking the
|
||||
* first available value and discarding the remaining values.
|
||||
*
|
||||
* @param identity
|
||||
* The {@link AssertedIdentity} representing the user identity
|
||||
* asserted by the SAML IdP.
|
||||
*
|
||||
* @return
|
||||
* A Map of key and single value pairs that should be made available
|
||||
* for substitution as parameter tokens.
|
||||
*/
|
||||
private Map<String, String> getTokens(AssertedIdentity identity) {
|
||||
return Collections.unmodifiableMap(identity.getAttributes().entrySet()
|
||||
.stream()
|
||||
.filter((entry) -> !entry.getValue().isEmpty())
|
||||
.collect(Collectors.toMap(
|
||||
(entry) -> TokenName.canonicalize(entry.getKey(), SAML_ATTRIBUTE_TOKEN_PREFIX),
|
||||
(entry) -> entry.getValue().get(0)
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of all group memberships asserted by the SAML IdP.
|
||||
*
|
||||
* @param identity
|
||||
* The {@link AssertedIdentity} representing the user identity
|
||||
* asserted by the SAML IdP.
|
||||
*
|
||||
* @return
|
||||
* A set of all groups that the SAML IdP asserts this user is a
|
||||
* member of.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If the configuration information necessary to retrieve group
|
||||
* memberships from a SAML response cannot be read.
|
||||
*/
|
||||
private Set<String> getGroups(AssertedIdentity identity)
|
||||
throws GuacamoleException {
|
||||
|
||||
List<String> samlGroups = identity.getAttributes().get(confService.getGroupAttribute());
|
||||
if (samlGroups == null || samlGroups.isEmpty())
|
||||
return Collections.emptySet();
|
||||
|
||||
return Collections.unmodifiableSet(new HashSet<>(samlGroups));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this AuthenticatedUser using the given
|
||||
* {@link AssertedIdentity} and credentials.
|
||||
*
|
||||
* @param identity
|
||||
* The {@link AssertedIdentity} representing the user identity
|
||||
* asserted by the SAML IdP.
|
||||
*
|
||||
* @param credentials
|
||||
* The credentials provided when this user was authenticated.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If configuration information required for processing the user's
|
||||
* identity and group memberships cannot be read.
|
||||
*/
|
||||
public void init(AssertedIdentity identity, Credentials credentials)
|
||||
throws GuacamoleException {
|
||||
super.init(identity.getUsername(), credentials, getGroups(identity), getTokens(identity));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
{
|
||||
|
||||
"guacamoleVersion" : "1.6.0",
|
||||
|
||||
"name" : "SAML Authentication Extension",
|
||||
"namespace" : "saml",
|
||||
|
||||
"authProviders" : [
|
||||
"org.apache.guacamole.auth.saml.SAMLAuthenticationProvider"
|
||||
],
|
||||
|
||||
"listeners" : [
|
||||
"org.apache.guacamole.auth.saml.SAMLAuthenticationEventListener"
|
||||
],
|
||||
|
||||
"css" : [
|
||||
"styles/sso-providers.css"
|
||||
],
|
||||
|
||||
"html" : [
|
||||
"html/sso-providers.html",
|
||||
"html/sso-provider-saml.html"
|
||||
],
|
||||
|
||||
"translations" : [
|
||||
"translations/ca.json",
|
||||
"translations/de.json",
|
||||
"translations/en.json",
|
||||
"translations/fr.json",
|
||||
"translations/ja.json",
|
||||
"translations/ko.json",
|
||||
"translations/pl.json",
|
||||
"translations/pt.json",
|
||||
"translations/ru.json",
|
||||
"translations/zh.json"
|
||||
]
|
||||
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
<meta name="after-children" content=".login-ui .sso-provider-list">
|
||||
<li class="sso-provider sso-provider-saml"><a href="api/ext/saml/login">{{
|
||||
'LOGIN.NAME_IDP_SAML' | 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.
|
||||
*/
|
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,172 @@
|
||||
<?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.6.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.6.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>2.1.9</version>
|
||||
|
||||
<exclusions>
|
||||
|
||||
<!--
|
||||
Force usage of known version of bc-fips, rather than a
|
||||
future unknown version (bcpkix-fips references bc-fips using
|
||||
a version range, resulting in newer versions getting pulled
|
||||
in automatically, breaking the automated license check).
|
||||
-->
|
||||
<exclusion>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bc-fips</artifactId>
|
||||
</exclusion>
|
||||
|
||||
<!--
|
||||
Force usage of known version of bcutil-fips, rather than a
|
||||
future unknown version (bcpkix-fips references bctuil-fips
|
||||
using a version range, resulting in newer versions getting
|
||||
pulled in automatically, breaking the automated license
|
||||
check).
|
||||
-->
|
||||
<exclusion>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcutil-fips</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
|
||||
</dependency>
|
||||
|
||||
<!-- Force usage of known version of bc-fips (see bcpkix-fips above) -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bc-fips</artifactId>
|
||||
<version>2.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Force usage of known version of bcutil-fips (see bcpkix-fips above) -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcutil-fips</artifactId>
|
||||
<version>2.1.4</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* 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.Arrays;
|
||||
import java.util.Collections;
|
||||
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.form.Field;
|
||||
import org.apache.guacamole.form.RedirectField;
|
||||
import org.apache.guacamole.language.TranslatableMessage;
|
||||
import org.apache.guacamole.net.auth.Credentials;
|
||||
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
|
||||
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
|
||||
|
||||
/**
|
||||
* Service that authenticates Guacamole users using SSL/TLS authentication
|
||||
* provided by an external SSL termination service.
|
||||
*/
|
||||
@Singleton
|
||||
public class AuthenticationProviderService implements SSOAuthenticationProviderService {
|
||||
|
||||
/**
|
||||
* Service for retrieving configuration information.
|
||||
*/
|
||||
@Inject
|
||||
private ConfigurationService confService;
|
||||
|
||||
/**
|
||||
* 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";
|
||||
|
||||
/**
|
||||
* Return the value of the session identifier associated with the given
|
||||
* credentials, or null if no session identifier is found in the credentials.
|
||||
*
|
||||
* @param credentials
|
||||
* The credentials from which to extract the session identifier.
|
||||
*
|
||||
* @return
|
||||
* The session identifier associated with the given credentials, or
|
||||
* null if no identifier is found.
|
||||
*/
|
||||
public static String getSessionIdentifier(Credentials credentials) {
|
||||
|
||||
// Return the session identifier from the request params, if set, or
|
||||
// null otherwise
|
||||
return credentials != null ? credentials.getParameter(AUTH_SESSION_PARAMETER_NAME) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the given credentials, returning the identity represented by
|
||||
* the auth session token present in that request associated with the
|
||||
* credentials. If no such token is present, or the token does not represent
|
||||
* a valid identity, null is returned.
|
||||
*
|
||||
* @param credentials
|
||||
* The credentials to extract the auth session token from.
|
||||
*
|
||||
* @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) {
|
||||
|
||||
String state = getSessionIdentifier(credentials);
|
||||
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 MUST have the domain associated with the request to ensure we
|
||||
// always get fresh SSL sessions when validating client certificates
|
||||
String host = credentials.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);
|
||||
|
||||
// All other requests are not allowed - redirect to proper hostname
|
||||
throw new GuacamoleInvalidCredentialsException("Authentication is "
|
||||
+ "only allowed against the primary URL of this Guacamole "
|
||||
+ "instance.",
|
||||
new CredentialsInfo(Arrays.asList(new Field[] {
|
||||
new RedirectField("primaryURI", confService.getPrimaryURI(),
|
||||
new TranslatableMessage("LOGIN.INFO_REDIRECT_PENDING"))
|
||||
}))
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
@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,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.ssl;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import org.apache.guacamole.auth.sso.SSOAuthenticationEventListener;
|
||||
import org.apache.guacamole.net.auth.Credentials;
|
||||
|
||||
/**
|
||||
* A Listener that will reactivate or invalidate SSL auth sessions depending on
|
||||
* overall auth success or failure.
|
||||
*/
|
||||
public class SSLAuthenticationEventListener extends SSOAuthenticationEventListener {
|
||||
|
||||
/**
|
||||
* Session manager for generating and maintaining unique tokens to
|
||||
* represent the authentication flow of a user who has only partially
|
||||
* authenticated.
|
||||
*
|
||||
* Requires static injection due to the fact that the webapp just calls the
|
||||
* constructor directly when creating new Listeners. The instances will not
|
||||
* be constructed by guice.
|
||||
*/
|
||||
@Inject
|
||||
protected static SSLAuthenticationSessionManager sessionManager;
|
||||
|
||||
@Override
|
||||
protected String getSessionIdentifier(Credentials credentials) {
|
||||
return AuthenticationProviderService.getSessionIdentifier(credentials);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reactivateSession(String sessionIdentifier) {
|
||||
sessionManager.reactivateSession(sessionIdentifier);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidateSession(String sessionIdentifier) {
|
||||
sessionManager.invalidateSession(sessionIdentifier);
|
||||
}
|
||||
|
||||
}
|
@@ -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,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.
|
||||
*/
|
||||
|
||||
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.ssl.conf.SSLEnvironment;
|
||||
import org.apache.guacamole.auth.sso.NonceService;
|
||||
import org.apache.guacamole.environment.Environment;
|
||||
|
||||
/**
|
||||
* Guice module which configures injections specific to SSO using SSL/TLS
|
||||
* client authentication.
|
||||
*/
|
||||
public class SSLAuthenticationProviderModule extends AbstractModule {
|
||||
|
||||
/**
|
||||
* The configuration environment of this server and extension.
|
||||
*/
|
||||
private final Environment environment = new SSLEnvironment();
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(ConfigurationService.class);
|
||||
bind(NonceService.class).in(Scopes.SINGLETON);
|
||||
bind(SSLAuthenticationSessionManager.class);
|
||||
|
||||
bind(Environment.class).toInstance(environment);
|
||||
|
||||
requestStaticInjection(SSLAuthenticationEventListener.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.net.auth.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.net.auth.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,439 @@
|
||||
/*
|
||||
* 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.Collection;
|
||||
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 == null || values.isEmpty())
|
||||
return null;
|
||||
|
||||
return values.get(0);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the provided URL-encoded string as UTF-8, returning the result.
|
||||
* <p>
|
||||
* NOTE: The escape() function of the Apache HTTPD server is known to not
|
||||
* encode plus signs, which can appear in the base64-encoded certificates
|
||||
* typically received here. To avoid mangling such certificates, this
|
||||
* function specifically avoids decoding plus signs as spaces (as would
|
||||
* otherwise happen if URLDecoder is used directly).
|
||||
*
|
||||
* @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 {
|
||||
|
||||
// Ensure all plus signs are decoded literally rather than as spaces
|
||||
// (the Apache HTTPD implementation of URL escaping that applies to
|
||||
// request headers does not encode plus signs, whereas the Nginx
|
||||
// implementation does)
|
||||
value = value.replace("+", "%2B");
|
||||
|
||||
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
|
||||
Collection<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);
|
||||
Object object = parser.readObject();
|
||||
|
||||
// Verify received data is indeed an X.509 certificate
|
||||
if (object == null || !(object instanceof X509CertificateHolder))
|
||||
throw new GuacamoleClientException("Certificate did not "
|
||||
+ "contain an X.509 certificate.");
|
||||
|
||||
// Verify sanity of received certificate (there should be only
|
||||
// one object here)
|
||||
if (parser.readObject() != null)
|
||||
throw new GuacamoleClientException("Certificate contains "
|
||||
+ "more than a single X.509 certificate.");
|
||||
|
||||
cert = (X509CertificateHolder) object;
|
||||
|
||||
// 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,481 @@
|
||||
/*
|
||||
* 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.Collection;
|
||||
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.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_AUTH_URI =
|
||||
new WildcardURIGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "ssl-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_AUTH_PRIMARY_URI =
|
||||
new URIGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "ssl-auth-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_AUTH_CLIENT_CERTIFICATE_HEADER =
|
||||
new StringGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "ssl-auth-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_AUTH_CLIENT_VERIFIED_HEADER =
|
||||
new StringGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "ssl-auth-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_AUTH_MAX_TOKEN_VALIDITY =
|
||||
new IntegerGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "ssl-auth-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 StringGuacamoleProperty SSL_AUTH_SUBJECT_USERNAME_ATTRIBUTE =
|
||||
new StringGuacamoleProperty () {
|
||||
|
||||
@Override
|
||||
public String getName() { return "ssl-auth-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_AUTH_SUBJECT_BASE_DN =
|
||||
new LdapNameGuacamoleProperty () {
|
||||
|
||||
@Override
|
||||
public String getName() { return "ssl-auth-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_AUTH_MAX_DOMAIN_VALIDITY =
|
||||
new IntegerGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "ssl-auth-max-domain-validity"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The Guacamole server environment.
|
||||
*/
|
||||
@Inject
|
||||
private Environment environment;
|
||||
|
||||
/**
|
||||
* Returns whether the given hostname matches the hostname of the given
|
||||
* URI. The provided hostname may be the value of an HTTP "Host" header,
|
||||
* and may include a port number. If a port number is included in the
|
||||
* hostname, it is ignored.
|
||||
*
|
||||
* @param hostname
|
||||
* The hostname to check, which may alternatively be the value of an
|
||||
* HTTP "Host" header, with or without port number. The port number is
|
||||
* not considered when determining whether this hostname matches the
|
||||
* hostname of the provided URI.
|
||||
*
|
||||
* @param offset
|
||||
* The character offset within the provided hostname where checking
|
||||
* should start. Any characters before this offset are ignored. This
|
||||
* offset does not affect where checking starts within the hostname of
|
||||
* the provided URI.
|
||||
*
|
||||
* @param uri
|
||||
* The URI to check the given hostname against.
|
||||
*
|
||||
* @return
|
||||
* true if the provided hostname from the given offset onward is
|
||||
* identical to the hostname of the given URI, false otherwise.
|
||||
*/
|
||||
private boolean hostnameMatches(String hostname, int offset, URI uri) {
|
||||
|
||||
// Locate end of actual hostname portion of "Host" header
|
||||
int endOfHostname = hostname.indexOf(':');
|
||||
if (endOfHostname == -1)
|
||||
endOfHostname = hostname.length();
|
||||
|
||||
// Before checking substring equivalence, we need to verify that the
|
||||
// length actually matches what we expect (we'd otherwise consider the
|
||||
// host to match if it starts with the expected hostname, ignoring any
|
||||
// remaining characters)
|
||||
String expectedHostname = uri.getHost();
|
||||
if (expectedHostname.length() != endOfHostname - offset)
|
||||
return false;
|
||||
|
||||
return hostname.regionMatches(true, offset, expectedHostname, 0, expectedHostname.length());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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_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;
|
||||
|
||||
// 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
|
||||
URI authURI = environment.getRequiredProperty(SSL_AUTH_URI);
|
||||
if (!hostnameMatches(hostname, firstPeriod + 1, authURI))
|
||||
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_AUTH_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 hostnameMatches(hostname, 0, primaryURI);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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_AUTH_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_AUTH_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_AUTH_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_AUTH_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_AUTH_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 Collection<String> getSubjectUsernameAttributes() throws GuacamoleException {
|
||||
return environment.getPropertyCollection(SSL_AUTH_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,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.conf;
|
||||
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.environment.DelegatingEnvironment;
|
||||
import org.apache.guacamole.environment.LocalEnvironment;
|
||||
|
||||
/**
|
||||
* An environment for retrieving SSL-related properties from the Guacamole
|
||||
* configuration.
|
||||
*/
|
||||
public class SSLEnvironment extends DelegatingEnvironment {
|
||||
|
||||
/**
|
||||
* Create a new instance of the configuration environment for the
|
||||
* SSL SSO module, pulling the default instance of the LocalEnvironment.
|
||||
*/
|
||||
public SSLEnvironment() {
|
||||
super(LocalEnvironment.getInstance());
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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 {
|
||||
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
// 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,39 @@
|
||||
{
|
||||
|
||||
"guacamoleVersion" : "1.6.0",
|
||||
|
||||
"name" : "SSL Authentication Extension",
|
||||
"namespace" : "ssl",
|
||||
|
||||
"authProviders" : [
|
||||
"org.apache.guacamole.auth.ssl.SSLAuthenticationProvider"
|
||||
],
|
||||
|
||||
"listeners" : [
|
||||
"org.apache.guacamole.auth.ssl.SSLAuthenticationEventListener"
|
||||
],
|
||||
|
||||
"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">
|
||||
<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');
|
Reference in New Issue
Block a user