Add .gitignore and .ratignore files for various directories
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
gyurix
2025-04-29 21:43:12 +02:00
parent 983ecbfc53
commit be9f66dee9
2167 changed files with 254128 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
target/
*~

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.sso;
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();
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
{
"LOGIN" : {
"INFO_IDP_REDIRECT_PENDING" : "Bitte warten, Sie werden zum Identitätsprovider weitergeleitet..."
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
{
"LOGIN" : {
"INFO_IDP_REDIRECT_PENDING" : "IDプロバイダへリダイレクトしています。"
}
}

View File

@@ -0,0 +1,11 @@
{
"DATA_SOURCE_SAML" : {
"NAME" : "SAML 인증 확장 프로그램"
},
"LOGIN" : {
"INFO_IDP_REDIRECT_PENDING" : "잠시만 기다려주십시오. ID 제공자로 리디렉션 중..."
}
}

View File

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

View File

@@ -0,0 +1,7 @@
{
"LOGIN" : {
"INFO_IDP_REDIRECT_PENDING" : "Por favor aguarde, redirecionando ao provedor de indentidade..."
}
}

View File

@@ -0,0 +1,15 @@
{
"DATA_SOURCE_CAS" : {
"NAME" : "Бэкенд CAS SSO"
},
"DATA_SOURCE_OPENID" : {
"NAME" : "Бэкенд OpenID SSO"
},
"LOGIN" : {
"INFO_IDP_REDIRECT_PENDING" : "Пожалуйста, подождите. Переадресую на страницу аутентификации..."
}
}

View File

@@ -0,0 +1,15 @@
{
"DATA_SOURCE_CAS" : {
"NAME" : "CAS SSO后端"
},
"DATA_SOURCE_OPENID" : {
"NAME" : "OpenID SSO后端"
},
"LOGIN" : {
"INFO_IDP_REDIRECT_PENDING" : "请稍候,正在重定向到身份提供者..."
}
}

View File

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

View File

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

View File

@@ -0,0 +1,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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,106 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.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();
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

View File

@@ -0,0 +1,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"));
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

View File

@@ -0,0 +1,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&param2=value2&...")
* to the format used by AngularJS ("#/?param1=value1&param2=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);
})();

View File

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

View File

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

View File

@@ -0,0 +1,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>

View File

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

View File

@@ -0,0 +1,66 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.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);
}
}

View File

@@ -0,0 +1,48 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.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";
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,59 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.saml.acs;
import com.google.inject.Singleton;
import org.apache.guacamole.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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,65 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.ssl;
/**
* REST API response that reports the result of attempting to authenticate the
* user using SSL/TLS client authentication. The information within this
* result is intentionally opaque and must be resubmitted in a separate
* authentication request for authentication to finally succeed or fail.
*/
public class OpaqueAuthenticationResult {
/**
* An arbitrary value representing the result of authenticating the
* current user.
*/
private final String state;
/**
* Creates a new OpaqueAuthenticationResult containing the given opaque
* state value. Successful authentication results must be indistinguishable
* from unsuccessful results with respect to this value. Only using this
* value within ANOTHER authentication attempt can determine whether
* authentication is successful.
*
* @param state
* An arbitrary value representing the result of authenticating the
* current user.
*/
public OpaqueAuthenticationResult(String state) {
this.state = state;
}
/**
* Returns an arbitrary value representing the result of authenticating the
* current user. This value may be resubmitted as the "state" parameter of
* an authentication request beneath the primary URI of the web application
* to finalize the authentication procedure and determine whether the
* operation has succeeded or failed.
*
* @return
* An arbitrary value representing the result of authenticating the
* current user.
*/
public String getState() {
return state;
}
}

View File

@@ -0,0 +1,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);
}
}

View File

@@ -0,0 +1,48 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.ssl;
import org.apache.guacamole.auth.sso.SSOAuthenticationProvider;
/**
* Guacamole authentication backend which authenticates users using SSL/TLS
* client authentication provided by some external SSL termination system. This
* SSL termination system must be configured to provide access to this same
* instance of Guacamole and must have both a wildcard certificate and wildcard
* DNS. No storage for connections is provided - only authentication. Storage
* must be provided by some other extension.
*/
public class SSLAuthenticationProvider extends SSOAuthenticationProvider {
/**
* Creates a new SSLAuthenticationProvider that authenticates users against
* an external SSL termination system using SSL/TLS client authentication.
*/
public SSLAuthenticationProvider() {
super(AuthenticationProviderService.class, SSLClientAuthenticationResource.class,
new SSLAuthenticationProviderModule());
}
@Override
public String getIdentifier() {
return "ssl";
}
}

View File

@@ -0,0 +1,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);
}
}

View File

@@ -0,0 +1,64 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.ssl;
import org.apache.guacamole.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;
}
}

View File

@@ -0,0 +1,60 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.ssl;
import com.google.inject.Singleton;
import org.apache.guacamole.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;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,50 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.ssl.conf;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.properties.GuacamoleProperty;
/**
* A GuacamoleProperty whose value is an LDAP name, such as a distinguished
* name.
*/
public abstract class LdapNameGuacamoleProperty implements GuacamoleProperty<LdapName> {
@Override
public LdapName parseValue(String value) throws GuacamoleException {
if (value == null)
return null;
try {
return new LdapName(value);
}
catch (InvalidNameException e) {
throw new GuacamoleServerException("Value \"" + value
+ "\" is not a valid LDAP name.", e);
}
}
}

View File

@@ -0,0 +1,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());
}
}

View File

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

View File

@@ -0,0 +1,51 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which automatically attempts to log the current user in using
* SSL/TLS client authentication when the associated element is clicked.
*/
angular.module('element').directive('guacSslAuth', ['$injector', function guacSslAuth($injector) {
// Required services
var clientAuthService = $injector.get('clientAuthService');
var directive = {
restrict: 'A'
};
directive.link = function linkGuacSslAuth($scope, $element) {
/**
* The element which will register the click.
*
* @type Element
*/
const element = $element[0];
// Attempt SSL/TLS client authentication upon click
element.addEventListener('click', function elementClicked() {
clientAuthService.authenticate();
});
};
return directive;
}]);

View File

@@ -0,0 +1,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"
]
}

View File

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

View File

@@ -0,0 +1,18 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

View File

@@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Service for authenticating a user using SSL/TLS client authentication.
*/
angular.module('guacSsoSsl').factory('clientAuthService', ['$injector',
function clientAuthServiceProvider($injector) {
// Required services
var requestService = $injector.get('requestService');
var authenticationService = $injector.get('authenticationService');
var service = {};
/**
* Attempt to authenticate using a unique token obtained through SSL/TLS
* client authentication.
*/
service.authenticate = function authenticate() {
// Transform SSL/TLS identity into an opaque "state" value and
// attempt authentication using that value
authenticationService.authenticate(
requestService({
method: 'GET',
headers : {
'Cache-Control' : undefined, // Avoid sending headers that would result in a pre-flight OPTIONS request for CORS
'Pragma' : undefined
},
url: 'api/ext/ssl/identity'
})
.then(function identityRetrieved(data) {
return { 'state' : data.state || '' };
})
)['catch'](requestService.IGNORE);
};
return service;
}]);

View File

@@ -0,0 +1,29 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* The module for code implementing SSO using SSL/TLS client authentication.
*/
angular.module('guacSsoSsl', [
'auth',
'rest'
]);
// Ensure the guacSsoSsl module is loaded along with the rest of the app
angular.module('index').requires.push('guacSsoSsl');